From 2d844d11df273f7815390eed70dfc2c954bd1b4a Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Fri, 5 Dec 2025 02:15:48 +0800 Subject: [PATCH] fix(vscode-ide-companion): improve message logging and permission handling - Increase message logging truncation limit from 500 to 1500 characters - Fix permission option mapping logic for reject_once/cancel options - Add TODO comments for diff accept/cancel responses during permission requests Resolves issues with permission handling and improves debugging capabilities. --- .../src/acp/acpConnection.ts | 4 +- .../src/acp/acpMessageHandler.ts | 21 +- .../vscode-ide-companion/src/extension.ts | 4 + .../vscode-ide-companion/src/webview/App.tsx | 61 ++- .../src/webview/components/InputForm.tsx | 7 + .../webview/components/MarkdownRenderer.tsx | 158 -------- .../MarkdownRenderer.css | 35 +- .../MarkdownRenderer/MarkdownRenderer.tsx | 263 +++++++++++++ .../src/webview/components/MessageContent.tsx | 2 +- .../webview/components/PermissionDrawer.tsx | 21 +- .../webview/components/PermissionRequest.tsx | 353 +++++++++--------- .../{ => Assistant}/AssistantMessage.css | 14 +- .../{ => Assistant}/AssistantMessage.tsx | 8 +- .../components/messages/StreamingMessage.tsx | 4 +- .../components/messages/UserMessage.tsx | 9 +- .../messages/Waiting/InterruptedMessage.tsx | 32 ++ .../messages/{ => Waiting}/WaitingMessage.css | 5 + .../messages/{ => Waiting}/WaitingMessage.tsx | 5 +- .../src/webview/components/messages/index.tsx | 5 +- .../components/toolcalls/Bash/Bash.tsx | 31 +- .../toolcalls/Edit/EditToolCall.tsx | 59 ++- .../components/toolcalls/Execute/Execute.tsx | 23 +- .../ExecuteNode/ExecuteNodeToolCall.tsx | 12 +- .../components/toolcalls/GenericToolCall.tsx | 30 +- .../toolcalls/Read/ReadToolCall.tsx | 15 +- .../toolcalls/{ => Search}/SearchToolCall.tsx | 27 +- .../toolcalls/{ => Think}/ThinkToolCall.tsx | 12 +- .../{ => Think}/TodoWriteToolCall.tsx | 10 +- .../toolcalls/{ => Write}/WriteToolCall.tsx | 21 +- .../webview/components/toolcalls/index.tsx | 22 +- .../toolcalls/shared/LayoutComponents.tsx | 14 +- .../components/toolcalls/shared/utils.ts | 29 +- .../webview/components/ui/CheckboxDisplay.tsx | 74 ++-- .../src/webview/components/ui/FileLink.tsx | 1 - .../webview/handlers/FileMessageHandler.ts | 7 +- .../src/webview/hooks/useWebViewMessages.ts | 27 ++ .../src/webview/styles/ClaudeCodeStyles.css | 4 +- .../src/webview/styles/tailwind.css | 17 + .../src/webview/utils/envUtils.ts | 15 + .../vscode-ide-companion/tailwind.config.js | 1 + 40 files changed, 933 insertions(+), 529 deletions(-) delete mode 100644 packages/vscode-ide-companion/src/webview/components/MarkdownRenderer.tsx rename packages/vscode-ide-companion/src/webview/components/{ => MarkdownRenderer}/MarkdownRenderer.css (84%) create mode 100644 packages/vscode-ide-companion/src/webview/components/MarkdownRenderer/MarkdownRenderer.tsx rename packages/vscode-ide-companion/src/webview/components/messages/{ => Assistant}/AssistantMessage.css (85%) rename packages/vscode-ide-companion/src/webview/components/messages/{ => Assistant}/AssistantMessage.tsx (90%) create mode 100644 packages/vscode-ide-companion/src/webview/components/messages/Waiting/InterruptedMessage.tsx rename packages/vscode-ide-companion/src/webview/components/messages/{ => Waiting}/WaitingMessage.css (89%) rename packages/vscode-ide-companion/src/webview/components/messages/{ => Waiting}/WaitingMessage.tsx (93%) rename packages/vscode-ide-companion/src/webview/components/toolcalls/{ => Search}/SearchToolCall.tsx (73%) rename packages/vscode-ide-companion/src/webview/components/toolcalls/{ => Think}/ThinkToolCall.tsx (82%) rename packages/vscode-ide-companion/src/webview/components/toolcalls/{ => Think}/TodoWriteToolCall.tsx (92%) rename packages/vscode-ide-companion/src/webview/components/toolcalls/{ => Write}/WriteToolCall.tsx (86%) create mode 100644 packages/vscode-ide-companion/src/webview/utils/envUtils.ts diff --git a/packages/vscode-ide-companion/src/acp/acpConnection.ts b/packages/vscode-ide-companion/src/acp/acpConnection.ts index b0040cc4..38b48fc4 100644 --- a/packages/vscode-ide-companion/src/acp/acpConnection.ts +++ b/packages/vscode-ide-companion/src/acp/acpConnection.ts @@ -50,11 +50,9 @@ export class AcpConnection { private nextRequestId = { value: 0 }; private backend: AcpBackend | null = null; - // Module instances private messageHandler: AcpMessageHandler; private sessionManager: AcpSessionManager; - // Callback functions onSessionUpdate: (data: AcpSessionUpdate) => void = () => {}; onPermissionRequest: (data: AcpPermissionRequest) => Promise<{ optionId: string; @@ -206,7 +204,7 @@ export class AcpConnection { const message = JSON.parse(line) as AcpMessage; console.log( '[ACP] <<< Received message:', - JSON.stringify(message).substring(0, 500), + JSON.stringify(message).substring(0, 500 * 3), ); this.handleMessage(message); } catch (_error) { diff --git a/packages/vscode-ide-companion/src/acp/acpMessageHandler.ts b/packages/vscode-ide-companion/src/acp/acpMessageHandler.ts index fc6938fb..e1914237 100644 --- a/packages/vscode-ide-companion/src/acp/acpMessageHandler.ts +++ b/packages/vscode-ide-companion/src/acp/acpMessageHandler.ts @@ -217,7 +217,8 @@ export class AcpMessageHandler { return { outcome: { outcome, - optionId: optionId === 'cancel' ? 'reject_once' : optionId, + // optionId: optionId === 'cancel' ? 'reject_once' : optionId, + optionId: optionId === 'reject_once' ? 'cancel' : optionId, }, }; } catch (_error) { @@ -230,3 +231,21 @@ export class AcpMessageHandler { } } } + +// [ +// { +// received: 'reject_once', +// code: 'invalid_enum_value', +// options: [ +// 'proceed_once', +// 'proceed_always', +// 'proceed_always_server', +// 'proceed_always_tool', +// 'modify_with_editor', +// 'cancel', +// ], +// path: [], +// message: +// "Invalid enum value. Expected 'proceed_once' | 'proceed_always' | 'proceed_always_server' | 'proceed_always_tool' | 'modify_with_editor' | 'cancel', received 'reject_once'", +// }, +// ]; diff --git a/packages/vscode-ide-companion/src/extension.ts b/packages/vscode-ide-companion/src/extension.ts index 52dcf17b..2342e466 100644 --- a/packages/vscode-ide-companion/src/extension.ts +++ b/packages/vscode-ide-companion/src/extension.ts @@ -181,12 +181,16 @@ export async function activate(context: vscode.ExtensionContext) { if (docUri && docUri.scheme === DIFF_SCHEME) { diffManager.acceptDiff(docUri); } + // TODO: 如果 webview 在 request_permission 需要回复 cli + console.log('[Extension] Diff accepted'); }), vscode.commands.registerCommand('qwen.diff.cancel', (uri?: vscode.Uri) => { const docUri = uri ?? vscode.window.activeTextEditor?.document.uri; if (docUri && docUri.scheme === DIFF_SCHEME) { diffManager.cancelDiff(docUri); } + // TODO: 如果 webview 在 request_permission 需要回复 cli + console.log('[Extension] Diff cancelled'); }), ); diff --git a/packages/vscode-ide-companion/src/webview/App.tsx b/packages/vscode-ide-companion/src/webview/App.tsx index 1df61679..19dd2e0b 100644 --- a/packages/vscode-ide-companion/src/webview/App.tsx +++ b/packages/vscode-ide-companion/src/webview/App.tsx @@ -39,6 +39,7 @@ import { AssistantMessage, ThinkingMessage, WaitingMessage, + InterruptedMessage, } from './components/messages/index.js'; import { InputForm } from './components/InputForm.js'; import { SessionSelector } from './components/session/SessionSelector.js'; @@ -172,15 +173,33 @@ export const App: React.FC = () => { isStreaming: messageHandling.isStreaming, }); - // Handle cancel streaming + // Handle cancel/stop from the input bar + // Emit a cancel to the extension and immediately reflect interruption locally. const handleCancel = useCallback(() => { - if (messageHandling.isStreaming) { - vscode.postMessage({ - type: 'cancelStreaming', - data: {}, + if (messageHandling.isStreaming || messageHandling.isWaitingForResponse) { + // Proactively end local states and add an 'Interrupted' line + try { + messageHandling.endStreaming?.(); + } catch { + /* no-op */ + } + try { + messageHandling.clearWaitingForResponse?.(); + } catch { + /* no-op */ + } + messageHandling.addMessage({ + role: 'assistant', + content: 'Interrupted', + timestamp: Date.now(), }); } - }, [messageHandling.isStreaming, vscode]); + // Notify extension/agent to cancel server-side work + vscode.postMessage({ + type: 'cancelStreaming', + data: {}, + }); + }, [messageHandling, vscode]); // Message handling useWebViewMessages({ @@ -562,14 +581,28 @@ export const App: React.FC = () => { ); } - return ( - - ); + { + const content = (msg.content || '').trim(); + if ( + content === 'Interrupted' || + content === 'Tool interrupted' + ) { + return ( + + ); + } + return ( + + ); + } } // case 'in-progress-tool-call': diff --git a/packages/vscode-ide-companion/src/webview/components/InputForm.tsx b/packages/vscode-ide-companion/src/webview/components/InputForm.tsx index 3e9b1950..f5a03b98 100644 --- a/packages/vscode-ide-companion/src/webview/components/InputForm.tsx +++ b/packages/vscode-ide-companion/src/webview/components/InputForm.tsx @@ -84,6 +84,7 @@ export const InputForm: React.FC = ({ inputText, inputFieldRef, isStreaming, + isWaitingForResponse, isComposing, editMode, thinkingEnabled, @@ -109,6 +110,12 @@ export const InputForm: React.FC = ({ const editModeInfo = getEditModeInfo(editMode); const handleKeyDown = (e: React.KeyboardEvent) => { + // ESC should cancel the current interaction (stop generation) + if (e.key === 'Escape') { + e.preventDefault(); + onCancel(); + return; + } // If composing (Chinese IME input), don't process Enter key if (e.key === 'Enter' && !e.shiftKey && !isComposing) { // If CompletionMenu is open, let it handle Enter key diff --git a/packages/vscode-ide-companion/src/webview/components/MarkdownRenderer.tsx b/packages/vscode-ide-companion/src/webview/components/MarkdownRenderer.tsx deleted file mode 100644 index ff3b0beb..00000000 --- a/packages/vscode-ide-companion/src/webview/components/MarkdownRenderer.tsx +++ /dev/null @@ -1,158 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - * - * MarkdownRenderer component - renders markdown content with syntax highlighting and clickable file paths - */ - -import type React from 'react'; -import MarkdownIt from 'markdown-it'; -import type { Options as MarkdownItOptions } from 'markdown-it'; -import './MarkdownRenderer.css'; - -interface MarkdownRendererProps { - content: string; - onFileClick?: (filePath: string) => void; -} - -/** - * Regular expressions for parsing content - */ -const FILE_PATH_REGEX = - /([a-zA-Z]:)?([/\\][\w\-. ]+)+\.(tsx?|jsx?|css|scss|json|md|py|java|go|rs|c|cpp|h|hpp|sh|yaml|yml|toml|xml|html|vue|svelte)/gi; -// Match file paths with optional line numbers like: path/file.ts#7-14 or path/file.ts#7 -const FILE_PATH_WITH_LINES_REGEX = - /([a-zA-Z]:)?([/\\][\w\-. ]+)+\.(tsx?|jsx?|css|scss|json|md|py|java|go|rs|c|cpp|h|hpp|sh|yaml|yml|toml|xml|html|vue|svelte)#(\d+)(?:-(\d+))?/gi; - -/** - * MarkdownRenderer component - renders markdown content with enhanced features - */ -export const MarkdownRenderer: React.FC = ({ - content, - onFileClick, -}) => { - /** - * Initialize markdown-it with plugins - */ - const getMarkdownInstance = (): MarkdownIt => { - // Create markdown-it instance with options - const md = new MarkdownIt({ - html: false, // Disable HTML for security - xhtmlOut: false, - breaks: true, - linkify: true, - typographer: true, - } as MarkdownItOptions); - - // Add syntax highlighting for code blocks - md.use((md) => { - md.renderer.rules.code_block = function ( - tokens, - idx: number, - _options, - _env, - ) { - const token = tokens[idx]; - const lang = token.info || 'plaintext'; - const content = token.content; - - // Add syntax highlighting classes - return `
${md.utils.escapeHtml(content)}
`; - }; - - md.renderer.rules.fence = function (tokens, idx: number, _options, _env) { - const token = tokens[idx]; - const lang = token.info || 'plaintext'; - const content = token.content; - - // Add syntax highlighting classes - return `
${md.utils.escapeHtml(content)}
`; - }; - }); - - return md; - }; - - /** - * Render markdown content to HTML - */ - const renderMarkdown = (): string => { - try { - const md = getMarkdownInstance(); - - // Process the markdown content - let html = md.render(content); - - // Post-process to add file path click handlers - html = processFilePaths(html); - - return html; - } catch (error) { - console.error('Error rendering markdown:', error); - // Fallback to plain text if markdown rendering fails - return escapeHtml(content); - } - }; - - /** - * Escape HTML characters for security - */ - const escapeHtml = (unsafe: string): string => - unsafe - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - - /** - * Process file paths in HTML to make them clickable - */ - const processFilePaths = (html: string): string => { - // Process file paths with line numbers - html = html.replace(FILE_PATH_WITH_LINES_REGEX, (match) => { - const filePath = match.split('#')[0]; - return ``; - }); - - // Process regular file paths - html = html.replace(FILE_PATH_REGEX, (match) => { - // Skip if this was already processed as a path with line numbers - if (FILE_PATH_WITH_LINES_REGEX.test(match)) { - return match; - } - return ``; - }); - - return html; - }; - - /** - * Handle file click event - */ - const handleFileClick = (filePath: string) => { - if (onFileClick) { - onFileClick(filePath); - } - }; - - // Attach the handler to window for use in HTML onclick attributes - if (typeof window !== 'undefined') { - ( - window as { handleFileClick?: (filePath: string) => void } - ).handleFileClick = handleFileClick; - } - - return ( -
- ); -}; diff --git a/packages/vscode-ide-companion/src/webview/components/MarkdownRenderer.css b/packages/vscode-ide-companion/src/webview/components/MarkdownRenderer/MarkdownRenderer.css similarity index 84% rename from packages/vscode-ide-companion/src/webview/components/MarkdownRenderer.css rename to packages/vscode-ide-companion/src/webview/components/MarkdownRenderer/MarkdownRenderer.css index 6eedb921..63d5e91d 100644 --- a/packages/vscode-ide-companion/src/webview/components/MarkdownRenderer.css +++ b/packages/vscode-ide-companion/src/webview/components/MarkdownRenderer/MarkdownRenderer.css @@ -55,11 +55,44 @@ .markdown-content ul, .markdown-content ol { - margin-top: 0; + margin-top: 1em; margin-bottom: 1em; padding-left: 2em; } +/* Ensure list markers are visible even with global CSS resets */ +.markdown-content ul { + list-style-type: disc; + list-style-position: outside; +} + +.markdown-content ol { + list-style-type: decimal; + list-style-position: outside; +} + +/* Nested list styles */ +.markdown-content ul ul { + list-style-type: circle; +} + +.markdown-content ul ul ul { + list-style-type: square; +} + +.markdown-content ol ol { + list-style-type: lower-alpha; +} + +.markdown-content ol ol ol { + list-style-type: lower-roman; +} + +/* Style the marker explicitly so themes don't hide it */ +.markdown-content li::marker { + color: var(--app-secondary-foreground); +} + .markdown-content li { margin-bottom: 0.25em; } diff --git a/packages/vscode-ide-companion/src/webview/components/MarkdownRenderer/MarkdownRenderer.tsx b/packages/vscode-ide-companion/src/webview/components/MarkdownRenderer/MarkdownRenderer.tsx new file mode 100644 index 00000000..fda351d6 --- /dev/null +++ b/packages/vscode-ide-companion/src/webview/components/MarkdownRenderer/MarkdownRenderer.tsx @@ -0,0 +1,263 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + * + * MarkdownRenderer component - renders markdown content with syntax highlighting and clickable file paths + */ + +import type React from 'react'; +import MarkdownIt from 'markdown-it'; +import type { Options as MarkdownItOptions } from 'markdown-it'; +import './MarkdownRenderer.css'; + +interface MarkdownRendererProps { + content: string; + onFileClick?: (filePath: string) => void; +} + +/** + * Regular expressions for parsing content + */ +const FILE_PATH_REGEX = + /([a-zA-Z]:)?([/\\][\w\-. ]+)+\.(tsx?|jsx?|css|scss|json|md|py|java|go|rs|c|cpp|h|hpp|sh|yaml|yml|toml|xml|html|vue|svelte)/gi; +// Match file paths with optional line numbers like: path/file.ts#7-14 or path/file.ts#7 +const FILE_PATH_WITH_LINES_REGEX = + /([a-zA-Z]:)?([/\\][\w\-. ]+)+\.(tsx?|jsx?|css|scss|json|md|py|java|go|rs|c|cpp|h|hpp|sh|yaml|yml|toml|xml|html|vue|svelte)#(\d+)(?:-(\d+))?/gi; + +/** + * MarkdownRenderer component - renders markdown content with enhanced features + */ +export const MarkdownRenderer: React.FC = ({ + content, + onFileClick, +}) => { + /** + * Initialize markdown-it with plugins + */ + const getMarkdownInstance = (): MarkdownIt => { + // Create markdown-it instance with options + const md = new MarkdownIt({ + html: false, // Disable HTML for security + xhtmlOut: false, + breaks: true, + linkify: true, + typographer: true, + } as MarkdownItOptions); + + // Add syntax highlighting for code blocks + md.use((md) => { + md.renderer.rules.code_block = function ( + tokens, + idx: number, + _options, + _env, + ) { + const token = tokens[idx]; + const lang = token.info || 'plaintext'; + const content = token.content; + + // Add syntax highlighting classes + return `
${md.utils.escapeHtml(content)}
`; + }; + + md.renderer.rules.fence = function (tokens, idx: number, _options, _env) { + const token = tokens[idx]; + const lang = token.info || 'plaintext'; + const content = token.content; + + // Add syntax highlighting classes + return `
${md.utils.escapeHtml(content)}
`; + }; + }); + + return md; + }; + + /** + * Render markdown content to HTML + */ + const renderMarkdown = (): string => { + try { + const md = getMarkdownInstance(); + + // Process the markdown content + let html = md.render(content); + + // Post-process to add file path click handlers + html = processFilePaths(html); + + return html; + } catch (error) { + console.error('Error rendering markdown:', error); + // Fallback to plain text if markdown rendering fails + return escapeHtml(content); + } + }; + + /** + * Escape HTML characters for security + */ + const escapeHtml = (unsafe: string): string => + unsafe + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + + /** + * Process file paths in HTML to make them clickable + */ + const processFilePaths = (html: string): string => { + // If DOM is not available, bail out to avoid breaking SSR + if (typeof document === 'undefined') return html; + + // Build non-global variants to avoid .test() statefulness + const FILE_PATH_NO_G = new RegExp( + FILE_PATH_REGEX.source, + FILE_PATH_REGEX.flags.replace('g', ''), + ); + const FILE_PATH_WITH_LINES_NO_G = new RegExp( + FILE_PATH_WITH_LINES_REGEX.source, + FILE_PATH_WITH_LINES_REGEX.flags.replace('g', ''), + ); + // Match a bare file name like README.md (no leading slash) + const BARE_FILE_REGEX = + /[\w\-. ]+\.(tsx?|jsx?|css|scss|json|md|py|java|go|rs|c|cpp|h|hpp|sh|ya?ml|toml|xml|html|vue|svelte)/i; + + // Parse HTML into a DOM tree so we don't replace inside attributes + const container = document.createElement('div'); + container.innerHTML = html; + + const union = new RegExp( + `${FILE_PATH_WITH_LINES_REGEX.source}|${FILE_PATH_REGEX.source}`, + 'gi', + ); + + const makeLink = (text: string) => { + const link = document.createElement('a'); + // Pass base path to the handler; keep the full text as label + const filePath = text.split('#')[0]; + link.className = 'file-path-link'; + link.textContent = text; + link.setAttribute('href', '#'); + link.setAttribute('title', `Open ${text}`); + // Carry file path via data attribute; click handled by event delegation + link.setAttribute('data-file-path', filePath); + return link; + }; + + const upgradeAnchorIfFilePath = (a: HTMLAnchorElement) => { + const href = a.getAttribute('href') || ''; + const text = (a.textContent || '').trim(); + + // If linkify turned a bare filename into http://, convert it back + const httpMatch = href.match(/^https?:\/\/(.+)$/i); + if (httpMatch && BARE_FILE_REGEX.test(text) && httpMatch[1] === text) { + // Treat as a file link instead of external URL + const filePath = text; // no leading slash + a.classList.add('file-path-link'); + a.setAttribute('href', '#'); + a.setAttribute('title', `Open ${text}`); + a.setAttribute('data-file-path', filePath); + return; + } + + // Ignore other external protocols + if (/^(https?|mailto|ftp|data):/i.test(href)) return; + + const candidate = href || text; + if ( + FILE_PATH_WITH_LINES_NO_G.test(candidate) || + FILE_PATH_NO_G.test(candidate) + ) { + const filePath = candidate.split('#')[0]; + a.classList.add('file-path-link'); + a.setAttribute('href', '#'); + a.setAttribute('title', `Open ${text || href}`); + a.setAttribute('data-file-path', filePath); + } + }; + + const walk = (node: Node) => { + // Do not transform inside existing anchors + if (node.nodeType === Node.ELEMENT_NODE) { + const el = node as HTMLElement; + if (el.tagName.toLowerCase() === 'a') { + upgradeAnchorIfFilePath(el as HTMLAnchorElement); + return; // Don't descend into + } + } + + for (let child = node.firstChild; child; ) { + const next = child.nextSibling; // child may be replaced + if (child.nodeType === Node.TEXT_NODE) { + const text = child.nodeValue || ''; + union.lastIndex = 0; + const hasMatch = union.test(text); + union.lastIndex = 0; + if (hasMatch) { + const frag = document.createDocumentFragment(); + let lastIndex = 0; + let m: RegExpExecArray | null; + while ((m = union.exec(text))) { + const matchText = m[0]; + const idx = m.index; + if (idx > lastIndex) { + frag.appendChild( + document.createTextNode(text.slice(lastIndex, idx)), + ); + } + frag.appendChild(makeLink(matchText)); + lastIndex = idx + matchText.length; + } + if (lastIndex < text.length) { + frag.appendChild(document.createTextNode(text.slice(lastIndex))); + } + node.replaceChild(frag, child); + } + } else if (child.nodeType === Node.ELEMENT_NODE) { + walk(child); + } + child = next; + } + }; + + walk(container); + return container.innerHTML; + }; + + // Event delegation: intercept clicks on generated file-path links + const handleContainerClick = ( + e: React.MouseEvent, + ) => { + const target = e.target as HTMLElement | null; + if (!target) return; + + // Find nearest anchor with our marker class + const anchor = (target.closest && + target.closest('a.file-path-link')) as HTMLAnchorElement | null; + if (!anchor) return; + + const filePath = anchor.getAttribute('data-file-path'); + if (!filePath) return; + + e.preventDefault(); + e.stopPropagation(); + onFileClick?.(filePath); + }; + + return ( +
+ ); +}; diff --git a/packages/vscode-ide-companion/src/webview/components/MessageContent.tsx b/packages/vscode-ide-companion/src/webview/components/MessageContent.tsx index b25a504b..52ce0fc2 100644 --- a/packages/vscode-ide-companion/src/webview/components/MessageContent.tsx +++ b/packages/vscode-ide-companion/src/webview/components/MessageContent.tsx @@ -7,7 +7,7 @@ */ import type React from 'react'; -import { MarkdownRenderer } from './MarkdownRenderer.tsx'; +import { MarkdownRenderer } from './MarkdownRenderer/MarkdownRenderer.js'; interface MessageContentProps { content: string; diff --git a/packages/vscode-ide-companion/src/webview/components/PermissionDrawer.tsx b/packages/vscode-ide-companion/src/webview/components/PermissionDrawer.tsx index 4918f7be..752ce8bd 100644 --- a/packages/vscode-ide-companion/src/webview/components/PermissionDrawer.tsx +++ b/packages/vscode-ide-companion/src/webview/components/PermissionDrawer.tsx @@ -31,6 +31,7 @@ export const PermissionDrawer: React.FC = ({ const containerRef = useRef(null); const customInputRef = useRef(null); + console.log('PermissionDrawer rendered with isOpen:', isOpen, toolCall); // Get the title for the permission request const getTitle = () => { if (toolCall.kind === 'edit' || toolCall.kind === 'write') { @@ -47,7 +48,7 @@ export const PermissionDrawer: React.FC = ({ ); } if (toolCall.kind === 'execute' || toolCall.kind === 'bash') { - return 'Allow command execution?'; + return 'Allow this bash command?'; } if (toolCall.kind === 'read') { const fileName = @@ -175,13 +176,8 @@ export const PermissionDrawer: React.FC = ({ onMouseEnter={() => setFocusedIndex(index)} > {/* Number badge */} - + {/* Plain number badge without hover background */} + {index + 1} @@ -208,13 +204,8 @@ export const PermissionDrawer: React.FC = ({ onClick={() => customInputRef.current?.focus()} > {/* Number badge (N+1) */} - + {/* Plain number badge without hover background */} + {options.length + 1} diff --git a/packages/vscode-ide-companion/src/webview/components/PermissionRequest.tsx b/packages/vscode-ide-companion/src/webview/components/PermissionRequest.tsx index 8aa63de1..78016e06 100644 --- a/packages/vscode-ide-companion/src/webview/components/PermissionRequest.tsx +++ b/packages/vscode-ide-companion/src/webview/components/PermissionRequest.tsx @@ -4,9 +4,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type React from 'react'; -import { useState } from 'react'; - export interface PermissionOption { name: string; kind: string; @@ -39,192 +36,192 @@ export interface PermissionRequestProps { onResponse: (optionId: string) => void; } -export const PermissionRequest: React.FC = ({ - options, - toolCall, - onResponse, -}) => { - const [selected, setSelected] = useState(null); - const [isResponding, setIsResponding] = useState(false); - const [hasResponded, setHasResponded] = useState(false); +// export const PermissionRequest: React.FC = ({ +// options, +// toolCall, +// onResponse, +// }) => { +// const [selected, setSelected] = useState(null); +// const [isResponding, setIsResponding] = useState(false); +// const [hasResponded, setHasResponded] = useState(false); - const getToolInfo = () => { - if (!toolCall) { - return { - title: 'Permission Request', - description: 'Agent is requesting permission', - icon: '🔐', - }; - } +// const getToolInfo = () => { +// if (!toolCall) { +// return { +// title: 'Permission Request', +// description: 'Agent is requesting permission', +// icon: '🔐', +// }; +// } - const displayTitle = - toolCall.title || toolCall.rawInput?.description || 'Permission Request'; +// const displayTitle = +// toolCall.title || toolCall.rawInput?.description || 'Permission Request'; - const kindIcons: Record = { - edit: '✏️', - read: '📖', - fetch: '🌐', - execute: '⚡', - delete: '🗑️', - move: '📦', - search: '🔍', - think: '💭', - other: '🔧', - }; +// const kindIcons: Record = { +// edit: '✏️', +// read: '📖', +// fetch: '🌐', +// execute: '⚡', +// delete: '🗑️', +// move: '📦', +// search: '🔍', +// think: '💭', +// other: '🔧', +// }; - return { - title: displayTitle, - icon: kindIcons[toolCall.kind || 'other'] || '🔧', - }; - }; +// return { +// title: displayTitle, +// icon: kindIcons[toolCall.kind || 'other'] || '🔧', +// }; +// }; - const { title, icon } = getToolInfo(); +// const { title, icon } = getToolInfo(); - const handleConfirm = async () => { - if (hasResponded || !selected) { - return; - } +// const handleConfirm = async () => { +// if (hasResponded || !selected) { +// return; +// } - setIsResponding(true); - try { - await onResponse(selected); - setHasResponded(true); - } catch (error) { - console.error('Error confirming permission:', error); - } finally { - setIsResponding(false); - } - }; +// setIsResponding(true); +// try { +// await onResponse(selected); +// setHasResponded(true); +// } catch (error) { +// console.error('Error confirming permission:', error); +// } finally { +// setIsResponding(false); +// } +// }; - if (!toolCall) { - return null; - } +// if (!toolCall) { +// return null; +// } - return ( -
-
- {/* Header with icon and title */} -
-
- {icon} -
-
-
{title}
-
Waiting for your approval
-
-
+// return ( +//
+//
+// {/* Header with icon and title */} +//
+//
+// {icon} +//
+//
+//
{title}
+//
Waiting for your approval
+//
+//
- {/* Show command if available */} - {(toolCall.rawInput?.command || toolCall.title) && ( -
-
-
- - COMMAND -
-
-
-
- IN - - {toolCall.rawInput?.command || toolCall.title} - -
- {toolCall.rawInput?.description && ( -
- {toolCall.rawInput.description} -
- )} -
-
- )} +// {/* Show command if available */} +// {(toolCall.rawInput?.command || toolCall.title) && ( +//
+//
+//
+// +// COMMAND +//
+//
+//
+//
+// IN +// +// {toolCall.rawInput?.command || toolCall.title} +// +//
+// {toolCall.rawInput?.description && ( +//
+// {toolCall.rawInput.description} +//
+// )} +//
+//
+// )} - {/* Show file locations if available */} - {toolCall.locations && toolCall.locations.length > 0 && ( -
-
Affected Files
- {toolCall.locations.map((location, index) => ( -
- 📄 - - {location.path} - - {location.line !== null && location.line !== undefined && ( - - ::{location.line} - - )} -
- ))} -
- )} +// {/* Show file locations if available */} +// {toolCall.locations && toolCall.locations.length > 0 && ( +//
+//
Affected Files
+// {toolCall.locations.map((location, index) => ( +//
+// 📄 +// +// {location.path} +// +// {location.line !== null && location.line !== undefined && ( +// +// ::{location.line} +// +// )} +//
+// ))} +//
+// )} - {/* Options */} - {!hasResponded && ( -
-
Choose an action:
-
- {options && options.length > 0 ? ( - options.map((option, index) => { - const isSelected = selected === option.optionId; - const isAllow = option.kind.includes('allow'); - const isAlways = option.kind.includes('always'); +// {/* Options */} +// {!hasResponded && ( +//
+//
Choose an action:
+//
+// {options && options.length > 0 ? ( +// options.map((option, index) => { +// const isSelected = selected === option.optionId; +// const isAllow = option.kind.includes('allow'); +// const isAlways = option.kind.includes('always'); - return ( - - ); - }) - ) : ( -
- No options available -
- )} -
-
- -
-
- )} +// return ( +// +// ); +// }) +// ) : ( +//
+// No options available +//
+// )} +//
+//
+// +//
+//
+// )} - {/* Success message */} - {hasResponded && ( -
- - - Response sent successfully - -
- )} -
-
- ); -}; +// {/* Success message */} +// {hasResponded && ( +//
+// +// +// Response sent successfully +// +//
+// )} +//
+//
+// ); +// }; diff --git a/packages/vscode-ide-companion/src/webview/components/messages/AssistantMessage.css b/packages/vscode-ide-companion/src/webview/components/messages/Assistant/AssistantMessage.css similarity index 85% rename from packages/vscode-ide-companion/src/webview/components/messages/AssistantMessage.css rename to packages/vscode-ide-companion/src/webview/components/messages/Assistant/AssistantMessage.css index db9901c7..56946662 100644 --- a/packages/vscode-ide-companion/src/webview/components/messages/AssistantMessage.css +++ b/packages/vscode-ide-companion/src/webview/components/messages/Assistant/AssistantMessage.css @@ -41,20 +41,12 @@ color: #e1c08d; } -/* Loading state - animated bullet (maps to .he) */ +/* Loading state - static bullet (maps to .he) */ .assistant-message-container.assistant-message-loading::before { color: var(--app-secondary-foreground); background-color: var(--app-secondary-background); - animation: assistantMessagePulse 1s linear infinite; } -/* Pulse animation for loading state */ -@keyframes assistantMessagePulse { - 0%, - 100% { - opacity: 1; - } - 50% { - opacity: 0.5; - } +.assistant-message-container.assistant-message-loading::after { + display: none } diff --git a/packages/vscode-ide-companion/src/webview/components/messages/AssistantMessage.tsx b/packages/vscode-ide-companion/src/webview/components/messages/Assistant/AssistantMessage.tsx similarity index 90% rename from packages/vscode-ide-companion/src/webview/components/messages/AssistantMessage.tsx rename to packages/vscode-ide-companion/src/webview/components/messages/Assistant/AssistantMessage.tsx index 5772aecd..8d1c4c43 100644 --- a/packages/vscode-ide-companion/src/webview/components/messages/AssistantMessage.tsx +++ b/packages/vscode-ide-companion/src/webview/components/messages/Assistant/AssistantMessage.tsx @@ -5,7 +5,7 @@ */ import type React from 'react'; -import { MessageContent } from '../MessageContent.js'; +import { MessageContent } from '../../MessageContent.js'; import './AssistantMessage.css'; interface AssistantMessageProps { @@ -13,6 +13,8 @@ interface AssistantMessageProps { timestamp: number; onFileClick?: (path: string) => void; status?: 'default' | 'success' | 'error' | 'warning' | 'loading'; + // When true, render without the left status bullet (no ::before dot) + hideStatusIcon?: boolean; } /** @@ -31,6 +33,7 @@ export const AssistantMessage: React.FC = ({ timestamp: _timestamp, onFileClick, status = 'default', + hideStatusIcon = false, }) => { // Empty content not rendered directly, avoid poor visual experience from only showing ::before dot if (!content || content.trim().length === 0) { @@ -39,6 +42,9 @@ export const AssistantMessage: React.FC = ({ // Map status to CSS class (only for ::before pseudo-element) const getStatusClass = () => { + if (hideStatusIcon) { + return ''; + } switch (status) { case 'success': return 'assistant-message-success'; diff --git a/packages/vscode-ide-companion/src/webview/components/messages/StreamingMessage.tsx b/packages/vscode-ide-companion/src/webview/components/messages/StreamingMessage.tsx index ac61ee0a..05a6b447 100644 --- a/packages/vscode-ide-companion/src/webview/components/messages/StreamingMessage.tsx +++ b/packages/vscode-ide-companion/src/webview/components/messages/StreamingMessage.tsx @@ -16,7 +16,7 @@ export const StreamingMessage: React.FC = ({ content, onFileClick, }) => ( -
+
= ({
● diff --git a/packages/vscode-ide-companion/src/webview/components/messages/UserMessage.tsx b/packages/vscode-ide-companion/src/webview/components/messages/UserMessage.tsx index ad665048..f69c43fe 100644 --- a/packages/vscode-ide-companion/src/webview/components/messages/UserMessage.tsx +++ b/packages/vscode-ide-companion/src/webview/components/messages/UserMessage.tsx @@ -45,7 +45,7 @@ export const UserMessage: React.FC = ({ return (
= ({ >
+ {/* File context indicator */} {fileContextDisplay && ( -
+
fileContext && onFileClick?.(fileContext.filePath)} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { fileContext && onFileClick?.(fileContext.filePath); } }} - style={{ cursor: 'pointer' }} >
= ({ style={{ fontSize: '12px', color: 'var(--app-secondary-foreground)', - opacity: 0.8, }} > {fileContextDisplay} diff --git a/packages/vscode-ide-companion/src/webview/components/messages/Waiting/InterruptedMessage.tsx b/packages/vscode-ide-companion/src/webview/components/messages/Waiting/InterruptedMessage.tsx new file mode 100644 index 00000000..6b574d58 --- /dev/null +++ b/packages/vscode-ide-companion/src/webview/components/messages/Waiting/InterruptedMessage.tsx @@ -0,0 +1,32 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import type React from 'react'; + +interface InterruptedMessageProps { + text?: string; +} + +// A lightweight status line similar to WaitingMessage but without the left status icon. +export const InterruptedMessage: React.FC = ({ + text = 'Interrupted', +}) => ( +
+
+ {text} +
+
+); diff --git a/packages/vscode-ide-companion/src/webview/components/messages/WaitingMessage.css b/packages/vscode-ide-companion/src/webview/components/messages/Waiting/WaitingMessage.css similarity index 89% rename from packages/vscode-ide-companion/src/webview/components/messages/WaitingMessage.css rename to packages/vscode-ide-companion/src/webview/components/messages/Waiting/WaitingMessage.css index 2a595838..9a109a08 100644 --- a/packages/vscode-ide-companion/src/webview/components/messages/WaitingMessage.css +++ b/packages/vscode-ide-companion/src/webview/components/messages/Waiting/WaitingMessage.css @@ -4,6 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +@import url('../Assistant/AssistantMessage.css'); + /* Subtle shimmering highlight across the loading text */ @keyframes waitingMessageShimmer { 0% { @@ -31,3 +33,6 @@ animation: waitingMessageShimmer 1.6s linear infinite; } +.interrupted-item::after { + display: none; +} diff --git a/packages/vscode-ide-companion/src/webview/components/messages/WaitingMessage.tsx b/packages/vscode-ide-companion/src/webview/components/messages/Waiting/WaitingMessage.tsx similarity index 93% rename from packages/vscode-ide-companion/src/webview/components/messages/WaitingMessage.tsx rename to packages/vscode-ide-companion/src/webview/components/messages/Waiting/WaitingMessage.tsx index d5362419..fceeaaa8 100644 --- a/packages/vscode-ide-companion/src/webview/components/messages/WaitingMessage.tsx +++ b/packages/vscode-ide-companion/src/webview/components/messages/Waiting/WaitingMessage.tsx @@ -6,9 +6,8 @@ import type React from 'react'; import { useEffect, useMemo, useState } from 'react'; -import './AssistantMessage.css'; import './WaitingMessage.css'; -import { WITTY_LOADING_PHRASES } from '../../../constants/loadingMessages.js'; +import { WITTY_LOADING_PHRASES } from '../../../../constants/loadingMessages.js'; interface WaitingMessageProps { loadingMessage: string; @@ -66,7 +65,7 @@ export const WaitingMessage: React.FC = ({ }, [phrases]); return ( -
+
{/* Use the same left status icon (pseudo-element) style as assistant-message-container */}
= ({ toolCall }) => { } }; + // Map tool status to container status for proper bullet coloring + const containerStatus: + | 'success' + | 'error' + | 'warning' + | 'loading' + | 'default' = + errors.length > 0 + ? 'error' + : toolCall.status === 'in_progress' || toolCall.status === 'pending' + ? 'loading' + : 'success'; + // Error case if (errors.length > 0) { return ( - + {/* Branch connector summary (Claude-like) */}
@@ -99,7 +116,11 @@ export const ExecuteToolCall: React.FC = ({ toolCall }) => { output.length > 500 ? output.substring(0, 500) + '...' : output; return ( - + {/* Branch connector summary (Claude-like) */}
@@ -141,7 +162,11 @@ export const ExecuteToolCall: React.FC = ({ toolCall }) => { // Success without output: show command with branch connector return ( - +
= ({ toolCall }) => { const getFileName = (path: string): string => path.split('/').pop() || path; // Automatically trigger openDiff when diff content is detected (Claude Code style) + // Only trigger once per tool call by checking toolCallId useEffect(() => { // Only auto-open if there are diffs and we have the required data if (diffs.length > 0) { @@ -76,16 +80,16 @@ export const EditToolCall: React.FC = ({ toolCall }) => { firstDiff.oldText !== undefined && firstDiff.newText !== undefined ) { - // TODO: 暂时注释 + // TODO: 暂时注释自动打开功能,避免频繁触发 // Add a small delay to ensure the component is fully rendered - // const timer = setTimeout(() => { - // handleOpenDiff(path, firstDiff.oldText, firstDiff.newText); - // }, 100); - let timer; + const timer = setTimeout(() => { + handleOpenDiff(path, firstDiff.oldText, firstDiff.newText); + }, 100); + // Proper cleanup function return () => timer && clearTimeout(timer); } } - }, [diffs, locations, handleOpenDiff]); + }, [diffs, handleOpenDiff, locations]); // Add missing dependencies // Error case: show error if (errors.length > 0) { @@ -121,15 +125,15 @@ export const EditToolCall: React.FC = ({ toolCall }) => { const openFirstDiff = () => handleOpenDiff(path, firstDiff.oldText, firstDiff.newText); + const containerStatus = mapToolStatusToContainerStatus(toolCall.status); return (
- {/* Keep content within overall width: pl-[30px] provides the bullet indent; */} {/* IMPORTANT: Always include min-w-0/max-w-full on inner wrappers to prevent overflow. */} -
+
{/* Align the inline Edit label styling with shared toolcall label: larger + bold */} @@ -143,38 +147,18 @@ export const EditToolCall: React.FC = ({ toolCall }) => { className="text-xs font-mono text-[var(--app-secondary-foreground)] hover:underline" /> )} - {/* {toolCallId && ( - - [{toolCallId.slice(-8)}] - - )} */}
- open
{summary}
-
- {/* Content area aligned with bullet indent. Do NOT exceed container width. */} - {/* For any custom blocks here, keep: min-w-0 max-w-full and avoid extra horizontal padding/margins. */} -
- {diffs.map( - ( - item: import('../shared/types.js').ToolCallContent, - idx: number, - ) => ( - - handleOpenDiff(item.path, item.oldText, item.newText) - } - /> - ), + {/* Show toolCallId only in development/debug mode */} + {toolCallId && isDevelopmentMode() && ( + + [{toolCallId.slice(-8)}] + )}
@@ -184,10 +168,11 @@ export const EditToolCall: React.FC = ({ toolCall }) => { // Success case without diff: show file in compact format if (locations && locations.length > 0) { const fileName = getFileName(locations[0].path); + const containerStatus = mapToolStatusToContainerStatus(toolCall.status); return (
diff --git a/packages/vscode-ide-companion/src/webview/components/toolcalls/Execute/Execute.tsx b/packages/vscode-ide-companion/src/webview/components/toolcalls/Execute/Execute.tsx index 1f9de179..1bb0637b 100644 --- a/packages/vscode-ide-companion/src/webview/components/toolcalls/Execute/Execute.tsx +++ b/packages/vscode-ide-companion/src/webview/components/toolcalls/Execute/Execute.tsx @@ -32,12 +32,25 @@ export const ExecuteToolCall: React.FC = ({ toolCall }) => { inputCommand = rawInput; } + // Map tool status to container status for proper bullet coloring + const containerStatus: + | 'success' + | 'error' + | 'warning' + | 'loading' + | 'default' = + errors.length > 0 || toolCall.status === 'failed' + ? 'error' + : toolCall.status === 'in_progress' || toolCall.status === 'pending' + ? 'loading' + : 'success'; + // Error case if (errors.length > 0) { return ( @@ -81,7 +94,7 @@ export const ExecuteToolCall: React.FC = ({ toolCall }) => { return ( {/* Branch connector summary (Claude-like) */} @@ -117,7 +130,11 @@ export const ExecuteToolCall: React.FC = ({ toolCall }) => { // Success without output: show command with branch connector return ( - +
{commandText} diff --git a/packages/vscode-ide-companion/src/webview/components/toolcalls/ExecuteNode/ExecuteNodeToolCall.tsx b/packages/vscode-ide-companion/src/webview/components/toolcalls/ExecuteNode/ExecuteNodeToolCall.tsx index 82569e1e..36826ade 100644 --- a/packages/vscode-ide-companion/src/webview/components/toolcalls/ExecuteNode/ExecuteNodeToolCall.tsx +++ b/packages/vscode-ide-companion/src/webview/components/toolcalls/ExecuteNode/ExecuteNodeToolCall.tsx @@ -9,7 +9,11 @@ import type React from 'react'; import type { BaseToolCallProps } from '../shared/types.js'; import { ToolCallContainer } from '../shared/LayoutComponents.js'; -import { safeTitle, groupContent } from '../shared/utils.js'; +import { + safeTitle, + groupContent, + mapToolStatusToContainerStatus, +} from '../shared/utils.js'; import './ExecuteNode.css'; /** @@ -60,7 +64,11 @@ export const ExecuteNodeToolCall: React.FC = ({ // Success case: show command with branch connector (similar to the example) return ( - +
{commandText} 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 17c29a8c..b74c4249 100644 --- a/packages/vscode-ide-companion/src/webview/components/toolcalls/GenericToolCall.tsx +++ b/packages/vscode-ide-companion/src/webview/components/toolcalls/GenericToolCall.tsx @@ -104,8 +104,16 @@ export const GenericToolCall: React.FC = ({ toolCall }) => { } // Short output - compact format + const statusFlag: 'success' | 'error' | 'warning' | 'loading' | 'default' = + toolCall.status === 'in_progress' || toolCall.status === 'pending' + ? 'loading' + : 'success'; return ( - + {operationText || output} ); @@ -113,8 +121,16 @@ export const GenericToolCall: React.FC = ({ toolCall }) => { // Success with files: show operation + file list in compact format if (locations && locations.length > 0) { + const statusFlag: 'success' | 'error' | 'warning' | 'loading' | 'default' = + toolCall.status === 'in_progress' || toolCall.status === 'pending' + ? 'loading' + : 'success'; return ( - + ); @@ -122,8 +138,16 @@ export const GenericToolCall: React.FC = ({ toolCall }) => { // No output - show just the operation if (operationText) { + const statusFlag: 'success' | 'error' | 'warning' | 'loading' | 'default' = + toolCall.status === 'in_progress' || toolCall.status === 'pending' + ? 'loading' + : 'success'; return ( - + {operationText} ); diff --git a/packages/vscode-ide-companion/src/webview/components/toolcalls/Read/ReadToolCall.tsx b/packages/vscode-ide-companion/src/webview/components/toolcalls/Read/ReadToolCall.tsx index 4ecd63b2..b848f380 100644 --- a/packages/vscode-ide-companion/src/webview/components/toolcalls/Read/ReadToolCall.tsx +++ b/packages/vscode-ide-companion/src/webview/components/toolcalls/Read/ReadToolCall.tsx @@ -9,7 +9,10 @@ import type React from 'react'; import type { BaseToolCallProps } from '../shared/types.js'; import { ToolCallContainer } from '../shared/LayoutComponents.js'; -import { groupContent } from '../shared/utils.js'; +import { + groupContent, + mapToolStatusToContainerStatus, +} from '../shared/utils.js'; import { FileLink } from '../../ui/FileLink.js'; /** @@ -23,6 +26,14 @@ export const ReadToolCall: React.FC = ({ toolCall }) => { // Group content by type const { errors } = groupContent(content); + // Compute container status based on toolCall.status (pending/in_progress -> loading) + const containerStatus: + | 'success' + | 'error' + | 'warning' + | 'loading' + | 'default' = mapToolStatusToContainerStatus(toolCall.status); + // Error case: show error if (errors.length > 0) { const path = locations?.[0]?.path || ''; @@ -54,7 +65,7 @@ export const ReadToolCall: React.FC = ({ toolCall }) => { = ({ toolCall }) => { // Success case with results: show search query + file list if (locations && locations.length > 0) { + const containerStatus = mapToolStatusToContainerStatus(toolCall.status); // If multiple results, use card layout; otherwise use compact format if (locations.length > 1) { return ( @@ -59,8 +64,13 @@ export const SearchToolCall: React.FC = ({ toolCall }) => { } // Single result - compact format return ( - - {queryText} + + {/* {queryText} */} @@ -69,8 +79,13 @@ export const SearchToolCall: React.FC = ({ toolCall }) => { // No results - show query only if (queryText) { + const containerStatus = mapToolStatusToContainerStatus(toolCall.status); return ( - + {queryText} ); diff --git a/packages/vscode-ide-companion/src/webview/components/toolcalls/ThinkToolCall.tsx b/packages/vscode-ide-companion/src/webview/components/toolcalls/Think/ThinkToolCall.tsx similarity index 82% rename from packages/vscode-ide-companion/src/webview/components/toolcalls/ThinkToolCall.tsx rename to packages/vscode-ide-companion/src/webview/components/toolcalls/Think/ThinkToolCall.tsx index 251367da..4c49b2cc 100644 --- a/packages/vscode-ide-companion/src/webview/components/toolcalls/ThinkToolCall.tsx +++ b/packages/vscode-ide-companion/src/webview/components/toolcalls/Think/ThinkToolCall.tsx @@ -7,13 +7,13 @@ */ import type React from 'react'; -import type { BaseToolCallProps } from './shared/types.js'; +import type { BaseToolCallProps } from '../shared/types.js'; import { ToolCallContainer, ToolCallCard, ToolCallRow, -} from './shared/LayoutComponents.js'; -import { groupContent } from './shared/utils.js'; +} from '../shared/LayoutComponents.js'; +import { groupContent } from '../shared/utils.js'; /** * Specialized component for Think tool calls @@ -56,8 +56,12 @@ export const ThinkToolCall: React.FC = ({ toolCall }) => { } // Short thoughts - compact format + const status = + toolCall.status === 'pending' || toolCall.status === 'in_progress' + ? 'loading' + : 'default'; return ( - + {thoughts} ); diff --git a/packages/vscode-ide-companion/src/webview/components/toolcalls/TodoWriteToolCall.tsx b/packages/vscode-ide-companion/src/webview/components/toolcalls/Think/TodoWriteToolCall.tsx similarity index 92% rename from packages/vscode-ide-companion/src/webview/components/toolcalls/TodoWriteToolCall.tsx rename to packages/vscode-ide-companion/src/webview/components/toolcalls/Think/TodoWriteToolCall.tsx index 26fa1e1d..b74a20fc 100644 --- a/packages/vscode-ide-companion/src/webview/components/toolcalls/TodoWriteToolCall.tsx +++ b/packages/vscode-ide-companion/src/webview/components/toolcalls/Think/TodoWriteToolCall.tsx @@ -7,10 +7,10 @@ */ import type React from 'react'; -import type { BaseToolCallProps } from './shared/types.js'; -import { ToolCallContainer } from './shared/LayoutComponents.js'; -import { groupContent, safeTitle } from './shared/utils.js'; -import { CheckboxDisplay } from '../ui/CheckboxDisplay.js'; +import type { BaseToolCallProps } from '../shared/types.js'; +import { ToolCallContainer } from '../shared/LayoutComponents.js'; +import { groupContent, safeTitle } from '../shared/utils.js'; +import { CheckboxDisplay } from '../../ui/CheckboxDisplay.js'; type EntryStatus = 'pending' | 'in_progress' | 'completed'; @@ -20,7 +20,7 @@ interface TodoEntry { } const mapToolStatusToBullet = ( - status: import('./shared/types.js').ToolCallStatus, + status: import('../shared/types.js').ToolCallStatus, ): 'success' | 'error' | 'warning' | 'loading' | 'default' => { switch (status) { case 'completed': diff --git a/packages/vscode-ide-companion/src/webview/components/toolcalls/WriteToolCall.tsx b/packages/vscode-ide-companion/src/webview/components/toolcalls/Write/WriteToolCall.tsx similarity index 86% rename from packages/vscode-ide-companion/src/webview/components/toolcalls/WriteToolCall.tsx rename to packages/vscode-ide-companion/src/webview/components/toolcalls/Write/WriteToolCall.tsx index c5d4a534..58a5841e 100644 --- a/packages/vscode-ide-companion/src/webview/components/toolcalls/WriteToolCall.tsx +++ b/packages/vscode-ide-companion/src/webview/components/toolcalls/Write/WriteToolCall.tsx @@ -7,10 +7,13 @@ */ import type React from 'react'; -import type { BaseToolCallProps } from './shared/types.js'; -import { ToolCallContainer } from './shared/LayoutComponents.js'; -import { groupContent } from './shared/utils.js'; -import { FileLink } from '../ui/FileLink.js'; +import type { BaseToolCallProps } from '../shared/types.js'; +import { ToolCallContainer } from '../shared/LayoutComponents.js'; +import { + groupContent, + mapToolStatusToContainerStatus, +} from '../shared/utils.js'; +import { FileLink } from '../../ui/FileLink.js'; /** * Specialized component for Write tool calls @@ -79,10 +82,11 @@ export const WriteToolCall: React.FC = ({ toolCall }) => { if (locations && locations.length > 0) { const path = locations[0].path; const lineCount = writeContent.split('\n').length; + const containerStatus = mapToolStatusToContainerStatus(toolCall.status); return ( = ({ toolCall }) => { // Fallback: show generic success if (textOutputs.length > 0) { + const containerStatus = mapToolStatusToContainerStatus(toolCall.status); return ( - + {textOutputs.join('\n')} ); diff --git a/packages/vscode-ide-companion/src/webview/components/toolcalls/index.tsx b/packages/vscode-ide-companion/src/webview/components/toolcalls/index.tsx index 3ba0be4f..37350a20 100644 --- a/packages/vscode-ide-companion/src/webview/components/toolcalls/index.tsx +++ b/packages/vscode-ide-companion/src/webview/components/toolcalls/index.tsx @@ -11,15 +11,14 @@ import type { BaseToolCallProps } from './shared/types.js'; import { shouldShowToolCall } from './shared/utils.js'; import { GenericToolCall } from './GenericToolCall.js'; import { ReadToolCall } from './Read/ReadToolCall.js'; -import { WriteToolCall } from './WriteToolCall.js'; +import { WriteToolCall } from './Write/WriteToolCall.js'; import { EditToolCall } from './Edit/EditToolCall.js'; import { ExecuteToolCall as BashExecuteToolCall } from './Bash/Bash.js'; import { ExecuteToolCall } from './Execute/Execute.js'; import { UpdatedPlanToolCall } from './UpdatedPlan/UpdatedPlanToolCall.js'; import { ExecuteNodeToolCall } from './ExecuteNode/ExecuteNodeToolCall.js'; -import { SearchToolCall } from './SearchToolCall.js'; -import { ThinkToolCall } from './ThinkToolCall.js'; -import { TodoWriteToolCall } from './TodoWriteToolCall.js'; +import { SearchToolCall } from './Search/SearchToolCall.js'; +import { ThinkToolCall } from './Think/ThinkToolCall.js'; /** * Factory function that returns the appropriate tool call component based on kind @@ -69,7 +68,10 @@ export const getToolCallComponent = ( case 'updated_plan': case 'updatedplan': case 'todo_write': + case 'update_todos': + case 'todowrite': return UpdatedPlanToolCall; + // return TodoWriteToolCall; case 'search': case 'grep': @@ -81,18 +83,6 @@ export const getToolCallComponent = ( case 'thinking': return ThinkToolCall; - case 'todowrite': - return TodoWriteToolCall; - // case 'todo_write': - case 'update_todos': - return TodoWriteToolCall; - - // Add more specialized components as needed - // case 'fetch': - // return FetchToolCall; - // case 'delete': - // return DeleteToolCall; - default: // Fallback to generic component return GenericToolCall; 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 2d54c5b2..b9ee02ed 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 @@ -9,6 +9,7 @@ import type React from 'react'; import { FileLink } from '../../ui/FileLink.js'; +import { isDevelopmentMode } from '../../../utils/envUtils.js'; import './LayoutComponents.css'; /** @@ -53,12 +54,6 @@ export const ToolCallContainer: React.FC = ({ {label} - {/* TODO: for 调试 */} - {_toolCallId && ( - - [{_toolCallId.slice(-8)}] - - )} {labelSuffix}
{children && ( @@ -66,6 +61,13 @@ export const ToolCallContainer: React.FC = ({ {children}
)} + + {/* Show toolCallId only in development/debug mode */} + {_toolCallId && isDevelopmentMode() && ( + + [{_toolCallId.slice(-8)}] + + )}
); diff --git a/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/utils.ts b/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/utils.ts index 8f20355b..92ebf5df 100644 --- a/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/utils.ts +++ b/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/utils.ts @@ -6,7 +6,11 @@ * Shared utility functions for tool call components */ -import type { ToolCallContent, GroupedContent } from './types.js'; +import type { + ToolCallContent, + GroupedContent, + ToolCallStatus, +} from './types.js'; /** * Format any value to a string for display @@ -194,3 +198,26 @@ export const groupContent = (content?: ToolCallContent[]): GroupedContent => { return { textOutputs, errors, diffs, otherData }; }; + +/** + * Map a tool call status to a ToolCallContainer status (bullet color) + * - pending/in_progress -> loading + * - completed -> success + * - failed -> error + * - default fallback + */ +export const mapToolStatusToContainerStatus = ( + status: ToolCallStatus, +): 'success' | 'error' | 'warning' | 'loading' | 'default' => { + switch (status) { + case 'pending': + case 'in_progress': + return 'loading'; + case 'failed': + return 'error'; + case 'completed': + return 'success'; + default: + return 'default'; + } +}; diff --git a/packages/vscode-ide-companion/src/webview/components/ui/CheckboxDisplay.tsx b/packages/vscode-ide-companion/src/webview/components/ui/CheckboxDisplay.tsx index a66eda84..a69b7c27 100644 --- a/packages/vscode-ide-companion/src/webview/components/ui/CheckboxDisplay.tsx +++ b/packages/vscode-ide-companion/src/webview/components/ui/CheckboxDisplay.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import type React from 'react'; export interface CheckboxDisplayProps { checked?: boolean; @@ -29,44 +29,54 @@ export const CheckboxDisplay: React.FC = ({ style, title, }) => { - const ref = React.useRef(null); - - React.useEffect(() => { - const el = ref.current; - if (!el) { - return; - } - el.indeterminate = !!indeterminate; - if (indeterminate) { - el.setAttribute('data-indeterminate', 'true'); - } else { - el.removeAttribute('data-indeterminate'); - } - }, [indeterminate, checked]); + // Render as a span (not ) so we can draw a checkmark with CSS. + // Pseudo-elements do not reliably render on in Chromium (VS Code webviews), + // which caused the missing icon. This version is font-free and uses borders. + const showCheck = !!checked && !indeterminate; + const showDash = !!indeterminate; return ( - + > + {showCheck ? ( + + ) : null} + {showDash ? ( + + ) : null} + ); }; diff --git a/packages/vscode-ide-companion/src/webview/components/ui/FileLink.tsx b/packages/vscode-ide-companion/src/webview/components/ui/FileLink.tsx index b020d834..356ffaf4 100644 --- a/packages/vscode-ide-companion/src/webview/components/ui/FileLink.tsx +++ b/packages/vscode-ide-companion/src/webview/components/ui/FileLink.tsx @@ -112,7 +112,6 @@ export const FileLink: React.FC = ({ void; appendThinkingChunk: (chunk: string) => void; clearThinking: () => void; + setWaitingForResponse: (message: string) => void; clearWaitingForResponse: () => void; }; @@ -421,6 +422,7 @@ export const useWebViewMessages = ({ toolCallData.type = toolCallData.sessionUpdate; } handlers.handleToolCallUpdate(toolCallData); + // Split assistant stream at tool boundaries similar to Claude/GPT rhythm const status = (toolCallData.status || '').toString(); const isStart = toolCallData.type === 'tool_call'; @@ -430,6 +432,31 @@ export const useWebViewMessages = ({ if (isStart || isFinalUpdate) { handlers.messageHandling.breakAssistantSegment(); } + + // While long-running tools (e.g., execute/bash/command) are in progress, + // surface a lightweight loading indicator and expose the Stop button. + try { + const kind = (toolCallData.kind || '').toString().toLowerCase(); + const isExec = + kind === 'execute' || kind === 'bash' || kind === 'command'; + if (isExec && (status === 'pending' || status === 'in_progress')) { + const rawInput = toolCallData.rawInput; + let cmd = ''; + if (typeof rawInput === 'string') { + cmd = rawInput; + } else if (rawInput && typeof rawInput === 'object') { + const maybe = rawInput as { command?: string }; + cmd = maybe.command || ''; + } + const hint = cmd ? `Running: ${cmd}` : 'Running command...'; + handlers.messageHandling.setWaitingForResponse(hint); + } + if (status === 'completed' || status === 'failed') { + handlers.messageHandling.clearWaitingForResponse(); + } + } catch (_err) { + // Best-effort UI hint; ignore errors + } break; } diff --git a/packages/vscode-ide-companion/src/webview/styles/ClaudeCodeStyles.css b/packages/vscode-ide-companion/src/webview/styles/ClaudeCodeStyles.css index 424b2a64..655816ae 100644 --- a/packages/vscode-ide-companion/src/webview/styles/ClaudeCodeStyles.css +++ b/packages/vscode-ide-companion/src/webview/styles/ClaudeCodeStyles.css @@ -9,10 +9,10 @@ /* Import component styles */ @import '../components/toolcalls/shared/DiffDisplay.css'; -@import '../components/messages/AssistantMessage.css'; +@import '../components/messages/Assistant/AssistantMessage.css'; @import '../components/toolcalls/shared/SimpleTimeline.css'; @import '../components/messages/QwenMessageTimeline.css'; -@import '../components/MarkdownRenderer.css'; +@import '../components/MarkdownRenderer/MarkdownRenderer.css'; /* =========================== diff --git a/packages/vscode-ide-companion/src/webview/styles/tailwind.css b/packages/vscode-ide-companion/src/webview/styles/tailwind.css index bb501050..9b137626 100644 --- a/packages/vscode-ide-companion/src/webview/styles/tailwind.css +++ b/packages/vscode-ide-companion/src/webview/styles/tailwind.css @@ -155,4 +155,21 @@ background-color: var(--app-qwen-clay-button-orange); color: var(--app-qwen-ivory); } + + /* + * File path styling inside tool call content + * Applies to: .toolcall-content-wrapper .file-link-path + * - Use monospace editor font + * - Slightly smaller size + * - Link color + * - Tighten top alignment and allow aggressive breaking for long paths + */ + .toolcall-content-wrapper .file-link-path { + /* Tailwind utilities where possible */ + @apply text-[0.85em] pt-px break-all min-w-0; + /* Not covered by Tailwind defaults: use CSS vars / properties */ + font-family: var(--app-monospace-font-family); + color: var(--app-link-color); + overflow-wrap: anywhere; + } } diff --git a/packages/vscode-ide-companion/src/webview/utils/envUtils.ts b/packages/vscode-ide-companion/src/webview/utils/envUtils.ts new file mode 100644 index 00000000..ae9c195c --- /dev/null +++ b/packages/vscode-ide-companion/src/webview/utils/envUtils.ts @@ -0,0 +1,15 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +export function isDevelopmentMode(): boolean { + // TODO: 调试用 + return false; + // return ( + // process.env.NODE_ENV === 'development' || + // process.env.DEBUG === 'true' || + // process.env.NODE_ENV !== 'production' + // ); +} diff --git a/packages/vscode-ide-companion/tailwind.config.js b/packages/vscode-ide-companion/tailwind.config.js index 573a8d67..f170d539 100644 --- a/packages/vscode-ide-companion/tailwind.config.js +++ b/packages/vscode-ide-companion/tailwind.config.js @@ -19,6 +19,7 @@ export default { './src/webview/components/PermissionDrawer.tsx', './src/webview/components/PlanDisplay.tsx', './src/webview/components/session/SessionSelector.tsx', + './src/webview/components/messages/UserMessage.tsx', ], theme: { extend: {