mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-22 09:47:47 +00:00
WIP: All changes including session and toolcall improvements
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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} />
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -56,6 +56,9 @@ export interface ToolCallData {
|
||||
*/
|
||||
export interface BaseToolCallProps {
|
||||
toolCall: ToolCallData;
|
||||
// Optional timeline flags for rendering connector line cropping
|
||||
isFirst?: boolean;
|
||||
isLast?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user