chore(vscode-ide-companion): code style & command register bugfix

This commit is contained in:
yiliang114
2025-12-06 01:32:52 +08:00
parent 96b275a756
commit 541d0b22e5
24 changed files with 222 additions and 459 deletions

View File

@@ -56,17 +56,17 @@
"title": "Qwen Code: View Third-Party Notices"
},
{
"command": "qwenCode.openChat",
"command": "qwen-code.openChat",
"title": "Qwen Code: Open Chat",
"icon": "./assets/icon.png"
},
{
"command": "qwenCode.clearAuthCache",
"title": "Qwen Code: Clear Authentication Cache"
"command": "qwen-code.login",
"title": "Qwen Code: Login"
},
{
"command": "qwenCode.login",
"title": "Qwen Code: Login"
"command": "qwen-code.clearAuthCache",
"title": "Qwen Code: Clear Authentication Cache"
}
],
"configuration": {
@@ -90,7 +90,7 @@
"when": "qwen.diff.isVisible"
},
{
"command": "qwenCode.login",
"command": "qwen-code.login",
"when": "false"
}
],
@@ -106,7 +106,7 @@
"group": "navigation"
},
{
"command": "qwenCode.openChat",
"command": "qwen-code.openChat",
"group": "navigation"
}
]
@@ -123,7 +123,7 @@
"when": "qwen.diff.isVisible"
},
{
"command": "qwenCode.openChat",
"command": "qwen-code.openChat",
"key": "ctrl+shift+a",
"mac": "cmd+shift+a"
}

View File

