build(vscode-ide-companion): 添加 SCSS 支持

- 在 esbuild.js 中添加 SCSS 文件处理逻辑
- 在 package.json 中添加 sass 依赖
- 新增代码使用 sass 编译 SCSS 文件,并将其注入到页面中
This commit is contained in:
yiliang114
2025-11-19 23:34:05 +08:00
parent bc2b503e8d
commit 018990b7f6
6 changed files with 595 additions and 13 deletions

View File

@@ -37,6 +37,7 @@ const esbuildProblemMatcherPlugin = {
const cssInjectPlugin = {
name: 'css-inject',
setup(build) {
// Handle CSS files
build.onLoad({ filter: /\.css$/ }, async (args) => {
const fs = await import('fs');
const css = await fs.promises.readFile(args.path, 'utf8');
@@ -49,6 +50,23 @@ const cssInjectPlugin = {
loader: 'js',
};
});
// Handle SCSS files
build.onLoad({ filter: /\.scss$/ }, async (args) => {
const sass = await import('sass');
const result = sass.compile(args.path, {
loadPaths: [args.path.substring(0, args.path.lastIndexOf('/'))],
});
const css = result.css;
return {
contents: `
const style = document.createElement('style');
style.textContent = ${JSON.stringify(css)};
document.head.appendChild(style);
`,
loader: 'js',
};
});
},
};

View File

@@ -178,6 +178,7 @@
"esbuild": "^0.25.3",
"eslint": "^9.25.1",
"npm-run-all2": "^8.0.2",
"sass": "^1.94.1",
"typescript": "^5.8.3",
"vitest": "^3.2.4"
},

View File

