diff --git a/packages/vscode-ide-companion/src/WebViewProvider.ts b/packages/vscode-ide-companion/src/WebViewProvider.ts index a0a6aea4..842ea1d8 100644 --- a/packages/vscode-ide-companion/src/WebViewProvider.ts +++ b/packages/vscode-ide-companion/src/WebViewProvider.ts @@ -194,6 +194,20 @@ export class WebViewProvider { this.disposables, ); + // Listen for active editor changes and notify WebView + const editorChangeDisposable = vscode.window.onDidChangeActiveTextEditor( + (editor) => { + const fileName = editor?.document.uri.fsPath + ? this.getFileName(editor.document.uri.fsPath) + : null; + this.sendMessageToWebView({ + type: 'activeEditorChanged', + data: { fileName }, + }); + }, + ); + this.disposables.push(editorChangeDisposable); + // Initialize agent connection only once if (!this.agentInitialized) { await this.initializeAgentConnection(); @@ -481,7 +495,7 @@ export class WebViewProvider { private async handleWebViewMessage(message: { type: string; - data?: { text?: string; id?: string; sessionId?: string }; + data?: { text?: string; id?: string; sessionId?: string; path?: string }; }): Promise { console.log('[WebViewProvider] Received message from webview:', message); const self = this as { @@ -525,6 +539,19 @@ export class WebViewProvider { await this.handleGetQwenSessions(); break; + case 'getActiveEditor': { + // 发送当前激活编辑器的文件名给 WebView + const editor = vscode.window.activeTextEditor; + const fileName = editor?.document.uri.fsPath + ? this.getFileName(editor.document.uri.fsPath) + : null; + this.sendMessageToWebView({ + type: 'activeEditorChanged', + data: { fileName }, + }); + break; + } + case 'switchQwenSession': await this.handleSwitchQwenSession(message.data?.sessionId || ''); break; @@ -539,6 +566,16 @@ export class WebViewProvider { await this.handleCancelPrompt(); break; + case 'openFile': + await this.handleOpenFile(message.data?.path); + break; + + case 'openDiff': + await this.handleOpenDiff( + message.data as { path?: string; oldText?: string; newText?: string }, + ); + break; + default: console.warn('[WebViewProvider] Unknown message type:', message.type); break; @@ -849,10 +886,163 @@ export class WebViewProvider { } } + /** + * Handle open file request from WebView + * Opens a file in VS Code editor, optionally at a specific line + */ + private async handleOpenFile(filePath?: string): Promise { + try { + if (!filePath) { + console.warn('[WebViewProvider] No file path provided'); + return; + } + + console.log('[WebViewProvider] Opening file:', filePath); + + // Parse file path and line number (format: path/to/file.ts:123) + const match = filePath.match(/^(.+?)(?::(\d+))?$/); + if (!match) { + console.warn('[WebViewProvider] Invalid file path format:', filePath); + return; + } + + const [, path, lineStr] = match; + const lineNumber = lineStr ? parseInt(lineStr, 10) - 1 : 0; // VS Code uses 0-based line numbers + + // Convert to absolute path if relative + let absolutePath = path; + if (!path.startsWith('/') && !path.match(/^[a-zA-Z]:/)) { + // Relative path - resolve against workspace + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + if (workspaceFolder) { + absolutePath = vscode.Uri.joinPath(workspaceFolder.uri, path).fsPath; + } + } + + // Open the document + const uri = vscode.Uri.file(absolutePath); + const document = await vscode.workspace.openTextDocument(uri); + const editor = await vscode.window.showTextDocument(document, { + preview: false, + preserveFocus: false, + }); + + // Navigate to line if specified + if (lineStr) { + const position = new vscode.Position(lineNumber, 0); + editor.selection = new vscode.Selection(position, position); + editor.revealRange( + new vscode.Range(position, position), + vscode.TextEditorRevealType.InCenter, + ); + } + + console.log('[WebViewProvider] File opened successfully:', absolutePath); + } catch (error) { + console.error('[WebViewProvider] Failed to open file:', error); + vscode.window.showErrorMessage(`Failed to open file: ${error}`); + } + } + + /** + * Handle open diff request from WebView + * Opens VS Code's diff viewer to compare old and new file contents + */ + private async handleOpenDiff(data?: { + path?: string; + oldText?: string; + newText?: string; + }): Promise { + try { + if (!data || !data.path) { + console.warn('[WebViewProvider] No file path provided for diff'); + return; + } + + const { path, oldText = '', newText = '' } = data; + console.log('[WebViewProvider] Opening diff for:', path); + + // Convert to absolute path if relative + let absolutePath = path; + if (!path.startsWith('/') && !path.match(/^[a-zA-Z]:/)) { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + if (workspaceFolder) { + absolutePath = vscode.Uri.joinPath(workspaceFolder.uri, path).fsPath; + } + } + + // Get the file name for display + const fileName = this.getFileName(absolutePath); + + // Create URIs for old and new content + // Use untitled scheme for old content (before changes) + const oldUri = vscode.Uri.parse(`untitled:${absolutePath}.old`).with({ + scheme: 'untitled', + }); + + // Use the actual file URI for new content + const newUri = vscode.Uri.file(absolutePath); + + // Create a TextDocument for the old content using an in-memory document + const _oldDocument = await vscode.workspace.openTextDocument( + oldUri.with({ scheme: 'untitled' }), + ); + + // Write old content to the document + const edit = new vscode.WorkspaceEdit(); + edit.insert( + oldUri.with({ scheme: 'untitled' }), + new vscode.Position(0, 0), + oldText, + ); + await vscode.workspace.applyEdit(edit); + + // Check if new file exists, if not create it with new content + try { + await vscode.workspace.fs.stat(newUri); + } catch { + // File doesn't exist, create it + const encoder = new TextEncoder(); + await vscode.workspace.fs.writeFile(newUri, encoder.encode(newText)); + } + + // Open diff view + await vscode.commands.executeCommand( + 'vscode.diff', + oldUri.with({ scheme: 'untitled' }), + newUri, + `${fileName} (Before ↔ After)`, + { + preview: false, + preserveFocus: false, + }, + ); + + console.log('[WebViewProvider] Diff opened successfully'); + } catch (error) { + console.error('[WebViewProvider] Failed to open diff:', error); + vscode.window.showErrorMessage(`Failed to open diff: ${error}`); + } + } + private sendMessageToWebView(message: unknown): void { this.panel?.webview.postMessage(message); } + /** + * 从完整路径中提取文件名 + * @param fsPath 文件的完整路径 + * @returns 文件名(不含路径) + */ + private getFileName(fsPath: string): string { + // 使用 path.basename 的逻辑:找到最后一个路径分隔符后的部分 + const lastSlash = Math.max( + fsPath.lastIndexOf('/'), + fsPath.lastIndexOf('\\'), + ); + return lastSlash >= 0 ? fsPath.substring(lastSlash + 1) : fsPath; + } + private getWebviewContent(): string { const scriptUri = this.panel!.webview.asWebviewUri( vscode.Uri.joinPath(this.extensionUri, 'dist', 'webview.js'), diff --git a/packages/vscode-ide-companion/src/webview/App.scss b/packages/vscode-ide-companion/src/webview/App.scss index bd3cf08e..f2f3b0e3 100644 --- a/packages/vscode-ide-companion/src/webview/App.scss +++ b/packages/vscode-ide-companion/src/webview/App.scss @@ -15,6 +15,7 @@ --app-qwen-clay-button-orange: #4f46e5; --app-qwen-ivory: #f5f5ff; --app-qwen-slate: #141420; + --app-qwen-green: #6bcf7f; /* Spacing */ --app-spacing-small: 4px; @@ -45,6 +46,11 @@ --app-input-placeholder-foreground: var(--vscode-input-placeholderForeground); --app-input-secondary-background: var(--vscode-menu-background); + /* Code Highlighting */ + --app-code-background: var(--vscode-textCodeBlock-background, rgba(0, 0, 0, 0.05)); + --app-link-foreground: var(--vscode-textLink-foreground, #007ACC); + --app-link-active-foreground: var(--vscode-textLink-activeForeground, #005A9E); + /* List Styles */ --app-list-hover-background: var(--vscode-list-hoverBackground); --app-list-active-background: var(--vscode-list-activeSelectionBackground); @@ -192,6 +198,11 @@ button { width: 24px; height: 24px; color: var(--app-primary-foreground); + + svg { + width: 16px; + height: 16px; + } } .new-session-header-button:hover, @@ -607,52 +618,72 @@ button { display: block; } -/* Form (.u) - The actual input form with border and shadow */ +/* Input Form Container - matches Claude Code style */ .input-form { - background: var(--app-input-background); + background: var(--app-input-secondary-background, var(--app-input-background)); border: 1px solid var(--app-input-border); border-radius: var(--corner-radius-large); color: var(--app-input-foreground); display: flex; flex-direction: column; - max-width: 680px; - margin: 0 auto; + // max-width: 680px; + // margin: 0 auto; position: relative; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + transition: border-color 0.2s ease, box-shadow 0.2s ease; } -/* Banner/Warning area (.Wr) */ +/* Inner background layer - creates depth effect */ +.input-form-background { + background: var(--app-input-background); + position: absolute; + border-radius: var(--corner-radius-large); + inset: 0; + z-index: 0; +} + +.input-form:focus-within { + border-color: var(--app-qwen-orange); + box-shadow: 0 0 0 1px var(--app-qwen-orange), 0 1px 2px rgba(0, 0, 0, 0.1); +} + +/* Banner area - for warnings/messages */ .input-banner { /* Empty for now, can be used for warnings/banners */ } -/* Input wrapper (.fo) */ +/* Input wrapper - contains the contenteditable field */ .input-wrapper { - /* padding: 12px 12px 0; */ + position: relative; + display: flex; + z-index: 1; } -/* Contenteditable input field (.d) */ +/* Contenteditable input field - matches Claude Code */ .input-field-editable { - width: 100%; - min-height: 40px; + padding: 10px 14px; + outline: none; + font-family: inherit; + line-height: 1.5; + overflow-y: auto; + position: relative; + flex: 1; + align-self: stretch; + user-select: text; + min-height: 1.5em; max-height: 200px; - padding: 8px 10px; background-color: transparent; color: var(--app-input-foreground); border: none; border-radius: 0; font-size: var(--vscode-chat-font-size, 13px); - font-family: var(--vscode-chat-font-family); - outline: none; - line-height: 1.5; - overflow-y: auto; overflow-x: hidden; word-wrap: break-word; white-space: pre-wrap; } .input-field-editable:focus { - /* No border change needed since we don't have a border */ + outline: none; } .input-field-editable:empty:before { @@ -662,12 +693,22 @@ button { position: absolute; } -/* Actions row (.ri) */ +.input-field-editable:disabled, +.input-field-editable[contenteditable='false'] { + color: #999; + cursor: not-allowed; +} + +/* Actions row - matches Claude Code */ .input-actions { display: flex; align-items: center; + padding: 5px; + color: var(--app-secondary-foreground); gap: 6px; - /* padding: 8px 12px 12px; */ + min-width: 0; + border-top: 0.5px solid var(--app-input-border); + z-index: 1; } /* Edit mode button (.l) */ @@ -682,8 +723,8 @@ button { border-radius: var(--corner-radius-small); color: var(--app-primary-foreground); cursor: pointer; - font-size: 13px; - font-weight: 500; + font-size: 12px; + // font-weight: 500; transition: background-color 0.15s; white-space: nowrap; } @@ -729,11 +770,58 @@ button { color: var(--app-primary-foreground); } +.action-icon-button.active { + background-color: var(--app-qwen-clay-button-orange); + color: var(--app-qwen-ivory); +} + +.action-icon-button.active svg { + stroke: var(--app-qwen-ivory); + fill: var(--app-qwen-ivory); +} + .action-icon-button svg { width: 16px; height: 16px; } +/* Spacer to push file indicator to the right */ +.input-actions-spacer { + flex: 1; + min-width: 0; +} + +/* Active file indicator - shows current file selection (.vo in Claude Code) */ +.active-file-indicator { + font-size: 0.85em; + font-family: inherit; + color: var(--app-primary-foreground); + opacity: 0.6; + background: none; + border: none; + padding: 2px 4px; + cursor: default; + text-align: right; + max-width: 50%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + border-radius: 2px; + flex-shrink: 1; + min-width: 0; +} + +.active-file-indicator:hover { + opacity: 1; +} + +/* Hide file indicator on very small screens */ +@media screen and (max-width: 330px) { + .active-file-indicator { + display: none; + } +} + /* Send button (.r) */ .send-button-icon { display: flex; @@ -742,18 +830,18 @@ button { width: 32px; height: 32px; padding: 0; - background: transparent; + background: var(--app-qwen-clay-button-orange); border: 1px solid transparent; border-radius: var(--corner-radius-small); - color: var(--app-primary-foreground); + color: var(--app-qwen-ivory); cursor: pointer; - transition: background-color 0.15s; + transition: background-color 0.15s, filter 0.15s; margin-left: auto; flex-shrink: 0; } .send-button-icon:hover:not(:disabled) { - background-color: var(--app-ghost-button-hover-background); + filter: brightness(1.1); } .send-button-icon:disabled { @@ -862,6 +950,80 @@ button { overflow-y: auto; } +/* =========================== + Diff Display Styles + =========================== */ +.diff-display-container { + margin: 8px 0; + border: 1px solid var(--app-input-border); + border-radius: var(--corner-radius-medium); + overflow: hidden; +} + +.diff-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + background: var(--app-input-secondary-background); + border-bottom: 1px solid var(--app-input-border); +} + +.diff-file-path { + font-family: var(--app-monospace-font-family); + font-size: 13px; + color: var(--app-primary-foreground); +} + +.open-diff-button { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + background: transparent; + border: 1px solid var(--app-input-border); + border-radius: var(--corner-radius-small); + color: var(--app-primary-foreground); + cursor: pointer; + font-size: 12px; + transition: background-color 0.15s; +} + +.open-diff-button:hover { + background: var(--app-ghost-button-hover-background); +} + +.open-diff-button svg { + width: 16px; + height: 16px; +} + +.diff-section { + margin: 0; +} + +.diff-label { + padding: 8px 12px; + background: var(--app-primary-background); + border-bottom: 1px solid var(--app-input-border); + font-size: 11px; + font-weight: 600; + color: var(--app-secondary-foreground); + text-transform: uppercase; +} + +.diff-section .code-block { + border: none; + border-radius: 0; + margin: 0; + max-height: none; /* Remove height limit for diffs */ + overflow-y: visible; +} + +.diff-section .code-content { + display: block; +} + /* =========================== Permission Request Card Styles =========================== */ diff --git a/packages/vscode-ide-companion/src/webview/App.tsx b/packages/vscode-ide-companion/src/webview/App.tsx index 16a84c6e..5db5e86e 100644 --- a/packages/vscode-ide-companion/src/webview/App.tsx +++ b/packages/vscode-ide-companion/src/webview/App.tsx @@ -14,6 +14,8 @@ import { import { PermissionDrawer } from './components/PermissionDrawer.js'; import { ToolCall, type ToolCallData } from './components/ToolCall.js'; import { EmptyState } from './components/EmptyState.js'; +import { PlanDisplay, type PlanEntry } from './components/PlanDisplay.js'; +import { MessageContent } from './components/MessageContent.js'; interface ToolCallUpdate { type: 'tool_call' | 'tool_call_update'; @@ -185,6 +187,8 @@ const getRandomLoadingMessage = () => Math.floor(Math.random() * WITTY_LOADING_PHRASES.length) ]; +type EditMode = 'ask' | 'auto' | 'plan'; + export const App: React.FC = () => { const vscode = useVSCode(); const [messages, setMessages] = useState([]); @@ -208,10 +212,14 @@ export const App: React.FC = () => { const [toolCalls, setToolCalls] = useState>( new Map(), ); + const [planEntries, setPlanEntries] = useState([]); const messagesEndRef = useRef(null); const inputFieldRef = useRef(null); const [showBanner, setShowBanner] = useState(true); const currentStreamContentRef = useRef(''); + const [editMode, setEditMode] = useState('ask'); + const [thinkingEnabled, setThinkingEnabled] = useState(false); + const [activeFileName, setActiveFileName] = useState(null); const handlePermissionRequest = React.useCallback( (request: { @@ -376,6 +384,14 @@ export const App: React.FC = () => { handlePermissionRequest(message.data); break; + case 'plan': + // Update plan entries + console.log('[App] Plan received:', message.data); + if (message.data.entries && Array.isArray(message.data.entries)) { + setPlanEntries(message.data.entries as PlanEntry[]); + } + break; + case 'toolCall': case 'toolCallUpdate': // Handle tool call updates @@ -448,6 +464,7 @@ export const App: React.FC = () => { } setCurrentStreamContent(''); setToolCalls(new Map()); + setPlanEntries([]); // Clear plan entries when switching sessions break; case 'conversationCleared': @@ -456,6 +473,13 @@ export const App: React.FC = () => { setToolCalls(new Map()); break; + case 'activeEditorChanged': { + // 从扩展接收当前激活编辑器的文件名 + const fileName = message.data?.fileName as string | null; + setActiveFileName(fileName); + break; + } + default: break; } @@ -475,6 +499,90 @@ export const App: React.FC = () => { vscode.postMessage({ type: 'getQwenSessions', data: {} }); }, [vscode]); + // Request current active editor on component mount + useEffect(() => { + vscode.postMessage({ type: 'getActiveEditor', data: {} }); + }, [vscode]); + + // Toggle edit mode: ask → auto → plan → ask + const handleToggleEditMode = () => { + setEditMode((prev) => { + if (prev === 'ask') { + return 'auto'; + } + if (prev === 'auto') { + return 'plan'; + } + return 'ask'; + }); + }; + + // Toggle thinking on/off + const handleToggleThinking = () => { + setThinkingEnabled((prev) => !prev); + }; + + // Get edit mode display info + const getEditModeInfo = () => { + switch (editMode) { + case 'ask': + return { + text: 'Ask before edits', + title: 'Qwen will ask before each edit. Click to switch modes.', + icon: ( + + ), + }; + case 'auto': + return { + text: 'Edit automatically', + title: 'Qwen will edit files automatically. Click to switch modes.', + icon: ( + + ), + }; + case 'plan': + return { + text: 'Plan mode', + title: 'Qwen will plan before executing. Click to switch modes.', + icon: ( + + ), + }; + default: + return { + text: 'Unknown mode', + title: 'Unknown edit mode', + icon: null, + }; + } + }; + const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); @@ -511,6 +619,8 @@ export const App: React.FC = () => { // Clear messages in UI setMessages([]); setCurrentStreamContent(''); + setPlanEntries([]); // Clear plan entries + setToolCalls(new Map()); // Clear tool calls }; // Time ago formatter (matching Claude Code) @@ -624,7 +734,11 @@ export const App: React.FC = () => { }; // Check if there are any messages or active content - const hasContent = messages.length > 0 || isStreaming || toolCalls.size > 0; + const hasContent = + messages.length > 0 || + isStreaming || + toolCalls.size > 0 || + planEntries.length > 0; return (
@@ -792,7 +906,15 @@ export const App: React.FC = () => { )} - {msg.content} + { + vscode.postMessage({ + type: 'openFile', + data: { path }, + }); + }} + />
{new Date(msg.timestamp).toLocaleTimeString()} @@ -806,6 +928,9 @@ export const App: React.FC = () => { ))} + {/* Plan Display - shows task list when available */} + {planEntries.length > 0 && } + {/* Loading/Waiting Message - in message list */} {isWaitingForResponse && loadingMessage && (
@@ -822,7 +947,17 @@ export const App: React.FC = () => { {isStreaming && currentStreamContent && (
-
{currentStreamContent}
+
+ { + vscode.postMessage({ + type: 'openFile', + data: { path }, + }); + }} + /> +
)} @@ -884,6 +1019,7 @@ export const App: React.FC = () => {
+
{
+
+ {activeFileName && ( + + {activeFileName} + + )} , + ); + + matchIndex++; + lastIndex = startIdx + fullMatch.length; + }); + + // Add remaining text + if (lastIndex < text.length) { + parts.push(text.slice(lastIndex)); + } + + return parts.length > 0 ? parts : [text]; + }; + + return <>{renderContent()}; +}; diff --git a/packages/vscode-ide-companion/src/webview/components/PermissionDrawer.tsx b/packages/vscode-ide-companion/src/webview/components/PermissionDrawer.tsx index 7c14fd7c..116ceef4 100644 --- a/packages/vscode-ide-companion/src/webview/components/PermissionDrawer.tsx +++ b/packages/vscode-ide-companion/src/webview/components/PermissionDrawer.tsx @@ -39,7 +39,9 @@ export const PermissionDrawer: React.FC = ({ // Close drawer on Escape key useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { - if (!isOpen) return; + if (!isOpen) { + return; + } // Close on Escape if (e.key === 'Escape' && onClose) { diff --git a/packages/vscode-ide-companion/src/webview/components/PlanDisplay.css b/packages/vscode-ide-companion/src/webview/components/PlanDisplay.css index 62f2ebb1..eed90c21 100644 --- a/packages/vscode-ide-companion/src/webview/components/PlanDisplay.css +++ b/packages/vscode-ide-companion/src/webview/components/PlanDisplay.css @@ -6,14 +6,15 @@ /** * PlanDisplay.css - Styles for the task plan component + * Simple, clean timeline-style design */ .plan-display { - background-color: rgba(100, 150, 255, 0.05); - border: 1px solid rgba(100, 150, 255, 0.3); - border-radius: 8px; + background: var(--app-secondary-background); + border: 1px solid var(--app-transparent-inner-border); + border-radius: var(--corner-radius-medium); padding: 16px; - margin: 8px 0; + margin: 12px 0; animation: fadeIn 0.3s ease-in; } @@ -21,92 +22,111 @@ display: flex; align-items: center; gap: 8px; - margin-bottom: 12px; - padding-bottom: 8px; - border-bottom: 1px solid rgba(255, 255, 255, 0.1); + margin-bottom: 16px; + color: var(--app-primary-foreground); } -.plan-icon { - font-size: 18px; +.plan-header-icon { + flex-shrink: 0; + opacity: 0.8; } .plan-title { font-size: 14px; font-weight: 600; - color: rgba(150, 180, 255, 1); + color: var(--app-primary-foreground); } .plan-entries { display: flex; flex-direction: column; - gap: 8px; + gap: 0; + position: relative; } .plan-entry { display: flex; + align-items: flex-start; + gap: 12px; + padding: 8px 0; + position: relative; +} + +/* Vertical line on the left */ +.plan-entry-line { + position: absolute; + left: 7px; + top: 24px; + bottom: -8px; + width: 2px; + background: var(--app-qwen-clay-button-orange); + opacity: 0.3; +} + +.plan-entry:last-child .plan-entry-line { + display: none; +} + +/* Icon container */ +.plan-entry-icon { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + position: relative; + z-index: 1; +} + +.plan-icon { + display: block; +} + +/* Content */ +.plan-entry-content { + flex: 1; + display: flex; + align-items: flex-start; gap: 8px; - padding: 8px; - background-color: var(--vscode-input-background); - border-radius: 4px; - border-left: 3px solid transparent; - transition: all 0.2s ease; + min-height: 24px; + padding-top: 1px; } -.plan-entry[data-priority="high"] { - border-left-color: #ff6b6b; +.plan-entry-number { + font-size: 13px; + font-weight: 500; + color: var(--app-secondary-foreground); + flex-shrink: 0; + min-width: 24px; } -.plan-entry[data-priority="medium"] { - border-left-color: #ffd93d; +.plan-entry-text { + flex: 1; + font-size: 13px; + line-height: 1.6; + color: var(--app-primary-foreground); } -.plan-entry[data-priority="low"] { - border-left-color: #6bcf7f; -} - -.plan-entry.completed { +/* Status-specific styles */ +.plan-entry.completed .plan-entry-text { opacity: 0.6; -} - -.plan-entry.completed .plan-entry-content { text-decoration: line-through; } -.plan-entry.in_progress { - background-color: rgba(100, 150, 255, 0.1); - border-left-width: 4px; +.plan-entry.in_progress .plan-entry-text { + font-weight: 500; } -.plan-entry-header { - display: flex; - align-items: center; - gap: 6px; - flex-shrink: 0; -} - -.plan-entry-status, -.plan-entry-priority { - font-size: 14px; -} - -.plan-entry-index { - font-size: 12px; +.plan-entry.in_progress .plan-entry-number { + color: var(--app-qwen-orange); font-weight: 600; - color: var(--vscode-descriptionForeground); - min-width: 20px; -} - -.plan-entry-content { - flex: 1; - font-size: 13px; - line-height: 1.5; - color: var(--vscode-foreground); } @keyframes fadeIn { from { opacity: 0; - transform: translateY(-10px); + transform: translateY(-8px); } to { opacity: 1; diff --git a/packages/vscode-ide-companion/src/webview/components/PlanDisplay.tsx b/packages/vscode-ide-companion/src/webview/components/PlanDisplay.tsx index 746bfb13..ba97d841 100644 --- a/packages/vscode-ide-companion/src/webview/components/PlanDisplay.tsx +++ b/packages/vscode-ide-companion/src/webview/components/PlanDisplay.tsx @@ -21,55 +21,123 @@ interface PlanDisplayProps { * PlanDisplay component - displays AI's task plan/todo list */ export const PlanDisplay: React.FC = ({ entries }) => { - const getPriorityIcon = (priority: string) => { - switch (priority) { - case 'high': - return '🔴'; - case 'medium': - return '🟡'; - case 'low': - return '🟢'; - default: - return '⚪'; - } - }; - - const getStatusIcon = (status: string) => { + const getStatusIcon = (status: string, _index: number) => { switch (status) { - case 'pending': - return '⏱️'; case 'in_progress': - return '⚙️'; + return ( + + + + + ); case 'completed': - return '✅'; + return ( + + + + + ); default: - return '❓'; + return ( + + + + ); } }; return (
- 📋 + + + + Task Plan
{entries.map((entry, index) => ( -
-
- - {getStatusIcon(entry.status)} - - - {getPriorityIcon(entry.priority)} - - {index + 1}. +
+
+
+ {getStatusIcon(entry.status, index)} +
+
+ {index + 1}. + {entry.content}
-
{entry.content}
))}
diff --git a/packages/vscode-ide-companion/src/webview/components/toolcalls/GenericToolCall.tsx b/packages/vscode-ide-companion/src/webview/components/toolcalls/GenericToolCall.tsx index 2346b0f2..47d40ab2 100644 --- a/packages/vscode-ide-companion/src/webview/components/toolcalls/GenericToolCall.tsx +++ b/packages/vscode-ide-companion/src/webview/components/toolcalls/GenericToolCall.tsx @@ -14,8 +14,8 @@ import { StatusIndicator, CodeBlock, LocationsList, - DiffDisplay, } from './shared/LayoutComponents.js'; +import { DiffDisplay } from './shared/DiffDisplay.js'; import { formatValue, safeTitle, diff --git a/packages/vscode-ide-companion/src/webview/components/toolcalls/WriteToolCall.tsx b/packages/vscode-ide-companion/src/webview/components/toolcalls/WriteToolCall.tsx index 385d9475..2f13a2dd 100644 --- a/packages/vscode-ide-companion/src/webview/components/toolcalls/WriteToolCall.tsx +++ b/packages/vscode-ide-companion/src/webview/components/toolcalls/WriteToolCall.tsx @@ -14,9 +14,10 @@ import { StatusIndicator, CodeBlock, LocationsList, - DiffDisplay, } from './shared/LayoutComponents.js'; +import { DiffDisplay } from './shared/DiffDisplay.js'; import { formatValue, safeTitle, groupContent } from './shared/utils.js'; +import { useVSCode } from '../../hooks/useVSCode.js'; /** * Specialized component for Write/Edit tool calls @@ -26,10 +27,24 @@ export const WriteToolCall: React.FC = ({ toolCall }) => { const { kind, title, status, rawInput, content, locations } = toolCall; const titleText = safeTitle(title); const isEdit = kind.toLowerCase() === 'edit'; + const vscode = useVSCode(); // Group content by type const { textOutputs, errors, diffs, otherData } = groupContent(content); + const handleOpenDiff = ( + path: string | undefined, + oldText: string | null | undefined, + newText: string | undefined, + ) => { + if (path) { + vscode.postMessage({ + type: 'openDiff', + data: { path, oldText: oldText || '', newText: newText || '' }, + }); + } + }; + return ( {/* Title row */} @@ -59,6 +74,9 @@ export const WriteToolCall: React.FC = ({ toolCall }) => { path={item.path} oldText={item.oldText} newText={item.newText} + onOpenDiff={() => + handleOpenDiff(item.path, item.oldText, item.newText) + } /> ), diff --git a/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/DiffDisplay.tsx b/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/DiffDisplay.tsx new file mode 100644 index 00000000..928d5077 --- /dev/null +++ b/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/DiffDisplay.tsx @@ -0,0 +1,72 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + * + * Diff display component for showing file changes + */ + +import type React from 'react'; + +/** + * Props for DiffDisplay + */ +interface DiffDisplayProps { + path?: string; + oldText?: string | null; + newText?: string; + onOpenDiff?: () => void; +} + +/** + * Display diff with before/after sections and option to open in VSCode diff viewer + */ +export const DiffDisplay: React.FC = ({ + path, + oldText, + newText, + onOpenDiff, +}) => ( +
+
+
+ {path || 'Unknown file'} +
+ {onOpenDiff && ( + + )} +
+ {oldText !== undefined && ( +
+
Before:
+
+          
{oldText || '(empty)'}
+
+
+ )} + {newText !== undefined && ( +
+
After:
+
+          
{newText}
+
+
+ )} +
+); diff --git a/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/LayoutComponents.tsx b/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/LayoutComponents.tsx index 9afd6911..2b555141 100644 --- a/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/LayoutComponents.tsx +++ b/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/LayoutComponents.tsx @@ -107,55 +107,3 @@ export const LocationsList: React.FC = ({ locations }) => ( ))} ); - -/** - * Props for DiffDisplay - */ -interface DiffDisplayProps { - path?: string; - oldText?: string | null; - newText?: string; -} - -/** - * Display diff with before/after sections - */ -export const DiffDisplay: React.FC = ({ - path, - oldText, - newText, -}) => ( -
-
- {path || 'Unknown file'} -
- {oldText !== undefined && ( -
-
- Before: -
-
{oldText || '(empty)'}
-
- )} - {newText !== undefined && ( -
-
- After: -
-
{newText}
-
- )} -
-);