WIP: All changes including session and toolcall improvements

This commit is contained in:
yiliang114
2025-12-06 16:53:40 +08:00
parent 541d0b22e5
commit 57a684ad97
18 changed files with 622 additions and 230 deletions

View File

@@ -508,6 +508,9 @@ export const App: React.FC = () => {
sessionManagement.setSessionSearchQuery('');
}}
onClose={() => sessionManagement.setShowSessionSelector(false)}
hasMore={sessionManagement.hasMore}
isLoading={sessionManagement.isLoading}
onLoadMore={sessionManagement.handleLoadMoreSessions}
/>
<ChatHeader
@@ -627,14 +630,26 @@ export const App: React.FC = () => {
// );
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<string, unknown>) &&
((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 (
<ToolCall
key={`completed-${(item.data as ToolCallData).toolCallId}`}
toolCall={item.data as ToolCallData}
// onFileClick={handleFileClick}
isFirst={isFirst}
isLast={isLast}
/>
);
}
default:
return null;

View File

@@ -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<void> {
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(

View File

@@ -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 }) => <ToolCallRouter toolCall={toolCall} />;
isFirst?: boolean;
isLast?: boolean;
}> = ({ toolCall, isFirst, isLast }) => (
<ToolCallRouter toolCall={toolCall} isFirst={isFirst} isLast={isLast} />
);

View File

@@ -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<SessionSelectorProps> = ({
onSearchChange,
onSelectSession,
onClose,
hasMore = false,
isLoading = false,
onLoadMore,
}) => {
if (!visible) {
return null;
@@ -66,7 +72,17 @@ export const SessionSelector: React.FC<SessionSelectorProps> = ({
</div>
{/* 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 ? (
<div
className="p-5 text-center text-[var(--app-secondary-foreground)]"
@@ -126,6 +142,11 @@ export const SessionSelector: React.FC<SessionSelectorProps> = ({
</React.Fragment>
))
)}
{hasMore && (
<div className="p-2 text-center opacity-60 text-[0.9em]">
{isLoading ? 'Loading…' : ''}
</div>
)}
</div>
</div>
</>

View File

@@ -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<BaseToolCallProps> = ({ toolCall }) => {
export const ReadToolCall: React.FC<BaseToolCallProps> = ({
toolCall,
isFirst,
isLast,
}) => {
const { content, locations, toolCallId } = toolCall;
const vscode = useVSCode();
@@ -71,76 +74,85 @@ export const ReadToolCall: React.FC<BaseToolCallProps> = ({ 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 (
<div
className={
`qwen-message message-item relative pl-[30px] py-2 select-text ` +
`before:absolute before:left-[8px] before:top-2 before:content-["\\25cf"] before:text-[10px] before:z-[1] ` +
beforeStatusClass
}
>
{/* timeline vertical line */}
<div
className={`absolute left-[12px] ${lineCropTop} ${lineCropBottom} w-px bg-[var(--app-primary-border-color)]`}
aria-hidden
/>
<div className="flex-1 min-w-0">
<div className="flex items-baseline gap-2 min-w-0">
<span className="text-[14px] leading-none font-bold text-[var(--app-primary-foreground)]">
Read
</span>
{path ? (
<FileLink
path={path}
showFullPath={false}
className="text-xs font-mono text-[var(--app-secondary-foreground)] hover:underline"
/>
) : null}
</div>
{children ? (
<div
className={`mt-1 text-[var(--app-secondary-foreground)] ${
isError ? 'text-qwen-error' : ''
}`}
>
{children}
</div>
) : null}
</div>
</div>
);
};
// Error case: show error
if (errors.length > 0) {
const path = locations?.[0]?.path || '';
return (
<ToolCallContainer
label={'Read'}
className="read-tool-call-error"
status="error"
toolCallId={toolCallId}
labelSuffix={
path ? (
<FileLink
path={path}
showFullPath={false}
className="text-xs font-mono text-[var(--app-secondary-foreground)] hover:underline"
/>
) : undefined
}
>
<ReadContainer status="error" path={path} isError>
{errors.join('\n')}
</ToolCallContainer>
</ReadContainer>
);
}
// 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 (
<ToolCallContainer
label={'Read'}
className={`read-tool-call-${containerStatus}`}
status={containerStatus}
toolCallId={toolCallId}
labelSuffix={
path ? (
<FileLink
path={path}
showFullPath={false}
className="text-xs font-mono text-[var(--app-secondary-foreground)] hover:underline"
/>
) : undefined
}
>
{null}
</ToolCallContainer>
);
return <ReadContainer status={containerStatus} path={path} />;
}
// Success case: show which file was read with filename in label
if (locations && locations.length > 0) {
const path = locations[0].path;
return (
<ToolCallContainer
label={'Read'}
className={`read-tool-call-${containerStatus}`}
status={containerStatus}
toolCallId={toolCallId}
labelSuffix={
path ? (
<FileLink
path={path}
showFullPath={false}
className="text-xs font-mono text-[var(--app-secondary-foreground)] hover:underline"
/>
) : undefined
}
>
{null}
</ToolCallContainer>
);
return <ReadContainer status={containerStatus} path={path} />;
}
// No file info, don't show

