From 541d0b22e52b039f0c45d8192a5690e178ca2074 Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Sat, 6 Dec 2025 01:32:52 +0800 Subject: [PATCH 1/2] chore(vscode-ide-companion): code style & command register bugfix --- packages/vscode-ide-companion/package.json | 16 +- .../src/acp/acpConnection.ts | 17 -- .../src/agents/qwenTypes.ts | 2 +- .../src/cli/cliVersionManager.ts | 3 - .../src/commands/index.ts | 47 ++-- .../src/constants/acpSchema.ts | 29 --- .../vscode-ide-companion/src/webview/App.tsx | 6 +- .../src/webview/WebViewProvider.ts | 3 +- .../MarkdownRenderer/MarkdownRenderer.css | 6 +- .../MarkdownRenderer/MarkdownRenderer.tsx | 48 +++- .../PermissionDrawer.tsx | 0 .../PermissionDrawer/PermissionRequest.tsx | 37 +++ .../webview/components/PermissionRequest.tsx | 227 ------------------ .../src/webview/components/PlanDisplay.tsx | 98 -------- .../components/messages/UserMessage.tsx | 2 +- .../src/webview/components/messages/index.tsx | 1 - .../toolcalls/Edit/EditToolCall.tsx | 69 +++++- .../toolcalls/Read/ReadToolCall.tsx | 4 +- .../UpdatedPlan/UpdatedPlanToolCall.tsx | 6 +- .../webview/components/ui/CheckboxDisplay.tsx | 4 +- .../webview/handlers/AuthMessageHandler.ts | 2 +- .../webview/handlers/SessionMessageHandler.ts | 30 +-- .../src/webview/hooks/useWebViewMessages.ts | 4 +- .../vscode-ide-companion/tailwind.config.js | 20 +- 24 files changed, 222 insertions(+), 459 deletions(-) rename packages/vscode-ide-companion/src/webview/components/{ => PermissionDrawer}/PermissionDrawer.tsx (100%) create mode 100644 packages/vscode-ide-companion/src/webview/components/PermissionDrawer/PermissionRequest.tsx delete mode 100644 packages/vscode-ide-companion/src/webview/components/PermissionRequest.tsx delete mode 100644 packages/vscode-ide-companion/src/webview/components/PlanDisplay.tsx diff --git a/packages/vscode-ide-companion/package.json b/packages/vscode-ide-companion/package.json index fc9d7836..6b132258 100644 --- a/packages/vscode-ide-companion/package.json +++ b/packages/vscode-ide-companion/package.json @@ -56,17 +56,17 @@ "title": "Qwen Code: View Third-Party Notices" }, { - "command": "qwenCode.openChat", + "command": "qwen-code.openChat", "title": "Qwen Code: Open Chat", "icon": "./assets/icon.png" }, { - "command": "qwenCode.clearAuthCache", - "title": "Qwen Code: Clear Authentication Cache" + "command": "qwen-code.login", + "title": "Qwen Code: Login" }, { - "command": "qwenCode.login", - "title": "Qwen Code: Login" + "command": "qwen-code.clearAuthCache", + "title": "Qwen Code: Clear Authentication Cache" } ], "configuration": { @@ -90,7 +90,7 @@ "when": "qwen.diff.isVisible" }, { - "command": "qwenCode.login", + "command": "qwen-code.login", "when": "false" } ], @@ -106,7 +106,7 @@ "group": "navigation" }, { - "command": "qwenCode.openChat", + "command": "qwen-code.openChat", "group": "navigation" } ] @@ -123,7 +123,7 @@ "when": "qwen.diff.isVisible" }, { - "command": "qwenCode.openChat", + "command": "qwen-code.openChat", "key": "ctrl+shift+a", "mac": "cmd+shift+a" } diff --git a/packages/vscode-ide-companion/src/acp/acpConnection.ts b/packages/vscode-ide-companion/src/acp/acpConnection.ts index 38b48fc4..27a111c6 100644 --- a/packages/vscode-ide-companion/src/acp/acpConnection.ts +++ b/packages/vscode-ide-companion/src/acp/acpConnection.ts @@ -26,23 +26,6 @@ import { determineNodePathForCli } from '../cli/cliPathDetector.js'; * ACP Connection Handler for VSCode Extension * * This class implements the client side of the ACP (Agent Communication Protocol). - * - * Implementation Status: - * - * Client Methods (Methods this class implements, called by CLI): - * ✅ session/update - Handle session updates via onSessionUpdate callback - * ✅ session/request_permission - Request user permission for tool execution - * ✅ fs/read_text_file - Read file from workspace - * ✅ fs/write_text_file - Write file to workspace - * - * Agent Methods (Methods CLI implements, called by this class): - * ✅ initialize - Initialize ACP protocol connection - * ✅ authenticate - Authenticate with selected auth method - * ✅ session/new - Create new chat session - * ✅ session/prompt - Send user message to agent - * ✅ session/cancel - Cancel current generation - * ✅ session/load - Load previous session - * ✅ session/save - Save current session */ export class AcpConnection { private child: ChildProcess | null = null; diff --git a/packages/vscode-ide-companion/src/agents/qwenTypes.ts b/packages/vscode-ide-companion/src/agents/qwenTypes.ts index 6cb5c625..2ac22c04 100644 --- a/packages/vscode-ide-companion/src/agents/qwenTypes.ts +++ b/packages/vscode-ide-companion/src/agents/qwenTypes.ts @@ -18,7 +18,7 @@ export interface PlanEntry { /** Entry content */ content: string; /** Priority */ - priority: 'high' | 'medium' | 'low'; + priority?: 'high' | 'medium' | 'low'; /** Status */ status: 'pending' | 'in_progress' | 'completed'; } diff --git a/packages/vscode-ide-companion/src/cli/cliVersionManager.ts b/packages/vscode-ide-companion/src/cli/cliVersionManager.ts index acf76bf6..383ae638 100644 --- a/packages/vscode-ide-companion/src/cli/cliVersionManager.ts +++ b/packages/vscode-ide-companion/src/cli/cliVersionManager.ts @@ -6,9 +6,6 @@ import { CliDetector, type CliDetectionResult } from './cliDetector.js'; -/** - * Minimum CLI version that supports session/list and session/load ACP methods - */ export const MIN_CLI_VERSION_FOR_SESSION_METHODS = '0.4.0'; /** diff --git a/packages/vscode-ide-companion/src/commands/index.ts b/packages/vscode-ide-companion/src/commands/index.ts index ef432825..7ca510a7 100644 --- a/packages/vscode-ide-companion/src/commands/index.ts +++ b/packages/vscode-ide-companion/src/commands/index.ts @@ -4,8 +4,12 @@ import type { WebViewProvider } from '../webview/WebViewProvider.js'; type Logger = (message: string) => void; +export const runQwenCodeCommand = 'qwen-code.runQwenCode'; export const showDiffCommand = 'qwenCode.showDiff'; -export const openChatCommand = 'qwenCode.openChat'; +export const openChatCommand = 'qwen-code.openChat'; +export const openNewChatTabCommand = 'qwenCode.openNewChatTab'; +export const loginCommand = 'qwen-code.login'; +export const clearAuthCacheCommand = 'qwen-code.clearAuthCache'; export function registerNewCommands( context: vscode.ExtensionContext, @@ -20,15 +24,15 @@ export function registerNewCommands( vscode.commands.registerCommand(openChatCommand, async () => { const config = vscode.workspace.getConfiguration('qwenCode'); const useTerminal = config.get('useTerminal', false); - console.log('[Command] Using terminal mode:', useTerminal); + + // Use terminal mode if (useTerminal) { - // 使用终端模式 await vscode.commands.executeCommand( - 'qwen-code.runQwenCode', - vscode.TerminalLocation.Editor, // 在编辑器区域创建终端, + runQwenCodeCommand, + vscode.TerminalLocation.Editor, // create a terminal in the editor area, ); } else { - // 使用 WebView 模式 + // Use WebView mode const providers = getWebViewProviders(); if (providers.length > 0) { await providers[providers.length - 1].show(); @@ -44,7 +48,6 @@ export function registerNewCommands( vscode.commands.registerCommand( showDiffCommand, async (args: { path: string; oldText: string; newText: string }) => { - log(`[Command] showDiff called for: ${args.path}`); try { let absolutePath = args.path; if (!args.path.startsWith('/') && !args.path.match(/^[a-zA-Z]:/)) { @@ -68,27 +71,14 @@ export function registerNewCommands( // TODO: qwenCode.openNewChatTab (not contributed in package.json; used programmatically) disposables.push( - vscode.commands.registerCommand('qwenCode.openNewChatTab', async () => { + vscode.commands.registerCommand(openNewChatTabCommand, async () => { const provider = createWebViewProvider(); await provider.show(); }), ); disposables.push( - vscode.commands.registerCommand('qwenCode.clearAuthCache', async () => { - const providers = getWebViewProviders(); - for (const provider of providers) { - await provider.clearAuthCache(); - } - vscode.window.showInformationMessage( - 'Qwen Code authentication cache cleared. You will need to login again on next connection.', - ); - log('Auth cache cleared by user'); - }), - ); - - disposables.push( - vscode.commands.registerCommand('qwenCode.login', async () => { + vscode.commands.registerCommand(loginCommand, async () => { const providers = getWebViewProviders(); if (providers.length > 0) { await providers[providers.length - 1].forceReLogin(); @@ -100,5 +90,18 @@ export function registerNewCommands( }), ); + disposables.push( + vscode.commands.registerCommand(clearAuthCacheCommand, async () => { + const providers = getWebViewProviders(); + for (const provider of providers) { + await provider.clearAuthCache(); + } + vscode.window.showInformationMessage( + 'Qwen Code authentication cache cleared. You will need to login again on next connection.', + ); + log('Auth cache cleared by user'); + }), + ); + context.subscriptions.push(...disposables); } diff --git a/packages/vscode-ide-companion/src/constants/acpSchema.ts b/packages/vscode-ide-companion/src/constants/acpSchema.ts index 5f826f97..65125b63 100644 --- a/packages/vscode-ide-companion/src/constants/acpSchema.ts +++ b/packages/vscode-ide-companion/src/constants/acpSchema.ts @@ -4,26 +4,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -/** - * ACP (Agent Communication Protocol) Method Definitions - * - * This file defines the protocol methods for communication between - * the VSCode extension (Client) and the qwen CLI (Agent/Server). - */ - -/** - * Methods that the Agent (CLI) implements and receives from Client (VSCode) - * - * Status in qwen CLI: - * ✅ initialize - Protocol initialization - * ✅ authenticate - User authentication - * ✅ session/new - Create new session - * ✅ session/load - Load existing session (v0.2.4+) - * ✅ session/list - List available sessions (v0.2.4+) - * ✅ session/prompt - Send user message to agent - * ✅ session/cancel - Cancel current generation - * ✅ session/save - Save current session - */ export const AGENT_METHODS = { authenticate: 'authenticate', initialize: 'initialize', @@ -35,15 +15,6 @@ export const AGENT_METHODS = { session_save: 'session/save', } as const; -/** - * Methods that the Client (VSCode) implements and receives from Agent (CLI) - * - * Status in VSCode extension: - * ✅ fs/read_text_file - Read file content - * ✅ fs/write_text_file - Write file content - * ✅ session/request_permission - Request user permission for tool execution - * ✅ session/update - Stream session updates (notification) - */ export const CLIENT_METHODS = { fs_read_text_file: 'fs/read_text_file', fs_write_text_file: 'fs/write_text_file', diff --git a/packages/vscode-ide-companion/src/webview/App.tsx b/packages/vscode-ide-companion/src/webview/App.tsx index 0a15af83..f4b853be 100644 --- a/packages/vscode-ide-companion/src/webview/App.tsx +++ b/packages/vscode-ide-companion/src/webview/App.tsx @@ -21,15 +21,14 @@ import { useMessageSubmit } from './hooks/useMessageSubmit.js'; import type { PermissionOption, ToolCall as PermissionToolCall, -} from './components/PermissionRequest.js'; +} from './components/PermissionDrawer/PermissionRequest.js'; import type { TextMessage } from './hooks/message/useMessageHandling.js'; import type { ToolCallData } from './components/ToolCall.js'; -import { PermissionDrawer } from './components/PermissionDrawer.js'; +import { PermissionDrawer } from './components/PermissionDrawer/PermissionDrawer.js'; import { ToolCall } from './components/ToolCall.js'; import { hasToolCallOutput } from './components/toolcalls/shared/utils.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'; import { useCompletionTrigger } from './hooks/useCompletionTrigger.js'; import { InfoBanner } from './components/ui/InfoBanner.js'; @@ -45,6 +44,7 @@ import { InputForm } from './components/InputForm.js'; import { SessionSelector } from './components/session/SessionSelector.js'; import { FileIcon, UserIcon } from './components/icons/index.js'; import type { EditMode } from './types/toolCall.js'; +import type { PlanEntry } from '../agents/qwenTypes.js'; export const App: React.FC = () => { const vscode = useVSCode(); diff --git a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts index 5d97ac31..f7b52d5c 100644 --- a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts +++ b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts @@ -16,6 +16,7 @@ import { WebViewContent } from '../webview/WebViewContent.js'; import { CliInstaller } from '../cli/cliInstaller.js'; import { getFileName } from './utils/webviewUtils.js'; import { authMethod } from '../auth/index.js'; +import { runQwenCodeCommand } from '../commands/index.js'; export class WebViewProvider { private panelManager: PanelManager; @@ -1067,7 +1068,7 @@ export class WebViewProvider { if (useTerminal) { // In terminal mode, execute the runQwenCode command to open a new terminal try { - await vscode.commands.executeCommand('qwen-code.runQwenCode'); + await vscode.commands.executeCommand(runQwenCodeCommand); console.log('[WebViewProvider] Opened new terminal session'); } catch (error) { console.error( diff --git a/packages/vscode-ide-companion/src/webview/components/MarkdownRenderer/MarkdownRenderer.css b/packages/vscode-ide-companion/src/webview/components/MarkdownRenderer/MarkdownRenderer.css index 8734887b..1382483d 100644 --- a/packages/vscode-ide-companion/src/webview/components/MarkdownRenderer/MarkdownRenderer.css +++ b/packages/vscode-ide-companion/src/webview/components/MarkdownRenderer/MarkdownRenderer.css @@ -135,7 +135,8 @@ border: 1px solid var(--app-primary-border-color); border-radius: var(--corner-radius-small, 4px); padding: 0.2em 0.4em; - white-space: nowrap; + white-space: pre-wrap; /* 支持自动换行 */ + word-break: break-word; /* 在必要时断词 */ } .markdown-content pre { @@ -207,7 +208,8 @@ background: none; border: none; padding: 0; - white-space: pre; + white-space: pre-wrap; /* 支持自动换行 */ + word-break: break-word; /* 在必要时断词 */ } .markdown-content .file-path-link { diff --git a/packages/vscode-ide-companion/src/webview/components/MarkdownRenderer/MarkdownRenderer.tsx b/packages/vscode-ide-companion/src/webview/components/MarkdownRenderer/MarkdownRenderer.tsx index 6e852e52..09c41652 100644 --- a/packages/vscode-ide-companion/src/webview/components/MarkdownRenderer/MarkdownRenderer.tsx +++ b/packages/vscode-ide-companion/src/webview/components/MarkdownRenderer/MarkdownRenderer.tsx @@ -19,11 +19,12 @@ interface MarkdownRendererProps { /** * Regular expressions for parsing content */ +// Match absolute file paths like: /path/to/file.ts or C:\path\to\file.ts 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 + /(?:[a-zA-Z]:)?[/\\](?:[\w\-. ]+[/\\])+[\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/to/file.ts#7-14 or C:\path\to\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; + /(?:[a-zA-Z]:)?[/\\](?:[\w\-. ]+[/\\])+[\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 @@ -166,9 +167,22 @@ export const MarkdownRenderer: React.FC = ({ const href = a.getAttribute('href') || ''; const text = (a.textContent || '').trim(); + // Helper function to check if a string looks like a code reference + const isCodeReference = (str: string): boolean => { + // Check if it looks like a code reference (e.g., module.property) + // Patterns like "vscode.contribution", "module.submodule.function" + const codeRefPattern = /^[a-zA-Z_$][\w$]*(\.[a-zA-Z_$][\w$]*)+$/; + return codeRefPattern.test(str); + }; + // 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) { + // Skip if it looks like a code reference + if (isCodeReference(text)) { + return; + } + // Treat as a file link instead of external URL const filePath = text; // no leading slash a.classList.add('file-path-link'); @@ -182,6 +196,12 @@ export const MarkdownRenderer: React.FC = ({ if (/^(https?|mailto|ftp|data):/i.test(href)) return; const candidate = href || text; + + // Skip if it looks like a code reference + if (isCodeReference(candidate)) { + return; + } + if ( FILE_PATH_WITH_LINES_NO_G.test(candidate) || FILE_PATH_NO_G.test(candidate) @@ -194,6 +214,14 @@ export const MarkdownRenderer: React.FC = ({ } }; + // Helper function to check if a string looks like a code reference + const isCodeReference = (str: string): boolean => { + // Check if it looks like a code reference (e.g., module.property) + // Patterns like "vscode.contribution", "module.submodule.function" + const codeRefPattern = /^[a-zA-Z_$][\w$]*(\.[a-zA-Z_$][\w$]*)+$/; + return codeRefPattern.test(str); + }; + const walk = (node: Node) => { // Do not transform inside existing anchors if (node.nodeType === Node.ELEMENT_NODE) { @@ -218,6 +246,20 @@ export const MarkdownRenderer: React.FC = ({ while ((m = union.exec(text))) { const matchText = m[0]; const idx = m.index; + + // Skip if it looks like a code reference + if (isCodeReference(matchText)) { + // Just add the text as-is without creating a link + if (idx > lastIndex) { + frag.appendChild( + document.createTextNode(text.slice(lastIndex, idx)), + ); + } + frag.appendChild(document.createTextNode(matchText)); + lastIndex = idx + matchText.length; + continue; + } + if (idx > lastIndex) { frag.appendChild( document.createTextNode(text.slice(lastIndex, idx)), diff --git a/packages/vscode-ide-companion/src/webview/components/PermissionDrawer.tsx b/packages/vscode-ide-companion/src/webview/components/PermissionDrawer/PermissionDrawer.tsx similarity index 100% rename from packages/vscode-ide-companion/src/webview/components/PermissionDrawer.tsx rename to packages/vscode-ide-companion/src/webview/components/PermissionDrawer/PermissionDrawer.tsx diff --git a/packages/vscode-ide-companion/src/webview/components/PermissionDrawer/PermissionRequest.tsx b/packages/vscode-ide-companion/src/webview/components/PermissionDrawer/PermissionRequest.tsx new file mode 100644 index 00000000..a7b7356c --- /dev/null +++ b/packages/vscode-ide-companion/src/webview/components/PermissionDrawer/PermissionRequest.tsx @@ -0,0 +1,37 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +export interface PermissionOption { + name: string; + kind: string; + optionId: string; +} + +export interface ToolCall { + title?: string; + kind?: string; + toolCallId?: string; + rawInput?: { + command?: string; + description?: string; + [key: string]: unknown; + }; + content?: Array<{ + type: string; + [key: string]: unknown; + }>; + locations?: Array<{ + path: string; + line?: number | null; + }>; + status?: string; +} + +export interface PermissionRequestProps { + options: PermissionOption[]; + toolCall: ToolCall; + onResponse: (optionId: string) => void; +} diff --git a/packages/vscode-ide-companion/src/webview/components/PermissionRequest.tsx b/packages/vscode-ide-companion/src/webview/components/PermissionRequest.tsx deleted file mode 100644 index 78016e06..00000000 --- a/packages/vscode-ide-companion/src/webview/components/PermissionRequest.tsx +++ /dev/null @@ -1,227 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - */ - -export interface PermissionOption { - name: string; - kind: string; - optionId: string; -} - -export interface ToolCall { - title?: string; - kind?: string; - toolCallId?: string; - rawInput?: { - command?: string; - description?: string; - [key: string]: unknown; - }; - content?: Array<{ - type: string; - [key: string]: unknown; - }>; - locations?: Array<{ - path: string; - line?: number | null; - }>; - status?: string; -} - -export interface PermissionRequestProps { - options: PermissionOption[]; - toolCall: ToolCall; - 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); - -// const getToolInfo = () => { -// if (!toolCall) { -// return { -// title: 'Permission Request', -// description: 'Agent is requesting permission', -// icon: '🔐', -// }; -// } - -// const displayTitle = -// toolCall.title || toolCall.rawInput?.description || 'Permission Request'; - -// const kindIcons: Record = { -// edit: '✏️', -// read: '📖', -// fetch: '🌐', -// execute: '⚡', -// delete: '🗑️', -// move: '📦', -// search: '🔍', -// think: '💭', -// other: '🔧', -// }; - -// return { -// title: displayTitle, -// icon: kindIcons[toolCall.kind || 'other'] || '🔧', -// }; -// }; - -// const { title, icon } = getToolInfo(); - -// 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); -// } -// }; - -// if (!toolCall) { -// return null; -// } - -// 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 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'); - -// return ( -// -// ); -// }) -// ) : ( -//
-// No options available -//
-// )} -//
-//
-// -//
-//
-// )} - -// {/* Success message */} -// {hasResponded && ( -//
-// -// -// Response sent successfully -// -//
-// )} -//
-//
-// ); -// }; diff --git a/packages/vscode-ide-companion/src/webview/components/PlanDisplay.tsx b/packages/vscode-ide-companion/src/webview/components/PlanDisplay.tsx deleted file mode 100644 index 046e4d8a..00000000 --- a/packages/vscode-ide-companion/src/webview/components/PlanDisplay.tsx +++ /dev/null @@ -1,98 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - */ - -import type React from 'react'; -import { CheckboxDisplay } from './ui/CheckboxDisplay.js'; - -export interface PlanEntry { - content: string; - priority: 'high' | 'medium' | 'low'; - status: 'pending' | 'in_progress' | 'completed'; -} - -interface PlanDisplayProps { - entries: PlanEntry[]; -} - -/** - * PlanDisplay component - displays AI's task plan/todo list - */ -export const PlanDisplay: React.FC = ({ 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 69086197..102b2756 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/index.tsx b/packages/vscode-ide-companion/src/webview/components/messages/index.tsx index 33bfe6c7..a8c7224a 100644 --- a/packages/vscode-ide-companion/src/webview/components/messages/index.tsx +++ b/packages/vscode-ide-companion/src/webview/components/messages/index.tsx @@ -10,4 +10,3 @@ export { ThinkingMessage } from './ThinkingMessage.js'; export { StreamingMessage } from './StreamingMessage.js'; export { WaitingMessage } from './Waiting/WaitingMessage.js'; export { InterruptedMessage } from './Waiting/InterruptedMessage.js'; -export { PlanDisplay } from '../PlanDisplay.js'; diff --git a/packages/vscode-ide-companion/src/webview/components/toolcalls/Edit/EditToolCall.tsx b/packages/vscode-ide-companion/src/webview/components/toolcalls/Edit/EditToolCall.tsx index 543bc745..29d6b762 100644 --- a/packages/vscode-ide-companion/src/webview/components/toolcalls/Edit/EditToolCall.tsx +++ b/packages/vscode-ide-companion/src/webview/components/toolcalls/Edit/EditToolCall.tsx @@ -16,6 +16,7 @@ import { import { useVSCode } from '../../../hooks/useVSCode.js'; import { FileLink } from '../../ui/FileLink.js'; import { handleOpenDiff } from '../../../utils/diffUtils.js'; +import { DiffDisplay } from '../shared/DiffDisplay.js'; /** * Calculate diff summary (added/removed lines) @@ -85,6 +86,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 || ''; @@ -99,7 +158,7 @@ export const EditToolCall: React.FC = ({ toolCall }) => { ) : undefined } @@ -118,21 +177,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/Read/ReadToolCall.tsx b/packages/vscode-ide-companion/src/webview/components/toolcalls/Read/ReadToolCall.tsx index 56a6aafd..3892bc00 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 @@ -101,7 +101,7 @@ export const ReadToolCall: React.FC = ({ toolCall }) => { return ( = ({ toolCall }) => { return ( { 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 243abec4..fe413a61 100644 --- a/packages/vscode-ide-companion/src/webview/components/ui/CheckboxDisplay.tsx +++ b/packages/vscode-ide-companion/src/webview/components/ui/CheckboxDisplay.tsx @@ -71,9 +71,9 @@ export const CheckboxDisplay: React.FC = ({ aria-hidden className={[ 'absolute inline-block', - 'left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2', + 'left-1/2 top-10px -translate-x-1/2 -translate-y-1/2', // Use a literal star; no icon font needed - 'text-[11px] leading-none text-[#e1c08d] select-none', + 'text-[16px] leading-none text-[#e1c08d] select-none', ].join(' ')} > * diff --git a/packages/vscode-ide-companion/src/webview/handlers/AuthMessageHandler.ts b/packages/vscode-ide-companion/src/webview/handlers/AuthMessageHandler.ts index dce9aed6..ab4b70b2 100644 --- a/packages/vscode-ide-companion/src/webview/handlers/AuthMessageHandler.ts +++ b/packages/vscode-ide-companion/src/webview/handlers/AuthMessageHandler.ts @@ -64,7 +64,7 @@ export class AuthMessageHandler extends BaseMessageHandler { vscode.window.showInformationMessage( 'Please wait while we connect to Qwen Code...', ); - await vscode.commands.executeCommand('qwenCode.login'); + await vscode.commands.executeCommand('qwen-code.login'); } } catch (error) { console.error('[AuthMessageHandler] Login failed:', error); diff --git a/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts b/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts index dfd0fd75..ccde5337 100644 --- a/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts +++ b/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts @@ -280,7 +280,7 @@ export class SessionMessageHandler extends BaseMessageHandler { vscode.window.showInformationMessage( 'Please wait while we connect to Qwen Code...', ); - await vscode.commands.executeCommand('qwenCode.login'); + await vscode.commands.executeCommand('qwen-code.login'); } } return; @@ -306,7 +306,7 @@ export class SessionMessageHandler extends BaseMessageHandler { vscode.window.showInformationMessage( 'Please wait while we connect to Qwen Code...', ); - await vscode.commands.executeCommand('qwenCode.login'); + await vscode.commands.executeCommand('qwen-code.login'); } } return; @@ -420,7 +420,7 @@ export class SessionMessageHandler extends BaseMessageHandler { if (this.loginHandler) { await this.loginHandler(); } else { - await vscode.commands.executeCommand('qwenCode.login'); + await vscode.commands.executeCommand('qwen-code.login'); } } @@ -456,7 +456,7 @@ export class SessionMessageHandler extends BaseMessageHandler { if (this.loginHandler) { await this.loginHandler(); } else { - await vscode.commands.executeCommand('qwenCode.login'); + await vscode.commands.executeCommand('qwen-code.login'); } } else { return; @@ -514,7 +514,7 @@ export class SessionMessageHandler extends BaseMessageHandler { if (this.loginHandler) { await this.loginHandler(); } else { - await vscode.commands.executeCommand('qwenCode.login'); + await vscode.commands.executeCommand('qwen-code.login'); } } @@ -551,7 +551,7 @@ export class SessionMessageHandler extends BaseMessageHandler { if (this.loginHandler) { await this.loginHandler(); } else { - await vscode.commands.executeCommand('qwenCode.login'); + await vscode.commands.executeCommand('qwen-code.login'); } } else if (selection === 'View Offline') { // Show messages from local cache only @@ -653,7 +653,7 @@ export class SessionMessageHandler extends BaseMessageHandler { if (this.loginHandler) { await this.loginHandler(); } else { - await vscode.commands.executeCommand('qwenCode.login'); + await vscode.commands.executeCommand('qwen-code.login'); } } @@ -709,7 +709,7 @@ export class SessionMessageHandler extends BaseMessageHandler { if (this.loginHandler) { await this.loginHandler(); } else { - await vscode.commands.executeCommand('qwenCode.login'); + await vscode.commands.executeCommand('qwen-code.login'); } } @@ -757,7 +757,7 @@ export class SessionMessageHandler extends BaseMessageHandler { if (this.loginHandler) { await this.loginHandler(); } else { - await vscode.commands.executeCommand('qwenCode.login'); + await vscode.commands.executeCommand('qwen-code.login'); } } @@ -807,7 +807,7 @@ export class SessionMessageHandler extends BaseMessageHandler { if (this.loginHandler) { await this.loginHandler(); } else { - await vscode.commands.executeCommand('qwenCode.login'); + await vscode.commands.executeCommand('qwen-code.login'); } } @@ -870,7 +870,7 @@ export class SessionMessageHandler extends BaseMessageHandler { if (this.loginHandler) { await this.loginHandler(); } else { - await vscode.commands.executeCommand('qwenCode.login'); + await vscode.commands.executeCommand('qwen-code.login'); } } @@ -917,7 +917,7 @@ export class SessionMessageHandler extends BaseMessageHandler { if (this.loginHandler) { await this.loginHandler(); } else { - await vscode.commands.executeCommand('qwenCode.login'); + await vscode.commands.executeCommand('qwen-code.login'); } } @@ -983,7 +983,7 @@ export class SessionMessageHandler extends BaseMessageHandler { if (this.loginHandler) { await this.loginHandler(); } else { - await vscode.commands.executeCommand('qwenCode.login'); + await vscode.commands.executeCommand('qwen-code.login'); } } else if (selection === 'View Offline') { const messages = @@ -1034,7 +1034,7 @@ export class SessionMessageHandler extends BaseMessageHandler { if (this.loginHandler) { await this.loginHandler(); } else { - await vscode.commands.executeCommand('qwenCode.login'); + await vscode.commands.executeCommand('qwen-code.login'); } } @@ -1084,7 +1084,7 @@ export class SessionMessageHandler extends BaseMessageHandler { if (this.loginHandler) { await this.loginHandler(); } else { - await vscode.commands.executeCommand('qwenCode.login'); + await vscode.commands.executeCommand('qwen-code.login'); } } diff --git a/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts b/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts index 4cb3eb78..53ed7468 100644 --- a/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts +++ b/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts @@ -10,9 +10,9 @@ import type { Conversation } from '../../storage/conversationStore.js'; import type { PermissionOption, ToolCall as PermissionToolCall, -} from '../components/PermissionRequest.js'; -import type { PlanEntry } from '../components/PlanDisplay.js'; +} from '../components/PermissionDrawer/PermissionRequest.js'; import type { ToolCallUpdate } from '../types/toolCall.js'; +import type { PlanEntry } from '../../agents/qwenTypes.js'; interface UseWebViewMessagesProps { // Session management diff --git a/packages/vscode-ide-companion/tailwind.config.js b/packages/vscode-ide-companion/tailwind.config.js index 72ea91b9..26e28794 100644 --- a/packages/vscode-ide-companion/tailwind.config.js +++ b/packages/vscode-ide-companion/tailwind.config.js @@ -9,17 +9,17 @@ export default { content: [ // Progressive adoption strategy: Only scan newly created Tailwind components - // './src/webview/App.tsx', + './src/webview/App.tsx', './src/webview/**/*.{js,jsx,ts,tsx}', - // './src/webview/components/messages/**/*.{js,jsx,ts,tsx}', - // './src/webview/components/toolcalls/**/*.{js,jsx,ts,tsx}', - // './src/webview/components/InProgressToolCall.tsx', - // './src/webview/components/MessageContent.tsx', - // './src/webview/components/InputForm.tsx', - // './src/webview/components/PermissionDrawer.tsx', - // './src/webview/components/PlanDisplay.tsx', - // './src/webview/components/session/SessionSelector.tsx', - // './src/webview/components/messages/UserMessage.tsx', + './src/webview/components/messages/**/*.{js,jsx,ts,tsx}', + './src/webview/components/toolcalls/**/*.{js,jsx,ts,tsx}', + './src/webview/components/InProgressToolCall.tsx', + './src/webview/components/MessageContent.tsx', + './src/webview/components/InputForm.tsx', + './src/webview/components/PermissionDrawer.tsx', + './src/webview/components/PlanDisplay.tsx', + './src/webview/components/session/SessionSelector.tsx', + './src/webview/components/messages/UserMessage.tsx', ], theme: { extend: { From 57a684ad97fbd8312100d263365124bf13d2bed3 Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Sat, 6 Dec 2025 16:53:40 +0800 Subject: [PATCH 2/2] WIP: All changes including session and toolcall improvements --- .../src/acp/acpConnection.ts | 11 +- .../src/acp/acpSessionManager.ts | 17 +- .../src/agents/qwenAgentManager.ts | 212 ++++++++++++++++-- .../src/agents/qwenConnectionHandler.ts | 54 +---- .../src/commands/index.ts | 6 + .../vscode-ide-companion/src/webview/App.tsx | 19 +- .../src/webview/WebViewProvider.ts | 98 ++++---- .../src/webview/components/ToolCall.tsx | 6 +- .../components/session/SessionSelector.tsx | 23 +- .../toolcalls/Read/ReadToolCall.tsx | 124 +++++----- .../toolcalls/Search/SearchToolCall.tsx | 179 ++++++++++++--- .../webview/components/toolcalls/index.tsx | 8 +- .../toolcalls/shared/LayoutComponents.tsx | 6 +- .../components/toolcalls/shared/types.ts | 3 + .../webview/handlers/SessionMessageHandler.ts | 31 ++- .../hooks/session/useSessionManagement.ts | 28 ++- .../src/webview/hooks/useWebViewMessages.ts | 22 +- .../vscode-ide-companion/tailwind.config.js | 5 + 18 files changed, 622 insertions(+), 230 deletions(-) diff --git a/packages/vscode-ide-companion/src/acp/acpConnection.ts b/packages/vscode-ide-companion/src/acp/acpConnection.ts index 27a111c6..b3ddf2a0 100644 --- a/packages/vscode-ide-companion/src/acp/acpConnection.ts +++ b/packages/vscode-ide-companion/src/acp/acpConnection.ts @@ -32,6 +32,9 @@ export class AcpConnection { private pendingRequests = new Map>(); private nextRequestId = { value: 0 }; private backend: AcpBackend | null = null; + // Remember the working dir provided at connect() so later ACP calls + // that require cwd (e.g. session/list) can include it. + private workingDir: string = process.cwd(); private messageHandler: AcpMessageHandler; private sessionManager: AcpSessionManager; @@ -66,6 +69,7 @@ export class AcpConnection { } this.backend = backend; + this.workingDir = workingDir; const isWindows = process.platform === 'win32'; const env = { ...process.env }; @@ -310,12 +314,13 @@ export class AcpConnection { * @param sessionId - Session ID * @returns Load response */ - async loadSession(sessionId: string): Promise { + async loadSession(sessionId: string, cwdOverride?: string): Promise { return this.sessionManager.loadSession( sessionId, this.child, this.pendingRequests, this.nextRequestId, + cwdOverride || this.workingDir, ); } @@ -324,11 +329,13 @@ export class AcpConnection { * * @returns Session list response */ - async listSessions(): Promise { + async listSessions(options?: { cursor?: number; size?: number }): Promise { return this.sessionManager.listSessions( this.child, this.pendingRequests, this.nextRequestId, + this.workingDir, + options, ); } diff --git a/packages/vscode-ide-companion/src/acp/acpSessionManager.ts b/packages/vscode-ide-companion/src/acp/acpSessionManager.ts index efe82331..db590053 100644 --- a/packages/vscode-ide-companion/src/acp/acpSessionManager.ts +++ b/packages/vscode-ide-companion/src/acp/acpSessionManager.ts @@ -247,11 +247,12 @@ export class AcpSessionManager { child: ChildProcess | null, pendingRequests: Map>, nextRequestId: { value: number }, + cwd: string = process.cwd(), ): Promise { console.log('[ACP] Sending session/load request for session:', sessionId); console.log('[ACP] Request parameters:', { sessionId, - cwd: process.cwd(), + cwd, mcpServers: [], }); @@ -260,7 +261,7 @@ export class AcpSessionManager { AGENT_METHODS.session_load, { sessionId, - cwd: process.cwd(), + cwd, mcpServers: [], }, child, @@ -278,6 +279,9 @@ export class AcpSessionManager { console.error('[ACP] Session load returned error:', response.error); } else { console.log('[ACP] Session load succeeded'); + // session/load returns null on success per schema; update local sessionId + // so subsequent prompts use the loaded session. + this.sessionId = sessionId; } return response; @@ -302,12 +306,19 @@ export class AcpSessionManager { child: ChildProcess | null, pendingRequests: Map>, nextRequestId: { value: number }, + cwd: string = process.cwd(), + options?: { cursor?: number; size?: number }, ): Promise { console.log('[ACP] Requesting session list...'); try { + // session/list requires cwd in params per ACP schema + const params: Record = { cwd }; + if (options?.cursor !== undefined) params.cursor = options.cursor; + if (options?.size !== undefined) params.size = options.size; + const response = await this.sendRequest( AGENT_METHODS.session_list, - {}, + params, child, pendingRequests, nextRequestId, diff --git a/packages/vscode-ide-companion/src/agents/qwenAgentManager.ts b/packages/vscode-ide-companion/src/agents/qwenAgentManager.ts index bed783a8..c7f020bf 100644 --- a/packages/vscode-ide-companion/src/agents/qwenAgentManager.ts +++ b/packages/vscode-ide-companion/src/agents/qwenAgentManager.ts @@ -220,16 +220,28 @@ export class QwenAgentManager { const response = await this.connection.listSessions(); console.log('[QwenAgentManager] ACP session list response:', response); - if (response.result && Array.isArray(response.result)) { - const sessions = response.result.map((session) => ({ - id: session.sessionId || session.id, - sessionId: session.sessionId || session.id, - title: session.title || session.name || 'Untitled Session', - name: session.title || session.name || 'Untitled Session', - startTime: session.startTime, - lastUpdated: session.lastUpdated, - messageCount: session.messageCount || 0, - projectHash: session.projectHash, + // sendRequest resolves with the JSON-RPC "result" directly + // Newer CLI returns an object: { items: [...], nextCursor?, hasMore } + // Older prototypes might return an array. Support both. + const res: any = response as any; + const items: any[] = Array.isArray(res) + ? res + : Array.isArray(res?.items) + ? res.items + : []; + + if (items.length > 0) { + const sessions = items.map((item) => ({ + id: item.sessionId || item.id, + sessionId: item.sessionId || item.id, + title: item.title || item.name || item.prompt || 'Untitled Session', + name: item.title || item.name || item.prompt || 'Untitled Session', + startTime: item.startTime, + lastUpdated: item.mtime || item.lastUpdated, + messageCount: item.messageCount || 0, + projectHash: item.projectHash, + filePath: item.filePath, + cwd: item.cwd, })); console.log( @@ -282,6 +294,100 @@ export class QwenAgentManager { } } + /** + * Get session list (paged) + * Uses ACP session/list with cursor-based pagination when available. + * Falls back to file system scan with equivalent pagination semantics. + */ + async getSessionListPaged(params?: { + cursor?: number; + size?: number; + }): Promise<{ + sessions: Array>; + nextCursor?: number; + hasMore: boolean; + }> { + const size = params?.size ?? 20; + const cursor = params?.cursor; + + const cliContextManager = CliContextManager.getInstance(); + const supportsSessionList = cliContextManager.supportsSessionList(); + + if (supportsSessionList) { + try { + const response = await this.connection.listSessions({ + size, + ...(cursor !== undefined ? { cursor } : {}), + }); + // sendRequest resolves with the JSON-RPC "result" directly + const res: any = response as any; + const items: any[] = Array.isArray(res) + ? res + : Array.isArray(res?.items) + ? res.items + : []; + + const mapped = items.map((item) => ({ + id: item.sessionId || item.id, + sessionId: item.sessionId || item.id, + title: item.title || item.name || item.prompt || 'Untitled Session', + name: item.title || item.name || item.prompt || 'Untitled Session', + startTime: item.startTime, + lastUpdated: item.mtime || item.lastUpdated, + messageCount: item.messageCount || 0, + projectHash: item.projectHash, + filePath: item.filePath, + cwd: item.cwd, + })); + + const nextCursor: number | undefined = Array.isArray(res?.items) + ? (res.nextCursor as number | undefined) + : undefined; + const hasMore: boolean = Array.isArray(res?.items) + ? Boolean(res.hasMore) + : false; + + return { sessions: mapped, nextCursor, hasMore }; + } catch (error) { + console.warn('[QwenAgentManager] Paged ACP session list failed:', error); + // fall through to file system + } + } + + // Fallback: file system for current project only (to match ACP semantics) + try { + const all = await this.sessionReader.getAllSessions( + this.currentWorkingDir, + false, + ); + // Sorted by lastUpdated desc already per reader + const allWithMtime = all.map((s) => ({ + raw: s, + mtime: new Date(s.lastUpdated).getTime(), + })); + const filtered = cursor !== undefined + ? allWithMtime.filter((x) => x.mtime < cursor) + : allWithMtime; + const page = filtered.slice(0, size); + const sessions = page.map((x) => ({ + id: x.raw.sessionId, + sessionId: x.raw.sessionId, + title: this.sessionReader.getSessionTitle(x.raw), + name: this.sessionReader.getSessionTitle(x.raw), + startTime: x.raw.startTime, + lastUpdated: x.raw.lastUpdated, + messageCount: x.raw.messages.length, + projectHash: x.raw.projectHash, + })); + const nextCursorVal = page.length > 0 ? page[page.length - 1].mtime : undefined; + const hasMore = filtered.length > size; + return { sessions, nextCursor: nextCursorVal, hasMore }; + } catch (error) { + console.error('[QwenAgentManager] File system paged list failed:', error); + return { sessions: [], hasMore: false }; + } + } + /** * Get session messages (read from disk) * @@ -290,6 +396,24 @@ export class QwenAgentManager { */ async getSessionMessages(sessionId: string): Promise { try { + // Prefer reading CLI's JSONL if we can find filePath from session/list + const cliContextManager = CliContextManager.getInstance(); + if (cliContextManager.supportsSessionList()) { + try { + const list = await this.getSessionList(); + const item = list.find( + (s) => s.sessionId === sessionId || s.id === sessionId, + ) as { filePath?: string } | undefined; + if (item?.filePath) { + const messages = await this.readJsonlMessages(item.filePath); + if (messages.length > 0) return messages; + } + } catch (e) { + console.warn('[QwenAgentManager] JSONL read path lookup failed:', e); + } + } + + // Fallback: legacy JSON session files const session = await this.sessionReader.getSession( sessionId, this.currentWorkingDir, @@ -297,24 +421,74 @@ export class QwenAgentManager { if (!session) { return []; } - return session.messages.map( (msg: { type: string; content: string; timestamp: string }) => ({ - role: - msg.type === 'user' ? ('user' as const) : ('assistant' as const), + role: msg.type === 'user' ? 'user' : 'assistant', content: msg.content, timestamp: new Date(msg.timestamp).getTime(), }), ); } catch (error) { - console.error( - '[QwenAgentManager] Failed to get session messages:', - error, - ); + console.error('[QwenAgentManager] Failed to get session messages:', error); return []; } } + // Read CLI JSONL session file and convert to ChatMessage[] for UI + private async readJsonlMessages(filePath: string): Promise { + const fs = await import('fs'); + const readline = await import('readline'); + try { + if (!fs.existsSync(filePath)) return []; + const fileStream = fs.createReadStream(filePath, { encoding: 'utf-8' }); + const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity }); + const records: Array = []; + for await (const line of rl) { + const trimmed = line.trim(); + if (!trimmed) continue; + try { + const obj = JSON.parse(trimmed); + records.push(obj); + } catch { + /* ignore */ + } + } + // Simple linear reconstruction: filter user/assistant and sort by timestamp + console.log('[QwenAgentManager] JSONL records read:', records.length, filePath); + const msgs = records + .filter((r) => r && (r.type === 'user' || r.type === 'assistant') && r.message) + .sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()) + .map((r) => ({ + role: r.type === 'user' ? ('user' as const) : ('assistant' as const), + content: this.contentToText(r.message), + timestamp: new Date(r.timestamp).getTime(), + })); + console.log('[QwenAgentManager] JSONL messages reconstructed:', msgs.length); + return msgs; + } catch (err) { + console.warn('[QwenAgentManager] Failed to read JSONL messages:', err); + return []; + } + } + + // Extract plain text from Content (genai Content) + private contentToText(message: any): string { + try { + const parts = Array.isArray(message?.parts) ? message.parts : []; + const texts: string[] = []; + for (const p of parts) { + if (typeof p?.text === 'string') { + texts.push(p.text); + } else if (typeof p?.data === 'string') { + texts.push(p.data); + } + } + return texts.join('\n'); + } catch { + return ''; + } + } + /** * Save session via /chat save command * Since CLI doesn't support session/save ACP method, we send /chat save command directly @@ -497,7 +671,7 @@ export class QwenAgentManager { * @param sessionId - Session ID * @returns Load response or error */ - async loadSessionViaAcp(sessionId: string): Promise { + async loadSessionViaAcp(sessionId: string, cwdOverride?: string): Promise { // Check if CLI supports session/load method const cliContextManager = CliContextManager.getInstance(); const supportsSessionLoad = cliContextManager.supportsSessionLoad(); @@ -513,7 +687,7 @@ export class QwenAgentManager { '[QwenAgentManager] Attempting session/load via ACP for session:', sessionId, ); - const response = await this.connection.loadSession(sessionId); + const response = await this.connection.loadSession(sessionId, cwdOverride); console.log( '[QwenAgentManager] Session load succeeded. Response:', JSON.stringify(response).substring(0, 200), diff --git a/packages/vscode-ide-companion/src/agents/qwenConnectionHandler.ts b/packages/vscode-ide-companion/src/agents/qwenConnectionHandler.ts index 5e372ab8..f0d937b2 100644 --- a/packages/vscode-ide-companion/src/agents/qwenConnectionHandler.ts +++ b/packages/vscode-ide-companion/src/agents/qwenConnectionHandler.ts @@ -89,55 +89,11 @@ export class QwenConnectionHandler { } // Try to restore existing session or create new session + // Note: Auto-restore on connect is disabled to avoid surprising loads + // when user opens a "New Chat" tab. Restoration is now an explicit action + // (session selector → session/load) or handled by higher-level flows. let sessionRestored = false; - // Try to get session from local files - console.log('[QwenAgentManager] Reading local session files...'); - try { - const sessions = await sessionReader.getAllSessions(workingDir); - - if (sessions.length > 0) { - console.log( - '[QwenAgentManager] Found existing sessions:', - sessions.length, - ); - const lastSession = sessions[0]; // Already sorted by lastUpdated - - try { - await connection.switchSession(lastSession.sessionId); - console.log( - '[QwenAgentManager] Restored session:', - lastSession.sessionId, - ); - sessionRestored = true; - - // Save auth state after successful session restore - if (authStateManager) { - console.log( - '[QwenAgentManager] Saving auth state after successful session restore', - ); - await authStateManager.saveAuthState(workingDir, authMethod); - } - } catch (switchError) { - console.log( - '[QwenAgentManager] session/switch not supported or failed:', - switchError instanceof Error - ? switchError.message - : String(switchError), - ); - } - } else { - console.log('[QwenAgentManager] No existing sessions found'); - } - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error); - console.log( - '[QwenAgentManager] Failed to read local sessions:', - errorMessage, - ); - } - // Create new session if unable to restore if (!sessionRestored) { console.log( @@ -190,9 +146,7 @@ export class QwenConnectionHandler { } try { - console.log( - '[QwenAgentManager] Creating new session after authentication...', - ); + console.log('[QwenAgentManager] Creating new session after authentication...'); await this.newSessionWithRetry( connection, workingDir, diff --git a/packages/vscode-ide-companion/src/commands/index.ts b/packages/vscode-ide-companion/src/commands/index.ts index 7ca510a7..e8830e11 100644 --- a/packages/vscode-ide-companion/src/commands/index.ts +++ b/packages/vscode-ide-companion/src/commands/index.ts @@ -73,6 +73,12 @@ 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 + } await provider.show(); }), ); diff --git a/packages/vscode-ide-companion/src/webview/App.tsx b/packages/vscode-ide-companion/src/webview/App.tsx index f4b853be..30bf5900 100644 --- a/packages/vscode-ide-companion/src/webview/App.tsx +++ b/packages/vscode-ide-companion/src/webview/App.tsx @@ -508,6 +508,9 @@ export const App: React.FC = () => { sessionManagement.setSessionSearchQuery(''); }} onClose={() => sessionManagement.setShowSessionSelector(false)} + hasMore={sessionManagement.hasMore} + isLoading={sessionManagement.isLoading} + onLoadMore={sessionManagement.handleLoadMoreSessions} /> { // ); case 'in-progress-tool-call': - case 'completed-tool-call': + case 'completed-tool-call': { + const prev = allMessages[index - 1]; + const next = allMessages[index + 1]; + const isToolCallType = (x: unknown) => + x && + typeof x === 'object' && + 'type' in (x as Record) && + ((x as { type: string }).type === 'in-progress-tool-call' || + (x as { type: string }).type === 'completed-tool-call'); + const isFirst = !isToolCallType(prev); + const isLast = !isToolCallType(next); return ( ); + } default: return null; diff --git a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts index f7b52d5c..b869761a 100644 --- a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts +++ b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts @@ -26,6 +26,8 @@ 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, @@ -240,6 +242,13 @@ 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(); @@ -682,53 +691,60 @@ export class WebViewProvider { authMethod, ); if (hasValidAuth) { - console.log( - '[WebViewProvider] Found valid cached auth, attempting session restoration', - ); + const allowAutoRestore = this.autoRestoreOnFirstConnect; + // Reset for subsequent connects (only once per panel lifecycle unless set again) + this.autoRestoreOnFirstConnect = true; + 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, + ); + this.sendMessageToWebView({ + type: 'qwenSessionSwitched', + data: { sessionId: targetId, messages }, + }); + console.log('[WebViewProvider] Auto-restored last session:', targetId); + 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, + ); + } + } else { + console.log('[WebViewProvider] Auto-restore suppressed for this panel'); + } + + // Create a fresh ACP session (no auto-restore or restore failed) try { - // Try to create a session (this will use cached auth) - const sessionId = await this.agentManager.createNewSession( + await this.agentManager.createNewSession( workingDir, this.authStateManager, ); - - if (sessionId) { - console.log( - '[WebViewProvider] ACP session restored successfully with ID:', - sessionId, - ); - } else { - console.log( - '[WebViewProvider] ACP session restoration returned no session ID', - ); - } - } catch (restoreError) { - console.warn( - '[WebViewProvider] Failed to restore ACP session:', - restoreError, + 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.`, ); - // Clear invalid auth cache - await this.authStateManager.clearAuthState(); - - // Fall back to creating a new session - try { - await this.agentManager.createNewSession( - workingDir, - this.authStateManager, - ); - console.log( - '[WebViewProvider] ACP session created successfully after restore failure', - ); - } 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 { console.log( diff --git a/packages/vscode-ide-companion/src/webview/components/ToolCall.tsx b/packages/vscode-ide-companion/src/webview/components/ToolCall.tsx index 2f6c60df..36a18368 100644 --- a/packages/vscode-ide-companion/src/webview/components/ToolCall.tsx +++ b/packages/vscode-ide-companion/src/webview/components/ToolCall.tsx @@ -35,4 +35,8 @@ export type { ToolCallContent } from './toolcalls/shared/types.js'; */ export const ToolCall: React.FC<{ toolCall: import('./toolcalls/shared/types.js').ToolCallData; -}> = ({ toolCall }) => ; + isFirst?: boolean; + isLast?: boolean; +}> = ({ toolCall, isFirst, isLast }) => ( + +); diff --git a/packages/vscode-ide-companion/src/webview/components/session/SessionSelector.tsx b/packages/vscode-ide-companion/src/webview/components/session/SessionSelector.tsx index 109aa2fa..ab7f6d51 100644 --- a/packages/vscode-ide-companion/src/webview/components/session/SessionSelector.tsx +++ b/packages/vscode-ide-companion/src/webview/components/session/SessionSelector.tsx @@ -17,6 +17,9 @@ interface SessionSelectorProps { onSearchChange: (query: string) => void; onSelectSession: (sessionId: string) => void; onClose: () => void; + hasMore?: boolean; + isLoading?: boolean; + onLoadMore?: () => void; } /** @@ -31,6 +34,9 @@ export const SessionSelector: React.FC = ({ onSearchChange, onSelectSession, onClose, + hasMore = false, + isLoading = false, + onLoadMore, }) => { if (!visible) { return null; @@ -66,7 +72,17 @@ export const SessionSelector: React.FC = ({
{/* Session List with Grouping */} -
+
{ + const el = e.currentTarget; + const distanceToBottom = + el.scrollHeight - (el.scrollTop + el.clientHeight); + if (distanceToBottom < 48 && hasMore && !isLoading) { + onLoadMore?.(); + } + }} + > {hasNoSessions ? (
= ({ )) )} + {hasMore && ( +
+ {isLoading ? 'Loading…' : ''} +
+ )}
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 3892bc00..a2c3d032 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,6 @@ import type React from 'react'; import { useCallback, useEffect, useMemo } from 'react'; import type { BaseToolCallProps } from '../shared/types.js'; -import { ToolCallContainer } from '../shared/LayoutComponents.js'; import { groupContent, mapToolStatusToContainerStatus, @@ -23,7 +22,11 @@ import { handleOpenDiff } from '../../../utils/diffUtils.js'; * Optimized for displaying file reading operations * Shows: Read filename (no content preview) */ -export const ReadToolCall: React.FC = ({ toolCall }) => { +export const ReadToolCall: React.FC = ({ + toolCall, + isFirst, + isLast, +}) => { const { content, locations, toolCallId } = toolCall; const vscode = useVSCode(); @@ -71,76 +74,85 @@ export const ReadToolCall: React.FC = ({ toolCall }) => { | 'loading' | 'default' = mapToolStatusToContainerStatus(toolCall.status); + // Compute pseudo-element classes for status dot (use ::before per requirement) + const beforeStatusClass = + containerStatus === 'success' + ? 'before:text-qwen-success' + : containerStatus === 'error' + ? 'before:text-qwen-error' + : containerStatus === 'warning' + ? 'before:text-qwen-warning' + : 'before:text-qwen-loading before:opacity-70 before:animate-pulse-slow'; + + const ReadContainer: React.FC<{ + status: typeof containerStatus; + path?: string; + children?: React.ReactNode; + isError?: boolean; + }> = ({ status, path, children, isError }) => { + // Adjust the connector line to crop for first/last items + const lineCropTop = isFirst ? 'top-[24px]' : 'top-0'; + const lineCropBottom = isLast ? 'bottom-auto h-[calc(100%-24px)]' : 'bottom-0'; + return ( +
+ {/* timeline vertical line */} +
+
+
+ + Read + + {path ? ( + + ) : null} +
+ {children ? ( +
+ {children} +
+ ) : null} +
+
+ ); + }; + // Error case: show error if (errors.length > 0) { const path = locations?.[0]?.path || ''; return ( - - ) : undefined - } - > + {errors.join('\n')} - + ); } // Success case with diff: keep UI compact; VS Code diff is auto-opened above if (diffs.length > 0) { const path = diffs[0]?.path || locations?.[0]?.path || ''; - return ( - - ) : undefined - } - > - {null} - - ); + return ; } // Success case: show which file was read with filename in label if (locations && locations.length > 0) { const path = locations[0].path; - return ( - - ) : undefined - } - > - {null} - - ); + return ; } // No file info, don't show diff --git a/packages/vscode-ide-companion/src/webview/components/toolcalls/Search/SearchToolCall.tsx b/packages/vscode-ide-companion/src/webview/components/toolcalls/Search/SearchToolCall.tsx index b9fe6f35..3a803893 100644 --- a/packages/vscode-ide-companion/src/webview/components/toolcalls/Search/SearchToolCall.tsx +++ b/packages/vscode-ide-companion/src/webview/components/toolcalls/Search/SearchToolCall.tsx @@ -8,12 +8,7 @@ import type React from 'react'; import type { BaseToolCallProps } from '../shared/types.js'; -import { - ToolCallContainer, - ToolCallCard, - ToolCallRow, - LocationsList, -} from '../shared/LayoutComponents.js'; +import { FileLink } from '../../ui/FileLink.js'; import { safeTitle, groupContent, @@ -25,7 +20,122 @@ import { * 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); @@ -35,14 +145,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')}
+
+ ); } @@ -52,28 +162,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} */} - - + + ); } @@ -81,11 +190,11 @@ export const SearchToolCall: React.FC = ({ toolCall }) => { if (textOutputs.length > 0) { const containerStatus = mapToolStatusToContainerStatus(toolCall.status); return ( -
{textOutputs.map((text, index) => ( @@ -98,7 +207,7 @@ export const SearchToolCall: React.FC = ({ toolCall }) => {
))}
- + ); } @@ -106,13 +215,9 @@ 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/index.tsx b/packages/vscode-ide-companion/src/webview/components/toolcalls/index.tsx index 37350a20..51e334b5 100644 --- a/packages/vscode-ide-companion/src/webview/components/toolcalls/index.tsx +++ b/packages/vscode-ide-companion/src/webview/components/toolcalls/index.tsx @@ -92,7 +92,11 @@ export const getToolCallComponent = ( /** * Main tool call component that routes to specialized implementations */ -export const ToolCallRouter: React.FC = ({ toolCall }) => { +export const ToolCallRouter: React.FC = ({ + toolCall, + isFirst, + isLast, +}) => { // Check if we should show this tool call (hide internal ones) if (!shouldShowToolCall(toolCall.kind)) { return null; @@ -102,7 +106,7 @@ export const ToolCallRouter: React.FC = ({ toolCall }) => { const Component = getToolCallComponent(toolCall.kind, toolCall); // Render the specialized component - return ; + return ; }; // Re-export types for convenience 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 d4668287..bf6b2cfa 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 d0866d21..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,9 @@ 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/SessionMessageHandler.ts b/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts index ccde5337..dc33f7ff 100644 --- a/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts +++ b/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts @@ -74,7 +74,10 @@ export class SessionMessageHandler extends BaseMessageHandler { break; case 'getQwenSessions': - await this.handleGetQwenSessions(); + await this.handleGetQwenSessions( + (data?.cursor as number | undefined) ?? undefined, + (data?.size as number | undefined) ?? undefined, + ); break; case 'saveSession': @@ -593,8 +596,8 @@ export class SessionMessageHandler extends BaseMessageHandler { } } - // Get session details - let sessionDetails = null; + // Get session details (includes cwd and filePath when using ACP) + let sessionDetails: Record | null = null; try { const allSessions = await this.agentManager.getSessionList(); sessionDetails = allSessions.find( @@ -613,8 +616,10 @@ export class SessionMessageHandler extends BaseMessageHandler { // Try to load session via ACP (now we should be connected) try { - const loadResponse = - await this.agentManager.loadSessionViaAcp(sessionId); + const loadResponse = await this.agentManager.loadSessionViaAcp( + sessionId, + (sessionDetails?.cwd as string | undefined) || undefined, + ); console.log( '[SessionMessageHandler] session/load succeeded:', loadResponse, @@ -778,12 +783,22 @@ export class SessionMessageHandler extends BaseMessageHandler { /** * Handle get Qwen sessions request */ - private async handleGetQwenSessions(): Promise { + private async handleGetQwenSessions( + cursor?: number, + size?: number, + ): Promise { try { - const sessions = await this.agentManager.getSessionList(); + // Paged when possible; falls back to full list if ACP not supported + const page = await this.agentManager.getSessionListPaged({ cursor, size }); + const append = typeof cursor === 'number'; this.sendToWebView({ type: 'qwenSessionList', - data: { sessions }, + data: { + sessions: page.sessions, + nextCursor: page.nextCursor, + hasMore: page.hasMore, + append, + }, }); } catch (error) { console.error('[SessionMessageHandler] Failed to get sessions:', error); diff --git a/packages/vscode-ide-companion/src/webview/hooks/session/useSessionManagement.ts b/packages/vscode-ide-companion/src/webview/hooks/session/useSessionManagement.ts index 47669f6a..63458855 100644 --- a/packages/vscode-ide-companion/src/webview/hooks/session/useSessionManagement.ts +++ b/packages/vscode-ide-companion/src/webview/hooks/session/useSessionManagement.ts @@ -21,6 +21,11 @@ export const useSessionManagement = (vscode: VSCodeAPI) => { const [showSessionSelector, setShowSessionSelector] = useState(false); const [sessionSearchQuery, setSessionSearchQuery] = useState(''); const [savedSessionTags, setSavedSessionTags] = useState([]); + const [nextCursor, setNextCursor] = useState(undefined); + const [hasMore, setHasMore] = useState(true); + const [isLoading, setIsLoading] = useState(false); + + const PAGE_SIZE = 20; /** * Filter session list @@ -44,10 +49,24 @@ export const useSessionManagement = (vscode: VSCodeAPI) => { * Load session list */ const handleLoadQwenSessions = useCallback(() => { - vscode.postMessage({ type: 'getQwenSessions', data: {} }); + // Reset pagination state and load first page + setQwenSessions([]); + setNextCursor(undefined); + setHasMore(true); + setIsLoading(true); + vscode.postMessage({ type: 'getQwenSessions', data: { size: PAGE_SIZE } }); setShowSessionSelector(true); }, [vscode]); + const handleLoadMoreSessions = useCallback(() => { + if (!hasMore || isLoading || nextCursor === undefined) return; + setIsLoading(true); + vscode.postMessage({ + type: 'getQwenSessions', + data: { cursor: nextCursor, size: PAGE_SIZE }, + }); + }, [hasMore, isLoading, nextCursor, vscode]); + /** * Create new session */ @@ -117,6 +136,9 @@ export const useSessionManagement = (vscode: VSCodeAPI) => { sessionSearchQuery, filteredSessions, savedSessionTags, + nextCursor, + hasMore, + isLoading, // State setters setQwenSessions, @@ -125,6 +147,9 @@ export const useSessionManagement = (vscode: VSCodeAPI) => { setShowSessionSelector, setSessionSearchQuery, setSavedSessionTags, + setNextCursor, + setHasMore, + setIsLoading, // Operations handleLoadQwenSessions, @@ -132,5 +157,6 @@ export const useSessionManagement = (vscode: VSCodeAPI) => { handleSwitchSession, handleSaveSession, handleSaveSessionResponse, + handleLoadMoreSessions, }; }; diff --git a/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts b/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts index 53ed7468..e3954d55 100644 --- a/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts +++ b/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts @@ -18,10 +18,17 @@ interface UseWebViewMessagesProps { // Session management sessionManagement: { currentSessionId: string | null; - setQwenSessions: (sessions: Array>) => void; + setQwenSessions: ( + sessions: + | Array> + | ((prev: Array>) => Array>), + ) => void; setCurrentSessionId: (id: string | null) => void; setCurrentSessionTitle: (title: string) => void; setShowSessionSelector: (show: boolean) => void; + setNextCursor: (cursor: number | undefined) => void; + setHasMore: (hasMore: boolean) => void; + setIsLoading: (loading: boolean) => void; handleSaveSessionResponse: (response: { success: boolean; message?: string; @@ -487,8 +494,17 @@ export const useWebViewMessages = ({ } case 'qwenSessionList': { - const sessions = message.data.sessions || []; - handlers.sessionManagement.setQwenSessions(sessions); + const sessions = (message.data.sessions as any[]) || []; + const append = Boolean(message.data.append); + const nextCursor = message.data.nextCursor as number | undefined; + const hasMore = Boolean(message.data.hasMore); + + handlers.sessionManagement.setQwenSessions((prev: any[]) => + append ? [...prev, ...sessions] : sessions, + ); + handlers.sessionManagement.setNextCursor(nextCursor); + handlers.sessionManagement.setHasMore(hasMore); + handlers.sessionManagement.setIsLoading(false); if ( handlers.sessionManagement.currentSessionId && sessions.length > 0 diff --git a/packages/vscode-ide-companion/tailwind.config.js b/packages/vscode-ide-companion/tailwind.config.js index 26e28794..ee07b39d 100644 --- a/packages/vscode-ide-companion/tailwind.config.js +++ b/packages/vscode-ide-companion/tailwind.config.js @@ -52,6 +52,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: {