@@ -26,23 +26,6 @@ import { determineNodePathForCli } from '../cli/cliPathDetector.js';
* ACP Connection Handler for VSCode Extension
*
* This class implements the client side of the ACP (Agent Communication Protocol).
*
* Implementation Status:
*
* Client Methods (Methods this class implements, called by CLI):
* ✅ session/update - Handle session updates via onSessionUpdate callback
* ✅ session/request_permission - Request user permission for tool execution
* ✅ fs/read_text_file - Read file from workspace
* ✅ fs/write_text_file - Write file to workspace
*
* Agent Methods (Methods CLI implements, called by this class):
* ✅ initialize - Initialize ACP protocol connection
* ✅ authenticate - Authenticate with selected auth method
* ✅ session/new - Create new chat session
* ✅ session/prompt - Send user message to agent
* ✅ session/cancel - Cancel current generation
* ✅ session/load - Load previous session
* ✅ session/save - Save current session
*/
export class AcpConnection {
private child: ChildProcess | null = null;

View File

@@ -18,7 +18,7 @@ export interface PlanEntry {
/** Entry content */
content: string;
/** Priority */
priority: 'high' | 'medium' | 'low';
priority?: 'high' | 'medium' | 'low';
/** Status */
status: 'pending' | 'in_progress' | 'completed';
}

View File

@@ -6,9 +6,6 @@
import { CliDetector, type CliDetectionResult } from './cliDetector.js';
/**
* Minimum CLI version that supports session/list and session/load ACP methods
*/
export const MIN_CLI_VERSION_FOR_SESSION_METHODS = '0.4.0';
/**

View File

@@ -4,8 +4,12 @@ import type { WebViewProvider } from '../webview/WebViewProvider.js';
type Logger = (message: string) => void;
export const runQwenCodeCommand = 'qwen-code.runQwenCode';
export const showDiffCommand = 'qwenCode.showDiff';
export const openChatCommand = 'qwenCode.openChat';
export const openChatCommand = 'qwen-code.openChat';
export const openNewChatTabCommand = 'qwenCode.openNewChatTab';
export const loginCommand = 'qwen-code.login';
export const clearAuthCacheCommand = 'qwen-code.clearAuthCache';
export function registerNewCommands(
context: vscode.ExtensionContext,
@@ -20,15 +24,15 @@ export function registerNewCommands(
vscode.commands.registerCommand(openChatCommand, async () => {
const config = vscode.workspace.getConfiguration('qwenCode');
const useTerminal = config.get<boolean>('useTerminal', false);
console.log('[Command] Using terminal mode:', useTerminal);
// Use terminal mode
if (useTerminal) {
// 使用终端模式
await vscode.commands.executeCommand(
'qwen-code.runQwenCode',
vscode.TerminalLocation.Editor, // 在编辑器区域创建终端,
runQwenCodeCommand,
vscode.TerminalLocation.Editor, // create a terminal in the editor area,
);
} else {
// 使用 WebView 模式
// Use WebView mode
const providers = getWebViewProviders();
if (providers.length > 0) {
await providers[providers.length - 1].show();
@@ -44,7 +48,6 @@ export function registerNewCommands(
vscode.commands.registerCommand(
showDiffCommand,
async (args: { path: string; oldText: string; newText: string }) => {
log(`[Command] showDiff called for: ${args.path}`);
try {
let absolutePath = args.path;
if (!args.path.startsWith('/') && !args.path.match(/^[a-zA-Z]:/)) {
@@ -68,27 +71,14 @@ export function registerNewCommands(
// TODO: qwenCode.openNewChatTab (not contributed in package.json; used programmatically)
disposables.push(
vscode.commands.registerCommand('qwenCode.openNewChatTab', async () => {
vscode.commands.registerCommand(openNewChatTabCommand, async () => {
const provider = createWebViewProvider();
await provider.show();
}),
);
disposables.push(
vscode.commands.registerCommand('qwenCode.clearAuthCache', async () => {
const providers = getWebViewProviders();
for (const provider of providers) {
await provider.clearAuthCache();
}
vscode.window.showInformationMessage(
'Qwen Code authentication cache cleared. You will need to login again on next connection.',
);
log('Auth cache cleared by user');
}),
);
disposables.push(
vscode.commands.registerCommand('qwenCode.login', async () => {
vscode.commands.registerCommand(loginCommand, async () => {
const providers = getWebViewProviders();
if (providers.length > 0) {
await providers[providers.length - 1].forceReLogin();
@@ -100,5 +90,18 @@ export function registerNewCommands(
}),
);
disposables.push(
vscode.commands.registerCommand(clearAuthCacheCommand, async () => {
const providers = getWebViewProviders();
for (const provider of providers) {
await provider.clearAuthCache();
}
vscode.window.showInformationMessage(
'Qwen Code authentication cache cleared. You will need to login again on next connection.',
);
log('Auth cache cleared by user');
}),
);
context.subscriptions.push(...disposables);
}

View File

@@ -4,26 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
/**
* ACP (Agent Communication Protocol) Method Definitions
*
* This file defines the protocol methods for communication between
* the VSCode extension (Client) and the qwen CLI (Agent/Server).
*/
/**
* Methods that the Agent (CLI) implements and receives from Client (VSCode)
*
* Status in qwen CLI:
* ✅ initialize - Protocol initialization
* ✅ authenticate - User authentication
* ✅ session/new - Create new session
* ✅ session/load - Load existing session (v0.2.4+)
* ✅ session/list - List available sessions (v0.2.4+)
* ✅ session/prompt - Send user message to agent
* ✅ session/cancel - Cancel current generation
* ✅ session/save - Save current session
*/
export const AGENT_METHODS = {
authenticate: 'authenticate',
initialize: 'initialize',
@@ -35,15 +15,6 @@ export const AGENT_METHODS = {
session_save: 'session/save',
} as const;
/**
* Methods that the Client (VSCode) implements and receives from Agent (CLI)
*
* Status in VSCode extension:
* ✅ fs/read_text_file - Read file content
* ✅ fs/write_text_file - Write file content
* ✅ session/request_permission - Request user permission for tool execution
* ✅ session/update - Stream session updates (notification)
*/
export const CLIENT_METHODS = {
fs_read_text_file: 'fs/read_text_file',
fs_write_text_file: 'fs/write_text_file',

View File

@@ -21,15 +21,14 @@ import { useMessageSubmit } from './hooks/useMessageSubmit.js';
import type {
PermissionOption,
ToolCall as PermissionToolCall,
} from './components/PermissionRequest.js';
} from './components/PermissionDrawer/PermissionRequest.js';
import type { TextMessage } from './hooks/message/useMessageHandling.js';
import type { ToolCallData } from './components/ToolCall.js';
import { PermissionDrawer } from './components/PermissionDrawer.js';
import { PermissionDrawer } from './components/PermissionDrawer/PermissionDrawer.js';
import { ToolCall } from './components/ToolCall.js';
import { hasToolCallOutput } from './components/toolcalls/shared/utils.js';
// import { InProgressToolCall } from './components/InProgressToolCall.js';
import { EmptyState } from './components/ui/EmptyState.js';
import type { PlanEntry } from './components/PlanDisplay.js';
import { type CompletionItem } from './types/CompletionTypes.js';
import { useCompletionTrigger } from './hooks/useCompletionTrigger.js';
import { InfoBanner } from './components/ui/InfoBanner.js';
@@ -45,6 +44,7 @@ import { InputForm } from './components/InputForm.js';
import { SessionSelector } from './components/session/SessionSelector.js';
import { FileIcon, UserIcon } from './components/icons/index.js';
import type { EditMode } from './types/toolCall.js';
import type { PlanEntry } from '../agents/qwenTypes.js';
export const App: React.FC = () => {
const vscode = useVSCode();

View File

@@ -16,6 +16,7 @@ import { WebViewContent } from '../webview/WebViewContent.js';
import { CliInstaller } from '../cli/cliInstaller.js';
import { getFileName } from './utils/webviewUtils.js';
import { authMethod } from '../auth/index.js';
import { runQwenCodeCommand } from '../commands/index.js';
export class WebViewProvider {
private panelManager: PanelManager;
@@ -1067,7 +1068,7 @@ export class WebViewProvider {
if (useTerminal) {
// In terminal mode, execute the runQwenCode command to open a new terminal
try {
await vscode.commands.executeCommand('qwen-code.runQwenCode');
await vscode.commands.executeCommand(runQwenCodeCommand);
console.log('[WebViewProvider] Opened new terminal session');
} catch (error) {
console.error(

View File

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

View File

@@ -19,11 +19,12 @@ interface MarkdownRendererProps {
/**
* Regular expressions for parsing content
*/
// Match absolute file paths like: /path/to/file.ts or C:\path\to\file.ts
const FILE_PATH_REGEX =
/([a-zA-Z]:)?([/\\][\w\-. ]+)+\.(tsx?|jsx?|css|scss|json|md|py|java|go|rs|c|cpp|h|hpp|sh|yaml|yml|toml|xml|html|vue|svelte)/gi;
// Match file paths with optional line numbers like: path/file.ts#7-14 or path/file.ts#7
/(?:[a-zA-Z]:)?[/\\](?:[\w\-. ]+[/\\])+[\w\-. ]+\.(tsx?|jsx?|css|scss|json|md|py|java|go|rs|c|cpp|h|hpp|sh|yaml|yml|toml|xml|html|vue|svelte)/gi;
// Match file paths with optional line numbers like: /path/to/file.ts#7-14 or C:\path\to\file.ts#7
const FILE_PATH_WITH_LINES_REGEX =
/([a-zA-Z]:)?([/\\][\w\-. ]+)+\.(tsx?|jsx?|css|scss|json|md|py|java|go|rs|c|cpp|h|hpp|sh|yaml|yml|toml|xml|html|vue|svelte)#(\d+)(?:-(\d+))?/gi;
/(?:[a-zA-Z]:)?[/\\](?:[\w\-. ]+[/\\])+[\w\-. ]+\.(tsx?|jsx?|css|scss|json|md|py|java|go|rs|c|cpp|h|hpp|sh|yaml|yml|toml|xml|html|vue|svelte)#(\d+)(?:-(\d+))?/gi;
/**
* MarkdownRenderer component - renders markdown content with enhanced features
@@ -166,9 +167,22 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
const href = a.getAttribute('href') || '';
const text = (a.textContent || '').trim();
// Helper function to check if a string looks like a code reference
const isCodeReference = (str: string): boolean => {
// Check if it looks like a code reference (e.g., module.property)
// Patterns like "vscode.contribution", "module.submodule.function"
const codeRefPattern = /^[a-zA-Z_$][\w$]*(\.[a-zA-Z_$][\w$]*)+$/;
return codeRefPattern.test(str);
};
// If linkify turned a bare filename into http://<filename>, convert it back
const httpMatch = href.match(/^https?:\/\/(.+)$/i);
if (httpMatch && BARE_FILE_REGEX.test(text) && httpMatch[1] === text) {
// Skip if it looks like a code reference
if (isCodeReference(text)) {
return;
}
// Treat as a file link instead of external URL
const filePath = text; // no leading slash
a.classList.add('file-path-link');
@@ -182,6 +196,12 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
if (/^(https?|mailto|ftp|data):/i.test(href)) return;
const candidate = href || text;
// Skip if it looks like a code reference
if (isCodeReference(candidate)) {
return;
}
if (
FILE_PATH_WITH_LINES_NO_G.test(candidate) ||
FILE_PATH_NO_G.test(candidate)
@@ -194,6 +214,14 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
}
};
// Helper function to check if a string looks like a code reference
const isCodeReference = (str: string): boolean => {
// Check if it looks like a code reference (e.g., module.property)
// Patterns like "vscode.contribution", "module.submodule.function"
const codeRefPattern = /^[a-zA-Z_$][\w$]*(\.[a-zA-Z_$][\w$]*)+$/;
return codeRefPattern.test(str);
};
const walk = (node: Node) => {
// Do not transform inside existing anchors
if (node.nodeType === Node.ELEMENT_NODE) {
@@ -218,6 +246,20 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
while ((m = union.exec(text))) {
const matchText = m[0];
const idx = m.index;
// Skip if it looks like a code reference
if (isCodeReference(matchText)) {
// Just add the text as-is without creating a link
if (idx > lastIndex) {
frag.appendChild(
document.createTextNode(text.slice(lastIndex, idx)),
);
}
frag.appendChild(document.createTextNode(matchText));
lastIndex = idx + matchText.length;
continue;
}
if (idx > lastIndex) {
frag.appendChild(
document.createTextNode(text.slice(lastIndex, idx)),

View File

@@ -0,0 +1,37 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
export interface PermissionOption {
name: string;
kind: string;
optionId: string;
}
export interface ToolCall {
title?: string;
kind?: string;
toolCallId?: string;
rawInput?: {
command?: string;
description?: string;
[key: string]: unknown;
};
content?: Array<{
type: string;
[key: string]: unknown;
}>;
locations?: Array<{
path: string;
line?: number | null;
}>;
status?: string;
}
export interface PermissionRequestProps {
options: PermissionOption[];
toolCall: ToolCall;
onResponse: (optionId: string) => void;
}

View File

@@ -1,227 +0,0 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
export interface PermissionOption {
name: string;
kind: string;
optionId: string;
}
export interface ToolCall {
title?: string;
kind?: string;
toolCallId?: string;
rawInput?: {
command?: string;
description?: string;
[key: string]: unknown;
};
content?: Array<{
type: string;
[key: string]: unknown;
}>;
locations?: Array<{
path: string;
line?: number | null;
}>;
status?: string;
}
export interface PermissionRequestProps {
options: PermissionOption[];
toolCall: ToolCall;
onResponse: (optionId: string) => void;
}
// export const PermissionRequest: React.FC<PermissionRequestProps> = ({
// options,
// toolCall,
// onResponse,
// }) => {
// const [selected, setSelected] = useState<string | null>(null);
// const [isResponding, setIsResponding] = useState(false);
// const [hasResponded, setHasResponded] = useState(false);
// const getToolInfo = () => {
// if (!toolCall) {
// return {
// title: 'Permission Request',
// description: 'Agent is requesting permission',
// icon: '🔐',
// };
// }
// const displayTitle =
// toolCall.title || toolCall.rawInput?.description || 'Permission Request';
// const kindIcons: Record<string, string> = {
// edit: '✏️',
// read: '📖',
// fetch: '🌐',
// execute: '⚡',
// delete: '🗑️',
// move: '📦',
// search: '🔍',
// think: '💭',
// other: '🔧',
// };
// return {
// title: displayTitle,
// icon: kindIcons[toolCall.kind || 'other'] || '🔧',
// };
// };
// const { title, icon } = getToolInfo();
// const handleConfirm = async () => {
// if (hasResponded || !selected) {
// return;
// }
// setIsResponding(true);
// try {
// await onResponse(selected);
// setHasResponded(true);
// } catch (error) {
// console.error('Error confirming permission:', error);
// } finally {
// setIsResponding(false);
// }
// };
// if (!toolCall) {
// return null;
// }
// return (
// <div className="permission-request-card">
// <div className="permission-card-body">
// {/* Header with icon and title */}
// <div className="permission-header">
// <div className="permission-icon-wrapper">
// <span className="permission-icon">{icon}</span>
// </div>
// <div className="permission-info">
// <div className="permission-title">{title}</div>
// <div className="permission-subtitle">Waiting for your approval</div>
// </div>
// </div>
// {/* Show command if available */}
// {(toolCall.rawInput?.command || toolCall.title) && (
// <div className="permission-command-section">
// <div className="permission-command-header">
// <div className="permission-command-status">
// <span className="permission-command-dot">●</span>
// <span className="permission-command-label">COMMAND</span>
// </div>
// </div>
// <div className="permission-command-content">
// <div className="permission-command-input-section">
// <span className="permission-command-io-label">IN</span>
// <code className="permission-command-code">
// {toolCall.rawInput?.command || toolCall.title}
// </code>
// </div>
// {toolCall.rawInput?.description && (
// <div className="permission-command-description">
// {toolCall.rawInput.description}
// </div>
// )}
// </div>
// </div>
// )}
// {/* Show file locations if available */}
// {toolCall.locations && toolCall.locations.length > 0 && (
// <div className="permission-locations-section">
// <div className="permission-locations-label">Affected Files</div>
// {toolCall.locations.map((location, index) => (
// <div key={index} className="permission-location-item">
// <span className="permission-location-icon">📄</span>
// <span className="permission-location-path">
// {location.path}
// </span>
// {location.line !== null && location.line !== undefined && (
// <span className="permission-location-line">
// ::{location.line}
// </span>
// )}
// </div>
// ))}
// </div>
// )}
// {/* Options */}
// {!hasResponded && (
// <div className="permission-options-section">
// <div className="permission-options-label">Choose an action:</div>
// <div className="permission-options-list">
// {options && options.length > 0 ? (
// options.map((option, index) => {
// const isSelected = selected === option.optionId;
// const isAllow = option.kind.includes('allow');
// const isAlways = option.kind.includes('always');
// return (
// <label
// key={option.optionId}
// className={`permission-option ${isSelected ? 'selected' : ''} ${
// isAllow ? 'allow' : 'reject'
// } ${isAlways ? 'always' : ''}`}
// >
// <input
// type="radio"
// name="permission"
// value={option.optionId}
// checked={isSelected}
// onChange={() => setSelected(option.optionId)}
// className="permission-radio"
// />
// <span className="permission-option-content">
// <span className="permission-option-number">
// {index + 1}
// </span>
// {isAlways && (
// <span className="permission-always-badge">⚡</span>
// )}
// {option.name}
// </span>
// </label>
// );
// })
// ) : (
// <div className="permission-no-options">
// No options available
// </div>
// )}
// </div>
// <div className="permission-actions">
// <button
// className="permission-confirm-button"
// disabled={!selected || isResponding}
// onClick={handleConfirm}
// >
// {isResponding ? 'Processing...' : 'Confirm'}
// </button>
// </div>
// </div>
// )}
// {/* Success message */}
// {hasResponded && (
// <div className="permission-success">
// <span className="permission-success-icon">✓</span>
// <span className="permission-success-text">
// Response sent successfully
// </span>
// </div>
// )}
// </div>
// </div>
// );
// };

View File

@@ -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>
);
};

View File

@@ -67,7 +67,7 @@ export const UserMessage: React.FC<UserMessageProps> = ({
<div
role="button"
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 hover:opacity-100"
className="mr inline-flex items-center py-0 pl-1 pr-2 ml-1 gap-1 rounded-sm cursor-pointer relative opacity-50"
onClick={() => fileContext && onFileClick?.(fileContext.filePath)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {

View File

@@ -10,4 +10,3 @@ export { ThinkingMessage } from './ThinkingMessage.js';
export { StreamingMessage } from './StreamingMessage.js';
export { WaitingMessage } from './Waiting/WaitingMessage.js';
export { InterruptedMessage } from './Waiting/InterruptedMessage.js';
export { PlanDisplay } from '../PlanDisplay.js';

View File

@@ -16,6 +16,7 @@ import {
import { useVSCode } from '../../../hooks/useVSCode.js';
import { FileLink } from '../../ui/FileLink.js';
import { handleOpenDiff } from '../../../utils/diffUtils.js';
import { DiffDisplay } from '../shared/DiffDisplay.js';
/**
* Calculate diff summary (added/removed lines)
@@ -85,6 +86,64 @@ export const EditToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [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
if (errors.length > 0) {
const path = diffs[0]?.path || locations?.[0]?.path || '';
@@ -99,7 +158,7 @@ export const EditToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
<FileLink
path={path}
showFullPath={false}
className="text-xs font-mono text-[var(--app-secondary-foreground)] hover:underline"
className="text-xs font-mono hover:underline"
/>
) : undefined
}
@@ -118,21 +177,19 @@ export const EditToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
return (
<div
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="flex items-center justify-between min-w-0">
<div className="flex items-center gap-2 min-w-0">
<div className="flex items-baseline gap-2 min-w-0">
{/* 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
</span>
{path && (
<FileLink
path={path}
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>

View File

@@ -101,7 +101,7 @@ export const ReadToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
return (
<ToolCallContainer
label={'Read'}
className="read-tool-call-success"
className={`read-tool-call-${containerStatus}`}
status={containerStatus}
toolCallId={toolCallId}
labelSuffix={
@@ -125,7 +125,7 @@ export const ReadToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
return (
<ToolCallContainer
label={'Read'}
className="read-tool-call-success"
className={`read-tool-call-${containerStatus}`}
status={containerStatus}
toolCallId={toolCallId}
labelSuffix={

View File

@@ -11,14 +11,10 @@ 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';
import type { PlanEntry } from '../../../../agents/qwenTypes.js';
type EntryStatus = 'pending' | 'in_progress' | 'completed';
interface PlanEntry {
content: string;
status: EntryStatus;
}
const mapToolStatusToBullet = (
status: import('../shared/types.js').ToolCallStatus,
): 'success' | 'error' | 'warning' | 'loading' | 'default' => {

View File

@@ -71,9 +71,9 @@ export const CheckboxDisplay: React.FC<CheckboxDisplayProps> = ({
aria-hidden
className={[
'absolute inline-block',
'left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2',
'left-1/2 top-10px -translate-x-1/2 -translate-y-1/2',
// Use a literal star; no icon font needed
'text-[11px] leading-none text-[#e1c08d] select-none',
'text-[16px] leading-none text-[#e1c08d] select-none',
].join(' ')}
>
*

View File

@@ -64,7 +64,7 @@ export class AuthMessageHandler extends BaseMessageHandler {
vscode.window.showInformationMessage(
'Please wait while we connect to Qwen Code...',
);
await vscode.commands.executeCommand('qwenCode.login');
await vscode.commands.executeCommand('qwen-code.login');
}
} catch (error) {
console.error('[AuthMessageHandler] Login failed:', error);

View File

@@ -280,7 +280,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
vscode.window.showInformationMessage(
'Please wait while we connect to Qwen Code...',
);
await vscode.commands.executeCommand('qwenCode.login');
await vscode.commands.executeCommand('qwen-code.login');
}
}
return;
@@ -306,7 +306,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
vscode.window.showInformationMessage(
'Please wait while we connect to Qwen Code...',
);
await vscode.commands.executeCommand('qwenCode.login');
await vscode.commands.executeCommand('qwen-code.login');
}
}
return;
@@ -420,7 +420,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwenCode.login');
await vscode.commands.executeCommand('qwen-code.login');
}
}
@@ -456,7 +456,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwenCode.login');
await vscode.commands.executeCommand('qwen-code.login');
}
} else {
return;
@@ -514,7 +514,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwenCode.login');
await vscode.commands.executeCommand('qwen-code.login');
}
}
@@ -551,7 +551,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwenCode.login');
await vscode.commands.executeCommand('qwen-code.login');
}
} else if (selection === 'View Offline') {
// Show messages from local cache only
@@ -653,7 +653,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwenCode.login');
await vscode.commands.executeCommand('qwen-code.login');
}
}
@@ -709,7 +709,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwenCode.login');
await vscode.commands.executeCommand('qwen-code.login');
}
}
@@ -757,7 +757,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwenCode.login');
await vscode.commands.executeCommand('qwen-code.login');
}
}
@@ -807,7 +807,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwenCode.login');
await vscode.commands.executeCommand('qwen-code.login');
}
}
@@ -870,7 +870,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwenCode.login');
await vscode.commands.executeCommand('qwen-code.login');
}
}
@@ -917,7 +917,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwenCode.login');
await vscode.commands.executeCommand('qwen-code.login');
}
}
@@ -983,7 +983,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwenCode.login');
await vscode.commands.executeCommand('qwen-code.login');
}
} else if (selection === 'View Offline') {
const messages =
@@ -1034,7 +1034,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwenCode.login');
await vscode.commands.executeCommand('qwen-code.login');
}
}
@@ -1084,7 +1084,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
if (this.loginHandler) {
await this.loginHandler();
} else {
await vscode.commands.executeCommand('qwenCode.login');
await vscode.commands.executeCommand('qwen-code.login');
}
}

View File

@@ -10,9 +10,9 @@ import type { Conversation } from '../../storage/conversationStore.js';
import type {
PermissionOption,
ToolCall as PermissionToolCall,
} from '../components/PermissionRequest.js';
import type { PlanEntry } from '../components/PlanDisplay.js';
} from '../components/PermissionDrawer/PermissionRequest.js';
import type { ToolCallUpdate } from '../types/toolCall.js';
import type { PlanEntry } from '../../agents/qwenTypes.js';
interface UseWebViewMessagesProps {
// Session management

View File

@@ -9,17 +9,17 @@
export default {
content: [
// Progressive adoption strategy: Only scan newly created Tailwind components
// './src/webview/App.tsx',
'./src/webview/App.tsx',
'./src/webview/**/*.{js,jsx,ts,tsx}',
// './src/webview/components/messages/**/*.{js,jsx,ts,tsx}',
// './src/webview/components/toolcalls/**/*.{js,jsx,ts,tsx}',
// './src/webview/components/InProgressToolCall.tsx',
// './src/webview/components/MessageContent.tsx',
// './src/webview/components/InputForm.tsx',
// './src/webview/components/PermissionDrawer.tsx',
// './src/webview/components/PlanDisplay.tsx',
// './src/webview/components/session/SessionSelector.tsx',
// './src/webview/components/messages/UserMessage.tsx',
'./src/webview/components/messages/**/*.{js,jsx,ts,tsx}',
'./src/webview/components/toolcalls/**/*.{js,jsx,ts,tsx}',
'./src/webview/components/InProgressToolCall.tsx',
'./src/webview/components/MessageContent.tsx',
'./src/webview/components/InputForm.tsx',
'./src/webview/components/PermissionDrawer.tsx',
'./src/webview/components/PlanDisplay.tsx',
'./src/webview/components/session/SessionSelector.tsx',
'./src/webview/components/messages/UserMessage.tsx',
],
theme: {
extend: {