mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
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
This commit is contained in:
@@ -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<ChatHeaderProps> = ({
|
||||||
|
currentSessionTitle,
|
||||||
|
onLoadSessions,
|
||||||
|
onSaveSession: _onSaveSession,
|
||||||
|
onNewSession,
|
||||||
|
}) => (
|
||||||
|
<div
|
||||||
|
className="flex gap-1 select-none py-1.5 px-2.5"
|
||||||
|
style={{
|
||||||
|
borderBottom: '1px solid var(--app-primary-border-color)',
|
||||||
|
backgroundColor: 'var(--app-header-background)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Past Conversations Button */}
|
||||||
|
<button
|
||||||
|
className="flex-none py-1 px-2 bg-transparent border border-transparent rounded cursor-pointer flex items-center justify-center outline-none font-medium transition-colors duration-200 hover:bg-[var(--app-ghost-button-hover-background)] focus:bg-[var(--app-ghost-button-hover-background)]"
|
||||||
|
style={{
|
||||||
|
borderRadius: 'var(--corner-radius-small)',
|
||||||
|
color: 'var(--app-primary-foreground)',
|
||||||
|
fontSize: 'var(--vscode-chat-font-size, 13px)',
|
||||||
|
}}
|
||||||
|
onClick={onLoadSessions}
|
||||||
|
title="Past conversations"
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<span style={{ fontSize: 'var(--vscode-chat-font-size, 13px)' }}>
|
||||||
|
{currentSessionTitle}
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
className="w-3.5 h-3.5"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Spacer */}
|
||||||
|
<div className="flex-1"></div>
|
||||||
|
|
||||||
|
{/* Save Session Button */}
|
||||||
|
{/* <button
|
||||||
|
className="flex-none p-0 bg-transparent border border-transparent rounded cursor-pointer flex items-center justify-center outline-none w-6 h-6 hover:bg-[var(--app-ghost-button-hover-background)] focus:bg-[var(--app-ghost-button-hover-background)]"
|
||||||
|
style={{
|
||||||
|
color: 'var(--app-primary-foreground)',
|
||||||
|
}}
|
||||||
|
onClick={onSaveSession}
|
||||||
|
title="Save Conversation"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
data-slot="icon"
|
||||||
|
className="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M4.25 2A2.25 2.25 0 0 0 2 4.25v11.5A2.25 2.25 0 0 0 4.25 18h11.5A2.25 2.25 0 0 0 18 15.75V8.25a.75.75 0 0 1 .217-.517l.083-.083a.75.75 0 0 1 1.061 0l2.239 2.239A.75.75 0 0 1 22 10.5v5.25a4.75 4.75 0 0 1-4.75 4.75H4.75A4.75 4.75 0 0 1 0 15.75V4.25A4.75 4.75 0 0 1 4.75 0h5a.75.75 0 0 1 0 1.5h-5ZM9.017 6.5a1.5 1.5 0 0 1 2.072.58l.43.862a1 1 0 0 0 .895.558h3.272a1.5 1.5 0 0 1 1.5 1.5v6.75a1.5 1.5 0 0 1-1.5 1.5h-7.5a1.5 1.5 0 0 1-1.5-1.5v-6.75a1.5 1.5 0 0 1 1.5-1.5h1.25a1 1 0 0 0 .895-.558l.43-.862a1.5 1.5 0 0 1 .511-.732ZM11.78 8.47a.75.75 0 0 0-1.06-1.06L8.75 9.379 7.78 8.41a.75.75 0 0 0-1.06 1.06l1.5 1.5a.75.75 0 0 0 1.06 0l2.5-2.5Z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</button> */}
|
||||||
|
|
||||||
|
{/* New Session Button */}
|
||||||
|
<button
|
||||||
|
className="flex-none p-0 bg-transparent border border-transparent rounded cursor-pointer flex items-center justify-center outline-none w-6 h-6 hover:bg-[var(--app-ghost-button-hover-background)] focus:bg-[var(--app-ghost-button-hover-background)]"
|
||||||
|
style={{
|
||||||
|
color: 'var(--app-primary-foreground)',
|
||||||
|
}}
|
||||||
|
onClick={onNewSession}
|
||||||
|
title="New Session"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
data-slot="icon"
|
||||||
|
className="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path d="M10.75 4.75a.75.75 0 0 0-1.5 0v4.5h-4.5a.75.75 0 0 0 0 1.5h4.5v4.5a.75.75 0 0 0 1.5 0v-4.5h4.5a.75.75 0 0 0 0-1.5h-4.5v-4.5Z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
@@ -17,6 +17,9 @@ interface SessionSelectorProps {
|
|||||||
onSearchChange: (query: string) => void;
|
onSearchChange: (query: string) => void;
|
||||||
onSelectSession: (sessionId: string) => void;
|
onSelectSession: (sessionId: string) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
hasMore?: boolean;
|
||||||
|
isLoading?: boolean;
|
||||||
|
onLoadMore?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,6 +34,9 @@ export const SessionSelector: React.FC<SessionSelectorProps> = ({
|
|||||||
onSearchChange,
|
onSearchChange,
|
||||||
onSelectSession,
|
onSelectSession,
|
||||||
onClose,
|
onClose,
|
||||||
|
hasMore = false,
|
||||||
|
isLoading = false,
|
||||||
|
onLoadMore,
|
||||||
}) => {
|
}) => {
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
return null;
|
return null;
|
||||||
@@ -66,7 +72,17 @@ export const SessionSelector: React.FC<SessionSelectorProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Session List with Grouping */}
|
{/* Session List with Grouping */}
|
||||||
<div className="session-list-content overflow-y-auto flex-1 select-none p-2">
|
<div
|
||||||
|
className="session-list-content overflow-y-auto flex-1 select-none p-2"
|
||||||
|
onScroll={(e) => {
|
||||||
|
const el = e.currentTarget;
|
||||||
|
const distanceToBottom =
|
||||||
|
el.scrollHeight - (el.scrollTop + el.clientHeight);
|
||||||
|
if (distanceToBottom < 48 && hasMore && !isLoading) {
|
||||||
|
onLoadMore?.();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
{hasNoSessions ? (
|
{hasNoSessions ? (
|
||||||
<div
|
<div
|
||||||
className="p-5 text-center text-[var(--app-secondary-foreground)]"
|
className="p-5 text-center text-[var(--app-secondary-foreground)]"
|
||||||
@@ -126,6 +142,11 @@ export const SessionSelector: React.FC<SessionSelectorProps> = ({
|
|||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
{hasMore && (
|
||||||
|
<div className="p-2 text-center opacity-60 text-[0.9em]">
|
||||||
|
{isLoading ? 'Loading…' : ''}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
|||||||
private currentStreamContent = '';
|
private currentStreamContent = '';
|
||||||
private isSavingCheckpoint = false;
|
private isSavingCheckpoint = false;
|
||||||
private loginHandler: (() => Promise<void>) | null = null;
|
private loginHandler: (() => Promise<void>) | null = null;
|
||||||
|
private isTitleSet = false; // Flag to track if title has been set
|
||||||
|
|
||||||
canHandle(messageType: string): boolean {
|
canHandle(messageType: string): boolean {
|
||||||
return [
|
return [
|
||||||
@@ -74,7 +75,10 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'getQwenSessions':
|
case 'getQwenSessions':
|
||||||
await this.handleGetQwenSessions();
|
await this.handleGetQwenSessions(
|
||||||
|
(data?.cursor as number | undefined) ?? undefined,
|
||||||
|
(data?.size as number | undefined) ?? undefined,
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'saveSession':
|
case 'saveSession':
|
||||||
@@ -231,8 +235,8 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate title for first message
|
// Generate title for first message, but only if it hasn't been set yet
|
||||||
if (isFirstMessage) {
|
if (isFirstMessage && !this.isTitleSet) {
|
||||||
const title = text.substring(0, 50) + (text.length > 50 ? '...' : '');
|
const title = text.substring(0, 50) + (text.length > 50 ? '...' : '');
|
||||||
this.sendToWebView({
|
this.sendToWebView({
|
||||||
type: 'sessionTitleUpdated',
|
type: 'sessionTitleUpdated',
|
||||||
@@ -241,6 +245,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
|||||||
title,
|
title,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
this.isTitleSet = true; // Mark title as set
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save user message
|
// Save user message
|
||||||
@@ -280,33 +285,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
|||||||
vscode.window.showInformationMessage(
|
vscode.window.showInformationMessage(
|
||||||
'Please wait while we connect to Qwen Code...',
|
'Please wait while we connect to Qwen Code...',
|
||||||
);
|
);
|
||||||
await vscode.commands.executeCommand('qwenCode.login');
|
await vscode.commands.executeCommand('qwen-code.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');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -379,7 +358,8 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
|||||||
console.error('[SessionMessageHandler] Error sending message:', error);
|
console.error('[SessionMessageHandler] Error sending message:', error);
|
||||||
|
|
||||||
const err = error as unknown as 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();
|
const lower = errorMsg.toLowerCase();
|
||||||
|
|
||||||
// Suppress user-cancelled/aborted errors (ESC/Stop button)
|
// Suppress user-cancelled/aborted errors (ESC/Stop button)
|
||||||
@@ -420,7 +400,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
|||||||
if (this.loginHandler) {
|
if (this.loginHandler) {
|
||||||
await this.loginHandler();
|
await this.loginHandler();
|
||||||
} else {
|
} 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) {
|
if (this.loginHandler) {
|
||||||
await this.loginHandler();
|
await this.loginHandler();
|
||||||
} else {
|
} else {
|
||||||
await vscode.commands.executeCommand('qwenCode.login');
|
await vscode.commands.executeCommand('qwen-code.login');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
@@ -489,13 +469,17 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
|||||||
type: 'conversationCleared',
|
type: 'conversationCleared',
|
||||||
data: {},
|
data: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Reset title flag when creating a new session
|
||||||
|
this.isTitleSet = false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
'[SessionMessageHandler] Failed to create new session:',
|
'[SessionMessageHandler] Failed to create new session:',
|
||||||
error,
|
error,
|
||||||
);
|
);
|
||||||
|
|
||||||
const errorMsg = String(error);
|
// Safely convert error to string
|
||||||
|
const errorMsg = error ? String(error) : 'Unknown error';
|
||||||
// Check for authentication/session expiration errors
|
// Check for authentication/session expiration errors
|
||||||
if (
|
if (
|
||||||
errorMsg.includes('Authentication required') ||
|
errorMsg.includes('Authentication required') ||
|
||||||
@@ -514,7 +498,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
|||||||
if (this.loginHandler) {
|
if (this.loginHandler) {
|
||||||
await this.loginHandler();
|
await this.loginHandler();
|
||||||
} else {
|
} 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) {
|
if (this.loginHandler) {
|
||||||
await this.loginHandler();
|
await this.loginHandler();
|
||||||
} else {
|
} else {
|
||||||
await vscode.commands.executeCommand('qwenCode.login');
|
await vscode.commands.executeCommand('qwen-code.login');
|
||||||
}
|
}
|
||||||
} else if (selection === 'View Offline') {
|
} else if (selection === 'View Offline') {
|
||||||
// Show messages from local cache only
|
// Show messages from local cache only
|
||||||
@@ -593,8 +577,8 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get session details
|
// Get session details (includes cwd and filePath when using ACP)
|
||||||
let sessionDetails = null;
|
let sessionDetails: Record<string, unknown> | null = null;
|
||||||
try {
|
try {
|
||||||
const allSessions = await this.agentManager.getSessionList();
|
const allSessions = await this.agentManager.getSessionList();
|
||||||
sessionDetails = allSessions.find(
|
sessionDetails = allSessions.find(
|
||||||
@@ -613,8 +597,10 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
|||||||
|
|
||||||
// Try to load session via ACP (now we should be connected)
|
// Try to load session via ACP (now we should be connected)
|
||||||
try {
|
try {
|
||||||
const loadResponse =
|
const loadResponse = await this.agentManager.loadSessionViaAcp(
|
||||||
await this.agentManager.loadSessionViaAcp(sessionId);
|
sessionId,
|
||||||
|
(sessionDetails?.cwd as string | undefined) || undefined,
|
||||||
|
);
|
||||||
console.log(
|
console.log(
|
||||||
'[SessionMessageHandler] session/load succeeded:',
|
'[SessionMessageHandler] session/load succeeded:',
|
||||||
loadResponse,
|
loadResponse,
|
||||||
@@ -628,13 +614,21 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
|||||||
type: 'qwenSessionSwitched',
|
type: 'qwenSessionSwitched',
|
||||||
data: { sessionId, messages, session: sessionDetails },
|
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) {
|
} catch (loadError) {
|
||||||
console.warn(
|
console.warn(
|
||||||
'[SessionMessageHandler] session/load failed, using fallback:',
|
'[SessionMessageHandler] session/load failed, using fallback:',
|
||||||
loadError,
|
loadError,
|
||||||
);
|
);
|
||||||
|
|
||||||
const errorMsg = String(loadError);
|
// Safely convert error to string
|
||||||
|
const errorMsg = loadError ? String(loadError) : 'Unknown error';
|
||||||
|
|
||||||
// Check for authentication/session expiration errors
|
// Check for authentication/session expiration errors
|
||||||
if (
|
if (
|
||||||
errorMsg.includes('Authentication required') ||
|
errorMsg.includes('Authentication required') ||
|
||||||
@@ -653,7 +647,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
|||||||
if (this.loginHandler) {
|
if (this.loginHandler) {
|
||||||
await this.loginHandler();
|
await this.loginHandler();
|
||||||
} else {
|
} 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 },
|
data: { sessionId, messages, session: sessionDetails },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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(
|
vscode.window.showWarningMessage(
|
||||||
'Session restored from local cache. Some context may be incomplete.',
|
'Session restored from local cache. Some context may be incomplete.',
|
||||||
);
|
);
|
||||||
|
}
|
||||||
} catch (createError) {
|
} catch (createError) {
|
||||||
console.error(
|
console.error(
|
||||||
'[SessionMessageHandler] Failed to create session:',
|
'[SessionMessageHandler] Failed to create session:',
|
||||||
createError,
|
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
|
// Check for authentication/session expiration errors in session creation
|
||||||
if (
|
if (
|
||||||
createErrorMsg.includes('Authentication required') ||
|
createErrorMsg.includes('Authentication required') ||
|
||||||
@@ -709,7 +716,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
|||||||
if (this.loginHandler) {
|
if (this.loginHandler) {
|
||||||
await this.loginHandler();
|
await this.loginHandler();
|
||||||
} else {
|
} 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) {
|
} catch (error) {
|
||||||
console.error('[SessionMessageHandler] Failed to switch session:', 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
|
// Check for authentication/session expiration errors
|
||||||
if (
|
if (
|
||||||
errorMsg.includes('Authentication required') ||
|
errorMsg.includes('Authentication required') ||
|
||||||
@@ -757,7 +765,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
|||||||
if (this.loginHandler) {
|
if (this.loginHandler) {
|
||||||
await this.loginHandler();
|
await this.loginHandler();
|
||||||
} else {
|
} 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
|
* Handle get Qwen sessions request
|
||||||
*/
|
*/
|
||||||
private async handleGetQwenSessions(): Promise<void> {
|
private async handleGetQwenSessions(
|
||||||
|
cursor?: number,
|
||||||
|
size?: number,
|
||||||
|
): Promise<void> {
|
||||||
try {
|
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({
|
this.sendToWebView({
|
||||||
type: 'qwenSessionList',
|
type: 'qwenSessionList',
|
||||||
data: { sessions },
|
data: {
|
||||||
|
sessions: page.sessions,
|
||||||
|
nextCursor: page.nextCursor,
|
||||||
|
hasMore: page.hasMore,
|
||||||
|
append,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[SessionMessageHandler] Failed to get sessions:', 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
|
// Check for authentication/session expiration errors
|
||||||
if (
|
if (
|
||||||
errorMsg.includes('Authentication required') ||
|
errorMsg.includes('Authentication required') ||
|
||||||
@@ -807,7 +829,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
|||||||
if (this.loginHandler) {
|
if (this.loginHandler) {
|
||||||
await this.loginHandler();
|
await this.loginHandler();
|
||||||
} else {
|
} 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,
|
data: response,
|
||||||
});
|
});
|
||||||
} catch (acpError) {
|
} catch (acpError) {
|
||||||
const errorMsg = String(acpError);
|
// Safely convert error to string
|
||||||
|
const errorMsg = acpError ? String(acpError) : 'Unknown error';
|
||||||
// Check for authentication/session expiration errors
|
// Check for authentication/session expiration errors
|
||||||
if (
|
if (
|
||||||
errorMsg.includes('Authentication required') ||
|
errorMsg.includes('Authentication required') ||
|
||||||
@@ -870,7 +893,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
|||||||
if (this.loginHandler) {
|
if (this.loginHandler) {
|
||||||
await this.loginHandler();
|
await this.loginHandler();
|
||||||
} else {
|
} 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) {
|
} catch (error) {
|
||||||
console.error('[SessionMessageHandler] Failed to save session:', 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
|
// Check for authentication/session expiration errors
|
||||||
if (
|
if (
|
||||||
errorMsg.includes('Authentication required') ||
|
errorMsg.includes('Authentication required') ||
|
||||||
@@ -917,7 +941,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
|||||||
if (this.loginHandler) {
|
if (this.loginHandler) {
|
||||||
await this.loginHandler();
|
await this.loginHandler();
|
||||||
} else {
|
} 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) {
|
if (this.loginHandler) {
|
||||||
await this.loginHandler();
|
await this.loginHandler();
|
||||||
} else {
|
} else {
|
||||||
await vscode.commands.executeCommand('qwenCode.login');
|
await vscode.commands.executeCommand('qwen-code.login');
|
||||||
}
|
}
|
||||||
} else if (selection === 'View Offline') {
|
} else if (selection === 'View Offline') {
|
||||||
const messages =
|
const messages =
|
||||||
@@ -1014,8 +1038,16 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
|||||||
type: 'qwenSessionSwitched',
|
type: 'qwenSessionSwitched',
|
||||||
data: { sessionId, messages },
|
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) {
|
} catch (acpError) {
|
||||||
const errorMsg = String(acpError);
|
// Safely convert error to string
|
||||||
|
const errorMsg = acpError ? String(acpError) : 'Unknown error';
|
||||||
// Check for authentication/session expiration errors
|
// Check for authentication/session expiration errors
|
||||||
if (
|
if (
|
||||||
errorMsg.includes('Authentication required') ||
|
errorMsg.includes('Authentication required') ||
|
||||||
@@ -1034,7 +1066,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
|||||||
if (this.loginHandler) {
|
if (this.loginHandler) {
|
||||||
await this.loginHandler();
|
await this.loginHandler();
|
||||||
} else {
|
} 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) {
|
} catch (error) {
|
||||||
console.error('[SessionMessageHandler] Failed to resume session:', 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
|
// Check for authentication/session expiration errors
|
||||||
if (
|
if (
|
||||||
errorMsg.includes('Authentication required') ||
|
errorMsg.includes('Authentication required') ||
|
||||||
@@ -1084,7 +1117,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
|||||||
if (this.loginHandler) {
|
if (this.loginHandler) {
|
||||||
await this.loginHandler();
|
await this.loginHandler();
|
||||||
} else {
|
} else {
|
||||||
await vscode.commands.executeCommand('qwenCode.login');
|
await vscode.commands.executeCommand('qwen-code.login');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ export const useSessionManagement = (vscode: VSCodeAPI) => {
|
|||||||
const [showSessionSelector, setShowSessionSelector] = useState(false);
|
const [showSessionSelector, setShowSessionSelector] = useState(false);
|
||||||
const [sessionSearchQuery, setSessionSearchQuery] = useState('');
|
const [sessionSearchQuery, setSessionSearchQuery] = useState('');
|
||||||
const [savedSessionTags, setSavedSessionTags] = useState<string[]>([]);
|
const [savedSessionTags, setSavedSessionTags] = useState<string[]>([]);
|
||||||
|
const [nextCursor, setNextCursor] = useState<number | undefined>(undefined);
|
||||||
|
const [hasMore, setHasMore] = useState<boolean>(true);
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const PAGE_SIZE = 20;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter session list
|
* Filter session list
|
||||||
@@ -44,10 +49,24 @@ export const useSessionManagement = (vscode: VSCodeAPI) => {
|
|||||||
* Load session list
|
* Load session list
|
||||||
*/
|
*/
|
||||||
const handleLoadQwenSessions = useCallback(() => {
|
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);
|
setShowSessionSelector(true);
|
||||||
}, [vscode]);
|
}, [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
|
* Create new session
|
||||||
*/
|
*/
|
||||||
@@ -117,6 +136,9 @@ export const useSessionManagement = (vscode: VSCodeAPI) => {
|
|||||||
sessionSearchQuery,
|
sessionSearchQuery,
|
||||||
filteredSessions,
|
filteredSessions,
|
||||||
savedSessionTags,
|
savedSessionTags,
|
||||||
|
nextCursor,
|
||||||
|
hasMore,
|
||||||
|
isLoading,
|
||||||
|
|
||||||
// State setters
|
// State setters
|
||||||
setQwenSessions,
|
setQwenSessions,
|
||||||
@@ -125,6 +147,9 @@ export const useSessionManagement = (vscode: VSCodeAPI) => {
|
|||||||
setShowSessionSelector,
|
setShowSessionSelector,
|
||||||
setSessionSearchQuery,
|
setSessionSearchQuery,
|
||||||
setSavedSessionTags,
|
setSavedSessionTags,
|
||||||
|
setNextCursor,
|
||||||
|
setHasMore,
|
||||||
|
setIsLoading,
|
||||||
|
|
||||||
// Operations
|
// Operations
|
||||||
handleLoadQwenSessions,
|
handleLoadQwenSessions,
|
||||||
@@ -132,5 +157,6 @@ export const useSessionManagement = (vscode: VSCodeAPI) => {
|
|||||||
handleSwitchSession,
|
handleSwitchSession,
|
||||||
handleSaveSession,
|
handleSaveSession,
|
||||||
handleSaveSessionResponse,
|
handleSaveSessionResponse,
|
||||||
|
handleLoadMoreSessions,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,18 +10,27 @@ import type { Conversation } from '../../storage/conversationStore.js';
|
|||||||
import type {
|
import type {
|
||||||
PermissionOption,
|
PermissionOption,
|
||||||
ToolCall as PermissionToolCall,
|
ToolCall as PermissionToolCall,
|
||||||
} from '../components/PermissionRequest.js';
|
} from '../components/PermissionDrawer/PermissionRequest.js';
|
||||||
import type { PlanEntry } from '../components/PlanDisplay.js';
|
|
||||||
import type { ToolCallUpdate } from '../types/toolCall.js';
|
import type { ToolCallUpdate } from '../types/toolCall.js';
|
||||||
|
import type { PlanEntry } from '../../agents/qwenTypes.js';
|
||||||
|
|
||||||
interface UseWebViewMessagesProps {
|
interface UseWebViewMessagesProps {
|
||||||
// Session management
|
// Session management
|
||||||
sessionManagement: {
|
sessionManagement: {
|
||||||
currentSessionId: string | null;
|
currentSessionId: string | null;
|
||||||
setQwenSessions: (sessions: Array<Record<string, unknown>>) => void;
|
setQwenSessions: (
|
||||||
|
sessions:
|
||||||
|
| Array<Record<string, unknown>>
|
||||||
|
| ((
|
||||||
|
prev: Array<Record<string, unknown>>,
|
||||||
|
) => Array<Record<string, unknown>>),
|
||||||
|
) => void;
|
||||||
setCurrentSessionId: (id: string | null) => void;
|
setCurrentSessionId: (id: string | null) => void;
|
||||||
setCurrentSessionTitle: (title: string) => void;
|
setCurrentSessionTitle: (title: string) => void;
|
||||||
setShowSessionSelector: (show: boolean) => void;
|
setShowSessionSelector: (show: boolean) => void;
|
||||||
|
setNextCursor: (cursor: number | undefined) => void;
|
||||||
|
setHasMore: (hasMore: boolean) => void;
|
||||||
|
setIsLoading: (loading: boolean) => void;
|
||||||
handleSaveSessionResponse: (response: {
|
handleSaveSessionResponse: (response: {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
message?: string;
|
message?: string;
|
||||||
@@ -487,8 +496,19 @@ export const useWebViewMessages = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'qwenSessionList': {
|
case 'qwenSessionList': {
|
||||||
const sessions = message.data.sessions || [];
|
const sessions =
|
||||||
handlers.sessionManagement.setQwenSessions(sessions);
|
(message.data.sessions as Array<Record<string, unknown>>) || [];
|
||||||
|
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<Record<string, unknown>>) =>
|
||||||
|
append ? [...prev, ...sessions] : sessions,
|
||||||
|
);
|
||||||
|
handlers.sessionManagement.setNextCursor(nextCursor);
|
||||||
|
handlers.sessionManagement.setHasMore(hasMore);
|
||||||
|
handlers.sessionManagement.setIsLoading(false);
|
||||||
if (
|
if (
|
||||||
handlers.sessionManagement.currentSessionId &&
|
handlers.sessionManagement.currentSessionId &&
|
||||||
sessions.length > 0
|
sessions.length > 0
|
||||||
@@ -533,8 +553,26 @@ export const useWebViewMessages = ({
|
|||||||
} else {
|
} else {
|
||||||
handlers.messageHandling.clearMessages();
|
handlers.messageHandling.clearMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear and restore tool calls if provided in session data
|
||||||
handlers.clearToolCalls();
|
handlers.clearToolCalls();
|
||||||
|
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([]);
|
handlers.setPlanEntries([]);
|
||||||
|
}
|
||||||
lastPlanSnapshotRef.current = null;
|
lastPlanSnapshotRef.current = null;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user