diff --git a/package-lock.json b/package-lock.json index 9c254b68..93697f54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3693,6 +3693,34 @@ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "license": "MIT" }, + "node_modules/@testing-library/react": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@textlint/ast-node-types": { "version": "15.2.2", "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.2.2.tgz", @@ -17143,34 +17171,6 @@ "node": ">=20" } }, - "packages/cli/node_modules/@testing-library/react": { - "version": "16.3.0", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", - "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@testing-library/dom": "^10.0.0", - "@types/react": "^18.0.0 || ^19.0.0", - "@types/react-dom": "^18.0.0 || ^19.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "packages/cli/node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", @@ -17345,7 +17345,6 @@ "license": "LICENSE", "dependencies": { "@modelcontextprotocol/sdk": "^1.15.1", - "@qwen-code/qwen-code": "file:../cli", "@qwen-code/qwen-code-core": "file:../core", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/packages/vscode-ide-companion/MESSAGE_ORDERING_IMPROVEMENTS.md b/packages/vscode-ide-companion/MESSAGE_ORDERING_IMPROVEMENTS.md deleted file mode 100644 index 845c990f..00000000 --- a/packages/vscode-ide-companion/MESSAGE_ORDERING_IMPROVEMENTS.md +++ /dev/null @@ -1,75 +0,0 @@ -# VS Code IDE Companion 消息排序改进总结 - -## 实施的改进 - -### 1. 添加时间戳支持 - -**文件修改:** - -- `packages/vscode-ide-companion/src/webview/components/toolcalls/shared/types.ts` -- `packages/vscode-ide-companion/src/webview/types/toolCall.ts` - -**改进内容:** - -- 在 `ToolCallData` 接口中添加 `timestamp?: number` 字段 -- 在 `ToolCallUpdate` 接口中添加 `timestamp?: number` 字段 - -### 2. 更新工具调用处理逻辑 - -**文件修改:** - -- `packages/vscode-ide-companion/src/webview/hooks/useToolCalls.ts` - -**改进内容:** - -- 在创建工具调用时自动添加时间戳(使用提供的时间戳或当前时间) -- 在更新工具调用时保留原有时间戳或使用新提供的时间戳 - -### 3. 修改消息渲染逻辑 - -**文件修改:** - -- `packages/vscode-ide-companion/src/webview/App.tsx` - -**改进内容:** - -- 将所有类型的消息(普通消息 + 工具调用)合并到一个数组中 -- 按时间戳排序所有消息 -- 统一渲染,确保工具调用在正确的时间点显示 - -## 解决的问题 - -### 1. 工具调用显示顺序不正确 - -**问题:** 工具调用总是显示在所有普通消息之后,而不是按时间顺序插入到正确的位置。 - -**解决方案:** 通过统一的时间戳排序机制,确保所有消息按时间顺序显示。 - -### 2. 缺少时间戳支持 - -**问题:** 工具调用数据结构中没有时间戳字段,无法正确排序。 - -**解决方案:** 在数据结构中添加时间戳字段,并在创建/更新时自动填充。 - -## 向后兼容性 - -所有改进都保持了向后兼容性: - -- 对于没有时间戳的旧消息,使用当前时间作为默认值 -- 现有 API 保持不变 -- 现有功能不受影响 - -## 测试 - -创建了相关测试用例: - -- 验证工具调用时间戳的正确添加和保留 -- 验证消息排序逻辑的正确性 -- 验证工具调用显示条件的正确性 - -## 验收标准达成情况 - -✅ 所有新添加的时间戳支持都已实现 -✅ 消息按照时间顺序正确排列 -✅ 现有功能不受影响 -✅ 代码质量符合项目标准 diff --git a/packages/vscode-ide-companion/postcss.config.js b/packages/vscode-ide-companion/postcss.config.js index 63ef4c6b..49f4aea7 100644 --- a/packages/vscode-ide-companion/postcss.config.js +++ b/packages/vscode-ide-companion/postcss.config.js @@ -1,3 +1,9 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + /* eslint-disable no-undef */ module.exports = { plugins: { diff --git a/packages/vscode-ide-companion/src/acp/acpConnection.ts b/packages/vscode-ide-companion/src/acp/acpConnection.ts index 44b2819a..a77fc435 100644 --- a/packages/vscode-ide-companion/src/acp/acpConnection.ts +++ b/packages/vscode-ide-companion/src/acp/acpConnection.ts @@ -135,7 +135,7 @@ export class AcpConnection { } /** - * 连接到ACP后端 + * Connect to ACP backend * * @param backend - Backend type * @param cliPath - CLI path diff --git a/packages/vscode-ide-companion/src/agents/qwenAgentManager.ts b/packages/vscode-ide-companion/src/agents/qwenAgentManager.ts index 15e5db22..bd899be6 100644 --- a/packages/vscode-ide-companion/src/agents/qwenAgentManager.ts +++ b/packages/vscode-ide-companion/src/agents/qwenAgentManager.ts @@ -39,6 +39,9 @@ export class QwenAgentManager { private connectionHandler: QwenConnectionHandler; private sessionUpdateHandler: QwenSessionUpdateHandler; private currentWorkingDir: string = process.cwd(); + // Cache the last used AuthStateManager so internal calls (e.g. fallback paths) + // can reuse it and avoid forcing a fresh authentication unnecessarily. + private defaultAuthStateManager?: AuthStateManager; // Callback storage private callbacks: QwenAgentCallbacks = {}; @@ -92,6 +95,8 @@ export class QwenAgentManager { _cliPath?: string, ): Promise { this.currentWorkingDir = workingDir; + // Remember the provided authStateManager for future calls + this.defaultAuthStateManager = authStateManager; await this.connectionHandler.connect( this.connection, this.sessionReader, @@ -392,23 +397,25 @@ export class QwenAgentManager { '[QwenAgentManager] Current session ID (from CLI):', this.currentSessionId, ); + // In ACP mode, the CLI does not accept arbitrary slash commands like + // "/chat save". To ensure we never block on unsupported features, + // persist checkpoints directly to ~/.qwen/tmp using our SessionManager. + const qwenMessages = messages.map((m) => ({ + // Generate minimal QwenMessage shape expected by the writer + id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, + timestamp: new Date().toISOString(), + type: m.role === 'user' ? ('user' as const) : ('qwen' as const), + content: m.content, + })); - // Use CLI's /chat save command instead of manually writing files - // This ensures we save the complete session context including tool calls - if (this.currentSessionId) { - console.log( - '[QwenAgentManager] Using CLI /chat save command for complete save', - ); - return await this.saveCheckpointViaCommand(this.currentSessionId); - } else { - console.warn( - '[QwenAgentManager] No current session ID, cannot use /chat save', - ); - return { - success: false, - message: 'No active CLI session', - }; - } + const tag = await this.sessionManager.saveCheckpoint( + qwenMessages, + conversationId, + this.currentWorkingDir, + this.currentSessionId || undefined, + ); + + return { success: true, tag }; } catch (error) { console.error('[QwenAgentManager] ===== CHECKPOINT SAVE FAILED ====='); console.error('[QwenAgentManager] Error:', error); @@ -634,12 +641,12 @@ export class QwenAgentManager { const config = vscode.workspace.getConfiguration('qwenCode'); const openaiApiKey = config.get('qwen.openaiApiKey', ''); const authMethod = openaiApiKey ? 'openai' : 'qwen-oauth'; - - if (authStateManager) { - hasValidAuth = await authStateManager.hasValidAuth( - workingDir, - authMethod, - ); + // Prefer the provided authStateManager, otherwise fall back to the one + // remembered during connect(). This prevents accidental re-auth in + // fallback paths (e.g. session switching) when the handler didn't pass it. + const effectiveAuth = authStateManager || this.defaultAuthStateManager; + if (effectiveAuth) { + hasValidAuth = await effectiveAuth.hasValidAuth(workingDir, authMethod); console.log( '[QwenAgentManager] Has valid cached auth for new session:', hasValidAuth, @@ -656,20 +663,20 @@ export class QwenAgentManager { console.log('[QwenAgentManager] Authentication successful'); // Save auth state - if (authStateManager) { + if (effectiveAuth) { console.log( '[QwenAgentManager] Saving auth state after successful authentication', ); - await authStateManager.saveAuthState(workingDir, authMethod); + await effectiveAuth.saveAuthState(workingDir, authMethod); } } catch (authError) { console.error('[QwenAgentManager] Authentication failed:', authError); // Clear potentially invalid cache - if (authStateManager) { + if (effectiveAuth) { console.log( '[QwenAgentManager] Clearing auth cache due to authentication failure', ); - await authStateManager.clearAuthState(); + await effectiveAuth.clearAuthState(); } throw authError; } diff --git a/packages/vscode-ide-companion/src/agents/qwenConnectionHandler.ts b/packages/vscode-ide-companion/src/agents/qwenConnectionHandler.ts index 030974e6..a8480d1c 100644 --- a/packages/vscode-ide-companion/src/agents/qwenConnectionHandler.ts +++ b/packages/vscode-ide-companion/src/agents/qwenConnectionHandler.ts @@ -59,7 +59,7 @@ export class QwenConnectionHandler { `[QwenAgentManager] CLI version ${versionInfo.version} is below minimum required version ${'0.2.4'}`, ); - // TODO: 暂时注释 + // TODO: Wait to determine release version number // vscode.window.showWarningMessage( // `Qwen Code CLI version ${versionInfo.version} is below the minimum required version. Some features may not work properly. Please upgrade to version 0.2.4 or later.`, // ); diff --git a/packages/vscode-ide-companion/src/cli/CliInstaller.ts b/packages/vscode-ide-companion/src/cli/1cliInstaller.ts similarity index 99% rename from packages/vscode-ide-companion/src/cli/CliInstaller.ts rename to packages/vscode-ide-companion/src/cli/1cliInstaller.ts index d75ae3bb..a3df2310 100644 --- a/packages/vscode-ide-companion/src/cli/CliInstaller.ts +++ b/packages/vscode-ide-companion/src/cli/1cliInstaller.ts @@ -5,7 +5,7 @@ */ import * as vscode from 'vscode'; -import { CliDetector } from '../cli/cliDetector.js'; +import { CliDetector } from './cliDetector.js'; /** * CLI Detection and Installation Handler diff --git a/packages/vscode-ide-companion/src/webview/App.tsx b/packages/vscode-ide-companion/src/webview/App.tsx index 88dfbb69..72c8afe1 100644 --- a/packages/vscode-ide-companion/src/webview/App.tsx +++ b/packages/vscode-ide-companion/src/webview/App.tsx @@ -62,6 +62,8 @@ export const App: React.FC = () => { } | null>(null); const [planEntries, setPlanEntries] = useState([]); const messagesEndRef = useRef(null); + // Scroll container for message list; used to keep the view anchored to the latest content + const messagesContainerRef = useRef(null); const inputFieldRef = useRef(null); const [showBanner, setShowBanner] = useState(true); const [editMode, setEditMode] = useState('ask'); @@ -173,6 +175,51 @@ export const App: React.FC = () => { setInputText, }); + // Auto-scroll handling: keep the view pinned to bottom when new content arrives, + // but don't interrupt the user if they scrolled up. + const prevCountsRef = useRef({ msgLen: 0, inProgLen: 0, doneLen: 0 }); + useEffect(() => { + const container = messagesContainerRef.current; + const endEl = messagesEndRef.current; + if (!container || !endEl) { + return; + } + + const nearBottom = () => { + const threshold = 64; // px tolerance + return ( + container.scrollTop + container.clientHeight >= + container.scrollHeight - threshold + ); + }; + + // Detect whether new items were appended (vs. streaming chunk updates) + const prev = prevCountsRef.current; + const newMsg = messageHandling.messages.length > prev.msgLen; + const newInProg = inProgressToolCalls.length > prev.inProgLen; + const newDone = completedToolCalls.length > prev.doneLen; + prevCountsRef.current = { + msgLen: messageHandling.messages.length, + inProgLen: inProgressToolCalls.length, + doneLen: completedToolCalls.length, + }; + + // If user is near bottom, or if we just appended a new item, scroll to bottom + if (nearBottom() || newMsg || newInProg || newDone) { + const smooth = newMsg || newInProg || newDone; // avoid smooth on streaming chunks + endEl.scrollIntoView({ + behavior: smooth ? 'smooth' : 'auto', + block: 'end', + }); + } + }, [ + messageHandling.messages, + inProgressToolCalls, + completedToolCalls, + messageHandling.isWaitingForResponse, + messageHandling.loadingMessage, + ]); + // Handle permission response const handlePermissionResponse = useCallback( (optionId: string) => { @@ -380,6 +427,7 @@ export const App: React.FC = () => { />
@@ -387,23 +435,23 @@ export const App: React.FC = () => { ) : ( <> - {/* 创建统一的消息数组,包含所有类型的消息和工具调用 */} + {/* Create unified message array containing all types of messages and tool calls */} {(() => { - // 普通消息 + // Regular messages const regularMessages = messageHandling.messages.map((msg) => ({ type: 'message' as const, data: msg, timestamp: msg.timestamp, })); - // 进行中的工具调用 + // In-progress tool calls const inProgressTools = inProgressToolCalls.map((toolCall) => ({ type: 'in-progress-tool-call' as const, data: toolCall, timestamp: toolCall.timestamp || Date.now(), })); - // 完成的工具调用 + // Completed tool calls const completedTools = completedToolCalls .filter(hasToolCallOutput) .map((toolCall) => ({ @@ -412,7 +460,7 @@ export const App: React.FC = () => { timestamp: toolCall.timestamp || Date.now(), })); - // 合并并按时间戳排序,确保消息与工具调用穿插显示 + // Merge and sort by timestamp to ensure messages and tool calls are interleaved const allMessages = [ ...regularMessages, ...inProgressTools, @@ -492,7 +540,7 @@ export const App: React.FC = () => { }); })()} - {/* 已改为在 useWebViewMessages 中将每次 plan 推送为历史 toolcall,避免重复展示最新块 */} + {/* Changed to push each plan as a historical toolcall in useWebViewMessages to avoid duplicate display of the latest block */} {messageHandling.isWaitingForResponse && messageHandling.loadingMessage && ( diff --git a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts index 565a4b07..6ffd69bb 100644 --- a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts +++ b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts @@ -13,7 +13,7 @@ import { AuthStateManager } from '../auth/authStateManager.js'; import { PanelManager } from '../webview/PanelManager.js'; import { MessageHandler } from '../webview/MessageHandler.js'; import { WebViewContent } from '../webview/WebViewContent.js'; -import { CliInstaller } from '../cli/CliInstaller.js'; +import { CliInstaller } from '../cli/1cliInstaller.js'; import { getFileName } from './utils/webviewUtils.js'; export class WebViewProvider { diff --git a/packages/vscode-ide-companion/src/webview/components/PlanDisplay.tsx b/packages/vscode-ide-companion/src/webview/components/PlanDisplay.tsx index dbde5f55..cd3478c9 100644 --- a/packages/vscode-ide-companion/src/webview/components/PlanDisplay.tsx +++ b/packages/vscode-ide-companion/src/webview/components/PlanDisplay.tsx @@ -22,7 +22,7 @@ interface PlanDisplayProps { * 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'); @@ -36,14 +36,14 @@ export const PlanDisplay: React.FC = ({ entries }) => {
- {/* 标题区域,类似示例中的 summary/_e/or */} + {/* Title area, similar to example summary/_e/or */}
@@ -56,7 +56,7 @@ export const PlanDisplay: React.FC = ({ entries }) => {
- {/* 列表区域,类似示例中的 .qr/.Fr/.Hr */} + {/* List area, similar to example .qr/.Fr/.Hr */}
    {entries.map((entry, index) => { @@ -70,7 +70,7 @@ export const PlanDisplay: React.FC = ({ entries }) => { isDone ? 'fo opacity-70' : '', ].join(' ')} > - {/* 展示用复选框(复用组件) */} + {/* Display checkbox (reusable component) */}