diff --git a/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts b/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts index c468aeec..dfd0fd75 100644 --- a/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts +++ b/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts @@ -378,7 +378,29 @@ export class SessionMessageHandler extends BaseMessageHandler { } catch (error) { console.error('[SessionMessageHandler] Error sending message:', error); + const err = error as unknown as Error; const errorMsg = String(error); + const lower = errorMsg.toLowerCase(); + + // Suppress user-cancelled/aborted errors (ESC/Stop button) + const isAbortLike = + (err && (err as Error).name === 'AbortError') || + lower.includes('abort') || + lower.includes('aborted') || + lower.includes('request was aborted') || + lower.includes('canceled') || + lower.includes('cancelled') || + lower.includes('user_cancelled'); + + if (isAbortLike) { + // Do not show VS Code error popup for intentional cancellations. + // Ensure the webview knows the stream ended due to user action. + this.sendToWebView({ + type: 'streamEnd', + data: { timestamp: Date.now(), reason: 'user_cancelled' }, + }); + return; + } // Check for session not found error and handle it appropriately if ( errorMsg.includes('Session not found') || 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 1177731f..0c0e27bb 100644 --- a/packages/vscode-ide-companion/src/webview/hooks/message/useMessageHandling.ts +++ b/packages/vscode-ide-companion/src/webview/hooks/message/useMessageHandling.ts @@ -105,7 +105,6 @@ export const useMessageHandling = () => { const endStreaming = useCallback(() => { // Finalize streaming; content already lives in the placeholder message setIsStreaming(false); - setIsWaitingForResponse(false); streamingMessageIndexRef.current = null; // Remove the thinking message if it exists (collapse thoughts) setMessages((prev) => { diff --git a/packages/vscode-ide-companion/src/webview/hooks/useMessageSubmit.ts b/packages/vscode-ide-companion/src/webview/hooks/useMessageSubmit.ts index 2efe8460..9f67bcc8 100644 --- a/packages/vscode-ide-companion/src/webview/hooks/useMessageSubmit.ts +++ b/packages/vscode-ide-companion/src/webview/hooks/useMessageSubmit.ts @@ -14,6 +14,8 @@ interface UseMessageSubmitProps { setInputText: (text: string) => void; inputFieldRef: React.RefObject; isStreaming: boolean; + // When true, do NOT auto-attach the active editor file/selection to context + skipAutoActiveContext?: boolean; fileContext: { getFileReference: (fileName: string) => string | undefined; @@ -38,6 +40,7 @@ export const useMessageSubmit = ({ setInputText, inputFieldRef, isStreaming, + skipAutoActiveContext = false, fileContext, messageHandling, }: UseMessageSubmitProps) => { @@ -94,8 +97,8 @@ export const useMessageSubmit = ({ } } - // Add active file selection context if present - if (fileContext.activeFilePath) { + // Add active file selection context if present and not skipped + if (fileContext.activeFilePath && !skipAutoActiveContext) { const fileName = fileContext.activeFileName || 'current file'; context.push({ type: 'file', @@ -115,7 +118,11 @@ export const useMessageSubmit = ({ } | undefined; - if (fileContext.activeFilePath && fileContext.activeFileName) { + if ( + fileContext.activeFilePath && + fileContext.activeFileName && + !skipAutoActiveContext + ) { fileContextForMessage = { fileName: fileContext.activeFileName, filePath: fileContext.activeFilePath, @@ -146,6 +153,7 @@ export const useMessageSubmit = ({ inputFieldRef, vscode, fileContext, + skipAutoActiveContext, messageHandling, ], ); diff --git a/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts b/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts index 91ae772f..4cb3eb78 100644 --- a/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts +++ b/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts @@ -112,6 +112,9 @@ export const useWebViewMessages = ({ }: UseWebViewMessagesProps) => { // VS Code API for posting messages back to the extension host const vscode = useVSCode(); + // Track active long-running tool calls (execute/bash/command) so we can + // keep the bottom "waiting" message visible until all of them complete. + const activeExecToolCallsRef = useRef>(new Set()); // Use ref to store callbacks to avoid useEffect dependency issues const handlersRef = useRef({ sessionManagement, @@ -260,14 +263,19 @@ export const useWebViewMessages = ({ // no-op: stream might not have been started console.warn('[PanelManager] Failed to end streaming:', err); } - try { - handlers.messageHandling.clearWaitingForResponse(); - } catch (err) { - // no-op: already cleared - console.warn( - '[PanelManager] Failed to clear waiting for response:', - err, - ); + // Important: Do NOT blindly clear the waiting message if there are + // still active tool calls running. We keep the waiting indicator + // tied to tool-call lifecycle instead. + if (activeExecToolCallsRef.current.size === 0) { + try { + handlers.messageHandling.clearWaitingForResponse(); + } catch (err) { + // no-op: already cleared + console.warn( + '[PanelManager] Failed to clear waiting for response:', + err, + ); + } } } break; @@ -293,6 +301,11 @@ export const useWebViewMessages = ({ case 'streamEnd': handlers.messageHandling.endStreaming(); handlers.messageHandling.clearThinking(); + // Clear the generic waiting indicator only if there are no active + // long-running tool calls. Otherwise, keep it visible. + if (activeExecToolCallsRef.current.size === 0) { + handlers.messageHandling.clearWaitingForResponse(); + } break; case 'error': @@ -439,20 +452,33 @@ export const useWebViewMessages = ({ 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 || ''; + + if (isExec) { + const id = (toolCallData.toolCallId || '').toString(); + + // Maintain the active set by status + if (status === 'pending' || status === 'in_progress') { + activeExecToolCallsRef.current.add(id); + + // Build a helpful hint from rawInput + 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); + } else if (status === 'completed' || status === 'failed') { + activeExecToolCallsRef.current.delete(id); + } + + // If no active exec tool remains, clear the waiting message. + if (activeExecToolCallsRef.current.size === 0) { + handlers.messageHandling.clearWaitingForResponse(); } - 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 diff --git a/packages/vscode-ide-companion/src/webview/utils/envUtils.ts b/packages/vscode-ide-companion/src/webview/utils/envUtils.ts index ae9c195c..1cbfd172 100644 --- a/packages/vscode-ide-companion/src/webview/utils/envUtils.ts +++ b/packages/vscode-ide-companion/src/webview/utils/envUtils.ts @@ -6,10 +6,10 @@ export function isDevelopmentMode(): boolean { // TODO: 调试用 - return false; - // return ( - // process.env.NODE_ENV === 'development' || - // process.env.DEBUG === 'true' || - // process.env.NODE_ENV !== 'production' - // ); + // 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 f170d539..6977ce46 100644 --- a/packages/vscode-ide-companion/tailwind.config.js +++ b/packages/vscode-ide-companion/tailwind.config.js @@ -34,10 +34,16 @@ export default { '0%, 100%': { opacity: '1' }, '50%': { opacity: '0.5' }, }, + // PermissionDrawer enter animation: slide up from bottom + 'slide-up': { + '0%': { transform: 'translateY(100%)', opacity: '0' }, + '100%': { transform: 'translateY(0)', opacity: '1' }, + }, }, animation: { 'completion-menu-enter': 'completion-menu-enter 150ms ease-out both', 'pulse-slow': 'pulse-slow 1.5s ease-in-out infinite', + 'slide-up': 'slide-up 200ms ease-out both', }, colors: { qwen: {