View File

@@ -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<BaseToolCallProps> = ({ 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 (
<div
className={
`qwen-message message-item relative pl-[30px] py-2 select-text ` +
`before:absolute before:left-[8px] before:top-2 before:content-["\\25cf"] before:text-[10px] before:z-[1] ` +
beforeStatusClass
}
>
{/* timeline vertical line */}
<div
className={`absolute left-[12px] ${lineCropTop} ${lineCropBottom} w-px bg-[var(--app-primary-border-color)]`}
aria-hidden
/>
<div className="flex-1 min-w-0">
<div className="flex items-baseline gap-2 min-w-0">
<span className="text-[14px] leading-none font-bold text-[var(--app-primary-foreground)]">
Search
</span>
{labelSuffix ? (
<span className="text-[11px] text-[var(--app-secondary-foreground)]">
{labelSuffix}
</span>
) : null}
</div>
{children ? (
<div className="mt-1 text-[var(--app-secondary-foreground)]">{children}</div>
) : null}
</div>
</div>
);
};
// 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 (
<div
className={
`qwen-message message-item relative pl-[30px] py-2 select-text ` +
`before:absolute before:left-[8px] before:top-2 before:content-["\\25cf"] before:text-[10px] before:z-[1] ` +
beforeStatusClass
}
>
{/* timeline vertical line */}
<div
className={`absolute left-[12px] ${lineCropTop} ${lineCropBottom} w-px bg-[var(--app-primary-border-color)]`}
aria-hidden
/>
<div className="bg-[var(--app-input-background)] border border-[var(--app-input-border)] rounded-medium p-large my-medium">
<div className="flex flex-col gap-3 min-w-0">{children}</div>
</div>
</div>
);
};
const SearchRow: React.FC<{ label: string; children: React.ReactNode }> = ({
label,
children,
}) => (
<div className="grid grid-cols-[80px_1fr] gap-medium min-w-0">
<div className="text-xs text-[var(--app-secondary-foreground)] font-medium pt-[2px]">
{label}
</div>
<div className="text-[var(--app-primary-foreground)] min-w-0 break-words">
{children}
</div>
</div>
);
const LocationsListLocal: React.FC<{
locations: Array<{ path: string; line?: number | null }>;
}> = ({ locations }) => (
<div className="flex flex-col gap-1 max-w-full">
{locations.map((loc, idx) => (
<FileLink key={idx} path={loc.path} line={loc.line} showFullPath={true} />
))}
</div>
);
export const SearchToolCall: React.FC<BaseToolCallProps> = ({
toolCall,
isFirst,
isLast,
}) => {
const { title, content, locations } = toolCall;
const queryText = safeTitle(title);
@@ -35,14 +145,14 @@ export const SearchToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
// Error case: show search query + error in card layout
if (errors.length > 0) {
return (
<ToolCallCard icon="🔍">
<ToolCallRow label="Search">
<SearchCard status="error" isFirst={isFirst} isLast={isLast}>
<SearchRow label="Search">
<div className="font-mono">{queryText}</div>
</ToolCallRow>
<ToolCallRow label="Error">
<div className="text-[#c74e39] font-medium">{errors.join('\n')}</div>
</ToolCallRow>
</ToolCallCard>
</SearchRow>
<SearchRow label="Error">
<div className="text-qwen-error font-medium">{errors.join('\n')}</div>
</SearchRow>
</SearchCard>
);
}
@@ -52,28 +162,27 @@ export const SearchToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
// If multiple results, use card layout; otherwise use compact format
if (locations.length > 1) {
return (
<ToolCallCard icon="🔍">
<ToolCallRow label="Search">
<SearchCard status={containerStatus} isFirst={isFirst} isLast={isLast}>
<SearchRow label="Search">
<div className="font-mono">{queryText}</div>
</ToolCallRow>
<ToolCallRow label={`Found (${locations.length})`}>
<LocationsList locations={locations} />
</ToolCallRow>
</ToolCallCard>
</SearchRow>
<SearchRow label={`Found (${locations.length})`}>
<LocationsListLocal locations={locations} />
</SearchRow>
</SearchCard>
);
}
// Single result - compact format
return (
<ToolCallContainer
label="Search"
<InlineContainer
status={containerStatus}
className="search-toolcall"
labelSuffix={`(${queryText})`}
isFirst={isFirst}
isLast={isLast}
>
{/* <span className="font-mono">{queryText}</span> */}
<span className="mx-2 opacity-50"></span>
<LocationsList locations={locations} />
</ToolCallContainer>
<LocationsListLocal locations={locations} />
</InlineContainer>
);
}
@@ -81,11 +190,11 @@ export const SearchToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
if (textOutputs.length > 0) {
const containerStatus = mapToolStatusToContainerStatus(toolCall.status);
return (
<ToolCallContainer
label="Search"
<InlineContainer
status={containerStatus}
className="search-toolcall"
labelSuffix={queryText ? `(${queryText})` : undefined}
isFirst={isFirst}
isLast={isLast}
>
<div className="flex flex-col">
{textOutputs.map((text, index) => (
@@ -98,7 +207,7 @@ export const SearchToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
</div>
))}
</div>
</ToolCallContainer>
</InlineContainer>
);
}
@@ -106,13 +215,9 @@ export const SearchToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
if (queryText) {
const containerStatus = mapToolStatusToContainerStatus(toolCall.status);
return (
<ToolCallContainer
label="Search"
status={containerStatus}
className="search-toolcall"
>
<InlineContainer status={containerStatus} isFirst={isFirst} isLast={isLast}>
<span className="font-mono">{queryText}</span>
</ToolCallContainer>
</InlineContainer>
);
}

