diff --git a/packages/vscode-ide-companion/src/commands/index.ts b/packages/vscode-ide-companion/src/commands/index.ts index e8830e11..a003b32c 100644 --- a/packages/vscode-ide-companion/src/commands/index.ts +++ b/packages/vscode-ide-companion/src/commands/index.ts @@ -73,12 +73,7 @@ export function registerNewCommands( disposables.push( vscode.commands.registerCommand(openNewChatTabCommand, async () => { const provider = createWebViewProvider(); - // Suppress auto-restore for this newly created tab so it starts clean - try { - provider.suppressAutoRestoreOnce?.(); - } catch { - // ignore if older provider does not implement the method - } + // Session restoration is now disabled by default, so no need to suppress it await provider.show(); }), ); diff --git a/packages/vscode-ide-companion/src/webview/App.tsx b/packages/vscode-ide-companion/src/webview/App.tsx index 0ecd892c..7e04351e 100644 --- a/packages/vscode-ide-companion/src/webview/App.tsx +++ b/packages/vscode-ide-companion/src/webview/App.tsx @@ -224,6 +224,7 @@ export const App: React.FC = () => { handlePermissionRequest: setPermissionRequest, inputFieldRef, setInputText, + setEditMode, }); // Auto-scroll handling: keep the view pinned to bottom when new content arrives, @@ -473,15 +474,22 @@ export const App: React.FC = () => { // Handle toggle edit mode const handleToggleEditMode = useCallback(() => { setEditMode((prev) => { - if (prev === 'ask') { - return 'auto'; + const next: EditMode = + prev === 'ask' ? 'auto' : prev === 'auto' ? 'plan' : 'ask'; + // Notify extension to set approval mode via ACP + try { + const toAcp = + next === 'plan' ? 'plan' : next === 'auto' ? 'auto-edit' : 'default'; + vscode.postMessage({ + type: 'setApprovalMode', + data: { modeId: toAcp }, + }); + } catch { + /* no-op */ } - if (prev === 'auto') { - return 'plan'; - } - return 'ask'; + return next; }); - }, []); + }, [vscode]); // Handle toggle thinking const handleToggleThinking = () => { diff --git a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts index 562f6389..2484ad51 100644 --- a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts +++ b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts @@ -26,8 +26,6 @@ export class WebViewProvider { private authStateManager: AuthStateManager; private disposables: vscode.Disposable[] = []; private agentInitialized = false; // Track if agent has been initialized - // Control whether to auto-restore last session on the very first connect of this panel - private autoRestoreOnFirstConnect = true; constructor( context: vscode.ExtensionContext, @@ -242,13 +240,6 @@ export class WebViewProvider { ); } - /** - * Suppress auto-restore once for this panel (used by "New Chat Tab"). - */ - suppressAutoRestoreOnce(): void { - this.autoRestoreOnFirstConnect = false; - } - async show(): Promise { const panel = this.panelManager.getPanel(); @@ -383,6 +374,22 @@ export class WebViewProvider { type: 'activeEditorChanged', data: { fileName, filePath, selection: selectionInfo }, }); + + // Surface available modes and current mode (from ACP initialize) + this.agentManager.onModeInfo((info) => { + this.sendMessageToWebView({ + type: 'modeInfo', + data: info || {}, + }); + }); + + // Surface mode changes (from ACP or immediate set_mode response) + this.agentManager.onModeChanged((modeId) => { + this.sendMessageToWebView({ + type: 'modeChanged', + data: { modeId }, + }); + }); } }); this.disposables.push(selectionChangeDisposable); @@ -681,199 +688,40 @@ export class WebViewProvider { /** * Load messages from current Qwen session - * Attempts to restore an existing session before creating a new one + * Skips session restoration and creates a new session directly */ private async loadCurrentSessionMessages(): Promise { try { console.log( - '[WebViewProvider] Initializing with session restoration attempt', + '[WebViewProvider] Initializing with new session (skipping restoration)', ); const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); - // First, try to restore an existing session if we have cached auth - if (this.authStateManager) { - const hasValidAuth = await this.authStateManager.hasValidAuth( + // Skip session restoration entirely and create a new session directly + try { + await this.agentManager.createNewSession( workingDir, - authMethod, + this.authStateManager, ); - if (hasValidAuth) { - const allowAutoRestore = this.autoRestoreOnFirstConnect; - // Reset for subsequent connects (only once per panel lifecycle unless set again) - this.autoRestoreOnFirstConnect = true; + console.log('[WebViewProvider] ACP session created successfully'); - if (allowAutoRestore) { - console.log( - '[WebViewProvider] Valid auth found, attempting auto-restore of last session...', - ); - try { - const page = await this.agentManager.getSessionListPaged({ - size: 1, - }); - const item = page.sessions[0] as - | { sessionId?: string; id?: string; cwd?: string } - | undefined; - if (item && (item.sessionId || item.id)) { - const targetId = (item.sessionId || item.id) as string; - await this.agentManager.loadSessionViaAcp( - targetId, - (item.cwd as string | undefined) ?? workingDir, - ); - - this.messageHandler.setCurrentConversationId(targetId); - const messages = - await this.agentManager.getSessionMessages(targetId); - - // Even if messages array is empty, we should still switch to the session - // This ensures we don't lose the session context - this.sendMessageToWebView({ - type: 'qwenSessionSwitched', - data: { sessionId: targetId, messages }, - }); - console.log( - '[WebViewProvider] Auto-restored last session:', - targetId, - ); - - // Ensure auth state is saved after successful session restore - if (this.authStateManager) { - await this.authStateManager.saveAuthState( - workingDir, - authMethod, - ); - console.log( - '[WebViewProvider] Auth state saved after session restore', - ); - } - - return; - } - console.log( - '[WebViewProvider] No sessions to auto-restore, creating new session', - ); - } catch (restoreError) { - console.warn( - '[WebViewProvider] Auto-restore failed, will create a new session:', - restoreError, - ); - - // Try to get session messages anyway, even if loadSessionViaAcp failed - // This can happen if the session exists locally but failed to load in the CLI - try { - const page = await this.agentManager.getSessionListPaged({ - size: 1, - }); - const item = page.sessions[0] as - | { sessionId?: string; id?: string } - | undefined; - if (item && (item.sessionId || item.id)) { - const targetId = (item.sessionId || item.id) as string; - const messages = - await this.agentManager.getSessionMessages(targetId); - - // Switch to the session with whatever messages we could get - this.messageHandler.setCurrentConversationId(targetId); - this.sendMessageToWebView({ - type: 'qwenSessionSwitched', - data: { sessionId: targetId, messages }, - }); - console.log( - '[WebViewProvider] Partially restored last session:', - targetId, - ); - - // Ensure auth state is saved after partial session restore - if (this.authStateManager) { - await this.authStateManager.saveAuthState( - workingDir, - authMethod, - ); - console.log( - '[WebViewProvider] Auth state saved after partial session restore', - ); - } - - return; - } - } catch (fallbackError) { - console.warn( - '[WebViewProvider] Fallback session restore also failed:', - fallbackError, - ); - } - } - } else { - console.log( - '[WebViewProvider] Auto-restore suppressed for this panel', - ); - } - - // Create a fresh ACP session (no auto-restore or restore failed) - try { - await this.agentManager.createNewSession( - workingDir, - this.authStateManager, - ); - console.log('[WebViewProvider] ACP session created successfully'); - - // Ensure auth state is saved after successful session creation - if (this.authStateManager) { - await this.authStateManager.saveAuthState(workingDir, authMethod); - console.log( - '[WebViewProvider] Auth state saved after session creation', - ); - } - } catch (sessionError) { - console.error( - '[WebViewProvider] Failed to create ACP session:', - sessionError, - ); - vscode.window.showWarningMessage( - `Failed to create ACP session: ${sessionError}. You may need to authenticate first.`, - ); - } - } else { + // Ensure auth state is saved after successful session creation + if (this.authStateManager) { + await this.authStateManager.saveAuthState(workingDir, authMethod); console.log( - '[WebViewProvider] No valid cached auth found, creating new session', + '[WebViewProvider] Auth state saved after session creation', ); - // No valid auth, create a new session (will trigger auth if needed) - try { - await this.agentManager.createNewSession( - workingDir, - this.authStateManager, - ); - console.log('[WebViewProvider] ACP session created successfully'); - } catch (sessionError) { - console.error( - '[WebViewProvider] Failed to create ACP session:', - sessionError, - ); - vscode.window.showWarningMessage( - `Failed to create ACP session: ${sessionError}. You may need to authenticate first.`, - ); - } } - } else { - // No auth state manager, create a new session - console.log( - '[WebViewProvider] No auth state manager, creating new session', + } catch (sessionError) { + console.error( + '[WebViewProvider] Failed to create ACP session:', + sessionError, + ); + vscode.window.showWarningMessage( + `Failed to create ACP session: ${sessionError}. You may need to authenticate first.`, ); - try { - await this.agentManager.createNewSession( - workingDir, - this.authStateManager, - ); - console.log('[WebViewProvider] ACP session created successfully'); - } catch (sessionError) { - console.error( - '[WebViewProvider] Failed to create ACP session:', - sessionError, - ); - vscode.window.showWarningMessage( - `Failed to create ACP session: ${sessionError}. You may need to authenticate first.`, - ); - } } await this.initializeEmptyConversation(); diff --git a/packages/vscode-ide-companion/src/webview/components/PermissionDrawer/PermissionDrawer.tsx b/packages/vscode-ide-companion/src/webview/components/PermissionDrawer/PermissionDrawer.tsx index 94ad29e1..e0ec3e84 100644 --- a/packages/vscode-ide-companion/src/webview/components/PermissionDrawer/PermissionDrawer.tsx +++ b/packages/vscode-ide-companion/src/webview/components/PermissionDrawer/PermissionDrawer.tsx @@ -185,7 +185,7 @@ export const PermissionDrawer: React.FC = ({ /> {/* Title + Description (from toolCall.title) */} -
+
{getTitle()}
@@ -198,6 +198,11 @@ export const PermissionDrawer: React.FC = ({
{toolCall.title} @@ -206,14 +211,14 @@ export const PermissionDrawer: React.FC = ({
{/* Options */} -
+
{options.map((option, index) => { const isFocused = focusedIndex === index; return ( ); })} @@ -283,15 +284,12 @@ const CustomMessageInputRow: React.FC = ({ inputRef, }) => (
inputRef.current?.focus()} > - {/* 输入行不显示序号徽标 */} - {/* Input field */} = ({ entries }) => { - // Calculate overall status for left dot color - const allCompleted = - entries.length > 0 && entries.every((e) => e.status === 'completed'); - const anyInProgress = entries.some((e) => e.status === 'in_progress'); - const statusDotClass = allCompleted - ? 'before:text-[#74c991]' - : anyInProgress - ? 'before:text-[#e1c08d]' - : 'before:text-[var(--app-secondary-foreground)]'; - - return ( -
- {/* Title area, similar to example summary/_e/or */} -
-
-
- -
- Update Todos -
-
-
-
-
- - {/* List area, similar to example .qr/.Fr/.Hr */} -
-
    - {entries.map((entry, index) => { - const isDone = entry.status === 'completed'; - const isIndeterminate = entry.status === 'in_progress'; - return ( -
  • - {/* Display checkbox (reusable component) */} - - -
    - {entry.content} -
    -
  • - ); - })} -
-
-
- ); -}; 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 102b2756..19fdb415 100644 --- a/packages/vscode-ide-companion/src/webview/components/messages/UserMessage.tsx +++ b/packages/vscode-ide-companion/src/webview/components/messages/UserMessage.tsx @@ -67,7 +67,7 @@ export const UserMessage: React.FC = ({
fileContext && onFileClick?.(fileContext.filePath)} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { 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 index fbe6ca5a..0c0e4c8d 100644 --- a/packages/vscode-ide-companion/src/webview/components/messages/Waiting/InterruptedMessage.tsx +++ b/packages/vscode-ide-companion/src/webview/components/messages/Waiting/InterruptedMessage.tsx @@ -15,17 +15,7 @@ export const InterruptedMessage: React.FC = ({ text = 'Interrupted', }) => (
-
+
{text}
diff --git a/packages/vscode-ide-companion/src/webview/components/toolcalls/UpdatedPlan/UpdatedPlanToolCall.tsx b/packages/vscode-ide-companion/src/webview/components/toolcalls/UpdatedPlan/UpdatedPlanToolCall.tsx index 10a567de..cdd0a09b 100644 --- a/packages/vscode-ide-companion/src/webview/components/toolcalls/UpdatedPlan/UpdatedPlanToolCall.tsx +++ b/packages/vscode-ide-companion/src/webview/components/toolcalls/UpdatedPlan/UpdatedPlanToolCall.tsx @@ -8,16 +8,41 @@ import type React from 'react'; import type { BaseToolCallProps } from '../shared/types.js'; -import { ToolCallContainer } from '../shared/LayoutComponents.js'; +import type { ToolCallContainerProps } from '../shared/LayoutComponents.js'; import { groupContent, safeTitle } from '../shared/utils.js'; import { CheckboxDisplay } from '../../ui/CheckboxDisplay.js'; +import type { PlanEntry } from '../../../../agents/qwenTypes.js'; type EntryStatus = 'pending' | 'in_progress' | 'completed'; -interface PlanEntry { - content: string; - status: EntryStatus; -} +export const ToolCallContainer: React.FC = ({ + label, + status = 'success', + children, + toolCallId: _toolCallId, + labelSuffix, + className: _className, +}) => ( +
+
+
+ + {label} + + + {labelSuffix} + +
+ {children && ( +
+ {children} +
+ )} +
+
+); const mapToolStatusToBullet = ( status: import('../shared/types.js').ToolCallStatus, diff --git a/packages/vscode-ide-companion/src/webview/components/toolcalls/done/Edit/EditToolCall.tsx b/packages/vscode-ide-companion/src/webview/components/toolcalls/done/Edit/EditToolCall.tsx index 14bb5503..e7e01a15 100644 --- a/packages/vscode-ide-companion/src/webview/components/toolcalls/done/Edit/EditToolCall.tsx +++ b/packages/vscode-ide-companion/src/webview/components/toolcalls/done/Edit/EditToolCall.tsx @@ -16,6 +16,7 @@ import { FileLink } from '../../../ui/FileLink.js'; import type { ToolCallContainerProps } from '../../shared/LayoutComponents.js'; import { useVSCode } from '../../../../hooks/useVSCode.js'; import { handleOpenDiff } from '../../../../utils/diffUtils.js'; +import { DiffDisplay } from '../../shared/DiffDisplay.js'; export const ToolCallContainer: React.FC = ({ label, @@ -109,6 +110,64 @@ export const EditToolCall: React.FC = ({ toolCall }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [toolCallId]); + // Failed case: show explicit failed message and render inline diffs + if (toolCall.status === 'failed') { + const firstDiff = diffs[0]; + const path = firstDiff?.path || locations?.[0]?.path || ''; + const containerStatus = mapToolStatusToContainerStatus(toolCall.status); + return ( +
+
+
+
+ + Edit + + {path && ( + + )} +
+
+ {/* Failed state text (replace summary) */} +
+ edit failed +
+ {/* Inline diff preview(s) */} + {diffs.length > 0 && ( +
+ {diffs.map( + ( + item: import('../../shared/types.js').ToolCallContent, + idx: number, + ) => ( + + handleOpenDiffInternal( + item.path || path, + item.oldText, + item.newText, + ) + } + /> + ), + )} +
+ )} +
+
+ ); + } + // Error case: show error if (errors.length > 0) { const path = diffs[0]?.path || locations?.[0]?.path || ''; @@ -122,7 +181,7 @@ export const EditToolCall: React.FC = ({ toolCall }) => { ) : undefined } @@ -141,21 +200,19 @@ export const EditToolCall: React.FC = ({ toolCall }) => { return (
- {/* 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 && ( )}
diff --git a/packages/vscode-ide-companion/src/webview/components/toolcalls/done/Search/SearchToolCall.tsx b/packages/vscode-ide-companion/src/webview/components/toolcalls/done/Search/SearchToolCall.tsx index 2440127b..84679171 100644 --- a/packages/vscode-ide-companion/src/webview/components/toolcalls/done/Search/SearchToolCall.tsx +++ b/packages/vscode-ide-companion/src/webview/components/toolcalls/done/Search/SearchToolCall.tsx @@ -8,11 +8,7 @@ import type React from 'react'; import type { BaseToolCallProps } from '../../shared/types.js'; -import { - ToolCallCard, - ToolCallRow, - LocationsList, -} from '../../shared/LayoutComponents.js'; +import { FileLink } from '../../../ui/FileLink.js'; import { safeTitle, groupContent, @@ -53,7 +49,128 @@ export const ToolCallContainer: React.FC = ({ * Optimized for displaying search operations and results * Shows query + result count or file list */ -export const SearchToolCall: React.FC = ({ toolCall }) => { +// Local, scoped inline container for compact search rows (single result/text-only) +const InlineContainer: React.FC<{ + status: 'success' | 'error' | 'warning' | 'loading' | 'default'; + labelSuffix?: string; + children?: React.ReactNode; + isFirst?: boolean; + isLast?: boolean; +}> = ({ status, labelSuffix, children, isFirst, isLast }) => { + const beforeStatusClass = + status === 'success' + ? 'before:text-qwen-success' + : status === 'error' + ? 'before:text-qwen-error' + : status === 'warning' + ? 'before:text-qwen-warning' + : 'before:text-qwen-loading before:opacity-70 before:animate-pulse-slow'; + const lineCropTop = isFirst ? 'top-[24px]' : 'top-0'; + const lineCropBottom = isLast + ? 'bottom-auto h-[calc(100%-24px)]' + : 'bottom-0'; + return ( +
+ {/* timeline vertical line */} +
+
+
+ + Search + + {labelSuffix ? ( + + {labelSuffix} + + ) : null} +
+ {children ? ( +
+ {children} +
+ ) : null} +
+
+ ); +}; + +// Local card layout for multi-result or error display +const SearchCard: React.FC<{ + status: 'success' | 'error' | 'warning' | 'loading' | 'default'; + children: React.ReactNode; + isFirst?: boolean; + isLast?: boolean; +}> = ({ status, children, isFirst, isLast }) => { + const beforeStatusClass = + status === 'success' + ? 'before:text-qwen-success' + : status === 'error' + ? 'before:text-qwen-error' + : status === 'warning' + ? 'before:text-qwen-warning' + : 'before:text-qwen-loading before:opacity-70 before:animate-pulse-slow'; + const lineCropTop = isFirst ? 'top-[24px]' : 'top-0'; + const lineCropBottom = isLast + ? 'bottom-auto h-[calc(100%-24px)]' + : 'bottom-0'; + return ( +
+ {/* timeline vertical line */} +
+
+
{children}
+
+
+ ); +}; + +const SearchRow: React.FC<{ label: string; children: React.ReactNode }> = ({ + label, + children, +}) => ( +
+
+ {label} +
+
+ {children} +
+
+); + +const LocationsListLocal: React.FC<{ + locations: Array<{ path: string; line?: number | null }>; +}> = ({ locations }) => ( +
+ {locations.map((loc, idx) => ( + + ))} +
+); + +export const SearchToolCall: React.FC = ({ + toolCall, + isFirst, + isLast, +}) => { const { title, content, locations } = toolCall; const queryText = safeTitle(title); @@ -63,14 +180,14 @@ export const SearchToolCall: React.FC = ({ toolCall }) => { // Error case: show search query + error in card layout if (errors.length > 0) { return ( - - + +
{queryText}
-
- -
{errors.join('\n')}
-
-
+ + +
{errors.join('\n')}
+
+ ); } @@ -80,28 +197,27 @@ export const SearchToolCall: React.FC = ({ toolCall }) => { // If multiple results, use card layout; otherwise use compact format if (locations.length > 1) { return ( - - + +
{queryText}
-
- - - -
+ + + + + ); } // Single result - compact format return ( - - {/* {queryText} */} - - + + ); } @@ -109,11 +225,11 @@ export const SearchToolCall: React.FC = ({ toolCall }) => { if (textOutputs.length > 0) { const containerStatus = mapToolStatusToContainerStatus(toolCall.status); return ( -
{textOutputs.map((text, index) => ( @@ -126,7 +242,7 @@ export const SearchToolCall: React.FC = ({ toolCall }) => {
))}
- + ); } @@ -134,13 +250,13 @@ export const SearchToolCall: React.FC = ({ toolCall }) => { if (queryText) { const containerStatus = mapToolStatusToContainerStatus(toolCall.status); return ( - {queryText} - + ); } 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 153ed8f1..0e24a9e0 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 @@ -47,10 +47,8 @@ export const ToolCallContainer: React.FC = ({
- {/* Timeline connector line using ::after pseudo-element */} - {/* TODO: gap-0 */} -
-
+
+
{label} diff --git a/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/types.ts b/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/types.ts index 57d3c700..0fccb186 100644 --- a/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/types.ts +++ b/packages/vscode-ide-companion/src/webview/components/toolcalls/shared/types.ts @@ -56,6 +56,7 @@ export interface ToolCallData { */ export interface BaseToolCallProps { toolCall: ToolCallData; + // Optional timeline flags for rendering connector line cropping isFirst?: boolean; isLast?: boolean; } diff --git a/packages/vscode-ide-companion/src/webview/handlers/SettingsMessageHandler.ts b/packages/vscode-ide-companion/src/webview/handlers/SettingsMessageHandler.ts index 966ef68c..bbd4eb48 100644 --- a/packages/vscode-ide-companion/src/webview/handlers/SettingsMessageHandler.ts +++ b/packages/vscode-ide-companion/src/webview/handlers/SettingsMessageHandler.ts @@ -13,7 +13,9 @@ import { BaseMessageHandler } from './BaseMessageHandler.js'; */ export class SettingsMessageHandler extends BaseMessageHandler { canHandle(messageType: string): boolean { - return ['openSettings', 'recheckCli'].includes(messageType); + return ['openSettings', 'recheckCli', 'setApprovalMode'].includes( + messageType, + ); } async handle(message: { type: string; data?: unknown }): Promise { @@ -26,6 +28,14 @@ export class SettingsMessageHandler extends BaseMessageHandler { await this.handleRecheckCli(); break; + case 'setApprovalMode': + await this.handleSetApprovalMode( + message.data as { + modeId?: 'plan' | 'default' | 'auto-edit' | 'yolo'; + }, + ); + break; + default: console.warn( '[SettingsMessageHandler] Unknown message type:', @@ -68,4 +78,29 @@ export class SettingsMessageHandler extends BaseMessageHandler { }); } } + + /** + * Set approval mode via agent (ACP session/set_mode) + */ + private async handleSetApprovalMode(data?: { + modeId?: 'plan' | 'default' | 'auto-edit' | 'yolo'; + }): Promise { + try { + const modeId = (data?.modeId || 'default') as + | 'plan' + | 'default' + | 'auto-edit' + | 'yolo'; + await this.agentManager.setApprovalModeFromUi( + modeId === 'plan' ? 'plan' : modeId === 'auto-edit' ? 'auto' : 'ask', + ); + // No explicit response needed; WebView listens for modeChanged + } catch (error) { + console.error('[SettingsMessageHandler] Failed to set mode:', error); + this.sendToWebView({ + type: 'error', + data: { message: `Failed to set mode: ${error}` }, + }); + } + } } diff --git a/packages/vscode-ide-companion/src/webview/hooks/message/useMessageHandling.ts b/packages/vscode-ide-companion/src/webview/hooks/message/useMessageHandling.ts index 0c0e27bb..a2354bd4 100644 --- a/packages/vscode-ide-companion/src/webview/hooks/message/useMessageHandling.ts +++ b/packages/vscode-ide-companion/src/webview/hooks/message/useMessageHandling.ts @@ -70,26 +70,32 @@ export const useMessageHandling = () => { /** * Add stream chunk */ - const appendStreamChunk = useCallback((chunk: string) => { - setMessages((prev) => { - let idx = streamingMessageIndexRef.current; - const next = prev.slice(); + const appendStreamChunk = useCallback( + (chunk: string) => { + // Ignore late chunks after user cancelled streaming (until next streamStart) + if (!isStreaming) return; - // If there is no active placeholder (e.g., after a tool call), start a new one - if (idx === null) { - idx = next.length; - streamingMessageIndexRef.current = idx; - next.push({ role: 'assistant', content: '', timestamp: Date.now() }); - } + setMessages((prev) => { + let idx = streamingMessageIndexRef.current; + const next = prev.slice(); - if (idx < 0 || idx >= next.length) { - return prev; - } - const target = next[idx]; - next[idx] = { ...target, content: (target.content || '') + chunk }; - return next; - }); - }, []); + // If there is no active placeholder (e.g., after a tool call), start a new one + if (idx === null) { + idx = next.length; + streamingMessageIndexRef.current = idx; + next.push({ role: 'assistant', content: '', timestamp: Date.now() }); + } + + if (idx < 0 || idx >= next.length) { + return prev; + } + const target = next[idx]; + next[idx] = { ...target, content: (target.content || '') + chunk }; + return next; + }); + }, + [isStreaming], + ); /** * Break current assistant stream segment (e.g., when a tool call starts/updates) @@ -150,6 +156,8 @@ export const useMessageHandling = () => { endStreaming, // Thought handling appendThinkingChunk: (chunk: string) => { + // Ignore late thoughts after user cancelled streaming + if (!isStreaming) return; setMessages((prev) => { let idx = thinkingMessageIndexRef.current; const next = prev.slice(); diff --git a/packages/vscode-ide-companion/tailwind.config.js b/packages/vscode-ide-companion/tailwind.config.js index ad8fd1ef..0c8baa20 100644 --- a/packages/vscode-ide-companion/tailwind.config.js +++ b/packages/vscode-ide-companion/tailwind.config.js @@ -43,6 +43,11 @@ export default { ivory: '#f5f5ff', slate: '#141420', green: '#6bcf7f', + // Status colors used by toolcall components + success: '#74c991', + error: '#c74e39', + warning: '#e1c08d', + loading: 'var(--app-secondary-foreground)', }, }, borderRadius: {