From ad301963a69d37c4324dbacbbc9d21a75405a4cf Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Sat, 6 Dec 2025 21:45:36 +0800 Subject: [PATCH] feat(vscode-ide-companion): enhance session management with pagination support Implement cursor-based pagination for session listing and improve session handling - Add pagination state management in useSessionManagement hook - Implement handleLoadMoreSessions for infinite scrolling - Update SessionMessageHandler to support paged session requests - Add ChatHeader component for improved UI layout - Fix session title duplication issue - Improve error handling in session operations --- .../webview/components/layouts/ChatHeader.tsx | 109 ++++++++++++ .../components/session/SessionSelector.tsx | 23 ++- .../webview/handlers/SessionMessageHandler.ts | 159 +++++++++++------- .../hooks/session/useSessionManagement.ts | 28 ++- .../src/webview/hooks/useWebViewMessages.ts | 50 +++++- 5 files changed, 298 insertions(+), 71 deletions(-) create mode 100644 packages/vscode-ide-companion/src/webview/components/layouts/ChatHeader.tsx diff --git a/packages/vscode-ide-companion/src/webview/components/layouts/ChatHeader.tsx b/packages/vscode-ide-companion/src/webview/components/layouts/ChatHeader.tsx new file mode 100644 index 00000000..2c51f9ea --- /dev/null +++ b/packages/vscode-ide-companion/src/webview/components/layouts/ChatHeader.tsx @@ -0,0 +1,109 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import type React from 'react'; + +interface ChatHeaderProps { + currentSessionTitle: string; + onLoadSessions: () => void; + onSaveSession: () => void; + onNewSession: () => void; +} + +export const ChatHeader: React.FC = ({ + currentSessionTitle, + onLoadSessions, + onSaveSession: _onSaveSession, + onNewSession, +}) => ( +
+ {/* Past Conversations Button */} + + + {/* Spacer */} +
+ + {/* Save Session Button */} + {/* */} + + {/* New Session Button */} + +
+); 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/handlers/SessionMessageHandler.ts b/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts index dfd0fd75..c46b4657 100644 --- a/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts +++ b/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts @@ -16,6 +16,7 @@ export class SessionMessageHandler extends BaseMessageHandler { private currentStreamContent = ''; private isSavingCheckpoint = false; private loginHandler: (() => Promise) | null = null; + private isTitleSet = false; // Flag to track if title has been set canHandle(messageType: string): boolean { return [ @@ -74,7 +75,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': @@ -231,8 +235,8 @@ export class SessionMessageHandler extends BaseMessageHandler { ); } - // Generate title for first message - if (isFirstMessage) { + // Generate title for first message, but only if it hasn't been set yet + if (isFirstMessage && !this.isTitleSet) { const title = text.substring(0, 50) + (text.length > 50 ? '...' : ''); this.sendToWebView({ type: 'sessionTitleUpdated', @@ -241,6 +245,7 @@ export class SessionMessageHandler extends BaseMessageHandler { title, }, }); + this.isTitleSet = true; // Mark title as set } // Save user message @@ -280,33 +285,7 @@ export class SessionMessageHandler extends BaseMessageHandler { vscode.window.showInformationMessage( 'Please wait while we connect to Qwen Code...', ); - await vscode.commands.executeCommand('qwenCode.login'); - } - } - return; - } - - // Validate current session before sending message - const isSessionValid = await this.agentManager.checkSessionValidity(); - if (!isSessionValid) { - console.warn('[SessionMessageHandler] Current session is not valid'); - - // Show non-modal notification with Login button - const result = await vscode.window.showWarningMessage( - 'Your session has expired. Please login again to continue using Qwen Code.', - 'Login Now', - ); - - if (result === 'Login Now') { - // Use login handler directly - if (this.loginHandler) { - await this.loginHandler(); - } else { - // Fallback to command - 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; @@ -379,7 +358,8 @@ export class SessionMessageHandler extends BaseMessageHandler { console.error('[SessionMessageHandler] Error sending message:', error); const err = error as unknown as Error; - const errorMsg = String(error); + // Safely convert error to string + const errorMsg = error ? String(error) : 'Unknown error'; const lower = errorMsg.toLowerCase(); // Suppress user-cancelled/aborted errors (ESC/Stop button) @@ -420,7 +400,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 +436,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; @@ -489,13 +469,17 @@ export class SessionMessageHandler extends BaseMessageHandler { type: 'conversationCleared', data: {}, }); + + // Reset title flag when creating a new session + this.isTitleSet = false; } catch (error) { console.error( '[SessionMessageHandler] Failed to create new session:', error, ); - const errorMsg = String(error); + // Safely convert error to string + const errorMsg = error ? String(error) : 'Unknown error'; // Check for authentication/session expiration errors if ( errorMsg.includes('Authentication required') || @@ -514,7 +498,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 +535,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 @@ -593,8 +577,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 +597,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, @@ -628,13 +614,21 @@ export class SessionMessageHandler extends BaseMessageHandler { type: 'qwenSessionSwitched', data: { sessionId, messages, session: sessionDetails }, }); + + // Reset title flag when switching sessions + this.isTitleSet = false; + + // Successfully loaded session, return early to avoid fallback logic + return; } catch (loadError) { console.warn( '[SessionMessageHandler] session/load failed, using fallback:', loadError, ); - const errorMsg = String(loadError); + // Safely convert error to string + const errorMsg = loadError ? String(loadError) : 'Unknown error'; + // Check for authentication/session expiration errors if ( errorMsg.includes('Authentication required') || @@ -653,7 +647,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'); } } @@ -681,16 +675,29 @@ export class SessionMessageHandler extends BaseMessageHandler { data: { sessionId, messages, session: sessionDetails }, }); - vscode.window.showWarningMessage( - 'Session restored from local cache. Some context may be incomplete.', - ); + // Only show the cache warning if we actually fell back to local cache + // and didn't successfully load via ACP + // Check if we truly fell back by checking if loadError is not null/undefined + // and if it's not a successful response that looks like an error + if ( + loadError && + typeof loadError === 'object' && + !('result' in loadError) + ) { + vscode.window.showWarningMessage( + 'Session restored from local cache. Some context may be incomplete.', + ); + } } catch (createError) { console.error( '[SessionMessageHandler] Failed to create session:', createError, ); - const createErrorMsg = String(createError); + // Safely convert error to string + const createErrorMsg = createError + ? String(createError) + : 'Unknown error'; // Check for authentication/session expiration errors in session creation if ( createErrorMsg.includes('Authentication required') || @@ -709,7 +716,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'); } } @@ -738,7 +745,8 @@ export class SessionMessageHandler extends BaseMessageHandler { } catch (error) { console.error('[SessionMessageHandler] Failed to switch session:', error); - const errorMsg = String(error); + // Safely convert error to string + const errorMsg = error ? String(error) : 'Unknown error'; // Check for authentication/session expiration errors if ( errorMsg.includes('Authentication required') || @@ -757,7 +765,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'); } } @@ -778,17 +786,31 @@ 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); - const errorMsg = String(error); + // Safely convert error to string + const errorMsg = error ? String(error) : 'Unknown error'; // Check for authentication/session expiration errors if ( errorMsg.includes('Authentication required') || @@ -807,7 +829,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'); } } @@ -851,7 +873,8 @@ export class SessionMessageHandler extends BaseMessageHandler { data: response, }); } catch (acpError) { - const errorMsg = String(acpError); + // Safely convert error to string + const errorMsg = acpError ? String(acpError) : 'Unknown error'; // Check for authentication/session expiration errors if ( errorMsg.includes('Authentication required') || @@ -870,7 +893,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'); } } @@ -898,7 +921,8 @@ export class SessionMessageHandler extends BaseMessageHandler { } catch (error) { console.error('[SessionMessageHandler] Failed to save session:', error); - const errorMsg = String(error); + // Safely convert error to string + const errorMsg = error ? String(error) : 'Unknown error'; // Check for authentication/session expiration errors if ( errorMsg.includes('Authentication required') || @@ -917,7 +941,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 +1007,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 = @@ -1014,8 +1038,16 @@ export class SessionMessageHandler extends BaseMessageHandler { type: 'qwenSessionSwitched', data: { sessionId, messages }, }); + + // Reset title flag when resuming sessions + this.isTitleSet = false; + + // Successfully loaded session, return early to avoid fallback logic + await this.handleGetQwenSessions(); + return; } catch (acpError) { - const errorMsg = String(acpError); + // Safely convert error to string + const errorMsg = acpError ? String(acpError) : 'Unknown error'; // Check for authentication/session expiration errors if ( errorMsg.includes('Authentication required') || @@ -1034,7 +1066,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'); } } @@ -1065,7 +1097,8 @@ export class SessionMessageHandler extends BaseMessageHandler { } catch (error) { console.error('[SessionMessageHandler] Failed to resume session:', error); - const errorMsg = String(error); + // Safely convert error to string + const errorMsg = error ? String(error) : 'Unknown error'; // Check for authentication/session expiration errors if ( errorMsg.includes('Authentication required') || @@ -1084,7 +1117,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/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 4cb3eb78..7d530404 100644 --- a/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts +++ b/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts @@ -10,18 +10,27 @@ 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 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 +496,19 @@ export const useWebViewMessages = ({ } case 'qwenSessionList': { - const sessions = message.data.sessions || []; - handlers.sessionManagement.setQwenSessions(sessions); + const sessions = + (message.data.sessions as Array>) || []; + const append = Boolean(message.data.append); + const nextCursor = message.data.nextCursor as number | undefined; + const hasMore = Boolean(message.data.hasMore); + + handlers.sessionManagement.setQwenSessions( + (prev: Array>) => + append ? [...prev, ...sessions] : sessions, + ); + handlers.sessionManagement.setNextCursor(nextCursor); + handlers.sessionManagement.setHasMore(hasMore); + handlers.sessionManagement.setIsLoading(false); if ( handlers.sessionManagement.currentSessionId && sessions.length > 0 @@ -533,8 +553,26 @@ export const useWebViewMessages = ({ } else { handlers.messageHandling.clearMessages(); } + + // Clear and restore tool calls if provided in session data handlers.clearToolCalls(); - handlers.setPlanEntries([]); + if (message.data.toolCalls && Array.isArray(message.data.toolCalls)) { + message.data.toolCalls.forEach((toolCall: unknown) => { + if (toolCall && typeof toolCall === 'object') { + handlers.handleToolCallUpdate(toolCall as ToolCallUpdate); + } + }); + } + + // Restore plan entries if provided + if ( + message.data.planEntries && + Array.isArray(message.data.planEntries) + ) { + handlers.setPlanEntries(message.data.planEntries); + } else { + handlers.setPlanEntries([]); + } lastPlanSnapshotRef.current = null; break;