refactor(vscode-ide-companion): update message handling and configuration

This commit is contained in:
yiliang114
2025-12-05 18:03:46 +08:00
parent ac9cb3a6d3
commit be44e7af56
6 changed files with 92 additions and 31 deletions

View File

@@ -378,7 +378,29 @@ export class SessionMessageHandler extends BaseMessageHandler {
} catch (error) { } catch (error) {
console.error('[SessionMessageHandler] Error sending message:', error); console.error('[SessionMessageHandler] Error sending message:', error);
const err = error as unknown as Error;
const errorMsg = String(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 // Check for session not found error and handle it appropriately
if ( if (
errorMsg.includes('Session not found') || errorMsg.includes('Session not found') ||

View File

@@ -105,7 +105,6 @@ export const useMessageHandling = () => {
const endStreaming = useCallback(() => { const endStreaming = useCallback(() => {
// Finalize streaming; content already lives in the placeholder message // Finalize streaming; content already lives in the placeholder message
setIsStreaming(false); setIsStreaming(false);
setIsWaitingForResponse(false);
streamingMessageIndexRef.current = null; streamingMessageIndexRef.current = null;
// Remove the thinking message if it exists (collapse thoughts) // Remove the thinking message if it exists (collapse thoughts)
setMessages((prev) => { setMessages((prev) => {

View File

@@ -14,6 +14,8 @@ interface UseMessageSubmitProps {
setInputText: (text: string) => void; setInputText: (text: string) => void;
inputFieldRef: React.RefObject<HTMLDivElement>; inputFieldRef: React.RefObject<HTMLDivElement>;
isStreaming: boolean; isStreaming: boolean;
// When true, do NOT auto-attach the active editor file/selection to context
skipAutoActiveContext?: boolean;
fileContext: { fileContext: {
getFileReference: (fileName: string) => string | undefined; getFileReference: (fileName: string) => string | undefined;
@@ -38,6 +40,7 @@ export const useMessageSubmit = ({
setInputText, setInputText,
inputFieldRef, inputFieldRef,
isStreaming, isStreaming,
skipAutoActiveContext = false,
fileContext, fileContext,
messageHandling, messageHandling,
}: UseMessageSubmitProps) => { }: UseMessageSubmitProps) => {
@@ -94,8 +97,8 @@ export const useMessageSubmit = ({
} }
} }
// Add active file selection context if present // Add active file selection context if present and not skipped
if (fileContext.activeFilePath) { if (fileContext.activeFilePath && !skipAutoActiveContext) {
const fileName = fileContext.activeFileName || 'current file'; const fileName = fileContext.activeFileName || 'current file';
context.push({ context.push({
type: 'file', type: 'file',
@@ -115,7 +118,11 @@ export const useMessageSubmit = ({
} }
| undefined; | undefined;
if (fileContext.activeFilePath && fileContext.activeFileName) { if (
fileContext.activeFilePath &&
fileContext.activeFileName &&
!skipAutoActiveContext
) {
fileContextForMessage = { fileContextForMessage = {
fileName: fileContext.activeFileName, fileName: fileContext.activeFileName,
filePath: fileContext.activeFilePath, filePath: fileContext.activeFilePath,
@@ -146,6 +153,7 @@ export const useMessageSubmit = ({
inputFieldRef, inputFieldRef,
vscode, vscode,
fileContext, fileContext,
skipAutoActiveContext,
messageHandling, messageHandling,
], ],
); );

View File

@@ -112,6 +112,9 @@ export const useWebViewMessages = ({
}: UseWebViewMessagesProps) => { }: UseWebViewMessagesProps) => {
// VS Code API for posting messages back to the extension host // VS Code API for posting messages back to the extension host
const vscode = useVSCode(); 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<Set<string>>(new Set());
// Use ref to store callbacks to avoid useEffect dependency issues // Use ref to store callbacks to avoid useEffect dependency issues
const handlersRef = useRef({ const handlersRef = useRef({
sessionManagement, sessionManagement,
@@ -260,14 +263,19 @@ export const useWebViewMessages = ({
// no-op: stream might not have been started // no-op: stream might not have been started
console.warn('[PanelManager] Failed to end streaming:', err); console.warn('[PanelManager] Failed to end streaming:', err);
} }
try { // Important: Do NOT blindly clear the waiting message if there are
handlers.messageHandling.clearWaitingForResponse(); // still active tool calls running. We keep the waiting indicator
} catch (err) { // tied to tool-call lifecycle instead.
// no-op: already cleared if (activeExecToolCallsRef.current.size === 0) {
console.warn( try {
'[PanelManager] Failed to clear waiting for response:', handlers.messageHandling.clearWaitingForResponse();
err, } catch (err) {
); // no-op: already cleared
console.warn(
'[PanelManager] Failed to clear waiting for response:',
err,
);
}
} }
} }
break; break;
@@ -293,6 +301,11 @@ export const useWebViewMessages = ({
case 'streamEnd': case 'streamEnd':
handlers.messageHandling.endStreaming(); handlers.messageHandling.endStreaming();
handlers.messageHandling.clearThinking(); 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; break;
case 'error': case 'error':
@@ -439,20 +452,33 @@ export const useWebViewMessages = ({
const kind = (toolCallData.kind || '').toString().toLowerCase(); const kind = (toolCallData.kind || '').toString().toLowerCase();
const isExec = const isExec =
kind === 'execute' || kind === 'bash' || kind === 'command'; kind === 'execute' || kind === 'bash' || kind === 'command';
if (isExec && (status === 'pending' || status === 'in_progress')) {
const rawInput = toolCallData.rawInput; if (isExec) {
let cmd = ''; const id = (toolCallData.toolCallId || '').toString();
if (typeof rawInput === 'string') {
cmd = rawInput; // Maintain the active set by status
} else if (rawInput && typeof rawInput === 'object') { if (status === 'pending' || status === 'in_progress') {
const maybe = rawInput as { command?: string }; activeExecToolCallsRef.current.add(id);
cmd = maybe.command || '';
// 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) { } catch (_err) {
// Best-effort UI hint; ignore errors // Best-effort UI hint; ignore errors

View File

@@ -6,10 +6,10 @@
export function isDevelopmentMode(): boolean { export function isDevelopmentMode(): boolean {
// TODO: 调试用 // TODO: 调试用
return false; // return false;
// return ( return (
// process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'development' ||
// process.env.DEBUG === 'true' || process.env.DEBUG === 'true' ||
// process.env.NODE_ENV !== 'production' process.env.NODE_ENV !== 'production'
// ); );
} }

View File

@@ -34,10 +34,16 @@ export default {
'0%, 100%': { opacity: '1' }, '0%, 100%': { opacity: '1' },
'50%': { opacity: '0.5' }, '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: { animation: {
'completion-menu-enter': 'completion-menu-enter 150ms ease-out both', 'completion-menu-enter': 'completion-menu-enter 150ms ease-out both',
'pulse-slow': 'pulse-slow 1.5s ease-in-out infinite', 'pulse-slow': 'pulse-slow 1.5s ease-in-out infinite',
'slide-up': 'slide-up 200ms ease-out both',
}, },
colors: { colors: {
qwen: { qwen: {