@@ -298,6 +298,22 @@ button {
padding: var(--app-spacing-medium);
}
.thinking-message {
background-color: var(--app-list-hover-background, rgba(100, 100, 255, 0.1));
opacity: 0.8;
font-style: italic;
position: relative;
padding-left: 24px;
}
.thinking-message::before {
content: "💭";
position: absolute;
left: 6px;
top: 50%;
transform: translateY(-50%);
}
.thinking-label {
font-size: 12px;
font-weight: 600;
@@ -314,6 +330,70 @@ button {
color: rgba(200, 200, 255, 0.9);
}
/* Waiting message styles - similar to Claude Code thinking state */
.message.waiting-message {
opacity: 0.85;
}
.waiting-message .message-content {
background-color: transparent;
border: none;
padding: 8px 0;
display: flex;
align-items: center;
gap: 8px;
}
/* Typing indicator for loading messages */
.typing-indicator {
display: inline-flex;
align-items: center;
margin-right: 0;
gap: 4px;
}
.typing-dot, .thinking-dot {
width: 6px;
height: 6px;
background-color: var(--app-secondary-foreground);
border-radius: 50%;
margin-right: 0;
opacity: 0.6;
animation: typingPulse 1.4s infinite ease-in-out;
}
.typing-dot:nth-child(1),
.thinking-dot:nth-child(1) {
animation-delay: 0s;
}
.typing-dot:nth-child(2),
.thinking-dot:nth-child(2) {
animation-delay: 0.2s;
}
.typing-dot:nth-child(3),
.thinking-dot:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typingPulse {
0%, 60%, 100% {
transform: scale(0.7);
opacity: 0.6;
}
30% {
transform: scale(1);
opacity: 1;
}
}
.loading-text {
opacity: 0.7;
font-style: italic;
color: var(--app-secondary-foreground);
}
/* ===========================
Scrollbar Styling
=========================== */

View File

@@ -190,13 +190,15 @@ export const App: React.FC = () => {
const [messages, setMessages] = useState<TextMessage[]>([]);
const [inputText, setInputText] = useState('');
const [isStreaming, setIsStreaming] = useState(false);
const [_isWaitingForResponse, setIsWaitingForResponse] = useState(false);
const [_loadingMessage, setLoadingMessage] = useState('');
const [isWaitingForResponse, setIsWaitingForResponse] = useState(false);
const [loadingMessage, setLoadingMessage] = useState('');
const [currentStreamContent, setCurrentStreamContent] = useState('');
const [qwenSessions, setQwenSessions] = useState<
Array<Record<string, unknown>>
>([]);
const [currentSessionId, setCurrentSessionId] = useState<string | null>(null);
const [currentSessionTitle, setCurrentSessionTitle] =
useState<string>('Past Conversations');
const [showSessionSelector, setShowSessionSelector] = useState(false);
const [sessionSearchQuery, setSessionSearchQuery] = useState('');
const [permissionRequest, setPermissionRequest] = useState<{
@@ -336,6 +338,18 @@ export const App: React.FC = () => {
break;
}
case 'thoughtChunk': {
const chunkData = message.data;
// Handle thought chunks for AI thinking display
const thinkingMessage: TextMessage = {
role: 'thinking',
content: chunkData.content || chunkData.chunk || '',
timestamp: Date.now(),
};
setMessages((prev) => [...prev, thinkingMessage]);
break;
}
case 'streamEnd':
// Finalize the streamed message
if (currentStreamContentRef.current) {
@@ -347,12 +361,14 @@ export const App: React.FC = () => {
setMessages((prev) => [...prev, assistantMessage]);
}
setIsStreaming(false);
setIsWaitingForResponse(false); // Clear waiting state
setCurrentStreamContent('');
currentStreamContentRef.current = '';
break;
case 'error':
setIsStreaming(false);
setIsWaitingForResponse(false);
break;
case 'permissionRequest':
@@ -371,10 +387,30 @@ export const App: React.FC = () => {
setQwenSessions(sessions);
// If no current session is selected and there are sessions, select the first one
if (!currentSessionId && sessions.length > 0) {
const firstSession = sessions[0];
const firstSessionId =
(sessions[0].id as string) || (sessions[0].sessionId as string);
(firstSession.id as string) || (firstSession.sessionId as string);
const firstSessionTitle =
(firstSession.title as string) ||
(firstSession.name as string) ||
'Past Conversations';
if (firstSessionId) {
setCurrentSessionId(firstSessionId);
setCurrentSessionTitle(firstSessionTitle);
}
} else if (currentSessionId && sessions.length > 0) {
// Update title for the current session if it exists in the list
const currentSession = sessions.find(
(s: Record<string, unknown>) =>
(s.id as string) === currentSessionId ||
(s.sessionId as string) === currentSessionId,
);
if (currentSession) {
const title =
(currentSession.title as string) ||
(currentSession.name as string) ||
'Past Conversations';
setCurrentSessionTitle(title);
}
}
break;
@@ -391,6 +427,14 @@ export const App: React.FC = () => {
message.data.sessionId,
);
}
// Update current session title
if (message.data.title || message.data.name) {
const title =
(message.data.title as string) ||
(message.data.name as string) ||
'Past Conversations';
setCurrentSessionTitle(title);
}
// Load messages from the session
if (message.data.messages) {
console.log(
@@ -463,6 +507,7 @@ export const App: React.FC = () => {
vscode.postMessage({ type: 'newQwenSession', data: {} });
setShowSessionSelector(false);
setCurrentSessionId(null);
setCurrentSessionTitle('Past Conversations'); // Reset title to default
// Clear messages in UI
setMessages([]);
setCurrentStreamContent('');
@@ -694,7 +739,7 @@ export const App: React.FC = () => {
title="Past conversations"
>
<span className="button-content">
<span className="button-text">Past Conversations</span>
<span className="button-text">{currentSessionTitle}</span>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
@@ -734,14 +779,31 @@ export const App: React.FC = () => {
<EmptyState />
) : (
<>
{messages.map((msg, index) => (
<div key={index} className={`message ${msg.role}`}>
<div className="message-content">{msg.content}</div>
<div className="message-timestamp">
{new Date(msg.timestamp).toLocaleTimeString()}
{messages.map((msg, index) => {
// Special styling for thinking messages (Claude Code style)
const messageClass =
msg.role === 'thinking'
? 'message assistant thinking-message'
: `message ${msg.role}`;
return (
<div key={index} className={messageClass}>
<div className="message-content">
{msg.role === 'thinking' && (
<span className="thinking-indicator">
<span className="thinking-dot"></span>
<span className="thinking-dot"></span>
<span className="thinking-dot"></span>
</span>
)}
{msg.content}
</div>
<div className="message-timestamp">
{new Date(msg.timestamp).toLocaleTimeString()}
</div>
</div>
</div>
))}
);
})}
{/* Tool Calls */}
{Array.from(toolCalls.values()).map((toolCall) => (
@@ -757,6 +819,20 @@ export const App: React.FC = () => {
/>
)}
{/* Loading/Waiting Message - in message list */}
{isWaitingForResponse && loadingMessage && (
<div className="message assistant waiting-message">
<div className="message-content">
<span className="typing-indicator">
<span className="typing-dot"></span>
<span className="typing-dot"></span>
<span className="typing-dot"></span>
</span>
<span className="loading-text">{loadingMessage}</span>
</div>
</div>
)}
{isStreaming && currentStreamContent && (
<div className="message assistant streaming">
<div className="message-content">{currentStreamContent}</div>
@@ -830,7 +906,7 @@ export const App: React.FC = () => {
role="textbox"
aria-label="Message input"
aria-multiline="true"
data-placeholder="Ask qwen to edit…"
data-placeholder="Ask Qwen Code …"
onInput={(e) => {
const target = e.target as HTMLDivElement;
setInputText(target.textContent || '');

View File

@@ -6,7 +6,7 @@
import ReactDOM from 'react-dom/client';
import { App } from './App.js';
import './App.css';
import './App.scss';
import './ClaudeCodeStyles.css';
const container = document.getElementById('root');