diff --git a/packages/vscode-ide-companion/src/webview/App.tsx b/packages/vscode-ide-companion/src/webview/App.tsx index 04831058..e091f9b6 100644 --- a/packages/vscode-ide-companion/src/webview/App.tsx +++ b/packages/vscode-ide-companion/src/webview/App.tsx @@ -27,7 +27,7 @@ import type { ToolCallData } from './components/ToolCall.js'; import { PermissionDrawer } from './components/PermissionDrawer.js'; import { ToolCall } from './components/ToolCall.js'; import { hasToolCallOutput } from './components/toolcalls/shared/utils.js'; -import { InProgressToolCall } from './components/InProgressToolCall.js'; +// import { InProgressToolCall } from './components/InProgressToolCall.js'; import { EmptyState } from './components/ui/EmptyState.js'; import type { PlanEntry } from './components/PlanDisplay.js'; import { type CompletionItem } from './types/CompletionTypes.js'; @@ -172,6 +172,16 @@ export const App: React.FC = () => { isStreaming: messageHandling.isStreaming, }); + // Handle cancel streaming + const handleCancel = useCallback(() => { + if (messageHandling.isStreaming) { + vscode.postMessage({ + type: 'cancelStreaming', + data: {}, + }); + } + }, [messageHandling.isStreaming, vscode]); + // Message handling useWebViewMessages({ sessionManagement, @@ -531,49 +541,47 @@ export const App: React.FC = () => { if (msg.role === 'thinking') { return ( -
- -
+ ); } if (msg.role === 'user') { return ( -
- -
+ ); } return ( -
- -
+ ); } - case 'in-progress-tool-call': - return ( - - ); + // case 'in-progress-tool-call': + // return ( + // + // ); + case 'in-progress-tool-call': case 'completed-tool-call': return ( { onCompositionEnd={() => setIsComposing(false)} onKeyDown={() => {}} onSubmit={handleSubmit.handleSubmit} + onCancel={handleCancel} onToggleEditMode={handleToggleEditMode} onToggleThinking={handleToggleThinking} onFocusActiveEditor={fileContext.focusActiveEditor} diff --git a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts index 20b328ed..f534fdb7 100644 --- a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts +++ b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts @@ -204,6 +204,17 @@ export class WebViewProvider { // Handle messages from WebView newPanel.webview.onDidReceiveMessage( async (message: { type: string; data?: unknown }) => { + // Allow webview to request updating the VS Code tab title + if (message.type === 'updatePanelTitle') { + const title = String( + (message.data as { title?: unknown } | undefined)?.title ?? '', + ).trim(); + const panelRef = this.panelManager.getPanel(); + if (panelRef) { + panelRef.title = title || 'Qwen Code'; + } + return; + } await this.messageHandler.route(message); }, null, @@ -790,9 +801,19 @@ export class WebViewProvider { panel.webview.html = WebViewContent.generate(panel, this.extensionUri); - // Handle messages from WebView + // Handle messages from WebView (restored panel) panel.webview.onDidReceiveMessage( async (message: { type: string; data?: unknown }) => { + if (message.type === 'updatePanelTitle') { + const title = String( + (message.data as { title?: unknown } | undefined)?.title ?? '', + ).trim(); + const panelRef = this.panelManager.getPanel(); + if (panelRef) { + panelRef.title = title || 'Qwen Code'; + } + return; + } await this.messageHandler.route(message); }, null, diff --git a/packages/vscode-ide-companion/src/webview/components/InProgressToolCall.tsx b/packages/vscode-ide-companion/src/webview/components/InProgressToolCall.tsx index da25fc02..67e13777 100644 --- a/packages/vscode-ide-companion/src/webview/components/InProgressToolCall.tsx +++ b/packages/vscode-ide-companion/src/webview/components/InProgressToolCall.tsx @@ -154,41 +154,50 @@ export const InProgressToolCall: React.FC = ({ }; return ( -
-
- - {kindLabel} - - {filePath && ( - - )} - {!filePath && fileName && ( - - {fileName} - - )} - - {diffData && ( - + {kindLabel} + + {filePath && ( + + )} + {!filePath && fileName && ( + + {fileName} + + )} + + {diffData && ( + + )} +
+ + {contentText && ( +
+
+ + + {contentText} + +
+
)}
- - {contentText && ( -
- {contentText} -
- )} ); }; diff --git a/packages/vscode-ide-companion/src/webview/components/InputForm.tsx b/packages/vscode-ide-companion/src/webview/components/InputForm.tsx index 61c5f7b3..33e1c5a7 100644 --- a/packages/vscode-ide-companion/src/webview/components/InputForm.tsx +++ b/packages/vscode-ide-companion/src/webview/components/InputForm.tsx @@ -14,6 +14,7 @@ import { SlashCommandIcon, LinkIcon, ArrowUpIcon, + StopIcon, } from './icons/index.js'; import { CompletionMenu } from './ui/CompletionMenu.js'; import type { CompletionItem } from '../types/CompletionTypes.js'; @@ -36,6 +37,7 @@ interface InputFormProps { onCompositionEnd: () => void; onKeyDown: (e: React.KeyboardEvent) => void; onSubmit: (e: React.FormEvent) => void; + onCancel: () => void; onToggleEditMode: () => void; onToggleThinking: () => void; onFocusActiveEditor: () => void; @@ -91,6 +93,7 @@ export const InputForm: React.FC = ({ onCompositionEnd, onKeyDown, onSubmit, + onCancel, onToggleEditMode, onToggleThinking, onFocusActiveEditor, @@ -198,17 +201,6 @@ export const InputForm: React.FC = ({ )} - {/* Divider */} -
- - {/* Spacer */} -
- {/* Thinking button */} - {/* Send button */} - + {/* Send/Stop button */} + {isStreaming ? ( + + ) : ( + + )}
diff --git a/packages/vscode-ide-companion/src/webview/components/PermissionDrawer.tsx b/packages/vscode-ide-companion/src/webview/components/PermissionDrawer.tsx index 323b7110..4918f7be 100644 --- a/packages/vscode-ide-companion/src/webview/components/PermissionDrawer.tsx +++ b/packages/vscode-ide-companion/src/webview/components/PermissionDrawer.tsx @@ -178,7 +178,7 @@ export const PermissionDrawer: React.FC = ({ @@ -211,7 +211,7 @@ export const PermissionDrawer: React.FC = ({ diff --git a/packages/vscode-ide-companion/src/webview/components/PlanDisplay.css b/packages/vscode-ide-companion/src/webview/components/PlanDisplay.css deleted file mode 100644 index d58cea69..00000000 --- a/packages/vscode-ide-companion/src/webview/components/PlanDisplay.css +++ /dev/null @@ -1,88 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - */ - -/* 容器 */ -.plan-display { - @apply bg-transparent border-0 py-2 px-4 my-2; -} - -/* 标题区 */ -.plan-header { - @apply flex items-center gap-1.5 mb-2; -} - -.plan-progress-icons { - @apply flex items-center gap-[2px]; -} - -.plan-progress-icon { - @apply shrink-0 text-[var(--app-secondary-foreground)] opacity-60; -} - -.plan-title { - @apply text-xs font-normal text-[var(--app-secondary-foreground)] opacity-80; -} - -/* 列表 */ -.plan-entries { - @apply flex flex-col gap-px; -} - -.plan-entry { - @apply flex items-center gap-2 py-[3px] min-h-[20px]; -} - -/* 图标容器(保留类名以兼容旧 DOM) */ -.plan-entry-icon { - @apply shrink-0 flex items-center justify-center w-[14px] h-[14px]; -} - -.plan-icon { - @apply block w-[14px] h-[14px]; -} - -/* 不同状态颜色(保留类名) */ -.plan-icon.pending { - @apply text-[var(--app-secondary-foreground)] opacity-30; -} - -.plan-icon.in-progress { - @apply text-[var(--app-secondary-foreground)] opacity-70; -} - -.plan-icon.completed { - @apply text-[#4caf50] opacity-80; /* 绿色勾号 */ -} - -/* 内容 */ -.plan-entry-content { - @apply flex-1 flex items-center; -} - -.plan-entry-text { - @apply flex-1 text-xs leading-[1.5] text-[var(--app-primary-foreground)] opacity-80; -} - -/* 状态化文本(保留选择器,兼容旧结构) */ -.plan-entry.completed .plan-entry-text { - @apply opacity-50 line-through; -} - -.plan-entry.in_progress .plan-entry-text { - @apply font-normal opacity-90; -} - -/* 保留 fadeIn 动画,供 App.tsx 使用 */ -@keyframes fadeIn { - from { - opacity: 0; - transform: translateY(-4px); - } - to { - opacity: 1; - transform: translateY(0); - } -} diff --git a/packages/vscode-ide-companion/src/webview/components/PlanDisplay.tsx b/packages/vscode-ide-companion/src/webview/components/PlanDisplay.tsx index cd3478c9..046e4d8a 100644 --- a/packages/vscode-ide-companion/src/webview/components/PlanDisplay.tsx +++ b/packages/vscode-ide-companion/src/webview/components/PlanDisplay.tsx @@ -5,7 +5,6 @@ */ import type React from 'react'; -import './PlanDisplay.css'; import { CheckboxDisplay } from './ui/CheckboxDisplay.js'; export interface PlanEntry { @@ -35,16 +34,17 @@ export const PlanDisplay: React.FC = ({ entries }) => { return (
{/* Title area, similar to example summary/_e/or */} -
+
diff --git a/packages/vscode-ide-companion/src/webview/components/icons/StopIcon.tsx b/packages/vscode-ide-companion/src/webview/components/icons/StopIcon.tsx new file mode 100644 index 00000000..40c23250 --- /dev/null +++ b/packages/vscode-ide-companion/src/webview/components/icons/StopIcon.tsx @@ -0,0 +1,33 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + * + * Stop icon for canceling operations + */ + +import type React from 'react'; +import type { IconProps } from './types.js'; + +/** + * Stop/square icon (16x16) + * Used for stop/cancel operations + */ +export const StopIcon: React.FC = ({ + size = 16, + className, + ...props +}) => ( + +); diff --git a/packages/vscode-ide-companion/src/webview/components/icons/index.ts b/packages/vscode-ide-companion/src/webview/components/icons/index.ts index 7f782ae3..74f1b4c7 100644 --- a/packages/vscode-ide-companion/src/webview/components/icons/index.ts +++ b/packages/vscode-ide-companion/src/webview/components/icons/index.ts @@ -51,3 +51,6 @@ export { PlayIcon, SwitchIcon } from './ActionIcons.js'; // Special icons export { ThinkingIcon, TerminalIcon } from './SpecialIcons.js'; + +// Stop icon +export { StopIcon } from './StopIcon.js'; diff --git a/packages/vscode-ide-companion/src/webview/components/messages/AssistantMessage.tsx b/packages/vscode-ide-companion/src/webview/components/messages/AssistantMessage.tsx index cb6fe5e1..b1f2582c 100644 --- a/packages/vscode-ide-companion/src/webview/components/messages/AssistantMessage.tsx +++ b/packages/vscode-ide-companion/src/webview/components/messages/AssistantMessage.tsx @@ -56,7 +56,7 @@ export const AssistantMessage: React.FC = ({ return (
= ({ timestamp: _timestamp, onFileClick, }) => ( -
+
= ({ const fileContextDisplay = getFileContextDisplay(); return ( -
+
= ({ toolCall }) => { const { title, content, rawInput, toolCallId } = toolCall; const commandText = safeTitle(title); + const vscode = useVSCode(); // Group content by type const { textOutputs, errors } = groupContent(content); @@ -32,6 +35,24 @@ export const ExecuteToolCall: React.FC = ({ toolCall }) => { inputCommand = rawInput; } + // Handle click on IN section + const handleInClick = () => { + createAndOpenTempFile( + vscode.postMessage, + inputCommand, + 'bash-input', + '.sh', + ); + }; + + // Handle click on OUT section + const handleOutClick = () => { + if (textOutputs.length > 0) { + const output = textOutputs.join('\n'); + createAndOpenTempFile(vscode.postMessage, output, 'bash-output', '.txt'); + } + }; + // Error case if (errors.length > 0) { return ( @@ -45,7 +66,11 @@ export const ExecuteToolCall: React.FC = ({ toolCall }) => {
{/* IN row */} -
+
IN
{inputCommand}
@@ -84,7 +109,11 @@ export const ExecuteToolCall: React.FC = ({ toolCall }) => {
{/* IN row */} -
+
IN
{inputCommand}
@@ -92,7 +121,11 @@ export const ExecuteToolCall: React.FC = ({ toolCall }) => {
{/* OUT row */} -
+
OUT
@@ -109,7 +142,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/EditToolCall.tsx b/packages/vscode-ide-companion/src/webview/components/toolcalls/Edit/EditToolCall.tsx similarity index 69% rename from packages/vscode-ide-companion/src/webview/components/toolcalls/EditToolCall.tsx rename to packages/vscode-ide-companion/src/webview/components/toolcalls/Edit/EditToolCall.tsx index 7e3906ba..897055b2 100644 --- a/packages/vscode-ide-companion/src/webview/components/toolcalls/EditToolCall.tsx +++ b/packages/vscode-ide-companion/src/webview/components/toolcalls/Edit/EditToolCall.tsx @@ -7,12 +7,12 @@ */ import { useEffect, useCallback } from 'react'; -import type { BaseToolCallProps } from './shared/types.js'; -import { ToolCallContainer } from './shared/LayoutComponents.js'; -import { DiffDisplay } from './shared/DiffDisplay.js'; -import { groupContent } from './shared/utils.js'; -import { useVSCode } from '../../hooks/useVSCode.js'; -import { FileLink } from '../ui/FileLink.js'; +import type { BaseToolCallProps } from '../shared/types.js'; +import { ToolCallContainer } from '../shared/LayoutComponents.js'; +import { DiffDisplay } from '../shared/DiffDisplay.js'; +import { groupContent } from '../shared/utils.js'; +import { useVSCode } from '../../../hooks/useVSCode.js'; +import { FileLink } from '../../ui/FileLink.js'; /** * Calculate diff summary (added/removed lines) @@ -76,11 +76,13 @@ export const EditToolCall: React.FC = ({ toolCall }) => { firstDiff.oldText !== undefined && firstDiff.newText !== undefined ) { + // TODO: 暂时注释 // Add a small delay to ensure the component is fully rendered - const timer = setTimeout(() => { - handleOpenDiff(path, firstDiff.oldText, firstDiff.newText); - }, 100); - return () => clearTimeout(timer); + // const timer = setTimeout(() => { + // handleOpenDiff(path, firstDiff.oldText, firstDiff.newText); + // }, 100); + let timer; + return () => timer && clearTimeout(timer); } } }, [diffs, locations, handleOpenDiff]); @@ -120,48 +122,47 @@ export const EditToolCall: React.FC = ({ toolCall }) => { handleOpenDiff(path, firstDiff.oldText, firstDiff.newText); 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 */} - - Edit - - {path && ( - - )} - {/* {toolCallId && ( +
+ {/* 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 */} + + Edit + + {path && ( + + )} + {/* {toolCallId && ( [{toolCallId.slice(-8)}] )} */} -
- open -
-
- - {summary}
+ 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, + item: import('../shared/types.js').ToolCallContent, idx: number, ) => ( = ({ toolCall }) => { + const { title, content, rawInput, toolCallId } = toolCall; + const commandText = safeTitle(title); + + // Group content by type + const { textOutputs, errors } = groupContent(content); + + // Extract command from rawInput if available + let inputCommand = commandText; + if (rawInput && typeof rawInput === 'object') { + const inputObj = rawInput as { command?: string }; + inputCommand = inputObj.command || commandText; + } else if (typeof rawInput === 'string') { + inputCommand = rawInput; + } + + // Error case + if (errors.length > 0) { + return ( + + {/* Branch connector summary (Claude-like) */} +
+ + {commandText} +
+ {/* Error card - semantic DOM + Tailwind styles */} +
+
+ {/* IN row */} +
+
IN
+
+
{inputCommand}
+
+
+ + {/* ERROR row */} +
+
Error
+
+
+                  {errors.join('\n')}
+                
+
+
+
+
+
+ ); + } + + // Success with output + if (textOutputs.length > 0) { + const output = textOutputs.join('\n'); + const truncatedOutput = + output.length > 500 ? output.substring(0, 500) + '...' : output; + + return ( + + {/* Branch connector summary (Claude-like) */} +
+ + {commandText} +
+ {/* Output card - semantic DOM + Tailwind styles */} +
+
+ {/* IN row */} +
+
IN
+
+
{inputCommand}
+
+
+ + {/* OUT row */} +
+
OUT
+
+
+
{truncatedOutput}
+
+
+
+
+
+
+ ); + } + + // Success without output: show command with branch connector + return ( + +
+ + {commandText} +
+
+ ); +}; 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 17dadd99..7f1ea623 100644 --- a/packages/vscode-ide-companion/src/webview/components/toolcalls/index.tsx +++ b/packages/vscode-ide-companion/src/webview/components/toolcalls/index.tsx @@ -12,8 +12,9 @@ import { shouldShowToolCall } from './shared/utils.js'; import { GenericToolCall } from './GenericToolCall.js'; import { ReadToolCall } from './ReadToolCall.js'; import { WriteToolCall } from './WriteToolCall.js'; -import { EditToolCall } from './EditToolCall.js'; -import { ExecuteToolCall } from './Bash/Bash.js'; +import { EditToolCall } from './Edit/EditToolCall.js'; +import { ExecuteToolCall as BashExecuteToolCall } from './Bash/Bash.js'; +import { ExecuteToolCall } from './Execute/Execute.js'; import { SearchToolCall } from './SearchToolCall.js'; import { ThinkToolCall } from './ThinkToolCall.js'; import { TodoWriteToolCall } from './TodoWriteToolCall.js'; @@ -38,9 +39,11 @@ export const getToolCallComponent = ( return EditToolCall; case 'execute': + return ExecuteToolCall; + case 'bash': case 'command': - return ExecuteToolCall; + return BashExecuteToolCall; case 'search': case 'grep': diff --git a/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/LayoutComponents.css b/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/LayoutComponents.css index 2bc34959..769d2127 100644 --- a/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/LayoutComponents.css +++ b/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/LayoutComponents.css @@ -153,8 +153,53 @@ max-width: 100%; } -/* Flex container with margin bottom */ +/* ToolCall header with loading indicator */ .toolcall-header { - /* TODO: 应该不需要? 待删除 */ - /* margin-bottom: 12px; */ + position: relative; +} + +.toolcall-header::before { + content: '\25cf'; + position: absolute; + left: -22px; + top: 50%; + transform: translateY(-50%); + font-size: 10px; + line-height: 1; + z-index: 1; + color: #e1c08d; + animation: toolcallHeaderPulse 1.5s ease-in-out infinite; +} + +/* Loading animation for toolcall header */ +@keyframes toolcallHeaderPulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +/* In-progress toolcall specific styles */ +.in-progress-toolcall .toolcall-content-wrapper { + display: flex; + flex-direction: column; + gap: 1; + min-width: 0; + max-width: 100%; +} + +.in-progress-toolcall .toolcall-header { + display: flex; + align-items: center; + gap: 2; + position: relative; + min-width: 0; +} + +.in-progress-toolcall .toolcall-content-text { + word-break: break-word; + white-space: pre-wrap; + width: 100%; } 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 f2dd01d0..b77583ab 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 @@ -43,11 +43,11 @@ export const ToolCallContainer: React.FC = ({ labelSuffix, }) => (
{/* Timeline connector line using ::after pseudo-element */}
-
+
{label} diff --git a/packages/vscode-ide-companion/src/webview/components/messages/SimpleTimeline.css b/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/MergedSimpleTimeline.css similarity index 75% rename from packages/vscode-ide-companion/src/webview/components/messages/SimpleTimeline.css rename to packages/vscode-ide-companion/src/webview/components/toolcalls/shared/MergedSimpleTimeline.css index 97893592..ae3829b2 100644 --- a/packages/vscode-ide-companion/src/webview/components/messages/SimpleTimeline.css +++ b/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/MergedSimpleTimeline.css @@ -3,10 +3,12 @@ * Copyright 2025 Qwen Team * SPDX-License-Identifier: Apache-2.0 * - * Simplified timeline styles for testing + * Simplified timeline styles for tool calls and messages + * Merged version of both SimpleTimeline.css files */ /* Basic timeline container */ +.simple-toolcall-container, .simple-timeline-container { position: relative; padding-left: 30px; @@ -15,6 +17,7 @@ } /* Timeline connector - simple version */ +.simple-toolcall-container::after, .simple-timeline-container::after { content: ''; position: absolute; @@ -26,11 +29,13 @@ } /* First item connector starts lower */ +.simple-toolcall-container:first-child::after, .simple-timeline-container:first-child::after { top: 24px; } /* Last item connector ends higher */ +.simple-toolcall-container:last-child::after, .simple-timeline-container:last-child::after { height: calc(100% - 24px); top: 0; @@ -38,6 +43,7 @@ } /* Bullet point */ +.simple-toolcall-container::before, .simple-timeline-container::before { content: '\25cf'; position: absolute; @@ -46,4 +52,4 @@ font-size: 10px; color: var(--app-secondary-foreground); z-index: 2; -} +} \ No newline at end of file diff --git a/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/SimpleTimeline.css b/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/SimpleTimeline.css deleted file mode 100644 index 1aafdb93..00000000 --- a/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/SimpleTimeline.css +++ /dev/null @@ -1,49 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - * - * Simplified timeline styles for tool calls - */ - -/* Basic timeline container */ -.simple-toolcall-container { - position: relative; - padding-left: 30px; - padding-top: 8px; - padding-bottom: 8px; -} - -/* Timeline connector - simple version */ -.simple-toolcall-container::after { - content: ''; - position: absolute; - left: 12px; - top: 0; - bottom: 0; - width: 1px; - background-color: var(--app-primary-border-color); -} - -/* First item connector starts lower */ -.simple-toolcall-container:first-child::after { - top: 24px; -} - -/* Last item connector ends higher */ -.simple-toolcall-container:last-child::after { - height: calc(100% - 24px); - top: 0; - bottom: auto; -} - -/* Bullet point */ -.simple-toolcall-container::before { - content: '\25cf'; - position: absolute; - left: 8px; - padding-top: 2px; - font-size: 10px; - color: var(--app-secondary-foreground); - z-index: 2; -} diff --git a/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts b/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts index 097d8eba..080d5744 100644 --- a/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts +++ b/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts @@ -5,6 +5,9 @@ */ import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; import { BaseMessageHandler } from './BaseMessageHandler.js'; import { getFileName } from '../utils/webviewUtils.js'; @@ -20,6 +23,7 @@ export class FileMessageHandler extends BaseMessageHandler { 'getWorkspaceFiles', 'openFile', 'openDiff', + 'createAndOpenTempFile', ].includes(messageType); } @@ -47,6 +51,10 @@ export class FileMessageHandler extends BaseMessageHandler { await this.handleOpenDiff(data); break; + case 'createAndOpenTempFile': + await this.handleCreateAndOpenTempFile(data); + break; + default: console.warn( '[FileMessageHandler] Unknown message type:', @@ -347,4 +355,52 @@ export class FileMessageHandler extends BaseMessageHandler { vscode.window.showErrorMessage(`Failed to open diff: ${error}`); } } + + /** + * Create and open temporary file + */ + private async handleCreateAndOpenTempFile( + data: Record | undefined, + ): Promise { + if (!data) { + console.warn( + '[FileMessageHandler] No data provided for createAndOpenTempFile', + ); + return; + } + + try { + const content = (data.content as string) || ''; + const fileName = (data.fileName as string) || 'temp'; + const fileExtension = (data.fileExtension as string) || '.txt'; + + // Create temporary file path + const tempDir = os.tmpdir(); + const tempFileName = `${fileName}-${Date.now()}${fileExtension}`; + const tempFilePath = path.join(tempDir, tempFileName); + + // Write content to temporary file + await fs.promises.writeFile(tempFilePath, content, 'utf8'); + + // Open the temporary file in VS Code + const uri = vscode.Uri.file(tempFilePath); + await vscode.window.showTextDocument(uri, { + preview: false, + preserveFocus: false, + }); + + console.log( + '[FileMessageHandler] Created and opened temporary file:', + tempFilePath, + ); + } catch (error) { + console.error( + '[FileMessageHandler] Failed to create and open temporary file:', + error, + ); + vscode.window.showErrorMessage( + `Failed to create and open temporary file: ${error}`, + ); + } + } } diff --git a/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts b/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts index 30364e88..c468aeec 100644 --- a/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts +++ b/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts @@ -25,6 +25,7 @@ export class SessionMessageHandler extends BaseMessageHandler { 'getQwenSessions', 'saveSession', 'resumeSession', + 'cancelStreaming', // UI action: open a new chat tab (new WebviewPanel) 'openNewChatTab', ].includes(messageType); @@ -102,6 +103,11 @@ export class SessionMessageHandler extends BaseMessageHandler { } break; + case 'cancelStreaming': + // Handle cancel streaming request from webview + await this.handleCancelStreaming(); + break; + default: console.warn( '[SessionMessageHandler] Unknown message type:', @@ -910,6 +916,34 @@ export class SessionMessageHandler extends BaseMessageHandler { } } + /** + * Handle cancel streaming request + */ + private async handleCancelStreaming(): Promise { + try { + console.log('[SessionMessageHandler] Canceling streaming...'); + + // Cancel the current streaming operation in the agent manager + await this.agentManager.cancelCurrentPrompt(); + + // Send streamEnd message to WebView to update UI + this.sendToWebView({ + type: 'streamEnd', + data: { timestamp: Date.now(), reason: 'user_cancelled' }, + }); + + console.log('[SessionMessageHandler] Streaming cancelled successfully'); + } catch (_error) { + console.log('[SessionMessageHandler] Streaming cancelled (interrupted)'); + + // Always send streamEnd to update UI, regardless of errors + this.sendToWebView({ + type: 'streamEnd', + data: { timestamp: Date.now(), reason: 'user_cancelled' }, + }); + } + } + /** * Handle resume session request */ diff --git a/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts b/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts index a67c1a86..96a8a492 100644 --- a/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts +++ b/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts @@ -5,6 +5,7 @@ */ import { useEffect, useRef, useCallback } from 'react'; +import { useVSCode } from './useVSCode.js'; import type { Conversation } from '../../storage/conversationStore.js'; import type { PermissionOption, @@ -108,6 +109,8 @@ export const useWebViewMessages = ({ inputFieldRef, setInputText, }: UseWebViewMessagesProps) => { + // VS Code API for posting messages back to the extension host + const vscode = useVSCode(); // Use ref to store callbacks to avoid useEffect dependency issues const handlersRef = useRef({ sessionManagement, @@ -469,6 +472,8 @@ export const useWebViewMessages = ({ (session.name as string) || 'Past Conversations'; handlers.sessionManagement.setCurrentSessionTitle(title); + // Update the VS Code webview tab title as well + vscode.postMessage({ type: 'updatePanelTitle', data: { title } }); } if (message.data.messages) { handlers.messageHandling.setMessages(message.data.messages); @@ -487,6 +492,11 @@ export const useWebViewMessages = ({ handlers.sessionManagement.setCurrentSessionTitle( 'Past Conversations', ); + // Reset the VS Code tab title to default label + vscode.postMessage({ + type: 'updatePanelTitle', + data: { title: 'Qwen Code' }, + }); lastPlanSnapshotRef.current = null; break; @@ -496,6 +506,8 @@ export const useWebViewMessages = ({ if (sessionId && title) { handlers.sessionManagement.setCurrentSessionId(sessionId); handlers.sessionManagement.setCurrentSessionTitle(title); + // Ask extension host to reflect this title in the tab label + vscode.postMessage({ type: 'updatePanelTitle', data: { title } }); } break; } @@ -563,11 +575,23 @@ export const useWebViewMessages = ({ break; } + case 'cancelStreaming': + // Handle cancel streaming request from webview + handlers.messageHandling.endStreaming(); + handlers.messageHandling.clearWaitingForResponse(); + // Add interrupted message + handlers.messageHandling.addMessage({ + role: 'assistant', + content: 'Interrupted', + timestamp: Date.now(), + }); + break; + default: break; } }, - [inputFieldRef, setInputText], + [inputFieldRef, setInputText, vscode], ); useEffect(() => { diff --git a/packages/vscode-ide-companion/src/webview/styles/ClaudeCodeStyles.css b/packages/vscode-ide-companion/src/webview/styles/ClaudeCodeStyles.css index b9eb5283..429263ce 100644 --- a/packages/vscode-ide-companion/src/webview/styles/ClaudeCodeStyles.css +++ b/packages/vscode-ide-companion/src/webview/styles/ClaudeCodeStyles.css @@ -10,8 +10,8 @@ /* Import component styles */ @import '../components/toolcalls/shared/DiffDisplay.css'; @import '../components/messages/AssistantMessage.css'; -@import '../components/messages/SimpleTimeline.css'; -@import '../components/toolcalls/shared/SimpleTimeline.css'; +@import '../components/toolcalls/shared/MergedSimpleTimeline.css'; +@import '../components/messages/QwenMessageTimeline.css'; /* =========================== diff --git a/packages/vscode-ide-companion/src/webview/utils/tempFileManager.ts b/packages/vscode-ide-companion/src/webview/utils/tempFileManager.ts new file mode 100644 index 00000000..8ab17e30 --- /dev/null +++ b/packages/vscode-ide-companion/src/webview/utils/tempFileManager.ts @@ -0,0 +1,33 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + * + * Temporary file manager for creating and opening temporary files in webview + */ + +/** + * Creates a temporary file with the given content and opens it in VS Code + * @param content The content to write to the temporary file + * @param fileName Optional file name (without extension) + * @param fileExtension Optional file extension (defaults to .txt) + */ +export async function createAndOpenTempFile( + postMessage: (message: { + type: string; + data: Record; + }) => void, + content: string, + fileName: string = 'temp', + fileExtension: string = '.txt', +): Promise { + // Send message to VS Code extension to create and open temp file + postMessage({ + type: 'createAndOpenTempFile', + data: { + content, + fileName, + fileExtension, + }, + }); +}