chore(vscode-ide-companion): refactor directory structure

This commit is contained in:
yiliang114
2025-12-08 00:54:26 +08:00
parent e47263f7c9
commit be71976a1f
81 changed files with 409 additions and 1540 deletions

View File

@@ -46,8 +46,8 @@ const cssInjectPlugin = {
let css = await fs.promises.readFile(args.path, 'utf8'); let css = await fs.promises.readFile(args.path, 'utf8');
// For ClaudeCodeStyles.css, we need to resolve @import statements // For styles.css, we need to resolve @import statements
if (args.path.endsWith('ClaudeCodeStyles.css')) { if (args.path.endsWith('styles.css')) {
// Read all imported CSS files and inline them // Read all imported CSS files and inline them
const importRegex = /@import\s+'([^']+)';/g; const importRegex = /@import\s+'([^']+)';/g;
let match; let match;

View File

@@ -130,7 +130,7 @@
"scripts": { "scripts": {
"prepackage": "npm run generate:notices && npm run check-types && npm run lint && npm run build:prod", "prepackage": "npm run generate:notices && npm run check-types && npm run lint && npm run build:prod",
"build": "npm run build:dev", "build": "npm run build:dev",
"build:dev": "node esbuild.js", "build:dev": "npm run check-types && npm run lint && node esbuild.js",
"build:prod": "node esbuild.js --production", "build:prod": "node esbuild.js --production",
"generate:notices": "node ./scripts/generate-notices.js", "generate:notices": "node ./scripts/generate-notices.js",
"prepare": "npm run generate:notices", "prepare": "npm run generate:notices",

View File

@@ -1,75 +0,0 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type { AcpPermissionRequest } from '../constants/acpTypes.js';
export interface ChatMessage {
role: 'user' | 'assistant';
content: string;
timestamp: number;
}
/**
* Plan Entry
*/
export interface PlanEntry {
/** Entry content */
content: string;
/** Priority */
priority?: 'high' | 'medium' | 'low';
/** Status */
status: 'pending' | 'in_progress' | 'completed';
}
/**
* Tool Call Update Data
*/
export interface ToolCallUpdateData {
/** Tool call ID */
toolCallId: string;
/** Tool type */
kind?: string;
/** Tool title */
title?: string;
/** Status */
status?: string;
/** Raw input */
rawInput?: unknown;
/** Content */
content?: Array<Record<string, unknown>>;
/** Location information */
locations?: Array<{ path: string; line?: number | null }>;
}
/**
* Callback Functions Collection
*/
export interface QwenAgentCallbacks {
/** Message callback */
onMessage?: (message: ChatMessage) => void;
/** Stream text chunk callback */
onStreamChunk?: (chunk: string) => void;
/** Thought text chunk callback */
onThoughtChunk?: (chunk: string) => void;
/** Tool call callback */
onToolCall?: (update: ToolCallUpdateData) => void;
/** Plan callback */
onPlan?: (entries: PlanEntry[]) => void;
/** Permission request callback */
onPermissionRequest?: (request: AcpPermissionRequest) => Promise<string>;
/** End of turn callback (e.g., stopReason === 'end_turn') */
onEndTurn?: () => void;
/** Initialize modes & capabilities info from ACP initialize */
onModeInfo?: (info: {
currentModeId?: 'plan' | 'default' | 'auto-edit' | 'yolo';
availableModes?: Array<{
id: 'plan' | 'default' | 'auto-edit' | 'yolo';
name: string;
description: string;
}>;
}) => void;
/** Mode changed notification */
onModeChanged?: (modeId: 'plan' | 'default' | 'auto-edit' | 'yolo') => void;
}

View File

@@ -6,11 +6,6 @@
import type { CliFeatureFlags, CliVersionInfo } from './cliVersionManager.js'; import type { CliFeatureFlags, CliVersionInfo } from './cliVersionManager.js';
/**
* CLI Context Manager
*
* Manages the current CLI context including version information and feature availability
*/
export class CliContextManager { export class CliContextManager {
private static instance: CliContextManager; private static instance: CliContextManager;
private currentVersionInfo: CliVersionInfo | null = null; private currentVersionInfo: CliVersionInfo | null = null;

View File

@@ -68,7 +68,6 @@ export function registerNewCommands(
), ),
); );
// TODO: qwenCode.openNewChatTab (not contributed in package.json; used programmatically)
disposables.push( disposables.push(
vscode.commands.registerCommand(openNewChatTabCommand, async () => { vscode.commands.registerCommand(openNewChatTabCommand, async () => {
const provider = createWebViewProvider(); const provider = createWebViewProvider();

View File

@@ -121,7 +121,9 @@ export async function activate(context: vscode.ExtensionContext) {
const providers = webViewProviders.filter( const providers = webViewProviders.filter(
(p) => typeof p.shouldSuppressDiff === 'function', (p) => typeof p.shouldSuppressDiff === 'function',
); );
if (providers.length === 0) return false; if (providers.length === 0) {
return false;
}
return providers.every((p) => p.shouldSuppressDiff()); return providers.every((p) => p.shouldSuppressDiff());
}, },
); );
@@ -194,7 +196,7 @@ export async function activate(context: vscode.ExtensionContext) {
if (docUri && docUri.scheme === DIFF_SCHEME) { if (docUri && docUri.scheme === DIFF_SCHEME) {
diffManager.acceptDiff(docUri); diffManager.acceptDiff(docUri);
} }
// 如果 WebView 正在 request_permission,主动选择一个允许选项(优先 once // If WebView is requesting permission, actively select an allow option (prefer once)
try { try {
for (const provider of webViewProviders) { for (const provider of webViewProviders) {
if (provider?.hasPendingPermission()) { if (provider?.hasPendingPermission()) {
@@ -211,7 +213,7 @@ export async function activate(context: vscode.ExtensionContext) {
if (docUri && docUri.scheme === DIFF_SCHEME) { if (docUri && docUri.scheme === DIFF_SCHEME) {
diffManager.cancelDiff(docUri); diffManager.cancelDiff(docUri);
} }
// 如果 WebView 正在 request_permission,主动选择拒绝/取消 // If WebView is requesting permission, actively select reject/cancel
try { try {
for (const provider of webViewProviders) { for (const provider of webViewProviders) {
if (provider?.hasPendingPermission()) { if (provider?.hasPendingPermission()) {

View File

@@ -4,20 +4,20 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import { JSONRPC_VERSION } from '../constants/acpTypes.js'; import { JSONRPC_VERSION } from '../types/acpTypes.js';
import type { import type {
AcpBackend, AcpBackend,
AcpMessage, AcpMessage,
AcpPermissionRequest, AcpPermissionRequest,
AcpResponse, AcpResponse,
AcpSessionUpdate, AcpSessionUpdate,
} from '../constants/acpTypes.js'; } from '../types/acpTypes.js';
import type { ChildProcess, SpawnOptions } from 'child_process'; import type { ChildProcess, SpawnOptions } from 'child_process';
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import type { import type {
PendingRequest, PendingRequest,
AcpConnectionCallbacks, AcpConnectionCallbacks,
} from './connectionTypes.js'; } from '../types/connectionTypes.js';
import { AcpMessageHandler } from './acpMessageHandler.js'; import { AcpMessageHandler } from './acpMessageHandler.js';
import { AcpSessionManager } from './acpSessionManager.js'; import { AcpSessionManager } from './acpSessionManager.js';
import { determineNodePathForCli } from '../cli/cliPathDetector.js'; import { determineNodePathForCli } from '../cli/cliPathDetector.js';

View File

@@ -17,13 +17,13 @@ import type {
AcpResponse, AcpResponse,
AcpSessionUpdate, AcpSessionUpdate,
AcpPermissionRequest, AcpPermissionRequest,
} from '../constants/acpTypes.js'; } from '../types/acpTypes.js';
import { CLIENT_METHODS } from '../constants/acpSchema.js'; import { CLIENT_METHODS } from '../constants/acpSchema.js';
import type { import type {
PendingRequest, PendingRequest,
AcpConnectionCallbacks, AcpConnectionCallbacks,
} from './connectionTypes.js'; } from '../types/connectionTypes.js';
import { AcpFileHandler } from './acpFileHandler.js'; import { AcpFileHandler } from '../services/acpFileHandler.js';
import type { ChildProcess } from 'child_process'; import type { ChildProcess } from 'child_process';
/** /**

View File

@@ -10,14 +10,14 @@
* Responsible for managing ACP protocol session operations, including initialization, authentication, session creation, and switching * Responsible for managing ACP protocol session operations, including initialization, authentication, session creation, and switching
*/ */
import { JSONRPC_VERSION } from '../constants/acpTypes.js'; import { JSONRPC_VERSION } from '../types/acpTypes.js';
import type { import type {
AcpRequest, AcpRequest,
AcpNotification, AcpNotification,
AcpResponse, AcpResponse,
} from '../constants/acpTypes.js'; } from '../types/acpTypes.js';
import { AGENT_METHODS } from '../constants/acpSchema.js'; import { AGENT_METHODS } from '../constants/acpSchema.js';
import type { PendingRequest } from './connectionTypes.js'; import type { PendingRequest } from '../types/connectionTypes.js';
import type { ChildProcess } from 'child_process'; import type { ChildProcess } from 'child_process';
/** /**
@@ -313,8 +313,12 @@ export class AcpSessionManager {
try { try {
// session/list requires cwd in params per ACP schema // session/list requires cwd in params per ACP schema
const params: Record<string, unknown> = { cwd }; const params: Record<string, unknown> = { cwd };
if (options?.cursor !== undefined) params.cursor = options.cursor; if (options?.cursor !== undefined) {
if (options?.size !== undefined) params.size = options.size; params.cursor = options.cursor;
}
if (options?.size !== undefined) {
params.size = options.size;
}
const response = await this.sendRequest<AcpResponse>( const response = await this.sendRequest<AcpResponse>(
AGENT_METHODS.session_list, AGENT_METHODS.session_list,

View File

@@ -5,7 +5,7 @@
*/ */
import type * as vscode from 'vscode'; import type * as vscode from 'vscode';
import type { ChatMessage } from '../agents/qwenAgentManager.js'; import type { ChatMessage } from './qwenAgentManager.js';
export interface Conversation { export interface Conversation {
id: string; id: string;

View File

@@ -3,27 +3,24 @@
* Copyright 2025 Qwen Team * Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import { AcpConnection } from '../acp/acpConnection.js'; import { AcpConnection } from './acpConnection.js';
import type { import type {
AcpSessionUpdate, AcpSessionUpdate,
AcpPermissionRequest, AcpPermissionRequest,
} from '../constants/acpTypes.js'; } from '../types/acpTypes.js';
import { import { QwenSessionReader, type QwenSession } from './qwenSessionReader.js';
QwenSessionReader, import { QwenSessionManager } from './qwenSessionManager.js';
type QwenSession, import type { AuthStateManager } from './authStateManager.js';
} from '../services/qwenSessionReader.js';
import { QwenSessionManager } from '../services/qwenSessionManager.js';
import type { AuthStateManager } from '../auth/authStateManager.js';
import type { import type {
ChatMessage, ChatMessage,
PlanEntry, PlanEntry,
ToolCallUpdateData, ToolCallUpdateData,
QwenAgentCallbacks, QwenAgentCallbacks,
} from './qwenTypes.js'; } from '../types/qwenTypes.js';
import { QwenConnectionHandler } from './qwenConnectionHandler.js'; import { QwenConnectionHandler } from '../services/qwenConnectionHandler.js';
import { QwenSessionUpdateHandler } from './qwenSessionUpdateHandler.js'; import { QwenSessionUpdateHandler } from './qwenSessionUpdateHandler.js';
import { CliContextManager } from '../cli/cliContextManager.js'; import { CliContextManager } from '../cli/cliContextManager.js';
import { authMethod } from '../auth/index.js'; import { authMethod } from '../constants/auth.js';
export type { ChatMessage, PlanEntry, ToolCallUpdateData }; export type { ChatMessage, PlanEntry, ToolCallUpdateData };
@@ -496,7 +493,9 @@ export class QwenAgentManager {
const fs = await import('fs'); const fs = await import('fs');
const readline = await import('readline'); const readline = await import('readline');
try { try {
if (!fs.existsSync(filePath)) return []; if (!fs.existsSync(filePath)) {
return [];
}
const fileStream = fs.createReadStream(filePath, { encoding: 'utf-8' }); const fileStream = fs.createReadStream(filePath, { encoding: 'utf-8' });
const rl = readline.createInterface({ const rl = readline.createInterface({
input: fileStream, input: fileStream,
@@ -505,7 +504,9 @@ export class QwenAgentManager {
const records: unknown[] = []; const records: unknown[] = [];
for await (const line of rl) { for await (const line of rl) {
const trimmed = line.trim(); const trimmed = line.trim();
if (!trimmed) continue; if (!trimmed) {
continue;
}
try { try {
const obj = JSON.parse(trimmed); const obj = JSON.parse(trimmed);
records.push(obj); records.push(obj);
@@ -988,7 +989,7 @@ export class QwenAgentManager {
if (!supportsSessionLoad) { if (!supportsSessionLoad) {
throw new Error( throw new Error(
`CLI version does not support session/load method. Please upgrade to version 0.2.4 or later.`, `CLI version does not support session/load method. Please upgrade to version 0.4.0 or later.`,
); );
} }

View File

@@ -11,12 +11,12 @@
*/ */
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import type { AcpConnection } from '../acp/acpConnection.js'; import type { AcpConnection } from './acpConnection.js';
import type { QwenSessionReader } from '../services/qwenSessionReader.js'; import type { QwenSessionReader } from '../services/qwenSessionReader.js';
import type { AuthStateManager } from '../auth/authStateManager.js'; import type { AuthStateManager } from '../services/authStateManager.js';
import { CliVersionManager } from '../cli/cliVersionManager.js'; import { CliVersionManager } from '../cli/cliVersionManager.js';
import { CliContextManager } from '../cli/cliContextManager.js'; import { CliContextManager } from '../cli/cliContextManager.js';
import { authMethod } from '../auth/index.js'; import { authMethod } from '../constants/auth.js';
/** /**
* Qwen Connection Handler class * Qwen Connection Handler class
@@ -54,12 +54,12 @@ export class QwenConnectionHandler {
// Show warning if CLI version is below minimum requirement // Show warning if CLI version is below minimum requirement
if (!versionInfo.isSupported) { if (!versionInfo.isSupported) {
console.warn( console.warn(
`[QwenAgentManager] CLI version ${versionInfo.version} is below minimum required version ${'0.2.4'}`, `[QwenAgentManager] CLI version ${versionInfo.version} is below minimum required version ${'0.4.0'}`,
); );
// TODO: Wait to determine release version number // TODO: Wait to determine release version number
// vscode.window.showWarningMessage( // vscode.window.showWarningMessage(
// `Qwen Code CLI version ${versionInfo.version} is below the minimum required version. Some features may not work properly. Please upgrade to version 0.2.4 or later.`, // `Qwen Code CLI version ${versionInfo.version} is below the minimum required version. Some features may not work properly. Please upgrade to version 0.4.0 or later.`,
// ); // );
} }

View File

@@ -170,120 +170,6 @@ export class QwenSessionManager {
} }
} }
/**
* Find checkpoint file for a given sessionId
* Tries both checkpoint-{sessionId}.json and searches session files for matching sessionId
*
* @param sessionId - Session ID to find checkpoint for
* @param workingDir - Current working directory
* @returns Checkpoint tag if found, null otherwise
*/
async findCheckpointTag(
sessionId: string,
workingDir: string,
): Promise<string | null> {
try {
const projectHash = this.getProjectHash(workingDir);
const projectDir = path.join(this.qwenDir, 'tmp', projectHash);
// First, try direct checkpoint with sessionId
const directCheckpoint = path.join(
projectDir,
`checkpoint-${sessionId}.json`,
);
if (fs.existsSync(directCheckpoint)) {
console.log(
'[QwenSessionManager] Found direct checkpoint:',
directCheckpoint,
);
return sessionId;
}
// Second, look for session file with this sessionId to get conversationId
const sessionDir = path.join(projectDir, 'chats');
if (fs.existsSync(sessionDir)) {
const files = fs.readdirSync(sessionDir);
for (const file of files) {
if (file.startsWith('session-') && file.endsWith('.json')) {
try {
const filePath = path.join(sessionDir, file);
const content = fs.readFileSync(filePath, 'utf-8');
const session = JSON.parse(content) as QwenSession;
if (session.sessionId === sessionId) {
console.log(
'[QwenSessionManager] Found matching session file:',
file,
);
// Now check if there's a checkpoint with this conversationId
// We need to store conversationId in session files or use another strategy
// For now, return null and let it fallback
break;
}
} catch {
// Skip invalid files
}
}
}
}
console.log(
'[QwenSessionManager] No checkpoint found for sessionId:',
sessionId,
);
return null;
} catch (error) {
console.error('[QwenSessionManager] Error finding checkpoint:', error);
return null;
}
}
/**
* Load a checkpoint by tag
*
* @param tag - Checkpoint tag
* @param workingDir - Current working directory
* @returns Loaded checkpoint messages or null if not found
*/
async loadCheckpoint(
tag: string,
workingDir: string,
): Promise<QwenMessage[] | null> {
try {
const projectHash = this.getProjectHash(workingDir);
const projectDir = path.join(this.qwenDir, 'tmp', projectHash);
const filename = `checkpoint-${tag}.json`;
const filePath = path.join(projectDir, filename);
if (!fs.existsSync(filePath)) {
console.log(
`[QwenSessionManager] Checkpoint file not found: ${filePath}`,
);
return null;
}
const content = fs.readFileSync(filePath, 'utf-8');
const checkpointMessages = JSON.parse(content) as Array<{
role: 'user' | 'model';
parts: Array<{ text: string }>;
}>;
// Convert back to QwenMessage format
const messages: QwenMessage[] = checkpointMessages.map((msg) => ({
id: crypto.randomUUID(),
timestamp: new Date().toISOString(),
type: msg.role === 'user' ? ('user' as const) : ('qwen' as const),
content: msg.parts[0]?.text || '',
}));
console.log(`[QwenSessionManager] Checkpoint loaded: ${filePath}`);
return messages;
} catch (error) {
console.error('[QwenSessionManager] Failed to load checkpoint:', error);
return null;
}
}
/** /**
* Save current conversation as a named session (checkpoint-like functionality) * Save current conversation as a named session (checkpoint-like functionality)
* *

View File

@@ -10,11 +10,8 @@
* Handles session updates from ACP and dispatches them to appropriate callbacks * Handles session updates from ACP and dispatches them to appropriate callbacks
*/ */
import type { import type { AcpSessionUpdate, ApprovalModeValue } from '../types/acpTypes.js';
AcpSessionUpdate, import type { QwenAgentCallbacks } from '../types/qwenTypes.js';
ApprovalModeValue,
} from '../constants/acpTypes.js';
import type { QwenAgentCallbacks } from './qwenTypes.js';
/** /**
* Qwen Session Update Handler class * Qwen Session Update Handler class

View File

@@ -4,13 +4,6 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
/**
* ACP Types for VSCode Extension
*
* This file provides types for ACP protocol communication.
*/
// ACP JSON-RPC Protocol Types
export const JSONRPC_VERSION = '2.0' as const; export const JSONRPC_VERSION = '2.0' as const;
export type AcpBackend = 'qwen' | 'claude' | 'gemini' | 'codex'; export type AcpBackend = 'qwen' | 'claude' | 'gemini' | 'codex';

View File

@@ -6,7 +6,6 @@
import type React from 'react'; import type React from 'react';
// Shared type for completion items used by the input completion system
export interface CompletionItem { export interface CompletionItem {
id: string; id: string;
label: string; label: string;

View File

@@ -4,60 +4,29 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
/**
* ACP Connection Type Definitions
*
* Contains all types and interface definitions required for ACP connection
*/
import type { ChildProcess } from 'child_process'; import type { ChildProcess } from 'child_process';
import type { import type { AcpSessionUpdate, AcpPermissionRequest } from './acpTypes.js';
AcpSessionUpdate,
AcpPermissionRequest,
} from '../constants/acpTypes.js';
/**
* Pending Request Information
*/
export interface PendingRequest<T = unknown> { export interface PendingRequest<T = unknown> {
/** Success callback */
resolve: (value: T) => void; resolve: (value: T) => void;
/** Failure callback */
reject: (error: Error) => void; reject: (error: Error) => void;
/** Timeout timer ID */
timeoutId?: NodeJS.Timeout; timeoutId?: NodeJS.Timeout;
/** Request method name */
method: string; method: string;
} }
/**
* ACP Connection Callback Function Types
*/
export interface AcpConnectionCallbacks { export interface AcpConnectionCallbacks {
/** Session update callback */
onSessionUpdate: (data: AcpSessionUpdate) => void; onSessionUpdate: (data: AcpSessionUpdate) => void;
/** Permission request callback */
onPermissionRequest: (data: AcpPermissionRequest) => Promise<{ onPermissionRequest: (data: AcpPermissionRequest) => Promise<{
optionId: string; optionId: string;
}>; }>;
/** Turn end callback */
onEndTurn: () => void; onEndTurn: () => void;
} }
/**
* ACP Connection State
*/
export interface AcpConnectionState { export interface AcpConnectionState {
/** Child process instance */
child: ChildProcess | null; child: ChildProcess | null;
/** Pending requests map */
pendingRequests: Map<number, PendingRequest<unknown>>; pendingRequests: Map<number, PendingRequest<unknown>>;
/** Next request ID */
nextRequestId: number; nextRequestId: number;
/** Current session ID */
sessionId: string | null; sessionId: string | null;
/** Whether initialized */
isInitialized: boolean; isInitialized: boolean;
/** Backend type */
backend: string | null; backend: string | null;
} }

View File

@@ -0,0 +1,112 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type { AcpPermissionRequest } from './acpTypes.js';
export interface ChatMessage {
role: 'user' | 'assistant';
content: string;
timestamp: number;
}
/**
* Plan Entry
* Represents a single step in the AI's execution plan
*/
export interface PlanEntry {
/** The detailed description of this plan step */
content: string;
/** The priority level of this plan step */
priority?: 'high' | 'medium' | 'low';
/** The current execution status of this plan step */
status: 'pending' | 'in_progress' | 'completed';
}
/**
* Tool Call Update Data
* Contains information about a tool call execution or update
*/
export interface ToolCallUpdateData {
/** Unique identifier for this tool call */
toolCallId: string;
/** The type of tool being called (e.g., 'read', 'write', 'execute') */
kind?: string;
/** Human-readable title or description of the tool call */
title?: string;
/** Current execution status of the tool call */
status?: string;
/** Raw input parameters passed to the tool */
rawInput?: unknown;
/** Content or output data from the tool execution */
content?: Array<Record<string, unknown>>;
/** File locations associated with this tool call */
locations?: Array<{ path: string; line?: number | null }>;
}
/**
* Callback Functions Collection
* Defines all possible callback functions for the Qwen Agent
*/
export interface QwenAgentCallbacks {
/** Callback for receiving chat messages from the agent */
onMessage?: (message: ChatMessage) => void;
/** Callback for receiving streamed text chunks during generation */
onStreamChunk?: (chunk: string) => void;
/** Callback for receiving thought process chunks during generation */
onThoughtChunk?: (chunk: string) => void;
/** Callback for receiving tool call updates during execution */
onToolCall?: (update: ToolCallUpdateData) => void;
/** Callback for receiving execution plan updates */
onPlan?: (entries: PlanEntry[]) => void;
/** Callback for handling permission requests from the agent */
onPermissionRequest?: (request: AcpPermissionRequest) => Promise<string>;
/** Callback triggered when the agent reaches the end of a turn */
onEndTurn?: () => void;
/** Callback for receiving mode information after ACP initialization */
onModeInfo?: (info: {
currentModeId?: 'plan' | 'default' | 'auto-edit' | 'yolo';
availableModes?: Array<{
id: 'plan' | 'default' | 'auto-edit' | 'yolo';
name: string;
description: string;
}>;
}) => void;
/** Callback for receiving notifications when the mode changes */
onModeChanged?: (modeId: 'plan' | 'default' | 'auto-edit' | 'yolo') => void;
}
/**
* Tool call update type
*/
export interface ToolCallUpdate {
type: 'tool_call' | 'tool_call_update';
toolCallId: string;
kind?: string;
title?: string;
status?: 'pending' | 'in_progress' | 'completed' | 'failed';
rawInput?: unknown;
content?: Array<{
type: 'content' | 'diff';
content?: {
type: string;
text?: string;
[key: string]: unknown;
};
path?: string;
oldText?: string | null;
newText?: string;
[key: string]: unknown;
}>;
locations?: Array<{
path: string;
line?: number | null;
}>;
timestamp?: number; // Add timestamp field for message ordering
}
/**
* Edit mode type
*/
export type EditMode = 'ask' | 'auto' | 'plan' | 'yolo';

View File

@@ -24,15 +24,15 @@ import type {
ToolCall as PermissionToolCall, ToolCall as PermissionToolCall,
} from './components/PermissionDrawer/PermissionRequest.js'; } from './components/PermissionDrawer/PermissionRequest.js';
import type { TextMessage } from './hooks/message/useMessageHandling.js'; import type { TextMessage } from './hooks/message/useMessageHandling.js';
import type { ToolCallData } from './components/ToolCall.js'; import type { ToolCallData } from './components/messages/toolcalls/ToolCall.js';
import { PermissionDrawer } from './components/PermissionDrawer/PermissionDrawer.js'; import { PermissionDrawer } from './components/PermissionDrawer/PermissionDrawer.js';
import { ToolCall } from './components/ToolCall.js'; import { ToolCall } from './components/messages/toolcalls/ToolCall.js';
import { hasToolCallOutput } from './components/toolcalls/shared/utils.js'; import { hasToolCallOutput } from './components/messages/toolcalls/shared/utils.js';
import { EmptyState } from './components/ui/EmptyState.js'; import { EmptyState } from './components/layout/EmptyState.js';
import { type CompletionItem } from './types/CompletionTypes.js'; import { type CompletionItem } from '../types/completionItemTypes.js';
import { useCompletionTrigger } from './hooks/useCompletionTrigger.js'; import { useCompletionTrigger } from './hooks/useCompletionTrigger.js';
import { InfoBanner } from './components/ui/InfoBanner.js'; import { InfoBanner } from './components/layout/InfoBanner.js';
import { ChatHeader } from './components/ui/layouts/ChatHeader.js'; import { ChatHeader } from './components/layout/ChatHeader.js';
import { import {
UserMessage, UserMessage,
AssistantMessage, AssistantMessage,
@@ -40,11 +40,11 @@ import {
WaitingMessage, WaitingMessage,
InterruptedMessage, InterruptedMessage,
} from './components/messages/index.js'; } from './components/messages/index.js';
import { InputForm } from './components/InputForm.js'; import { InputForm } from './components/layout/InputForm.js';
import { SessionSelector } from './components/session/SessionSelector.js'; import { SessionSelector } from './components/layout/SessionSelector.js';
import { FileIcon, UserIcon } from './components/icons/index.js'; import { FileIcon, UserIcon } from './components/icons/index.js';
import type { EditMode } from './types/toolCall.js'; import type { EditMode } from '../types/qwenTypes.js';
import type { PlanEntry } from '../agents/qwenTypes.js'; import type { PlanEntry } from '../types/qwenTypes.js';
export const App: React.FC = () => { export const App: React.FC = () => {
const vscode = useVSCode(); const vscode = useVSCode();
@@ -609,7 +609,7 @@ export const App: React.FC = () => {
const isToolCallType = ( const isToolCallType = (
x: unknown, x: unknown,
): x is { type: 'in-progress-tool-call' | 'completed-tool-call' } => ): x is { type: 'in-progress-tool-call' | 'completed-tool-call' } =>
x && !!x &&
typeof x === 'object' && typeof x === 'object' &&
'type' in (x as Record<string, unknown>) && 'type' in (x as Record<string, unknown>) &&
((x as { type: string }).type === 'in-progress-tool-call' || ((x as { type: string }).type === 'in-progress-tool-call' ||
@@ -782,8 +782,6 @@ export const App: React.FC = () => {
onClose={() => setPermissionRequest(null)} onClose={() => setPermissionRequest(null)}
/> />
)} )}
{/* Claude-style dropdown is rendered inside InputForm for proper anchoring */}
</div> </div>
); );
}; };

View File

@@ -4,8 +4,8 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import type { QwenAgentManager } from '../agents/qwenAgentManager.js'; import type { QwenAgentManager } from '../services/qwenAgentManager.js';
import type { ConversationStore } from '../storage/conversationStore.js'; import type { ConversationStore } from '../services/conversationStore.js';
import { MessageRouter } from './handlers/MessageRouter.js'; import { MessageRouter } from './handlers/MessageRouter.js';
/** /**

View File

@@ -5,17 +5,17 @@
*/ */
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { QwenAgentManager } from '../agents/qwenAgentManager.js'; import { QwenAgentManager } from '../services/qwenAgentManager.js';
import { ConversationStore } from '../storage/conversationStore.js'; import { ConversationStore } from '../services/conversationStore.js';
import type { AcpPermissionRequest } from '../constants/acpTypes.js'; import type { AcpPermissionRequest } from '../types/acpTypes.js';
import { CliDetector } from '../cli/cliDetector.js'; import { CliDetector } from '../cli/cliDetector.js';
import { AuthStateManager } from '../auth/authStateManager.js'; import { AuthStateManager } from '../services/authStateManager.js';
import { PanelManager } from '../webview/PanelManager.js'; import { PanelManager } from '../webview/PanelManager.js';
import { MessageHandler } from '../webview/MessageHandler.js'; import { MessageHandler } from '../webview/MessageHandler.js';
import { WebViewContent } from '../webview/WebViewContent.js'; import { WebViewContent } from '../webview/WebViewContent.js';
import { CliInstaller } from '../cli/cliInstaller.js'; import { CliInstaller } from '../cli/cliInstaller.js';
import { getFileName } from './utils/webviewUtils.js'; import { getFileName } from './utils/webviewUtils.js';
import { authMethod } from '../auth/index.js'; import { authMethod } from '../constants/auth.js';
import { runQwenCodeCommand } from '../commands/index.js'; import { runQwenCodeCommand } from '../commands/index.js';
export class WebViewProvider { export class WebViewProvider {

View File

@@ -29,7 +29,7 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
const [focusedIndex, setFocusedIndex] = useState(0); const [focusedIndex, setFocusedIndex] = useState(0);
const [customMessage, setCustomMessage] = useState(''); const [customMessage, setCustomMessage] = useState('');
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
// 将自定义输入的 ref 类型修正为 HTMLInputElement避免后续强转 // Correct the ref type for custom input to HTMLInputElement to avoid subsequent forced casting
const customInputRef = useRef<HTMLInputElement>(null); const customInputRef = useRef<HTMLInputElement>(null);
console.log('PermissionDrawer rendered with isOpen:', isOpen, toolCall); console.log('PermissionDrawer rendered with isOpen:', isOpen, toolCall);
@@ -139,7 +139,9 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
options.find((o) => o.optionId === 'cancel')?.optionId || options.find((o) => o.optionId === 'cancel')?.optionId ||
'cancel'; 'cancel';
onResponse(rejectOptionId); onResponse(rejectOptionId);
if (onClose) onClose(); if (onClose) {
onClose();
}
} }
}; };
@@ -196,7 +198,7 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
toolCall.kind === 'bash') && toolCall.kind === 'bash') &&
toolCall.title && ( toolCall.title && (
<div <div
/* 13px,常规字重;正常空白折行 + 长词断行;最多 3 行溢出省略 */ /* 13px, normal font weight; normal whitespace wrapping + long word breaking; maximum 3 lines with overflow ellipsis */
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={{ style={{
fontSize: '.9em', fontSize: '.9em',
@@ -249,7 +251,9 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
setCustomMessage={setCustomMessage} setCustomMessage={setCustomMessage}
onFocusRow={() => setFocusedIndex(options.length)} onFocusRow={() => setFocusedIndex(options.length)}
onSubmitReject={() => { onSubmitReject={() => {
if (rejectOptionId) onResponse(rejectOptionId); if (rejectOptionId) {
onResponse(rejectOptionId);
}
}} }}
inputRef={customInputRef} inputRef={customInputRef}
/> />
@@ -264,14 +268,14 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
}; };
/** /**
* CustomMessageInputRow: 复用的自定义输入行组件(无 hooks * CustomMessageInputRow: Reusable custom input row component (without hooks)
*/ */
interface CustomMessageInputRowProps { interface CustomMessageInputRowProps {
isFocused: boolean; isFocused: boolean;
customMessage: string; customMessage: string;
setCustomMessage: (val: string) => void; setCustomMessage: (val: string) => void;
onFocusRow: () => void; // 鼠标移入或输入框 focus 时设置焦点 onFocusRow: () => void; // Set focus when mouse enters or input box is focused
onSubmitReject: () => void; // Enter 提交时触发(选择 reject 选项) onSubmitReject: () => void; // Triggered when Enter is pressed (selecting reject option)
inputRef: React.RefObject<HTMLInputElement>; inputRef: React.RefObject<HTMLInputElement>;
} }

View File

@@ -16,10 +16,6 @@ interface ThinkingIconProps extends IconProps {
enabled?: boolean; enabled?: boolean;
} }
/**
* Thinking/brain wave icon (16x16)
* Used for thinking mode toggle
*/
export const ThinkingIcon: React.FC<ThinkingIconProps> = ({ export const ThinkingIcon: React.FC<ThinkingIconProps> = ({
size = 16, size = 16,
className, className,
@@ -53,10 +49,6 @@ export const ThinkingIcon: React.FC<ThinkingIconProps> = ({
</svg> </svg>
); );
/**
* Terminal/code editor icon (20x20)
* Used for terminal preference info banner
*/
export const TerminalIcon: React.FC<IconProps> = ({ export const TerminalIcon: React.FC<IconProps> = ({
size = 20, size = 20,
className, className,

View File

@@ -5,7 +5,7 @@
*/ */
import type React from 'react'; import type React from 'react';
import { ChevronDownIcon, PlusIcon } from '../../icons/index.js'; import { ChevronDownIcon, PlusIcon } from '../icons/index.js';
interface ChatHeaderProps { interface ChatHeaderProps {
currentSessionTitle: string; currentSessionTitle: string;

View File

@@ -6,7 +6,7 @@
import type React from 'react'; import type React from 'react';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import type { CompletionItem } from '../../types/CompletionTypes.js'; import type { CompletionItem } from '../../../types/completionItemTypes.js';
interface CompletionMenuProps { interface CompletionMenuProps {
items: CompletionItem[]; items: CompletionItem[];

View File

@@ -16,11 +16,10 @@ import {
LinkIcon, LinkIcon,
ArrowUpIcon, ArrowUpIcon,
StopIcon, StopIcon,
} from './icons/index.js'; } from '../icons/index.js';
import { CompletionMenu } from './ui/CompletionMenu.js'; import { CompletionMenu } from '../layout/CompletionMenu.js';
import type { CompletionItem } from '../types/CompletionTypes.js'; import type { CompletionItem } from '../../../types/completionItemTypes.js';
import type { EditMode } from '../../../types/qwenTypes.js';
type EditMode = 'ask' | 'auto' | 'plan' | 'yolo';
interface InputFormProps { interface InputFormProps {
inputText: string; inputText: string;

View File

@@ -1,109 +0,0 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
interface ChatHeaderProps {
currentSessionTitle: string;
onLoadSessions: () => void;
onSaveSession: () => void;
onNewSession: () => void;
}
export const ChatHeader: React.FC<ChatHeaderProps> = ({
currentSessionTitle,
onLoadSessions,
onSaveSession: _onSaveSession,
onNewSession,
}) => (
<div
className="flex gap-1 select-none py-1.5 px-2.5"
style={{
borderBottom: '1px solid var(--app-primary-border-color)',
backgroundColor: 'var(--app-header-background)',
}}
>
{/* Past Conversations Button */}
<button
className="flex-none py-1 px-2 bg-transparent border border-transparent rounded cursor-pointer flex items-center justify-center outline-none font-medium transition-colors duration-200 hover:bg-[var(--app-ghost-button-hover-background)] focus:bg-[var(--app-ghost-button-hover-background)]"
style={{
borderRadius: 'var(--corner-radius-small)',
color: 'var(--app-primary-foreground)',
fontSize: 'var(--vscode-chat-font-size, 13px)',
}}
onClick={onLoadSessions}
title="Past conversations"
>
<span className="flex items-center gap-1">
<span style={{ fontSize: 'var(--vscode-chat-font-size, 13px)' }}>
{currentSessionTitle}
</span>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
className="w-3.5 h-3.5"
>
<path
fillRule="evenodd"
d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z"
clipRule="evenodd"
></path>
</svg>
</span>
</button>
{/* Spacer */}
<div className="flex-1"></div>
{/* Save Session Button */}
{/* <button
className="flex-none p-0 bg-transparent border border-transparent rounded cursor-pointer flex items-center justify-center outline-none w-6 h-6 hover:bg-[var(--app-ghost-button-hover-background)] focus:bg-[var(--app-ghost-button-hover-background)]"
style={{
color: 'var(--app-primary-foreground)',
}}
onClick={onSaveSession}
title="Save Conversation"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
data-slot="icon"
className="w-4 h-4"
>
<path
fillRule="evenodd"
d="M4.25 2A2.25 2.25 0 0 0 2 4.25v11.5A2.25 2.25 0 0 0 4.25 18h11.5A2.25 2.25 0 0 0 18 15.75V8.25a.75.75 0 0 1 .217-.517l.083-.083a.75.75 0 0 1 1.061 0l2.239 2.239A.75.75 0 0 1 22 10.5v5.25a4.75 4.75 0 0 1-4.75 4.75H4.75A4.75 4.75 0 0 1 0 15.75V4.25A4.75 4.75 0 0 1 4.75 0h5a.75.75 0 0 1 0 1.5h-5ZM9.017 6.5a1.5 1.5 0 0 1 2.072.58l.43.862a1 1 0 0 0 .895.558h3.272a1.5 1.5 0 0 1 1.5 1.5v6.75a1.5 1.5 0 0 1-1.5 1.5h-7.5a1.5 1.5 0 0 1-1.5-1.5v-6.75a1.5 1.5 0 0 1 1.5-1.5h1.25a1 1 0 0 0 .895-.558l.43-.862a1.5 1.5 0 0 1 .511-.732ZM11.78 8.47a.75.75 0 0 0-1.06-1.06L8.75 9.379 7.78 8.41a.75.75 0 0 0-1.06 1.06l1.5 1.5a.75.75 0 0 0 1.06 0l2.5-2.5Z"
clipRule="evenodd"
></path>
</svg>
</button> */}
{/* New Session Button */}
<button
className="flex-none p-0 bg-transparent border border-transparent rounded cursor-pointer flex items-center justify-center outline-none w-6 h-6 hover:bg-[var(--app-ghost-button-hover-background)] focus:bg-[var(--app-ghost-button-hover-background)]"
style={{
color: 'var(--app-primary-foreground)',
}}
onClick={onNewSession}
title="New Session"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
data-slot="icon"
className="w-4 h-4"
>
<path d="M10.75 4.75a.75.75 0 0 0-1.5 0v4.5h-4.5a.75.75 0 0 0 0 1.5h4.5v4.5a.75.75 0 0 0 1.5 0v-4.5h4.5a.75.75 0 0 0 0-1.5h-4.5v-4.5Z"></path>
</svg>
</button>
</div>
);

View File

@@ -5,7 +5,7 @@
*/ */
import type React from 'react'; import type React from 'react';
import { MessageContent } from '../../MessageContent.js'; import { MessageContent } from '../MessageContent.js';
import './AssistantMessage.css'; import './AssistantMessage.css';
interface AssistantMessageProps { interface AssistantMessageProps {

View File

@@ -135,8 +135,8 @@
border: 1px solid var(--app-primary-border-color); border: 1px solid var(--app-primary-border-color);
border-radius: var(--corner-radius-small, 4px); border-radius: var(--corner-radius-small, 4px);
padding: 0.2em 0.4em; padding: 0.2em 0.4em;
white-space: pre-wrap; /* 支持自动换行 */ white-space: pre-wrap; /* Support automatic line wrapping */
word-break: break-word; /* 在必要时断词 */ word-break: break-word; /* Break words when necessary */
} }
.markdown-content pre { .markdown-content pre {
@@ -207,8 +207,8 @@
background: none; background: none;
border: none; border: none;
padding: 0; padding: 0;
white-space: pre-wrap; /* 支持自动换行 */ white-space: pre-wrap; /* Support automatic line wrapping */
word-break: break-word; /* 在必要时断词 */ word-break: break-word; /* Break words when necessary */
} }
.markdown-content .file-path-link { .markdown-content .file-path-link {

View File

@@ -126,7 +126,9 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
*/ */
const processFilePaths = (html: string): string => { const processFilePaths = (html: string): string => {
// If DOM is not available, bail out to avoid breaking SSR // If DOM is not available, bail out to avoid breaking SSR
if (typeof document === 'undefined') return html; if (typeof document === 'undefined') {
return html;
}
// Build non-global variants to avoid .test() statefulness // Build non-global variants to avoid .test() statefulness
const FILE_PATH_NO_G = new RegExp( const FILE_PATH_NO_G = new RegExp(
@@ -193,7 +195,9 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
} }
// Ignore other external protocols // Ignore other external protocols
if (/^(https?|mailto|ftp|data):/i.test(href)) return; if (/^(https?|mailto|ftp|data):/i.test(href)) {
return;
}
const candidate = href || text; const candidate = href || text;
@@ -289,7 +293,9 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
e: React.MouseEvent<HTMLDivElement, MouseEvent>, e: React.MouseEvent<HTMLDivElement, MouseEvent>,
) => { ) => {
const target = e.target as HTMLElement | null; const target = e.target as HTMLElement | null;
if (!target) return; if (!target) {
return;
}
// Handle copy button clicks for fenced code blocks // Handle copy button clicks for fenced code blocks
const copyBtn = (target.closest && const copyBtn = (target.closest &&
@@ -322,10 +328,14 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
// Find nearest anchor with our marker class // Find nearest anchor with our marker class
const anchor = (target.closest && const anchor = (target.closest &&
target.closest('a.file-path-link')) as HTMLAnchorElement | null; target.closest('a.file-path-link')) as HTMLAnchorElement | null;
if (!anchor) return; if (!anchor) {
return;
}
const filePath = anchor.getAttribute('data-file-path'); const filePath = anchor.getAttribute('data-file-path');
if (!filePath) return; if (!filePath) {
return;
}
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();

View File

@@ -1,61 +0,0 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*
* Custom timeline styles for qwen-message message-item elements
*
* 实现原理:
* 1. 为所有AI消息项(qwen-message.message-item:not(.user-message-container))创建垂直连接线
* 2. 当用户消息(user-message-container)隔断AI消息序列时会自动重新开始一组新的时间线规则
* 3. 每组AI消息序列的开始元素(top设为15px),结束元素(底部预留15px)
*/
/* 默认的连接线样式 - 为所有AI消息项创建完整高度的连接线 */
.qwen-message.message-item:not(.user-message-container)::after {
content: '';
position: absolute;
left: 12px;
top: 0;
bottom: 0;
width: 1px;
background-color: var(--app-primary-border-color);
z-index: 0;
}
/* 处理每组AI消息序列的开始 - 包括整个消息列表的第一个AI消息和被用户消息隔断后的新AI消息 */
.qwen-message.message-item:not(.user-message-container):first-child::after,
.user-message-container + .qwen-message.message-item:not(.user-message-container)::after,
/* 如果前一个兄弟不是 .qwen-message.message-item例如等待提示、哨兵元素或卡片样式工具调用也作为一组新开始 */
.chat-messages > :not(.qwen-message.message-item)
+ .qwen-message.message-item:not(.user-message-container)::after {
top: 15px;
}
/* 处理每组AI消息序列的结尾 */
/* 后一个兄弟是用户消息时 */
.qwen-message.message-item:not(.user-message-container):has(+ .user-message-container)::after,
/* 或者后一个兄弟不是 .qwen-message.message-item如等待提示、哨兵元素、卡片样式工具调用等时 */
.qwen-message.message-item:not(.user-message-container):has(+ :not(.qwen-message.message-item))::after,
/* 真正是父容器最后一个子元素时 */
.qwen-message.message-item:not(.user-message-container):last-child::after {
/* 注意:同时设置 top 和 bottom 时高度为 (容器高度 - top - bottom)。
* 这里期望“底部留 15px 间距”,因此 bottom 应为 15px而不是 calc(100% - 15px))。 */
top: 0;
bottom: calc(100% - 15px);
}
.user-message-container:first-child {
margin-top: 0;
}
.message-item {
padding: 8px 0;
width: 100%;
align-items: flex-start;
padding-left: 30px;
user-select: text;
position: relative;
padding-top: 8px;
padding-bottom: 8px;
}

View File

@@ -1,39 +0,0 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import { MessageContent } from '../MessageContent.js';
interface StreamingMessageProps {
content: string;
onFileClick?: (path: string) => void;
}
export const StreamingMessage: React.FC<StreamingMessageProps> = ({
content,
onFileClick,
}) => (
<div className="flex gap-0 items-start text-left flex-col relative">
<div
className="inline-block my-1 relative whitespace-pre-wrap rounded-md max-w-full overflow-x-auto overflow-y-hidden select-text leading-[1.5]"
style={{
border: '1px solid var(--app-input-border)',
borderRadius: 'var(--corner-radius-medium)',
backgroundColor: 'var(--app-input-background)',
padding: '4px 6px',
color: 'var(--app-primary-foreground)',
}}
>
<MessageContent content={content} onFileClick={onFileClick} />
</div>
<div
className="absolute right-3 bottom-3"
style={{ color: 'var(--app-primary-foreground)' }}
>
</div>
</div>
);

View File

@@ -5,7 +5,7 @@
*/ */
import type React from 'react'; import type React from 'react';
import { MessageContent } from '../MessageContent.js'; import { MessageContent } from './MessageContent.js';
interface ThinkingMessageProps { interface ThinkingMessageProps {
content: string; content: string;

View File

@@ -5,7 +5,7 @@
*/ */
import type React from 'react'; import type React from 'react';
import { MessageContent } from '../MessageContent.js'; import { MessageContent } from './MessageContent.js';
interface FileContext { interface FileContext {
fileName: string; fileName: string;

View File

@@ -7,6 +7,5 @@
export { UserMessage } from './UserMessage.js'; export { UserMessage } from './UserMessage.js';
export { AssistantMessage } from './Assistant/AssistantMessage.js'; export { AssistantMessage } from './Assistant/AssistantMessage.js';
export { ThinkingMessage } from './ThinkingMessage.js'; export { ThinkingMessage } from './ThinkingMessage.js';
export { StreamingMessage } from './StreamingMessage.js';
export { WaitingMessage } from './Waiting/WaitingMessage.js'; export { WaitingMessage } from './Waiting/WaitingMessage.js';
export { InterruptedMessage } from './Waiting/InterruptedMessage.js'; export { InterruptedMessage } from './Waiting/InterruptedMessage.js';

View File

@@ -10,8 +10,8 @@ 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 { ToolCallContainer } from '../shared/LayoutComponents.js';
import { safeTitle, groupContent } from '../shared/utils.js'; import { safeTitle, groupContent } from '../shared/utils.js';
import { useVSCode } from '../../../hooks/useVSCode.js'; import { useVSCode } from '../../../../hooks/useVSCode.js';
import { createAndOpenTempFile } from '../../../utils/tempFileManager.js'; import { createAndOpenTempFile } from '../../../../utils/tempFileManager.js';
import './Bash.css'; import './Bash.css';
/** /**

View File

@@ -7,16 +7,15 @@
*/ */
import { useEffect, useCallback, useMemo } from 'react'; import { useEffect, useCallback, useMemo } from 'react';
import type { BaseToolCallProps } from '../../shared/types.js'; import type { BaseToolCallProps } from '../shared/types.js';
import { import {
groupContent, groupContent,
mapToolStatusToContainerStatus, mapToolStatusToContainerStatus,
} from '../../shared/utils.js'; } from '../shared/utils.js';
import { FileLink } from '../../../ui/FileLink.js'; import { FileLink } from '../../../layout/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,
@@ -138,31 +137,6 @@ export const EditToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
<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"> <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> <span className="flex-shrink-0 w-full">edit failed</span>
</div> </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>
</div> </div>
); );

View File

@@ -7,10 +7,10 @@
*/ */
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 { safeTitle, groupContent } from '../../shared/utils.js'; import { safeTitle, groupContent } from '../shared/utils.js';
import './Execute.css'; import './Execute.css';
import type { ToolCallContainerProps } from '../../shared/LayoutComponents.js'; import type { ToolCallContainerProps } from '../shared/LayoutComponents.js';
export const ToolCallContainer: React.FC<ToolCallContainerProps> = ({ export const ToolCallContainer: React.FC<ToolCallContainerProps> = ({
label, label,

View File

@@ -14,10 +14,7 @@ import {
ToolCallRow, ToolCallRow,
LocationsList, LocationsList,
} from './shared/LayoutComponents.js'; } from './shared/LayoutComponents.js';
import { DiffDisplay } from './shared/DiffDisplay.js';
import { safeTitle, groupContent } from './shared/utils.js'; import { safeTitle, groupContent } from './shared/utils.js';
import { useVSCode } from '../../hooks/useVSCode.js';
import { handleOpenDiff } from '../../utils/diffUtils.js';
/** /**
* Generic tool call component that can display any tool call type * Generic tool call component that can display any tool call type
@@ -27,10 +24,9 @@ import { handleOpenDiff } from '../../utils/diffUtils.js';
export const GenericToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => { export const GenericToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
const { kind, title, content, locations, toolCallId } = toolCall; const { kind, title, content, locations, toolCallId } = toolCall;
const operationText = safeTitle(title); const operationText = safeTitle(title);
const vscode = useVSCode();
// Group content by type // Group content by type
const { textOutputs, errors, diffs } = groupContent(content); const { textOutputs, errors } = groupContent(content);
// Error case: show operation + error in card layout // Error case: show operation + error in card layout
if (errors.length > 0) { if (errors.length > 0) {
@@ -46,28 +42,6 @@ export const GenericToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
); );
} }
// Success with diff: show diff in card layout
if (diffs.length > 0) {
return (
<ToolCallCard icon="🔧">
{diffs.map(
(item: import('./shared/types.js').ToolCallContent, idx: number) => (
<div key={`diff-${idx}`} style={{ gridColumn: '1 / -1' }}>
<DiffDisplay
path={item.path}
oldText={item.oldText}
newText={item.newText}
onOpenDiff={() =>
handleOpenDiff(vscode, item.path, item.oldText, item.newText)
}
/>
</div>
),
)}
</ToolCallCard>
);
}
// Success with output: use card for long output, compact for short // Success with output: use card for long output, compact for short
if (textOutputs.length > 0) { if (textOutputs.length > 0) {
const output = textOutputs.join('\n'); const output = textOutputs.join('\n');

View File

@@ -8,15 +8,15 @@
import type React from 'react'; import type React from 'react';
import { useCallback, useEffect, useMemo } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
import type { BaseToolCallProps } from '../../shared/types.js'; import type { BaseToolCallProps } from '../shared/types.js';
import { import {
groupContent, groupContent,
mapToolStatusToContainerStatus, mapToolStatusToContainerStatus,
} from '../../shared/utils.js'; } from '../shared/utils.js';
import { FileLink } from '../../../ui/FileLink.js'; import { FileLink } from '../../../layout/FileLink.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 type { ToolCallContainerProps } from '../../shared/LayoutComponents.js'; import type { ToolCallContainerProps } from '../shared/LayoutComponents.js';
export const ToolCallContainer: React.FC<ToolCallContainerProps> = ({ export const ToolCallContainer: React.FC<ToolCallContainerProps> = ({
label, label,

View File

@@ -7,15 +7,15 @@
*/ */
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 { FileLink } from '../../../ui/FileLink.js'; import { FileLink } from '../../../layout/FileLink.js';
import { import {
safeTitle, safeTitle,
groupContent, groupContent,
mapToolStatusToContainerStatus, mapToolStatusToContainerStatus,
} from '../../shared/utils.js'; } from '../shared/utils.js';
import type { ToolCallContainerProps } from '../../shared/LayoutComponents.js'; import type { ToolCallContainerProps } from '../shared/LayoutComponents.js';
export const ToolCallContainer: React.FC<ToolCallContainerProps> = ({ export const ToolCallContainer: React.FC<ToolCallContainerProps> = ({
label, label,

View File

@@ -10,31 +10,18 @@
*/ */
import type React from 'react'; import type React from 'react';
import { ToolCallRouter } from './toolcalls/index.js'; import { ToolCallRouter } from './index.js';
// Re-export types from the toolcalls module for backward compatibility // Re-export types from the toolcalls module for backward compatibility
export type { export type {
ToolCallData, ToolCallData,
BaseToolCallProps as ToolCallProps, BaseToolCallProps as ToolCallProps,
} from './toolcalls/shared/types.js'; } from './shared/types.js';
// Re-export the content type for external use // Re-export the content type for external use
export type { ToolCallContent } from './toolcalls/shared/types.js'; export type { ToolCallContent } from './shared/types.js';
/**
* Main ToolCall component
* Routes to specialized components based on the tool call kind
*
* Supported kinds:
* - read: File reading operations
* - write/edit: File writing and editing operations
* - execute/bash/command: Command execution
* - search/grep/glob/find: Search operations
* - think/thinking: AI reasoning
* - All others: Generic display
*/
export const ToolCall: React.FC<{ export const ToolCall: React.FC<{
toolCall: import('./toolcalls/shared/types.js').ToolCallData; toolCall: import('./shared/types.js').ToolCallData;
isFirst?: boolean; isFirst?: boolean;
isLast?: boolean; isLast?: boolean;
}> = ({ toolCall, isFirst, isLast }) => ( }> = ({ toolCall, isFirst, isLast }) => (

View File

@@ -10,8 +10,8 @@ import type React from 'react';
import type { BaseToolCallProps } from '../shared/types.js'; import type { BaseToolCallProps } from '../shared/types.js';
import type { ToolCallContainerProps } 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 './CheckboxDisplay.js';
import type { PlanEntry } from '../../../../agents/qwenTypes.js'; import type { PlanEntry } from '../../../../../types/qwenTypes.js';
type EntryStatus = 'pending' | 'in_progress' | 'completed'; type EntryStatus = 'pending' | 'in_progress' | 'completed';

View File

@@ -13,7 +13,7 @@ import {
groupContent, groupContent,
mapToolStatusToContainerStatus, mapToolStatusToContainerStatus,
} from '../shared/utils.js'; } from '../shared/utils.js';
import { FileLink } from '../../ui/FileLink.js'; import { FileLink } from '../../../layout/FileLink.js';
/** /**
* Specialized component for Write tool calls * Specialized component for Write tool calls

View File

@@ -10,14 +10,13 @@ import type React from 'react';
import type { BaseToolCallProps } from './shared/types.js'; import type { BaseToolCallProps } from './shared/types.js';
import { shouldShowToolCall } from './shared/utils.js'; import { shouldShowToolCall } from './shared/utils.js';
import { GenericToolCall } from './GenericToolCall.js'; import { GenericToolCall } from './GenericToolCall.js';
import { ReadToolCall } from './done/Read/ReadToolCall.js'; import { ReadToolCall } from './Read/ReadToolCall.js';
import { WriteToolCall } from './Write/WriteToolCall.js'; import { WriteToolCall } from './Write/WriteToolCall.js';
import { EditToolCall } from './done/Edit/EditToolCall.js'; import { EditToolCall } from './Edit/EditToolCall.js';
import { ExecuteToolCall as BashExecuteToolCall } from './Bash/Bash.js'; import { ExecuteToolCall as BashExecuteToolCall } from './Bash/Bash.js';
import { ExecuteToolCall } from './done/Execute/Execute.js'; import { ExecuteToolCall } from './Execute/Execute.js';
import { UpdatedPlanToolCall } from './UpdatedPlan/UpdatedPlanToolCall.js'; import { UpdatedPlanToolCall } from './UpdatedPlan/UpdatedPlanToolCall.js';
import { ExecuteNodeToolCall } from './ExecuteNode/ExecuteNodeToolCall.js'; import { SearchToolCall } from './Search/SearchToolCall.js';
import { SearchToolCall } from './done/Search/SearchToolCall.js';
import { ThinkToolCall } from './Think/ThinkToolCall.js'; import { ThinkToolCall } from './Think/ThinkToolCall.js';
/** /**
@@ -25,7 +24,6 @@ import { ThinkToolCall } from './Think/ThinkToolCall.js';
*/ */
export const getToolCallComponent = ( export const getToolCallComponent = (
kind: string, kind: string,
toolCall?: import('./shared/types.js').ToolCallData,
): React.FC<BaseToolCallProps> => { ): React.FC<BaseToolCallProps> => {
const normalizedKind = kind.toLowerCase(); const normalizedKind = kind.toLowerCase();
@@ -41,24 +39,6 @@ export const getToolCallComponent = (
return EditToolCall; return EditToolCall;
case 'execute': case 'execute':
// Check if this is a node/npm version check command
if (toolCall) {
const commandText =
typeof toolCall.rawInput === 'string'
? toolCall.rawInput
: typeof toolCall.rawInput === 'object' &&
toolCall.rawInput !== null
? (toolCall.rawInput as { command?: string }).command || ''
: '';
// TODO:
if (
commandText.includes('node --version') ||
commandText.includes('npm --version')
) {
return ExecuteNodeToolCall;
}
}
return ExecuteToolCall; return ExecuteToolCall;
case 'bash': case 'bash':
@@ -71,7 +51,6 @@ export const getToolCallComponent = (
case 'update_todos': case 'update_todos':
case 'todowrite': case 'todowrite':
return UpdatedPlanToolCall; return UpdatedPlanToolCall;
// return TodoWriteToolCall;
case 'search': case 'search':
case 'grep': case 'grep':

View File

@@ -8,7 +8,7 @@
*/ */
import type React from 'react'; import type React from 'react';
import { FileLink } from '../../ui/FileLink.js'; import { FileLink } from '../../../layout/FileLink.js';
import './LayoutComponents.css'; import './LayoutComponents.css';
/** /**

View File

@@ -20,7 +20,7 @@ export const formatValue = (value: unknown): string => {
return ''; return '';
} }
if (typeof value === 'string') { if (typeof value === 'string') {
// TODO: 尝试从 string 取出 Output 部分 // TODO: Trying to take out the Output part from the string
try { try {
value = (JSON.parse(value) as { output?: unknown }).output ?? value; value = (JSON.parse(value) as { output?: unknown }).output ?? value;
} catch (_error) { } catch (_error) {

View File

@@ -1,38 +0,0 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*
* ExecuteNode tool call styles
*/
/* Error content styling */
.execute-node-error-content {
color: #c74e39;
margin-top: 4px;
}
/* Preformatted content */
.execute-node-pre {
margin: 0;
font-family: var(--app-monospace-font-family);
font-size: 0.85em;
white-space: pre-wrap;
word-break: break-word;
}
/* Error preformatted content */
.execute-node-error-pre {
color: #c74e39;
}
/* Output content styling */
.execute-node-output-content {
background-color: var(--app-code-background);
border: 0.5px solid var(--app-input-border);
border-radius: 5px;
margin: 8px 0;
padding: 8px;
max-width: 100%;
box-sizing: border-box;
}

View File

@@ -1,83 +0,0 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*
* ExecuteNode tool call component - specialized for node/npm execution operations
*/
import type React from 'react';
import type { BaseToolCallProps } from '../shared/types.js';
import { ToolCallContainer } from '../shared/LayoutComponents.js';
import {
safeTitle,
groupContent,
mapToolStatusToContainerStatus,
} from '../shared/utils.js';
import './ExecuteNode.css';
/**
* Specialized component for ExecuteNode tool calls
* Shows: Execute bullet + description + branch connector
*/
export const ExecuteNodeToolCall: React.FC<BaseToolCallProps> = ({
toolCall,
}) => {
const { title, content, rawInput, toolCallId } = toolCall;
const commandText = safeTitle(title);
// Group content by type
const { textOutputs, errors } = groupContent(content);
// Extract command from rawInput if available
let _inputCommand = commandText;
if (rawInput && typeof rawInput === 'object') {
const inputObj = rawInput as { command?: string };
_inputCommand = inputObj.command || commandText;
} else if (typeof rawInput === 'string') {
_inputCommand = rawInput;
}
// Error case
if (errors.length > 0) {
return (
<ToolCallContainer
label="Execute"
status="error"
toolCallId={toolCallId}
className="execute-toolcall"
>
{/* Branch connector summary (Claude-like) */}
<div className="inline-flex text-[var(--app-secondary-foreground)] text-[0.85em] opacity-70 mt-[2px] mb-[2px] flex-row items-start w-full gap-1">
<span className="flex-shrink-0 relative top-[-0.1em]"></span>
<span className="flex-shrink-0 w-full">{commandText}</span>
</div>
{/* Error content */}
<div className="execute-node-error-content">
<pre className="execute-node-pre execute-node-error-pre">
{errors.join('\n')}
</pre>
</div>
</ToolCallContainer>
);
}
// Success case: show command with branch connector (similar to the example)
return (
<ToolCallContainer
label="Execute"
status={mapToolStatusToContainerStatus(toolCall.status)}
toolCallId={toolCallId}
>
<div className="inline-flex text-[var(--app-secondary-foreground)] text-[0.85em] opacity-70 mt-[2px] mb-[2px] flex-row items-start w-full gap-1">
<span className="flex-shrink-0 relative top-[-0.1em]"></span>
<span className="flex-shrink-0 w-full">{commandText}</span>
</div>
{textOutputs.length > 0 && (
<div className="execute-node-output-content">
<pre className="execute-node-pre">{textOutputs.join('\n')}</pre>
</div>
)}
</ToolCallContainer>
);
};

View File

@@ -1,137 +0,0 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*
* TodoWrite tool call component - specialized for todo list operations
*/
import type React from 'react';
import type { BaseToolCallProps } from '../shared/types.js';
import { ToolCallContainer } from '../shared/LayoutComponents.js';
import { groupContent, safeTitle } from '../shared/utils.js';
import { CheckboxDisplay } from '../../ui/CheckboxDisplay.js';
type EntryStatus = 'pending' | 'in_progress' | 'completed';
interface TodoEntry {
content: string;
status: EntryStatus;
}
const mapToolStatusToBullet = (
status: import('../shared/types.js').ToolCallStatus,
): 'success' | 'error' | 'warning' | 'loading' | 'default' => {
switch (status) {
case 'completed':
return 'success';
case 'failed':
return 'error';
case 'in_progress':
return 'warning';
case 'pending':
return 'loading';
default:
return 'default';
}
};
// Parse todo list with - [ ] / - [x] from text as much as possible
const parseTodoEntries = (textOutputs: string[]): TodoEntry[] => {
const text = textOutputs.join('\n');
const lines = text.split(/\r?\n/);
const entries: TodoEntry[] = [];
// Accept [ ], [x]/[X] and in-progress markers [-] or [*]
const todoRe = /^(?:\s*(?:[-*]|\d+[.)])\s*)?\[( |x|X|-|\*)\]\s+(.*)$/;
for (const line of lines) {
const m = line.match(todoRe);
if (m) {
const mark = m[1];
const title = m[2].trim();
const status: EntryStatus =
mark === 'x' || mark === 'X'
? 'completed'
: mark === '-' || mark === '*'
? 'in_progress'
: 'pending';
if (title) {
entries.push({ content: title, status });
}
}
}
// If no match is found, fall back to treating non-empty lines as pending items
if (entries.length === 0) {
for (const line of lines) {
const title = line.trim();
if (title) {
entries.push({ content: title, status: 'pending' });
}
}
}
return entries;
};
/**
* Specialized component for TodoWrite tool calls
* Optimized for displaying todo list update operations
*/
export const TodoWriteToolCall: React.FC<BaseToolCallProps> = ({
toolCall,
}) => {
const { content, status } = toolCall;
const { errors, textOutputs } = groupContent(content);
// Error-first display
if (errors.length > 0) {
return (
<ToolCallContainer label="Update Todos" status="error">
{errors.join('\n')}
</ToolCallContainer>
);
}
const entries = parseTodoEntries(textOutputs);
const label = safeTitle(toolCall.title) || 'Update Todos';
return (
<ToolCallContainer label={label} status={mapToolStatusToBullet(status)}>
<ul className="Fr list-none p-0 m-0 flex flex-col gap-1">
{entries.map((entry, idx) => {
const isDone = entry.status === 'completed';
const isIndeterminate = entry.status === 'in_progress';
return (
<li
key={idx}
className={[
'Hr flex items-start gap-2 p-0 rounded text-[var(--app-primary-foreground)]',
isDone ? 'fo opacity-70' : '',
].join(' ')}
>
<label className="flex items-start gap-2">
<CheckboxDisplay
checked={isDone}
indeterminate={isIndeterminate}
/>
</label>
<div
className={[
'vo 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>
</ToolCallContainer>
);
};

View File

@@ -1,322 +0,0 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*
* DiffDisplay 组件样式
*/
/* ========================================
容器样式
======================================== */
.diff-display-container {
font-family: var(--vscode-font-family);
font-size: var(--vscode-font-size);
width: 100%;
}
/* ========================================
紧凑视图样式 - 超简洁版本
======================================== */
.diff-display-container {
border: 1px solid var(--vscode-panel-border);
border-radius: 4px;
background: var(--vscode-editor-background);
overflow: hidden;
}
.diff-compact-clickable {
padding: 6px 10px;
cursor: pointer;
user-select: none;
transition: all 0.15s ease-in-out;
}
.diff-compact-clickable:hover {
background: var(--vscode-list-hoverBackground);
}
.diff-compact-clickable:active {
opacity: 0.9;
}
.diff-compact-clickable:focus {
outline: 1px solid var(--vscode-focusBorder);
outline-offset: -1px;
}
.diff-compact-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.diff-file-info {
flex: 1;
min-width: 0; /* 允许文字截断 */
font-weight: 500;
font-size: 0.95em;
}
.diff-file-info .file-link {
color: var(--vscode-foreground);
}
.diff-stats {
display: flex;
gap: 8px;
align-items: center;
font-family: var(--vscode-editor-font-family, 'Menlo', 'Monaco', 'Courier New', monospace);
font-size: 0.85em;
flex-shrink: 0;
}
.diff-stats > span {
font-weight: 600;
white-space: nowrap;
}
.stat-added {
color: var(--vscode-gitDecoration-addedResourceForeground, #4ec9b0);
}
.stat-removed {
color: var(--vscode-gitDecoration-deletedResourceForeground, #f48771);
}
.stat-changed {
color: var(--vscode-gitDecoration-modifiedResourceForeground, #e5c07b);
}
.stat-no-change {
color: var(--vscode-descriptionForeground);
opacity: 0.7;
}
.diff-compact-actions {
padding: 6px 10px 8px;
border-top: 1px solid var(--vscode-panel-border);
background: var(--vscode-editorGroupHeader-tabsBackground);
display: flex;
justify-content: flex-end;
}
/* ========================================
完整视图样式
======================================== */
/* 已移除完整视图,统一为简洁模式 + 预览 */
/* 预览区域(仅变更行) */
.diff-preview {
margin: 0;
padding: 8px 10px;
background: var(--vscode-textCodeBlock-background, rgba(0, 0, 0, 0.06));
border-top: 1px solid var(--vscode-panel-border);
max-height: 320px;
overflow: auto;
}
.diff-file-path {
font-weight: 500;
flex: 1;
}
.diff-header-actions {
display: flex;
gap: 8px;
}
.diff-line {
white-space: pre;
font-family: var(--vscode-editor-font-family, 'Menlo', 'Monaco', 'Courier New', monospace);
font-size: 0.88em;
line-height: 1.45;
}
.diff-line.added {
background: var(--vscode-diffEditor-insertedLineBackground, rgba(76, 175, 80, 0.18));
color: var(--vscode-diffEditor-insertedTextForeground, #b5f1cc);
}
.diff-line.removed {
background: var(--vscode-diffEditor-removedLineBackground, rgba(244, 67, 54, 0.18));
color: var(--vscode-diffEditor-removedTextForeground, #f6b1a7);
}
.diff-line.no-change {
color: var(--vscode-descriptionForeground);
opacity: 0.8;
}
.diff-omitted {
color: var(--vscode-descriptionForeground);
font-style: italic;
padding-top: 6px;
}
.diff-section {
padding: 12px;
background: var(--vscode-editor-background);
}
.diff-section + .diff-section {
border-top: 1px solid var(--vscode-panel-border);
}
.diff-label {
font-size: 0.85em;
color: var(--vscode-descriptionForeground);
margin-bottom: 6px;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 600;
}
/* ========================================
按钮样式
======================================== */
.diff-action-button {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border: none;
border-radius: 4px;
font-size: 0.9em;
font-family: var(--vscode-font-family);
cursor: pointer;
transition: all 0.15s ease-in-out;
white-space: nowrap;
}
.diff-action-button svg {
flex-shrink: 0;
}
.diff-action-button.primary {
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
}
.diff-action-button.primary:hover {
background: var(--vscode-button-hoverBackground);
}
.diff-action-button.primary:active {
opacity: 0.9;
}
.diff-action-button.secondary {
background: transparent;
color: var(--vscode-textLink-foreground);
padding: 4px 8px;
font-size: 0.85em;
}
.diff-action-button.secondary:hover {
background: var(--vscode-button-secondaryHoverBackground);
text-decoration: underline;
}
.diff-action-button:focus {
outline: 1px solid var(--vscode-focusBorder);
outline-offset: 2px;
}
.diff-action-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* ========================================
代码块样式
======================================== */
.diff-section .code-block {
margin: 0;
padding: 12px;
background: var(--vscode-textCodeBlock-background, rgba(0, 0, 0, 0.1));
border: 1px solid var(--vscode-panel-border);
border-radius: 4px;
overflow-x: auto;
font-family: var(--vscode-editor-font-family, 'Menlo', 'Monaco', 'Courier New', monospace);
font-size: 0.9em;
line-height: 1.5;
}
.diff-section .code-content {
white-space: pre;
color: var(--vscode-editor-foreground);
}
/* ========================================
响应式调整
======================================== */
@media (max-width: 600px) {
.diff-compact-header {
flex-direction: column;
align-items: flex-start;
}
.diff-stats {
align-self: flex-start;
}
}
/* ========================================
高对比度模式支持
======================================== */
@media (prefers-contrast: high) {
.diff-compact-view,
.diff-full-view {
border-width: 2px;
}
.diff-stats > span {
font-weight: 700;
border: 1px solid currentColor;
}
.diff-action-button {
border: 1px solid currentColor;
}
}
/* ========================================
深色主题优化
======================================== */
@media (prefers-color-scheme: dark) {
.diff-compact-view:hover {
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3);
}
.stat-added {
background: rgba(78, 201, 176, 0.2);
}
.stat-removed {
background: rgba(244, 135, 113, 0.2);
}
.stat-changed {
background: rgba(229, 192, 123, 0.2);
}
}
/* ========================================
LocationsList 样式(用于 FileLink 列表)
======================================== */
.locations-list {
display: flex;
flex-direction: column;
gap: 6px;
}

View File

@@ -1,160 +0,0 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*
* Diff display component for showing file changes
*/
import type React from 'react';
import { useMemo } from 'react';
import { FileLink } from '../../ui/FileLink.js';
import {
calculateDiffStats,
formatDiffStatsDetailed,
} from '../../../utils/diffStats.js';
import { OpenDiffIcon } from '../../icons/index.js';
import './DiffDisplay.css';
import {
computeLineDiff,
truncateOps,
type DiffOp,
} from '../../../utils/simpleDiff.js';
/**
* Props for DiffDisplay
*/
interface DiffDisplayProps {
path?: string;
oldText?: string | null;
newText?: string;
onOpenDiff?: () => void;
/** Whether to display statistics */
showStats?: boolean;
}
/**
* Display diff with compact stats or full before/after sections
* Supports toggling between compact and full view modes
*/
export const DiffDisplay: React.FC<DiffDisplayProps> = ({
path,
oldText,
newText,
onOpenDiff,
showStats = true,
}) => {
// Statistics (recalculate only when text changes)
const stats = useMemo(
() => calculateDiffStats(oldText, newText),
[oldText, newText],
);
// Only generate changed lines (additions/deletions), do not render context
const ops: DiffOp[] = useMemo(
() => computeLineDiff(oldText, newText),
[oldText, newText],
);
const {
items: previewOps,
truncated,
omitted,
} = useMemo(() => truncateOps<DiffOp>(ops), [ops]);
return (
<div className="diff-display-container">
<div
className="diff-compact-clickable"
onClick={onOpenDiff}
role="button"
tabIndex={0}
title="Click to open diff in VS Code"
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onOpenDiff?.();
}
}}
>
<div className="diff-compact-header">
{path && (
<div className="diff-file-info">
<FileLink
path={path}
showFullPath={false}
className="diff-file-link"
disableClick={true}
/>
</div>
)}
{showStats && (
<div className="diff-stats" title={formatDiffStatsDetailed(stats)}>
{stats.added > 0 && (
<span className="stat-added">+{stats.added}</span>
)}
{stats.removed > 0 && (
<span className="stat-removed">-{stats.removed}</span>
)}
{stats.changed > 0 && (
<span className="stat-changed">~{stats.changed}</span>
)}
{stats.total === 0 && (
<span className="stat-no-change">No changes</span>
)}
</div>
)}
</div>
</div>
{/* Only draw preview area for diff lines */}
<pre className="diff-preview code-block" aria-label="Diff preview">
<div className="code-content">
{previewOps.length === 0 && (
<div className="diff-line no-change">(no changes)</div>
)}
{previewOps.map((op, idx) => {
if (op.type === 'add') {
const line = op.line;
return (
<div key={`add-${idx}`} className="diff-line added">
+{line || ' '}
</div>
);
}
if (op.type === 'remove') {
const line = op.line;
return (
<div key={`rm-${idx}`} className="diff-line removed">
-{line || ' '}
</div>
);
}
return null;
})}
{truncated && (
<div
className="diff-omitted"
title={`${omitted} lines omitted in preview`}
>
{omitted} lines omitted
</div>
)}
</div>
</pre>
{/* Provide explicit open button below preview (optional) */}
{onOpenDiff && (
<div className="diff-compact-actions">
<button
className="diff-action-button primary"
onClick={onOpenDiff}
title="Open in VS Code diff viewer"
>
<OpenDiffIcon width="14" height="14" />
Open Diff
</button>
</div>
)}
</div>
);
};

View File

@@ -1,70 +0,0 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*
* Simplified timeline styles for tool calls and messages
* Only keeping actually used styles
*/
/* ToolCallContainer timeline styles (from LayoutComponents.css) */
.toolcall-container {
position: relative;
padding-left: 30px;
padding-top: 8px;
padding-bottom: 8px;
}
/* ToolCallContainer timeline connector */
.toolcall-container::after {
content: '';
position: absolute;
left: 12px;
top: 0;
bottom: 0;
width: 1px;
background-color: var(--app-primary-border-color);
}
/* First item: connector starts from status point position */
.toolcall-container:first-child::after {
top: 24px;
}
/* Last item: connector shows only upper part */
.toolcall-container:last-child::after {
height: calc(100% - 24px);
top: 0;
bottom: auto;
}
/* AssistantMessage timeline styles */
.assistant-message-container {
position: relative;
padding-left: 30px;
padding-top: 8px;
padding-bottom: 8px;
}
/* AssistantMessage timeline connector */
.assistant-message-container::after {
content: '';
position: absolute;
left: 12px;
top: 0;
bottom: 0;
width: 1px;
background-color: var(--app-primary-border-color);
}
/* First item: connector starts from status point position */
.assistant-message-container:first-child::after {
top: 24px;
}
/* Last item: connector shows only upper part */
.assistant-message-container:last-child::after {
height: calc(100% - 24px);
top: 0;
bottom: auto;
}

View File

@@ -4,8 +4,8 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import type { QwenAgentManager } from '../../agents/qwenAgentManager.js'; import type { QwenAgentManager } from '../../services/qwenAgentManager.js';
import type { ConversationStore } from '../../storage/conversationStore.js'; import type { ConversationStore } from '../../services/conversationStore.js';
/** /**
* Base message handler interface * Base message handler interface

View File

@@ -5,8 +5,8 @@
*/ */
import type { IMessageHandler } from './BaseMessageHandler.js'; import type { IMessageHandler } from './BaseMessageHandler.js';
import type { QwenAgentManager } from '../../agents/qwenAgentManager.js'; import type { QwenAgentManager } from '../../services/qwenAgentManager.js';
import type { ConversationStore } from '../../storage/conversationStore.js'; import type { ConversationStore } from '../../services/conversationStore.js';
import { SessionMessageHandler } from './SessionMessageHandler.js'; import { SessionMessageHandler } from './SessionMessageHandler.js';
import { FileMessageHandler } from './FileMessageHandler.js'; import { FileMessageHandler } from './FileMessageHandler.js';
import { EditorMessageHandler } from './EditorMessageHandler.js'; import { EditorMessageHandler } from './EditorMessageHandler.js';

View File

@@ -6,7 +6,7 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { BaseMessageHandler } from './BaseMessageHandler.js'; import { BaseMessageHandler } from './BaseMessageHandler.js';
import type { ChatMessage } from '../../agents/qwenAgentManager.js'; import type { ChatMessage } from '../../services/qwenAgentManager.js';
/** /**
* Session message handler * Session message handler
@@ -581,10 +581,11 @@ export class SessionMessageHandler extends BaseMessageHandler {
let sessionDetails: Record<string, unknown> | null = null; let sessionDetails: Record<string, unknown> | null = null;
try { try {
const allSessions = await this.agentManager.getSessionList(); const allSessions = await this.agentManager.getSessionList();
sessionDetails = allSessions.find( sessionDetails =
allSessions.find(
(s: { id?: string; sessionId?: string }) => (s: { id?: string; sessionId?: string }) =>
s.id === sessionId || s.sessionId === sessionId, s.id === sessionId || s.sessionId === sessionId,
); ) || null;
} catch (err) { } catch (err) {
console.log( console.log(
'[SessionMessageHandler] Could not get session details:', '[SessionMessageHandler] Could not get session details:',

View File

@@ -73,7 +73,9 @@ export const useMessageHandling = () => {
const appendStreamChunk = useCallback( const appendStreamChunk = useCallback(
(chunk: string) => { (chunk: string) => {
// Ignore late chunks after user cancelled streaming (until next streamStart) // Ignore late chunks after user cancelled streaming (until next streamStart)
if (!isStreaming) return; if (!isStreaming) {
return;
}
setMessages((prev) => { setMessages((prev) => {
let idx = streamingMessageIndexRef.current; let idx = streamingMessageIndexRef.current;
@@ -157,7 +159,9 @@ export const useMessageHandling = () => {
// Thought handling // Thought handling
appendThinkingChunk: (chunk: string) => { appendThinkingChunk: (chunk: string) => {
// Ignore late thoughts after user cancelled streaming // Ignore late thoughts after user cancelled streaming
if (!isStreaming) return; if (!isStreaming) {
return;
}
setMessages((prev) => { setMessages((prev) => {
let idx = thinkingMessageIndexRef.current; let idx = thinkingMessageIndexRef.current;
const next = prev.slice(); const next = prev.slice();

View File

@@ -59,7 +59,9 @@ export const useSessionManagement = (vscode: VSCodeAPI) => {
}, [vscode]); }, [vscode]);
const handleLoadMoreSessions = useCallback(() => { const handleLoadMoreSessions = useCallback(() => {
if (!hasMore || isLoading || nextCursor === undefined) return; if (!hasMore || isLoading || nextCursor === undefined) {
return;
}
setIsLoading(true); setIsLoading(true);
vscode.postMessage({ vscode.postMessage({
type: 'getQwenSessions', type: 'getQwenSessions',

View File

@@ -6,7 +6,7 @@
import type { RefObject } from 'react'; import type { RefObject } from 'react';
import { useState, useEffect, useCallback, useRef, useMemo } from 'react'; import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import type { CompletionItem } from '../types/CompletionTypes.js'; import type { CompletionItem } from '../../types/completionItemTypes.js';
interface CompletionTriggerState { interface CompletionTriggerState {
isOpen: boolean; isOpen: boolean;

View File

@@ -5,8 +5,8 @@
*/ */
import { useState, useCallback } from 'react'; import { useState, useCallback } from 'react';
import type { ToolCallData } from '../components/ToolCall.js'; import type { ToolCallData } from '../components/messages/toolcalls/ToolCall.js';
import type { ToolCallUpdate } from '../types/toolCall.js'; import type { ToolCallUpdate } from '../../types/qwenTypes.js';
/** /**
* Tool call management Hook * Tool call management Hook

View File

@@ -6,13 +6,13 @@
import { useEffect, useRef, useCallback } from 'react'; import { useEffect, useRef, useCallback } from 'react';
import { useVSCode } from './useVSCode.js'; import { useVSCode } from './useVSCode.js';
import type { Conversation } from '../../storage/conversationStore.js'; import type { Conversation } from '../../services/conversationStore.js';
import type { import type {
PermissionOption, PermissionOption,
ToolCall as PermissionToolCall, ToolCall as PermissionToolCall,
} from '../components/PermissionDrawer/PermissionRequest.js'; } from '../components/PermissionDrawer/PermissionRequest.js';
import type { ToolCallUpdate, EditMode } from '../types/toolCall.js'; import type { ToolCallUpdate, EditMode } from '../../types/qwenTypes.js';
import type { PlanEntry } from '../../agents/qwenTypes.js'; import type { PlanEntry } from '../../types/qwenTypes.js';
interface UseWebViewMessagesProps { interface UseWebViewMessagesProps {
// Session management // Session management

View File

@@ -11,7 +11,7 @@ import './styles/tailwind.css';
// eslint-disable-next-line import/no-internal-modules // eslint-disable-next-line import/no-internal-modules
import './styles/App.css'; import './styles/App.css';
// eslint-disable-next-line import/no-internal-modules // eslint-disable-next-line import/no-internal-modules
import './styles/ClaudeCodeStyles.css'; import './styles/styles.css';
const container = document.getElementById('root'); const container = document.getElementById('root');
if (container) { if (container) {

View File

@@ -8,12 +8,9 @@
*/ */
/* Import component styles */ /* Import component styles */
@import '../components/toolcalls/shared/DiffDisplay.css';
@import '../components/messages/Assistant/AssistantMessage.css'; @import '../components/messages/Assistant/AssistantMessage.css';
@import '../components/toolcalls/shared/SimpleTimeline.css'; @import './timeline.css';
@import '../components/messages/QwenMessageTimeline.css'; @import '../components/messages/MarkdownRenderer/MarkdownRenderer.css';
@import '../components/MarkdownRenderer/MarkdownRenderer.css';
/* =========================== /* ===========================
CSS Variables (from Claude Code root styles) CSS Variables (from Claude Code root styles)
@@ -56,7 +53,10 @@
--app-code-background: var(--vscode-textCodeBlock-background); --app-code-background: var(--vscode-textCodeBlock-background);
/* Warning/Error Styles */ /* Warning/Error Styles */
--app-warning-background: var(--vscode-editorWarning-background, rgba(255, 204, 0, 0.1)); --app-warning-background: var(
--vscode-editorWarning-background,
rgba(255, 204, 0, 0.1)
);
--app-warning-border: var(--vscode-editorWarning-foreground, #ffcc00); --app-warning-border: var(--vscode-editorWarning-foreground, #ffcc00);
--app-warning-foreground: var(--vscode-editorWarning-foreground, #ffcc00); --app-warning-foreground: var(--vscode-editorWarning-foreground, #ffcc00);
} }

View File

@@ -0,0 +1,126 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*
* Unified timeline styles for tool calls and messages
*/
/* ==========================================
ToolCallContainer timeline styles
========================================== */
.toolcall-container {
position: relative;
padding-left: 30px;
padding-top: 8px;
padding-bottom: 8px;
}
/* ToolCallContainer timeline connector */
.toolcall-container::after {
content: '';
position: absolute;
left: 12px;
top: 0;
bottom: 0;
width: 1px;
background-color: var(--app-primary-border-color);
}
/* First item: connector starts from status point position */
.toolcall-container:first-child::after {
top: 24px;
}
/* Last item: connector shows only upper part */
.toolcall-container:last-child::after {
height: calc(100% - 24px);
top: 0;
bottom: auto;
}
/* ==========================================
AssistantMessage timeline styles
========================================== */
.assistant-message-container {
position: relative;
padding-left: 30px;
padding-top: 8px;
padding-bottom: 8px;
}
/* AssistantMessage timeline connector */
.assistant-message-container::after {
content: '';
position: absolute;
left: 12px;
top: 0;
bottom: 0;
width: 1px;
background-color: var(--app-primary-border-color);
}
/* First item: connector starts from status point position */
.assistant-message-container:first-child::after {
top: 24px;
}
/* Last item: connector shows only upper part */
.assistant-message-container:last-child::after {
height: calc(100% - 24px);
top: 0;
bottom: auto;
}
/* ==========================================
Custom timeline styles for qwen-message message-item elements
========================================== */
/* Default connector style - creates full-height connectors for all AI message items */
.qwen-message.message-item:not(.user-message-container)::after {
content: '';
position: absolute;
left: 12px;
top: 0;
bottom: 0;
width: 1px;
background-color: var(--app-primary-border-color);
z-index: 0;
}
/* Handle the start of each AI message sequence - includes the first AI message in the entire message list and new AI messages interrupted by user messages */
.qwen-message.message-item:not(.user-message-container):first-child::after,
.user-message-container + .qwen-message.message-item:not(.user-message-container)::after,
/* If the previous sibling is not .qwen-message.message-item (such as waiting prompts, sentinel elements, or card-style tool calls), also treat as a new group start */
.chat-messages > :not(.qwen-message.message-item)
+ .qwen-message.message-item:not(.user-message-container)::after {
top: 15px;
}
/* Handle the end of each AI message sequence */
/* When the next sibling is a user message */
.qwen-message.message-item:not(.user-message-container):has(+ .user-message-container)::after,
/* Or when the next sibling is not .qwen-message.message-item (such as waiting prompts, sentinel elements, card-style tool calls, etc.) */
.qwen-message.message-item:not(.user-message-container):has(+ :not(.qwen-message.message-item))::after,
/* When it's truly the last child element of the parent container */
.qwen-message.message-item:not(.user-message-container):last-child::after {
/* Note: When setting both top and bottom, the height is (container height - top - bottom).
* Here we expect "15px spacing at the bottom", so bottom should be 15px (not calc(100% - 15px)). */
top: 0;
bottom: calc(100% - 15px);
}
.user-message-container:first-child {
margin-top: 0;
}
.message-item {
padding: 8px 0;
width: 100%;
align-items: flex-start;
padding-left: 30px;
user-select: text;
position: relative;
padding-top: 8px;
padding-bottom: 8px;
}

View File

@@ -1,39 +0,0 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Tool call update type
*/
export interface ToolCallUpdate {
type: 'tool_call' | 'tool_call_update';
toolCallId: string;
kind?: string;
title?: string;
status?: 'pending' | 'in_progress' | 'completed' | 'failed';
rawInput?: unknown;
content?: Array<{
type: 'content' | 'diff';
content?: {
type: string;
text?: string;
[key: string]: unknown;
};
path?: string;
oldText?: string | null;
newText?: string;
[key: string]: unknown;
}>;
locations?: Array<{
path: string;
line?: number | null;
}>;
timestamp?: number; // Add timestamp field for message ordering
}
/**
* Edit mode type
*/
export type EditMode = 'ask' | 'auto' | 'plan' | 'yolo';

View File

@@ -6,7 +6,7 @@
* Shared utilities for handling diff operations in the webview * Shared utilities for handling diff operations in the webview
*/ */
import type { WebviewApi } from 'vscode-webview'; import type { VSCodeAPI } from '../hooks/useVSCode.js';
/** /**
* Handle opening a diff view for a file * Handle opening a diff view for a file
@@ -16,7 +16,7 @@ import type { WebviewApi } from 'vscode-webview';
* @param newText New content (right side) * @param newText New content (right side)
*/ */
export const handleOpenDiff = ( export const handleOpenDiff = (
vscode: WebviewApi<unknown>, vscode: VSCodeAPI,
path: string | undefined, path: string | undefined,
oldText: string | null | undefined, oldText: string | null | undefined,
newText: string | undefined, newText: string | undefined,

View File

@@ -7,11 +7,7 @@
/* eslint-env node */ /* eslint-env node */
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default { export default {
content: [ content: ['./src/webview/**/**/*.{js,jsx,ts,tsx}'],
// Progressive adoption strategy: Only scan newly created Tailwind components
'./src/webview/**/**/*.{js,jsx,ts,tsx}',
'./src/webview/components/ui/CheckboxDisplay.tsx',
],
theme: { theme: {
extend: { extend: {
keyframes: { keyframes: {