mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
style(vscode-ide-companion): adjust chat session initialization logic and optimize tool invocation component style
This commit is contained in:
@@ -73,12 +73,7 @@ export function registerNewCommands(
|
|||||||
disposables.push(
|
disposables.push(
|
||||||
vscode.commands.registerCommand(openNewChatTabCommand, async () => {
|
vscode.commands.registerCommand(openNewChatTabCommand, async () => {
|
||||||
const provider = createWebViewProvider();
|
const provider = createWebViewProvider();
|
||||||
// Suppress auto-restore for this newly created tab so it starts clean
|
// Session restoration is now disabled by default, so no need to suppress it
|
||||||
try {
|
|
||||||
provider.suppressAutoRestoreOnce?.();
|
|
||||||
} catch {
|
|
||||||
// ignore if older provider does not implement the method
|
|
||||||
}
|
|
||||||
await provider.show();
|
await provider.show();
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -224,6 +224,7 @@ export const App: React.FC = () => {
|
|||||||
handlePermissionRequest: setPermissionRequest,
|
handlePermissionRequest: setPermissionRequest,
|
||||||
inputFieldRef,
|
inputFieldRef,
|
||||||
setInputText,
|
setInputText,
|
||||||
|
setEditMode,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auto-scroll handling: keep the view pinned to bottom when new content arrives,
|
// Auto-scroll handling: keep the view pinned to bottom when new content arrives,
|
||||||
@@ -473,15 +474,22 @@ export const App: React.FC = () => {
|
|||||||
// Handle toggle edit mode
|
// Handle toggle edit mode
|
||||||
const handleToggleEditMode = useCallback(() => {
|
const handleToggleEditMode = useCallback(() => {
|
||||||
setEditMode((prev) => {
|
setEditMode((prev) => {
|
||||||
if (prev === 'ask') {
|
const next: EditMode =
|
||||||
return 'auto';
|
prev === 'ask' ? 'auto' : prev === 'auto' ? 'plan' : 'ask';
|
||||||
|
// Notify extension to set approval mode via ACP
|
||||||
|
try {
|
||||||
|
const toAcp =
|
||||||
|
next === 'plan' ? 'plan' : next === 'auto' ? 'auto-edit' : 'default';
|
||||||
|
vscode.postMessage({
|
||||||
|
type: 'setApprovalMode',
|
||||||
|
data: { modeId: toAcp },
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
/* no-op */
|
||||||
}
|
}
|
||||||
if (prev === 'auto') {
|
return next;
|
||||||
return 'plan';
|
|
||||||
}
|
|
||||||
return 'ask';
|
|
||||||
});
|
});
|
||||||
}, []);
|
}, [vscode]);
|
||||||
|
|
||||||
// Handle toggle thinking
|
// Handle toggle thinking
|
||||||
const handleToggleThinking = () => {
|
const handleToggleThinking = () => {
|
||||||
|
|||||||
@@ -26,8 +26,6 @@ export class WebViewProvider {
|
|||||||
private authStateManager: AuthStateManager;
|
private authStateManager: AuthStateManager;
|
||||||
private disposables: vscode.Disposable[] = [];
|
private disposables: vscode.Disposable[] = [];
|
||||||
private agentInitialized = false; // Track if agent has been initialized
|
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(
|
constructor(
|
||||||
context: vscode.ExtensionContext,
|
context: vscode.ExtensionContext,
|
||||||
@@ -242,13 +240,6 @@ export class WebViewProvider {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Suppress auto-restore once for this panel (used by "New Chat Tab").
|
|
||||||
*/
|
|
||||||
suppressAutoRestoreOnce(): void {
|
|
||||||
this.autoRestoreOnFirstConnect = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async show(): Promise<void> {
|
async show(): Promise<void> {
|
||||||
const panel = this.panelManager.getPanel();
|
const panel = this.panelManager.getPanel();
|
||||||
|
|
||||||
@@ -383,6 +374,22 @@ export class WebViewProvider {
|
|||||||
type: 'activeEditorChanged',
|
type: 'activeEditorChanged',
|
||||||
data: { fileName, filePath, selection: selectionInfo },
|
data: { fileName, filePath, selection: selectionInfo },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Surface available modes and current mode (from ACP initialize)
|
||||||
|
this.agentManager.onModeInfo((info) => {
|
||||||
|
this.sendMessageToWebView({
|
||||||
|
type: 'modeInfo',
|
||||||
|
data: info || {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Surface mode changes (from ACP or immediate set_mode response)
|
||||||
|
this.agentManager.onModeChanged((modeId) => {
|
||||||
|
this.sendMessageToWebView({
|
||||||
|
type: 'modeChanged',
|
||||||
|
data: { modeId },
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.disposables.push(selectionChangeDisposable);
|
this.disposables.push(selectionChangeDisposable);
|
||||||
@@ -681,199 +688,40 @@ export class WebViewProvider {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Load messages from current Qwen session
|
* Load messages from current Qwen session
|
||||||
* Attempts to restore an existing session before creating a new one
|
* Skips session restoration and creates a new session directly
|
||||||
*/
|
*/
|
||||||
private async loadCurrentSessionMessages(): Promise<void> {
|
private async loadCurrentSessionMessages(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log(
|
console.log(
|
||||||
'[WebViewProvider] Initializing with session restoration attempt',
|
'[WebViewProvider] Initializing with new session (skipping restoration)',
|
||||||
);
|
);
|
||||||
|
|
||||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||||
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
|
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
|
||||||
|
|
||||||
// First, try to restore an existing session if we have cached auth
|
// Skip session restoration entirely and create a new session directly
|
||||||
if (this.authStateManager) {
|
try {
|
||||||
const hasValidAuth = await this.authStateManager.hasValidAuth(
|
await this.agentManager.createNewSession(
|
||||||
workingDir,
|
workingDir,
|
||||||
authMethod,
|
this.authStateManager,
|
||||||
);
|
);
|
||||||
if (hasValidAuth) {
|
console.log('[WebViewProvider] ACP session created successfully');
|
||||||
const allowAutoRestore = this.autoRestoreOnFirstConnect;
|
|
||||||
// Reset for subsequent connects (only once per panel lifecycle unless set again)
|
|
||||||
this.autoRestoreOnFirstConnect = true;
|
|
||||||
|
|
||||||
if (allowAutoRestore) {
|
// Ensure auth state is saved after successful session creation
|
||||||
console.log(
|
if (this.authStateManager) {
|
||||||
'[WebViewProvider] Valid auth found, attempting auto-restore of last session...',
|
await this.authStateManager.saveAuthState(workingDir, authMethod);
|
||||||
);
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Even if messages array is empty, we should still switch to the session
|
|
||||||
// This ensures we don't lose the session context
|
|
||||||
this.sendMessageToWebView({
|
|
||||||
type: 'qwenSessionSwitched',
|
|
||||||
data: { sessionId: targetId, messages },
|
|
||||||
});
|
|
||||||
console.log(
|
|
||||||
'[WebViewProvider] Auto-restored last session:',
|
|
||||||
targetId,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Ensure auth state is saved after successful session restore
|
|
||||||
if (this.authStateManager) {
|
|
||||||
await this.authStateManager.saveAuthState(
|
|
||||||
workingDir,
|
|
||||||
authMethod,
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
'[WebViewProvider] Auth state saved after session restore',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Try to get session messages anyway, even if loadSessionViaAcp failed
|
|
||||||
// This can happen if the session exists locally but failed to load in the CLI
|
|
||||||
try {
|
|
||||||
const page = await this.agentManager.getSessionListPaged({
|
|
||||||
size: 1,
|
|
||||||
});
|
|
||||||
const item = page.sessions[0] as
|
|
||||||
| { sessionId?: string; id?: string }
|
|
||||||
| undefined;
|
|
||||||
if (item && (item.sessionId || item.id)) {
|
|
||||||
const targetId = (item.sessionId || item.id) as string;
|
|
||||||
const messages =
|
|
||||||
await this.agentManager.getSessionMessages(targetId);
|
|
||||||
|
|
||||||
// Switch to the session with whatever messages we could get
|
|
||||||
this.messageHandler.setCurrentConversationId(targetId);
|
|
||||||
this.sendMessageToWebView({
|
|
||||||
type: 'qwenSessionSwitched',
|
|
||||||
data: { sessionId: targetId, messages },
|
|
||||||
});
|
|
||||||
console.log(
|
|
||||||
'[WebViewProvider] Partially restored last session:',
|
|
||||||
targetId,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Ensure auth state is saved after partial session restore
|
|
||||||
if (this.authStateManager) {
|
|
||||||
await this.authStateManager.saveAuthState(
|
|
||||||
workingDir,
|
|
||||||
authMethod,
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
'[WebViewProvider] Auth state saved after partial session restore',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (fallbackError) {
|
|
||||||
console.warn(
|
|
||||||
'[WebViewProvider] Fallback session restore also failed:',
|
|
||||||
fallbackError,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
'[WebViewProvider] Auto-restore suppressed for this panel',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a fresh ACP session (no auto-restore or restore failed)
|
|
||||||
try {
|
|
||||||
await this.agentManager.createNewSession(
|
|
||||||
workingDir,
|
|
||||||
this.authStateManager,
|
|
||||||
);
|
|
||||||
console.log('[WebViewProvider] ACP session created successfully');
|
|
||||||
|
|
||||||
// Ensure auth state is saved after successful session creation
|
|
||||||
if (this.authStateManager) {
|
|
||||||
await this.authStateManager.saveAuthState(workingDir, authMethod);
|
|
||||||
console.log(
|
|
||||||
'[WebViewProvider] Auth state saved after session creation',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} 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(
|
console.log(
|
||||||
'[WebViewProvider] No valid cached auth found, creating new session',
|
'[WebViewProvider] Auth state saved after session creation',
|
||||||
);
|
);
|
||||||
// No valid auth, create a new session (will trigger auth if needed)
|
|
||||||
try {
|
|
||||||
await this.agentManager.createNewSession(
|
|
||||||
workingDir,
|
|
||||||
this.authStateManager,
|
|
||||||
);
|
|
||||||
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.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} catch (sessionError) {
|
||||||
// No auth state manager, create a new session
|
console.error(
|
||||||
console.log(
|
'[WebViewProvider] Failed to create ACP session:',
|
||||||
'[WebViewProvider] No auth state manager, creating new session',
|
sessionError,
|
||||||
|
);
|
||||||
|
vscode.window.showWarningMessage(
|
||||||
|
`Failed to create ACP session: ${sessionError}. You may need to authenticate first.`,
|
||||||
);
|
);
|
||||||
try {
|
|
||||||
await this.agentManager.createNewSession(
|
|
||||||
workingDir,
|
|
||||||
this.authStateManager,
|
|
||||||
);
|
|
||||||
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.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.initializeEmptyConversation();
|
await this.initializeEmptyConversation();
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Title + Description (from toolCall.title) */}
|
{/* Title + Description (from toolCall.title) */}
|
||||||
<div className="relative z-[1] px-1 text-[1.1em] text-[var(--app-primary-foreground)] flex flex-col min-h-0">
|
<div className="relative z-[1] text-[1.1em] text-[var(--app-primary-foreground)] flex flex-col min-h-0">
|
||||||
<div className="font-bold text-[var(--app-primary-foreground)] mb-0.5">
|
<div className="font-bold text-[var(--app-primary-foreground)] mb-0.5">
|
||||||
{getTitle()}
|
{getTitle()}
|
||||||
</div>
|
</div>
|
||||||
@@ -198,6 +198,11 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
|
|||||||
<div
|
<div
|
||||||
/* 13px,常规字重;正常空白折行 + 长词断行;最多 3 行溢出省略 */
|
/* 13px,常规字重;正常空白折行 + 长词断行;最多 3 行溢出省略 */
|
||||||
className="text-[13px] font-normal text-[var(--app-secondary-foreground)] opacity-90 font-mono whitespace-normal break-words q-line-clamp-3 mb-2"
|
className="text-[13px] font-normal text-[var(--app-secondary-foreground)] opacity-90 font-mono whitespace-normal break-words q-line-clamp-3 mb-2"
|
||||||
|
style={{
|
||||||
|
fontSize: '.9em',
|
||||||
|
color: 'var(--app-secondary-foreground)',
|
||||||
|
marginBottom: '6px',
|
||||||
|
}}
|
||||||
title={toolCall.title}
|
title={toolCall.title}
|
||||||
>
|
>
|
||||||
{toolCall.title}
|
{toolCall.title}
|
||||||
@@ -206,14 +211,14 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Options */}
|
{/* Options */}
|
||||||
<div className="relative z-[1] flex flex-col gap-1 px-1 pb-1">
|
<div className="relative z-[1] flex flex-col gap-1 pb-1">
|
||||||
{options.map((option, index) => {
|
{options.map((option, index) => {
|
||||||
const isFocused = focusedIndex === index;
|
const isFocused = focusedIndex === index;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={option.optionId}
|
key={option.optionId}
|
||||||
className={`flex items-center gap-2 px-2 py-1.5 text-left w-full box-border rounded-[4px] border-0 shadow-[inset_0_0_0_1px_var(--app-transparent-inner-border)] transition-colors duration-150 text-[var(--app-primary-foreground)] hover:bg-[var(--app-list-hover-background)] ${
|
className={`flex items-center gap-2 px-2 py-1.5 text-left w-full box-border rounded-[4px] border-0 shadow-[inset_0_0_0_1px_var(--app-transparent-inner-border)] transition-colors duration-150 text-[var(--app-primary-foreground)] hover:bg-[var(--app-button-background)] ${
|
||||||
isFocused
|
isFocused
|
||||||
? 'text-[var(--app-list-active-foreground)] bg-[var(--app-list-active-background)] hover:text-[var(--app-button-foreground)] hover:font-bold hover:relative hover:border-0'
|
? 'text-[var(--app-list-active-foreground)] bg-[var(--app-list-active-background)] hover:text-[var(--app-button-foreground)] hover:font-bold hover:relative hover:border-0'
|
||||||
: 'hover:bg-[var(--app-button-background)] hover:text-[var(--app-button-foreground)] hover:font-bold hover:relative hover:border-0'
|
: 'hover:bg-[var(--app-button-background)] hover:text-[var(--app-button-foreground)] hover:font-bold hover:relative hover:border-0'
|
||||||
@@ -222,15 +227,11 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
|
|||||||
onMouseEnter={() => setFocusedIndex(index)}
|
onMouseEnter={() => setFocusedIndex(index)}
|
||||||
>
|
>
|
||||||
{/* Number badge */}
|
{/* Number badge */}
|
||||||
{/* Plain number badge without hover background */}
|
<span className="inline-flex items-center justify-center min-w-[10px] h-5 font-semibold opacity-60">
|
||||||
<span className="inline-flex items-center justify-center min-w-[10px] h-5 font-semibold">
|
|
||||||
{index + 1}
|
{index + 1}
|
||||||
</span>
|
</span>
|
||||||
{/* Option text */}
|
{/* Option text */}
|
||||||
<span className="font-semibold">{option.name}</span>
|
<span className="font-semibold">{option.name}</span>
|
||||||
|
|
||||||
{/* Always badge */}
|
|
||||||
{/* {isAlways && <span className="text-sm">⚡</span>} */}
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -283,15 +284,12 @@ const CustomMessageInputRow: React.FC<CustomMessageInputRowProps> = ({
|
|||||||
inputRef,
|
inputRef,
|
||||||
}) => (
|
}) => (
|
||||||
<div
|
<div
|
||||||
// 无过渡:hover 样式立即生效;输入行不加 hover 背景,也不加粗文字
|
|
||||||
className={`flex items-center gap-2 px-2 py-1.5 text-left w-full box-border rounded-[4px] border-0 shadow-[inset_0_0_0_1px_var(--app-transparent-inner-border)] cursor-text text-[var(--app-primary-foreground)] ${
|
className={`flex items-center gap-2 px-2 py-1.5 text-left w-full box-border rounded-[4px] border-0 shadow-[inset_0_0_0_1px_var(--app-transparent-inner-border)] cursor-text text-[var(--app-primary-foreground)] ${
|
||||||
isFocused ? 'text-[var(--app-list-active-foreground)]' : ''
|
isFocused ? 'text-[var(--app-list-active-foreground)]' : ''
|
||||||
}`}
|
}`}
|
||||||
onMouseEnter={onFocusRow}
|
onMouseEnter={onFocusRow}
|
||||||
onClick={() => inputRef.current?.focus()}
|
onClick={() => inputRef.current?.focus()}
|
||||||
>
|
>
|
||||||
{/* 输入行不显示序号徽标 */}
|
|
||||||
{/* Input field */}
|
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type="text"
|
type="text"
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2025 Qwen Team
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type React from 'react';
|
|
||||||
import { CheckboxDisplay } from './ui/CheckboxDisplay.js';
|
|
||||||
|
|
||||||
export interface PlanEntry {
|
|
||||||
content: string;
|
|
||||||
priority: 'high' | 'medium' | 'low';
|
|
||||||
status: 'pending' | 'in_progress' | 'completed';
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PlanDisplayProps {
|
|
||||||
entries: PlanEntry[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PlanDisplay component - displays AI's task plan/todo list
|
|
||||||
*/
|
|
||||||
export const PlanDisplay: React.FC<PlanDisplayProps> = ({ entries }) => {
|
|
||||||
// Calculate overall status for left dot color
|
|
||||||
const allCompleted =
|
|
||||||
entries.length > 0 && entries.every((e) => e.status === 'completed');
|
|
||||||
const anyInProgress = entries.some((e) => e.status === 'in_progress');
|
|
||||||
const statusDotClass = allCompleted
|
|
||||||
? 'before:text-[#74c991]'
|
|
||||||
: anyInProgress
|
|
||||||
? 'before:text-[#e1c08d]'
|
|
||||||
: 'before:text-[var(--app-secondary-foreground)]';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={[
|
|
||||||
// Container: Similar to example .A/.e
|
|
||||||
'relative flex flex-col items-start py-2 pl-[30px] select-text text-[var(--app-primary-foreground)]',
|
|
||||||
// Left status dot, similar to example .e:before
|
|
||||||
'before:content-["\\25cf"] before:absolute before:left-[10px] before:top-[12px] before:text-[10px] before:z-[1]',
|
|
||||||
statusDotClass,
|
|
||||||
// Original plan-display styles: bg-transparent border-0 py-2 px-4 my-2
|
|
||||||
'bg-transparent border-0 my-2',
|
|
||||||
].join(' ')}
|
|
||||||
>
|
|
||||||
{/* Title area, similar to example summary/_e/or */}
|
|
||||||
<div className="w-full flex items-center gap-1.5 mb-2">
|
|
||||||
<div className="relative">
|
|
||||||
<div className="list-none line-clamp-2 max-w-full overflow-hidden _e">
|
|
||||||
<span>
|
|
||||||
<div>
|
|
||||||
<span className="or font-bold mr-1">Update Todos</span>
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* List area, similar to example .qr/.Fr/.Hr */}
|
|
||||||
<div className="qr grid-cols-1 flex flex-col py-2">
|
|
||||||
<ul className="Fr list-none p-0 m-0 flex flex-col gap-1">
|
|
||||||
{entries.map((entry, index) => {
|
|
||||||
const isDone = entry.status === 'completed';
|
|
||||||
const isIndeterminate = entry.status === 'in_progress';
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
key={index}
|
|
||||||
className={[
|
|
||||||
'Hr flex items-start gap-2 p-0 rounded text-[var(--app-primary-foreground)]',
|
|
||||||
isDone ? 'fo opacity-70' : '',
|
|
||||||
].join(' ')}
|
|
||||||
>
|
|
||||||
{/* Display checkbox (reusable component) */}
|
|
||||||
<label className="flex items-start gap-2">
|
|
||||||
<CheckboxDisplay
|
|
||||||
checked={isDone}
|
|
||||||
indeterminate={isIndeterminate}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={[
|
|
||||||
'vo plan-entry-text flex-1 text-xs leading-[1.5] text-[var(--app-primary-foreground)]',
|
|
||||||
isDone
|
|
||||||
? 'line-through text-[var(--app-secondary-foreground)] opacity-70'
|
|
||||||
: 'opacity-85',
|
|
||||||
].join(' ')}
|
|
||||||
>
|
|
||||||
{entry.content}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -67,7 +67,7 @@ export const UserMessage: React.FC<UserMessageProps> = ({
|
|||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className="mr inline-flex items-center py-0 pl-1 pr-2 ml-1 gap-1 rounded-sm cursor-pointer relative opacity-50"
|
className="mr inline-flex items-center py-0 pr-2 gap-1 rounded-sm cursor-pointer relative opacity-50"
|
||||||
onClick={() => fileContext && onFileClick?.(fileContext.filePath)}
|
onClick={() => fileContext && onFileClick?.(fileContext.filePath)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
|||||||
@@ -15,17 +15,7 @@ export const InterruptedMessage: React.FC<InterruptedMessageProps> = ({
|
|||||||
text = 'Interrupted',
|
text = 'Interrupted',
|
||||||
}) => (
|
}) => (
|
||||||
<div className="flex gap-0 items-start text-left py-2 flex-col opacity-85">
|
<div className="flex gap-0 items-start text-left py-2 flex-col opacity-85">
|
||||||
<div
|
<div className="interrupted-item w-full relative">
|
||||||
className="qwen-message message-item interrupted-item"
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
paddingLeft: '10px', // keep alignment with other assistant messages, but no status icon
|
|
||||||
position: 'relative',
|
|
||||||
paddingTop: '8px',
|
|
||||||
paddingBottom: '8px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="opacity-70 italic">{text}</span>
|
<span className="opacity-70 italic">{text}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,16 +8,41 @@
|
|||||||
|
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import type { BaseToolCallProps } from '../shared/types.js';
|
import type { BaseToolCallProps } from '../shared/types.js';
|
||||||
import { ToolCallContainer } from '../shared/LayoutComponents.js';
|
import type { ToolCallContainerProps } from '../shared/LayoutComponents.js';
|
||||||
import { groupContent, safeTitle } from '../shared/utils.js';
|
import { groupContent, safeTitle } from '../shared/utils.js';
|
||||||
import { CheckboxDisplay } from '../../ui/CheckboxDisplay.js';
|
import { CheckboxDisplay } from '../../ui/CheckboxDisplay.js';
|
||||||
|
import type { PlanEntry } from '../../../../agents/qwenTypes.js';
|
||||||
|
|
||||||
type EntryStatus = 'pending' | 'in_progress' | 'completed';
|
type EntryStatus = 'pending' | 'in_progress' | 'completed';
|
||||||
|
|
||||||
interface PlanEntry {
|
export const ToolCallContainer: React.FC<ToolCallContainerProps> = ({
|
||||||
content: string;
|
label,
|
||||||
status: EntryStatus;
|
status = 'success',
|
||||||
}
|
children,
|
||||||
|
toolCallId: _toolCallId,
|
||||||
|
labelSuffix,
|
||||||
|
className: _className,
|
||||||
|
}) => (
|
||||||
|
<div
|
||||||
|
className={`qwen-message message-item ${_className || ''} relative pl-[30px] py-2 select-text toolcall-container toolcall-status-${status}`}
|
||||||
|
>
|
||||||
|
<div className="UpdatedPlanToolCall 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>
|
||||||
|
<span className="text-[11px] text-[var(--app-secondary-foreground)]">
|
||||||
|
{labelSuffix}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{children && (
|
||||||
|
<div className="text-[var(--app-secondary-foreground)] py-1">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
const mapToolStatusToBullet = (
|
const mapToolStatusToBullet = (
|
||||||
status: import('../shared/types.js').ToolCallStatus,
|
status: import('../shared/types.js').ToolCallStatus,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { FileLink } from '../../../ui/FileLink.js';
|
|||||||
import type { ToolCallContainerProps } from '../../shared/LayoutComponents.js';
|
import type { ToolCallContainerProps } from '../../shared/LayoutComponents.js';
|
||||||
import { useVSCode } from '../../../../hooks/useVSCode.js';
|
import { useVSCode } from '../../../../hooks/useVSCode.js';
|
||||||
import { handleOpenDiff } from '../../../../utils/diffUtils.js';
|
import { handleOpenDiff } from '../../../../utils/diffUtils.js';
|
||||||
|
import { DiffDisplay } from '../../shared/DiffDisplay.js';
|
||||||
|
|
||||||
export const ToolCallContainer: React.FC<ToolCallContainerProps> = ({
|
export const ToolCallContainer: React.FC<ToolCallContainerProps> = ({
|
||||||
label,
|
label,
|
||||||
@@ -109,6 +110,64 @@ export const EditToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [toolCallId]);
|
}, [toolCallId]);
|
||||||
|
|
||||||
|
// Failed case: show explicit failed message and render inline diffs
|
||||||
|
if (toolCall.status === 'failed') {
|
||||||
|
const firstDiff = diffs[0];
|
||||||
|
const path = firstDiff?.path || locations?.[0]?.path || '';
|
||||||
|
const containerStatus = mapToolStatusToContainerStatus(toolCall.status);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`qwen-message message-item relative py-2 select-text toolcall-container toolcall-status-${containerStatus}`}
|
||||||
|
>
|
||||||
|
<div className="toolcall-edit-content flex flex-col gap-1 min-w-0 max-w-full">
|
||||||
|
<div className="flex items-center justify-between min-w-0">
|
||||||
|
<div className="flex items-baseline gap-2 min-w-0">
|
||||||
|
<span className="text-[13px] leading-none font-bold text-[var(--app-primary-foreground)]">
|
||||||
|
Edit
|
||||||
|
</span>
|
||||||
|
{path && (
|
||||||
|
<FileLink
|
||||||
|
path={path}
|
||||||
|
showFullPath={false}
|
||||||
|
className="font-mono text-[var(--app-secondary-foreground)] hover:underline"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Failed state text (replace summary) */}
|
||||||
|
<div className="inline-flex text-[var(--app-secondary-foreground)] text-[0.85em] opacity-70 flex-row items-start w-full gap-1 flex items-center">
|
||||||
|
<span className="flex-shrink-0 w-full">edit failed</span>
|
||||||
|
</div>
|
||||||
|
{/* Inline diff preview(s) */}
|
||||||
|
{diffs.length > 0 && (
|
||||||
|
<div className="flex flex-col gap-2 mt-1">
|
||||||
|
{diffs.map(
|
||||||
|
(
|
||||||
|
item: import('../../shared/types.js').ToolCallContent,
|
||||||
|
idx: number,
|
||||||
|
) => (
|
||||||
|
<DiffDisplay
|
||||||
|
key={`diff-${idx}`}
|
||||||
|
path={item.path}
|
||||||
|
oldText={item.oldText}
|
||||||
|
newText={item.newText}
|
||||||
|
onOpenDiff={() =>
|
||||||
|
handleOpenDiffInternal(
|
||||||
|
item.path || path,
|
||||||
|
item.oldText,
|
||||||
|
item.newText,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Error case: show error
|
// Error case: show error
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
const path = diffs[0]?.path || locations?.[0]?.path || '';
|
const path = diffs[0]?.path || locations?.[0]?.path || '';
|
||||||
@@ -122,7 +181,7 @@ export const EditToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
|||||||
<FileLink
|
<FileLink
|
||||||
path={path}
|
path={path}
|
||||||
showFullPath={false}
|
showFullPath={false}
|
||||||
className="text-xs font-mono text-[var(--app-secondary-foreground)] hover:underline"
|
className="text-xs font-mono hover:underline"
|
||||||
/>
|
/>
|
||||||
) : undefined
|
) : undefined
|
||||||
}
|
}
|
||||||
@@ -141,21 +200,19 @@ export const EditToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`qwen-message message-item relative py-2 select-text toolcall-container toolcall-status-${containerStatus}`}
|
className={`qwen-message message-item relative py-2 select-text toolcall-container toolcall-status-${containerStatus}`}
|
||||||
title="Open diff in VS Code"
|
|
||||||
>
|
>
|
||||||
{/* IMPORTANT: Always include min-w-0/max-w-full on inner wrappers to prevent overflow. */}
|
|
||||||
<div className="toolcall-edit-content flex flex-col gap-1 min-w-0 max-w-full">
|
<div className="toolcall-edit-content flex flex-col gap-1 min-w-0 max-w-full">
|
||||||
<div className="flex items-center justify-between min-w-0">
|
<div className="flex items-center justify-between min-w-0">
|
||||||
<div className="flex items-baseline gap-1.5 min-w-0">
|
<div className="flex items-baseline gap-1.5 min-w-0">
|
||||||
{/* Align the inline Edit label styling with shared toolcall label: larger + bold */}
|
{/* Align the inline Edit label styling with shared toolcall label: larger + bold */}
|
||||||
<span className="text-[14px] leading-none font-bold text-[var(--app-primary-foreground)]">
|
<span className="text-[13px] leading-none font-bold text-[var(--app-primary-foreground)]">
|
||||||
Edit
|
Edit
|
||||||
</span>
|
</span>
|
||||||
{path && (
|
{path && (
|
||||||
<FileLink
|
<FileLink
|
||||||
path={path}
|
path={path}
|
||||||
showFullPath={false}
|
showFullPath={false}
|
||||||
className="text-xs font-mono text-[var(--app-secondary-foreground)] hover:underline"
|
className="font-mono text-[var(--app-secondary-foreground)] hover:underline"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,11 +8,7 @@
|
|||||||
|
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import type { BaseToolCallProps } from '../../shared/types.js';
|
import type { BaseToolCallProps } from '../../shared/types.js';
|
||||||
import {
|
import { FileLink } from '../../../ui/FileLink.js';
|
||||||
ToolCallCard,
|
|
||||||
ToolCallRow,
|
|
||||||
LocationsList,
|
|
||||||
} from '../../shared/LayoutComponents.js';
|
|
||||||
import {
|
import {
|
||||||
safeTitle,
|
safeTitle,
|
||||||
groupContent,
|
groupContent,
|
||||||
@@ -53,7 +49,128 @@ export const ToolCallContainer: React.FC<ToolCallContainerProps> = ({
|
|||||||
* Optimized for displaying search operations and results
|
* Optimized for displaying search operations and results
|
||||||
* Shows query + result count or file list
|
* 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 { title, content, locations } = toolCall;
|
||||||
const queryText = safeTitle(title);
|
const queryText = safeTitle(title);
|
||||||
|
|
||||||
@@ -63,14 +180,14 @@ export const SearchToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
|||||||
// Error case: show search query + error in card layout
|
// Error case: show search query + error in card layout
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
return (
|
return (
|
||||||
<ToolCallCard icon="🔍">
|
<SearchCard status="error" isFirst={isFirst} isLast={isLast}>
|
||||||
<ToolCallRow label="Search">
|
<SearchRow label="Search">
|
||||||
<div className="font-mono">{queryText}</div>
|
<div className="font-mono">{queryText}</div>
|
||||||
</ToolCallRow>
|
</SearchRow>
|
||||||
<ToolCallRow label="Error">
|
<SearchRow label="Error">
|
||||||
<div className="text-[#c74e39] font-medium">{errors.join('\n')}</div>
|
<div className="text-qwen-error font-medium">{errors.join('\n')}</div>
|
||||||
</ToolCallRow>
|
</SearchRow>
|
||||||
</ToolCallCard>
|
</SearchCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,28 +197,27 @@ export const SearchToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
|||||||
// If multiple results, use card layout; otherwise use compact format
|
// If multiple results, use card layout; otherwise use compact format
|
||||||
if (locations.length > 1) {
|
if (locations.length > 1) {
|
||||||
return (
|
return (
|
||||||
<ToolCallCard icon="🔍">
|
<SearchCard status={containerStatus} isFirst={isFirst} isLast={isLast}>
|
||||||
<ToolCallRow label="Search">
|
<SearchRow label="Search">
|
||||||
<div className="font-mono">{queryText}</div>
|
<div className="font-mono">{queryText}</div>
|
||||||
</ToolCallRow>
|
</SearchRow>
|
||||||
<ToolCallRow label={`Found (${locations.length})`}>
|
<SearchRow label={`Found (${locations.length})`}>
|
||||||
<LocationsList locations={locations} />
|
<LocationsListLocal locations={locations} />
|
||||||
</ToolCallRow>
|
</SearchRow>
|
||||||
</ToolCallCard>
|
</SearchCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Single result - compact format
|
// Single result - compact format
|
||||||
return (
|
return (
|
||||||
<ToolCallContainer
|
<InlineContainer
|
||||||
label="Search"
|
|
||||||
status={containerStatus}
|
status={containerStatus}
|
||||||
className="search-toolcall"
|
|
||||||
labelSuffix={`(${queryText})`}
|
labelSuffix={`(${queryText})`}
|
||||||
|
isFirst={isFirst}
|
||||||
|
isLast={isLast}
|
||||||
>
|
>
|
||||||
{/* <span className="font-mono">{queryText}</span> */}
|
|
||||||
<span className="mx-2 opacity-50">→</span>
|
<span className="mx-2 opacity-50">→</span>
|
||||||
<LocationsList locations={locations} />
|
<LocationsListLocal locations={locations} />
|
||||||
</ToolCallContainer>
|
</InlineContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,11 +225,11 @@ export const SearchToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
|||||||
if (textOutputs.length > 0) {
|
if (textOutputs.length > 0) {
|
||||||
const containerStatus = mapToolStatusToContainerStatus(toolCall.status);
|
const containerStatus = mapToolStatusToContainerStatus(toolCall.status);
|
||||||
return (
|
return (
|
||||||
<ToolCallContainer
|
<InlineContainer
|
||||||
label="Search"
|
|
||||||
status={containerStatus}
|
status={containerStatus}
|
||||||
className="search-toolcall"
|
|
||||||
labelSuffix={queryText ? `(${queryText})` : undefined}
|
labelSuffix={queryText ? `(${queryText})` : undefined}
|
||||||
|
isFirst={isFirst}
|
||||||
|
isLast={isLast}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
{textOutputs.map((text, index) => (
|
{textOutputs.map((text, index) => (
|
||||||
@@ -126,7 +242,7 @@ export const SearchToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</ToolCallContainer>
|
</InlineContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,13 +250,13 @@ export const SearchToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
|||||||
if (queryText) {
|
if (queryText) {
|
||||||
const containerStatus = mapToolStatusToContainerStatus(toolCall.status);
|
const containerStatus = mapToolStatusToContainerStatus(toolCall.status);
|
||||||
return (
|
return (
|
||||||
<ToolCallContainer
|
<InlineContainer
|
||||||
label="Search"
|
|
||||||
status={containerStatus}
|
status={containerStatus}
|
||||||
className="search-toolcall"
|
isFirst={isFirst}
|
||||||
|
isLast={isLast}
|
||||||
>
|
>
|
||||||
<span className="font-mono">{queryText}</span>
|
<span className="font-mono">{queryText}</span>
|
||||||
</ToolCallContainer>
|
</InlineContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,10 +47,8 @@ export const ToolCallContainer: React.FC<ToolCallContainerProps> = ({
|
|||||||
<div
|
<div
|
||||||
className={`qwen-message message-item ${_className || ''} relative pl-[30px] py-2 select-text toolcall-container toolcall-status-${status}`}
|
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 */}
|
<div className="toolcall-content-wrapper flex flex-col gap-2 min-w-0 max-w-full">
|
||||||
{/* TODO: gap-0 */}
|
<div className="flex items-baseline gap-1 relative min-w-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">
|
|
||||||
<span className="text-[14px] leading-none font-bold text-[var(--app-primary-foreground)]">
|
<span className="text-[14px] leading-none font-bold text-[var(--app-primary-foreground)]">
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ export interface ToolCallData {
|
|||||||
*/
|
*/
|
||||||
export interface BaseToolCallProps {
|
export interface BaseToolCallProps {
|
||||||
toolCall: ToolCallData;
|
toolCall: ToolCallData;
|
||||||
|
// Optional timeline flags for rendering connector line cropping
|
||||||
isFirst?: boolean;
|
isFirst?: boolean;
|
||||||
isLast?: boolean;
|
isLast?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ import { BaseMessageHandler } from './BaseMessageHandler.js';
|
|||||||
*/
|
*/
|
||||||
export class SettingsMessageHandler extends BaseMessageHandler {
|
export class SettingsMessageHandler extends BaseMessageHandler {
|
||||||
canHandle(messageType: string): boolean {
|
canHandle(messageType: string): boolean {
|
||||||
return ['openSettings', 'recheckCli'].includes(messageType);
|
return ['openSettings', 'recheckCli', 'setApprovalMode'].includes(
|
||||||
|
messageType,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handle(message: { type: string; data?: unknown }): Promise<void> {
|
async handle(message: { type: string; data?: unknown }): Promise<void> {
|
||||||
@@ -26,6 +28,14 @@ export class SettingsMessageHandler extends BaseMessageHandler {
|
|||||||
await this.handleRecheckCli();
|
await this.handleRecheckCli();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'setApprovalMode':
|
||||||
|
await this.handleSetApprovalMode(
|
||||||
|
message.data as {
|
||||||
|
modeId?: 'plan' | 'default' | 'auto-edit' | 'yolo';
|
||||||
|
},
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.warn(
|
console.warn(
|
||||||
'[SettingsMessageHandler] Unknown message type:',
|
'[SettingsMessageHandler] Unknown message type:',
|
||||||
@@ -68,4 +78,29 @@ export class SettingsMessageHandler extends BaseMessageHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set approval mode via agent (ACP session/set_mode)
|
||||||
|
*/
|
||||||
|
private async handleSetApprovalMode(data?: {
|
||||||
|
modeId?: 'plan' | 'default' | 'auto-edit' | 'yolo';
|
||||||
|
}): Promise<void> {
|
||||||
|
try {
|
||||||
|
const modeId = (data?.modeId || 'default') as
|
||||||
|
| 'plan'
|
||||||
|
| 'default'
|
||||||
|
| 'auto-edit'
|
||||||
|
| 'yolo';
|
||||||
|
await this.agentManager.setApprovalModeFromUi(
|
||||||
|
modeId === 'plan' ? 'plan' : modeId === 'auto-edit' ? 'auto' : 'ask',
|
||||||
|
);
|
||||||
|
// No explicit response needed; WebView listens for modeChanged
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[SettingsMessageHandler] Failed to set mode:', error);
|
||||||
|
this.sendToWebView({
|
||||||
|
type: 'error',
|
||||||
|
data: { message: `Failed to set mode: ${error}` },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,26 +70,32 @@ export const useMessageHandling = () => {
|
|||||||
/**
|
/**
|
||||||
* Add stream chunk
|
* Add stream chunk
|
||||||
*/
|
*/
|
||||||
const appendStreamChunk = useCallback((chunk: string) => {
|
const appendStreamChunk = useCallback(
|
||||||
setMessages((prev) => {
|
(chunk: string) => {
|
||||||
let idx = streamingMessageIndexRef.current;
|
// Ignore late chunks after user cancelled streaming (until next streamStart)
|
||||||
const next = prev.slice();
|
if (!isStreaming) return;
|
||||||
|
|
||||||
// If there is no active placeholder (e.g., after a tool call), start a new one
|
setMessages((prev) => {
|
||||||
if (idx === null) {
|
let idx = streamingMessageIndexRef.current;
|
||||||
idx = next.length;
|
const next = prev.slice();
|
||||||
streamingMessageIndexRef.current = idx;
|
|
||||||
next.push({ role: 'assistant', content: '', timestamp: Date.now() });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (idx < 0 || idx >= next.length) {
|
// If there is no active placeholder (e.g., after a tool call), start a new one
|
||||||
return prev;
|
if (idx === null) {
|
||||||
}
|
idx = next.length;
|
||||||
const target = next[idx];
|
streamingMessageIndexRef.current = idx;
|
||||||
next[idx] = { ...target, content: (target.content || '') + chunk };
|
next.push({ role: 'assistant', content: '', timestamp: Date.now() });
|
||||||
return next;
|
}
|
||||||
});
|
|
||||||
}, []);
|
if (idx < 0 || idx >= next.length) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
const target = next[idx];
|
||||||
|
next[idx] = { ...target, content: (target.content || '') + chunk };
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[isStreaming],
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Break current assistant stream segment (e.g., when a tool call starts/updates)
|
* Break current assistant stream segment (e.g., when a tool call starts/updates)
|
||||||
@@ -150,6 +156,8 @@ export const useMessageHandling = () => {
|
|||||||
endStreaming,
|
endStreaming,
|
||||||
// Thought handling
|
// Thought handling
|
||||||
appendThinkingChunk: (chunk: string) => {
|
appendThinkingChunk: (chunk: string) => {
|
||||||
|
// Ignore late thoughts after user cancelled streaming
|
||||||
|
if (!isStreaming) return;
|
||||||
setMessages((prev) => {
|
setMessages((prev) => {
|
||||||
let idx = thinkingMessageIndexRef.current;
|
let idx = thinkingMessageIndexRef.current;
|
||||||
const next = prev.slice();
|
const next = prev.slice();
|
||||||
|
|||||||
@@ -43,6 +43,11 @@ export default {
|
|||||||
ivory: '#f5f5ff',
|
ivory: '#f5f5ff',
|
||||||
slate: '#141420',
|
slate: '#141420',
|
||||||
green: '#6bcf7f',
|
green: '#6bcf7f',
|
||||||
|
// Status colors used by toolcall components
|
||||||
|
success: '#74c991',
|
||||||
|
error: '#c74e39',
|
||||||
|
warning: '#e1c08d',
|
||||||
|
loading: 'var(--app-secondary-foreground)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
|
|||||||
Reference in New Issue
Block a user