View File

@@ -92,7 +92,11 @@ export const getToolCallComponent = (
/**
* Main tool call component that routes to specialized implementations
*/
export const ToolCallRouter: React.FC<BaseToolCallProps> = ({ toolCall }) => {
export const ToolCallRouter: React.FC<BaseToolCallProps> = ({
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<BaseToolCallProps> = ({ toolCall }) => {
const Component = getToolCallComponent(toolCall.kind, toolCall);
// Render the specialized component
return <Component toolCall={toolCall} />;
return <Component toolCall={toolCall} isFirst={isFirst} isLast={isLast} />;
};
// Re-export types for convenience

View File

@@ -47,10 +47,8 @@ export const ToolCallContainer: React.FC<ToolCallContainerProps> = ({
<div
className={`qwen-message message-item ${_className || ''} relative pl-[30px] py-2 select-text toolcall-container toolcall-status-${status}`}
>
{/* Timeline connector line using ::after pseudo-element */}
{/* TODO: gap-0 */}
<div className="toolcall-content-wrapper flex flex-col gap-1 min-w-0 max-w-full">
<div className="flex items-center gap-1 relative min-w-0">
<div className="toolcall-content-wrapper flex flex-col gap-2 min-w-0 max-w-full">
<div className="flex items-baseline gap-1 relative min-w-0">
<span className="text-[14px] leading-none font-bold text-[var(--app-primary-foreground)]">
{label}
</span>

View File

@@ -56,6 +56,9 @@ export interface ToolCallData {
*/
export interface BaseToolCallProps {
toolCall: ToolCallData;
// Optional timeline flags for rendering connector line cropping
isFirst?: boolean;
isLast?: boolean;
}
/**

View File

@@ -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<string, unknown> | 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<void> {
private async handleGetQwenSessions(
cursor?: number,
size?: number,
): Promise<void> {
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);

View File

@@ -21,6 +21,11 @@ export const useSessionManagement = (vscode: VSCodeAPI) => {
const [showSessionSelector, setShowSessionSelector] = useState(false);
const [sessionSearchQuery, setSessionSearchQuery] = useState('');
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
@@ -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,
};
};

View File

@@ -18,10 +18,17 @@ interface UseWebViewMessagesProps {
// Session management
sessionManagement: {
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;
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