mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
feat(vscode-ide-companion): add cancel streaming functionality
- Add handleCancel callback to App component - Implement cancelStreaming message posting to VS Code - Add onCancel prop to InputForm component - Replace send button with stop button during streaming
This commit is contained in:
@@ -27,7 +27,7 @@ import type { ToolCallData } from './components/ToolCall.js';
|
||||
import { PermissionDrawer } from './components/PermissionDrawer.js';
|
||||
import { ToolCall } from './components/ToolCall.js';
|
||||
import { hasToolCallOutput } from './components/toolcalls/shared/utils.js';
|
||||
import { InProgressToolCall } from './components/InProgressToolCall.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';
|
||||
@@ -172,6 +172,16 @@ export const App: React.FC = () => {
|
||||
isStreaming: messageHandling.isStreaming,
|
||||
});
|
||||
|
||||
// Handle cancel streaming
|
||||
const handleCancel = useCallback(() => {
|
||||
if (messageHandling.isStreaming) {
|
||||
vscode.postMessage({
|
||||
type: 'cancelStreaming',
|
||||
data: {},
|
||||
});
|
||||
}
|
||||
}, [messageHandling.isStreaming, vscode]);
|
||||
|
||||
// Message handling
|
||||
useWebViewMessages({
|
||||
sessionManagement,
|
||||
@@ -531,49 +541,47 @@ export const App: React.FC = () => {
|
||||
|
||||
if (msg.role === 'thinking') {
|
||||
return (
|
||||
<div key={`message-${index}`} className="message-item">
|
||||
<ThinkingMessage
|
||||
content={msg.content || ''}
|
||||
timestamp={msg.timestamp || 0}
|
||||
onFileClick={handleFileClick}
|
||||
/>
|
||||
</div>
|
||||
<ThinkingMessage
|
||||
key={`message-${index}`}
|
||||
content={msg.content || ''}
|
||||
timestamp={msg.timestamp || 0}
|
||||
onFileClick={handleFileClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (msg.role === 'user') {
|
||||
return (
|
||||
<div key={`message-${index}`} className="message-item">
|
||||
<UserMessage
|
||||
content={msg.content || ''}
|
||||
timestamp={msg.timestamp || 0}
|
||||
onFileClick={handleFileClick}
|
||||
fileContext={msg.fileContext}
|
||||
/>
|
||||
</div>
|
||||
<UserMessage
|
||||
key={`message-${index}`}
|
||||
content={msg.content || ''}
|
||||
timestamp={msg.timestamp || 0}
|
||||
onFileClick={handleFileClick}
|
||||
fileContext={msg.fileContext}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={`message-${index}`} className="message-item">
|
||||
<AssistantMessage
|
||||
content={msg.content || ''}
|
||||
timestamp={msg.timestamp || 0}
|
||||
onFileClick={handleFileClick}
|
||||
/>
|
||||
</div>
|
||||
<AssistantMessage
|
||||
key={`message-${index}`}
|
||||
content={msg.content || ''}
|
||||
timestamp={msg.timestamp || 0}
|
||||
onFileClick={handleFileClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
case 'in-progress-tool-call':
|
||||
return (
|
||||
<InProgressToolCall
|
||||
key={`in-progress-${(item.data as ToolCallData).toolCallId}`}
|
||||
toolCall={item.data as ToolCallData}
|
||||
// onFileClick={handleFileClick}
|
||||
/>
|
||||
);
|
||||
// case 'in-progress-tool-call':
|
||||
// return (
|
||||
// <InProgressToolCall
|
||||
// key={`in-progress-${(item.data as ToolCallData).toolCallId}`}
|
||||
// toolCall={item.data as ToolCallData}
|
||||
// // onFileClick={handleFileClick}
|
||||
// />
|
||||
// );
|
||||
|
||||
case 'in-progress-tool-call':
|
||||
case 'completed-tool-call':
|
||||
return (
|
||||
<ToolCall
|
||||
@@ -626,6 +634,7 @@ export const App: React.FC = () => {
|
||||
onCompositionEnd={() => setIsComposing(false)}
|
||||
onKeyDown={() => {}}
|
||||
onSubmit={handleSubmit.handleSubmit}
|
||||
onCancel={handleCancel}
|
||||
onToggleEditMode={handleToggleEditMode}
|
||||
onToggleThinking={handleToggleThinking}
|
||||
onFocusActiveEditor={fileContext.focusActiveEditor}
|
||||
|
||||
@@ -204,6 +204,17 @@ export class WebViewProvider {
|
||||
// Handle messages from WebView
|
||||
newPanel.webview.onDidReceiveMessage(
|
||||
async (message: { type: string; data?: unknown }) => {
|
||||
// Allow webview to request updating the VS Code tab title
|
||||
if (message.type === 'updatePanelTitle') {
|
||||
const title = String(
|
||||
(message.data as { title?: unknown } | undefined)?.title ?? '',
|
||||
).trim();
|
||||
const panelRef = this.panelManager.getPanel();
|
||||
if (panelRef) {
|
||||
panelRef.title = title || 'Qwen Code';
|
||||
}
|
||||
return;
|
||||
}
|
||||
await this.messageHandler.route(message);
|
||||
},
|
||||
null,
|
||||
@@ -790,9 +801,19 @@ export class WebViewProvider {
|
||||
|
||||
panel.webview.html = WebViewContent.generate(panel, this.extensionUri);
|
||||
|
||||
// Handle messages from WebView
|
||||
// Handle messages from WebView (restored panel)
|
||||
panel.webview.onDidReceiveMessage(
|
||||
async (message: { type: string; data?: unknown }) => {
|
||||
if (message.type === 'updatePanelTitle') {
|
||||
const title = String(
|
||||
(message.data as { title?: unknown } | undefined)?.title ?? '',
|
||||
).trim();
|
||||
const panelRef = this.panelManager.getPanel();
|
||||
if (panelRef) {
|
||||
panelRef.title = title || 'Qwen Code';
|
||||
}
|
||||
return;
|
||||
}
|
||||
await this.messageHandler.route(message);
|
||||
},
|
||||
null,
|
||||
|
||||
@@ -154,41 +154,50 @@ export const InProgressToolCall: React.FC<InProgressToolCallProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative py-2 toolcall-container">
|
||||
<div className="flex items-center gap-2 mb-1 relative before:content-['\25cf'] before:absolute before:left-[-22px] before:top-1/2 before:-translate-y-1/2 before:text-[10px] before:leading-none before:text-[#e1c08d] before:animate-pulse-slow">
|
||||
<span className={`text-xs font-medium ${kindColorClass}`}>
|
||||
{kindLabel}
|
||||
</span>
|
||||
{filePath && (
|
||||
<FileLink
|
||||
path={filePath}
|
||||
line={fileLine ?? undefined}
|
||||
showFullPath={false}
|
||||
className="text-xs"
|
||||
/>
|
||||
)}
|
||||
{!filePath && fileName && (
|
||||
<span className="text-xs text-[var(--app-secondary-foreground)] font-mono">
|
||||
{fileName}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{diffData && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleOpenDiff}
|
||||
className="text-[11px] px-2 py-0.5 border border-[var(--app-input-border)] rounded-small text-[var(--app-primary-foreground)] bg-transparent hover:bg-[var(--app-ghost-button-hover-background)] cursor-pointer"
|
||||
<div className="relative pl-[30px] py-2 select-text toolcall-container in-progress-toolcall">
|
||||
<div className="toolcall-content-wrapper flex flex-col gap-1 min-w-0 max-w-full">
|
||||
<div className="flex items-center gap-2 relative min-w-0 toolcall-header">
|
||||
<span
|
||||
className={`text-[14px] leading-none font-bold ${kindColorClass}`}
|
||||
>
|
||||
Open Diff
|
||||
</button>
|
||||
{kindLabel}
|
||||
</span>
|
||||
{filePath && (
|
||||
<FileLink
|
||||
path={filePath}
|
||||
line={fileLine ?? undefined}
|
||||
showFullPath={false}
|
||||
className="text-[14px]"
|
||||
/>
|
||||
)}
|
||||
{!filePath && fileName && (
|
||||
<span className="text-[14px] leading-none text-[var(--app-secondary-foreground)]">
|
||||
{fileName}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{diffData && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleOpenDiff}
|
||||
className="text-[11px] px-2 py-0.5 border border-[var(--app-input-border)] rounded-small text-[var(--app-primary-foreground)] bg-transparent hover:bg-[var(--app-ghost-button-hover-background)] cursor-pointer"
|
||||
>
|
||||
Open Diff
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{contentText && (
|
||||
<div className="text-[var(--app-secondary-foreground)]">
|
||||
<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="toolcall-content-text flex-shrink-0 w-full">
|
||||
{contentText}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{contentText && (
|
||||
<div className="text-xs text-[var(--app-secondary-foreground)] font-mono mt-1 whitespace-pre-wrap break-words max-h-[200px] overflow-y-auto p-1 bg-[var(--app-input-background)] border border-[var(--app-input-border)] rounded">
|
||||
{contentText}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
SlashCommandIcon,
|
||||
LinkIcon,
|
||||
ArrowUpIcon,
|
||||
StopIcon,
|
||||
} from './icons/index.js';
|
||||
import { CompletionMenu } from './ui/CompletionMenu.js';
|
||||
import type { CompletionItem } from '../types/CompletionTypes.js';
|
||||
@@ -36,6 +37,7 @@ interface InputFormProps {
|
||||
onCompositionEnd: () => void;
|
||||
onKeyDown: (e: React.KeyboardEvent) => void;
|
||||
onSubmit: (e: React.FormEvent) => void;
|
||||
onCancel: () => void;
|
||||
onToggleEditMode: () => void;
|
||||
onToggleThinking: () => void;
|
||||
onFocusActiveEditor: () => void;
|
||||
@@ -91,6 +93,7 @@ export const InputForm: React.FC<InputFormProps> = ({
|
||||
onCompositionEnd,
|
||||
onKeyDown,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
onToggleEditMode,
|
||||
onToggleThinking,
|
||||
onFocusActiveEditor,
|
||||
@@ -198,17 +201,6 @@ export const InputForm: React.FC<InputFormProps> = ({
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Divider */}
|
||||
<div
|
||||
className="w-px h-[26px] mx-0.5 flex-shrink-0"
|
||||
style={{
|
||||
backgroundColor: 'var(--app-transparent-inner-border)',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Spacer */}
|
||||
<div className="flex-1 min-w-0" />
|
||||
|
||||
{/* Thinking button */}
|
||||
<button
|
||||
type="button"
|
||||
@@ -239,14 +231,25 @@ export const InputForm: React.FC<InputFormProps> = ({
|
||||
<LinkIcon />
|
||||
</button>
|
||||
|
||||
{/* Send button */}
|
||||
<button
|
||||
type="submit"
|
||||
className="btn-send-compact [&>svg]:w-5 [&>svg]:h-5"
|
||||
disabled={isStreaming || !inputText.trim()}
|
||||
>
|
||||
<ArrowUpIcon />
|
||||
</button>
|
||||
{/* Send/Stop button */}
|
||||
{isStreaming ? (
|
||||
<button
|
||||
type="button"
|
||||
className="btn-send-compact [&>svg]:w-5 [&>svg]:h-5"
|
||||
onClick={onCancel}
|
||||
title="Stop generation"
|
||||
>
|
||||
<StopIcon />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="submit"
|
||||
className="btn-send-compact [&>svg]:w-5 [&>svg]:h-5"
|
||||
disabled={!inputText.trim()}
|
||||
>
|
||||
<ArrowUpIcon />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -178,7 +178,7 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
|
||||
<span
|
||||
className={`inline-flex items-center justify-center min-w-[20px] h-5 px-1.5 text-xs font-semibold rounded ${
|
||||
isFocused
|
||||
? 'bg-white/20 text-inherit'
|
||||
? 'text-inherit'
|
||||
: 'bg-[var(--app-list-hover-background)]'
|
||||
}`}
|
||||
>
|
||||
@@ -211,7 +211,7 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
|
||||
<span
|
||||
className={`inline-flex items-center justify-center min-w-[20px] h-5 px-1.5 text-xs font-semibold rounded ${
|
||||
isFocused
|
||||
? 'bg-white/20 text-inherit'
|
||||
? 'text-inherit'
|
||||
: 'bg-[var(--app-list-hover-background)]'
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/* 容器 */
|
||||
.plan-display {
|
||||
@apply bg-transparent border-0 py-2 px-4 my-2;
|
||||
}
|
||||
|
||||
/* 标题区 */
|
||||
.plan-header {
|
||||
@apply flex items-center gap-1.5 mb-2;
|
||||
}
|
||||
|
||||
.plan-progress-icons {
|
||||
@apply flex items-center gap-[2px];
|
||||
}
|
||||
|
||||
.plan-progress-icon {
|
||||
@apply shrink-0 text-[var(--app-secondary-foreground)] opacity-60;
|
||||
}
|
||||
|
||||
.plan-title {
|
||||
@apply text-xs font-normal text-[var(--app-secondary-foreground)] opacity-80;
|
||||
}
|
||||
|
||||
/* 列表 */
|
||||
.plan-entries {
|
||||
@apply flex flex-col gap-px;
|
||||
}
|
||||
|
||||
.plan-entry {
|
||||
@apply flex items-center gap-2 py-[3px] min-h-[20px];
|
||||
}
|
||||
|
||||
/* 图标容器(保留类名以兼容旧 DOM) */
|
||||
.plan-entry-icon {
|
||||
@apply shrink-0 flex items-center justify-center w-[14px] h-[14px];
|
||||
}
|
||||
|
||||
.plan-icon {
|
||||
@apply block w-[14px] h-[14px];
|
||||
}
|
||||
|
||||
/* 不同状态颜色(保留类名) */
|
||||
.plan-icon.pending {
|
||||
@apply text-[var(--app-secondary-foreground)] opacity-30;
|
||||
}
|
||||
|
||||
.plan-icon.in-progress {
|
||||
@apply text-[var(--app-secondary-foreground)] opacity-70;
|
||||
}
|
||||
|
||||
.plan-icon.completed {
|
||||
@apply text-[#4caf50] opacity-80; /* 绿色勾号 */
|
||||
}
|
||||
|
||||
/* 内容 */
|
||||
.plan-entry-content {
|
||||
@apply flex-1 flex items-center;
|
||||
}
|
||||
|
||||
.plan-entry-text {
|
||||
@apply flex-1 text-xs leading-[1.5] text-[var(--app-primary-foreground)] opacity-80;
|
||||
}
|
||||
|
||||
/* 状态化文本(保留选择器,兼容旧结构) */
|
||||
.plan-entry.completed .plan-entry-text {
|
||||
@apply opacity-50 line-through;
|
||||
}
|
||||
|
||||
.plan-entry.in_progress .plan-entry-text {
|
||||
@apply font-normal opacity-90;
|
||||
}
|
||||
|
||||
/* 保留 fadeIn 动画,供 App.tsx 使用 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import './PlanDisplay.css';
|
||||
import { CheckboxDisplay } from './ui/CheckboxDisplay.js';
|
||||
|
||||
export interface PlanEntry {
|
||||
@@ -35,16 +34,17 @@ export const PlanDisplay: React.FC<PlanDisplayProps> = ({ entries }) => {
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
'plan-display',
|
||||
// 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="plan-header w-full">
|
||||
<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>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Stop icon for canceling operations
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import type { IconProps } from './types.js';
|
||||
|
||||
/**
|
||||
* Stop/square icon (16x16)
|
||||
* Used for stop/cancel operations
|
||||
*/
|
||||
export const StopIcon: React.FC<IconProps> = ({
|
||||
size = 16,
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
width={size}
|
||||
height={size}
|
||||
className={className}
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<rect x="4" y="4" width="8" height="8" rx="1" />
|
||||
</svg>
|
||||
);
|
||||
@@ -51,3 +51,6 @@ export { PlayIcon, SwitchIcon } from './ActionIcons.js';
|
||||
|
||||
// Special icons
|
||||
export { ThinkingIcon, TerminalIcon } from './SpecialIcons.js';
|
||||
|
||||
// Stop icon
|
||||
export { StopIcon } from './StopIcon.js';
|
||||
|
||||
@@ -56,7 +56,7 @@ export const AssistantMessage: React.FC<AssistantMessageProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`assistant-message-container simple-timeline-container ${getStatusClass()}`}
|
||||
className={`qwen-message message-item assistant-message-container simple-timeline-container ${getStatusClass()}`}
|
||||
style={{
|
||||
width: '100%',
|
||||
alignItems: 'flex-start',
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* @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),结束元素(bottom设为calc(100% - 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 {
|
||||
top: 15px;
|
||||
}
|
||||
|
||||
/* 处理每组AI消息序列的结尾 */
|
||||
.qwen-message.message-item:not(.user-message-container):last-child::after,
|
||||
.qwen-message.message-item:not(.user-message-container):has(+ .user-message-container)::after {
|
||||
bottom: calc(100% - 15px);
|
||||
top: 0;
|
||||
}
|
||||
@@ -18,7 +18,7 @@ export const ThinkingMessage: React.FC<ThinkingMessageProps> = ({
|
||||
timestamp: _timestamp,
|
||||
onFileClick,
|
||||
}) => (
|
||||
<div className="flex gap-0 items-start text-left py-2 flex-col relative opacity-80 italic pl-6 animate-[fadeIn_0.2s_ease-in]">
|
||||
<div className="qwen-message thinking-message flex gap-0 items-start text-left py-2 flex-col relative opacity-80 italic pl-6 animate-[fadeIn_0.2s_ease-in]">
|
||||
<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={{
|
||||
|
||||
@@ -44,7 +44,10 @@ export const UserMessage: React.FC<UserMessageProps> = ({
|
||||
const fileContextDisplay = getFileContextDisplay();
|
||||
|
||||
return (
|
||||
<div className="flex gap-0 items-start text-left flex-col relative animate-[fadeIn_0.2s_ease-in]">
|
||||
<div
|
||||
className="qwen-message message-item user-message-container flex gap-0 items-start text-left flex-col relative animate-[fadeIn_0.2s_ease-in]"
|
||||
style={{ position: 'relative' }}
|
||||
>
|
||||
<div
|
||||
className="inline-block relative whitespace-pre-wrap rounded-md max-w-full overflow-x-auto overflow-y-hidden select-text leading-[1.5]"
|
||||
style={{
|
||||
|
||||
@@ -10,7 +10,9 @@ import type React from 'react';
|
||||
import type { BaseToolCallProps } from '../shared/types.js';
|
||||
import { ToolCallContainer } from '../shared/LayoutComponents.js';
|
||||
import { safeTitle, groupContent } from '../shared/utils.js';
|
||||
import './ExecuteToolCall.css';
|
||||
import { useVSCode } from '../../../hooks/useVSCode.js';
|
||||
import { createAndOpenTempFile } from '../../../utils/tempFileManager.js';
|
||||
import './Bash.css';
|
||||
|
||||
/**
|
||||
* Specialized component for Execute/Bash tool calls
|
||||
@@ -19,6 +21,7 @@ import './ExecuteToolCall.css';
|
||||
export const ExecuteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
const { title, content, rawInput, toolCallId } = toolCall;
|
||||
const commandText = safeTitle(title);
|
||||
const vscode = useVSCode();
|
||||
|
||||
// Group content by type
|
||||
const { textOutputs, errors } = groupContent(content);
|
||||
@@ -32,6 +35,24 @@ export const ExecuteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
inputCommand = rawInput;
|
||||
}
|
||||
|
||||
// Handle click on IN section
|
||||
const handleInClick = () => {
|
||||
createAndOpenTempFile(
|
||||
vscode.postMessage,
|
||||
inputCommand,
|
||||
'bash-input',
|
||||
'.sh',
|
||||
);
|
||||
};
|
||||
|
||||
// Handle click on OUT section
|
||||
const handleOutClick = () => {
|
||||
if (textOutputs.length > 0) {
|
||||
const output = textOutputs.join('\n');
|
||||
createAndOpenTempFile(vscode.postMessage, output, 'bash-output', '.txt');
|
||||
}
|
||||
};
|
||||
|
||||
// Error case
|
||||
if (errors.length > 0) {
|
||||
return (
|
||||
@@ -45,7 +66,11 @@ export const ExecuteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
<div className="bash-toolcall-card">
|
||||
<div className="bash-toolcall-content">
|
||||
{/* IN row */}
|
||||
<div className="bash-toolcall-row">
|
||||
<div
|
||||
className="bash-toolcall-row"
|
||||
onClick={handleInClick}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<div className="bash-toolcall-label">IN</div>
|
||||
<div className="bash-toolcall-row-content">
|
||||
<pre className="bash-toolcall-pre">{inputCommand}</pre>
|
||||
@@ -84,7 +109,11 @@ export const ExecuteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
<div className="bash-toolcall-card">
|
||||
<div className="bash-toolcall-content">
|
||||
{/* IN row */}
|
||||
<div className="bash-toolcall-row">
|
||||
<div
|
||||
className="bash-toolcall-row"
|
||||
onClick={handleInClick}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<div className="bash-toolcall-label">IN</div>
|
||||
<div className="bash-toolcall-row-content">
|
||||
<pre className="bash-toolcall-pre">{inputCommand}</pre>
|
||||
@@ -92,7 +121,11 @@ export const ExecuteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
</div>
|
||||
|
||||
{/* OUT row */}
|
||||
<div className="bash-toolcall-row">
|
||||
<div
|
||||
className="bash-toolcall-row"
|
||||
onClick={handleOutClick}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<div className="bash-toolcall-label">OUT</div>
|
||||
<div className="bash-toolcall-row-content">
|
||||
<div className="bash-toolcall-output-subtle">
|
||||
@@ -109,7 +142,11 @@ export const ExecuteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
// Success without output: show command with branch connector
|
||||
return (
|
||||
<ToolCallContainer label="Bash" status="success" 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">
|
||||
<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"
|
||||
onClick={handleInClick}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<span className="flex-shrink-0 relative top-[-0.1em]">⎿</span>
|
||||
<span className="flex-shrink-0 w-full">{commandText}</span>
|
||||
</div>
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
*/
|
||||
|
||||
import { useEffect, useCallback } from 'react';
|
||||
import type { BaseToolCallProps } from './shared/types.js';
|
||||
import { ToolCallContainer } from './shared/LayoutComponents.js';
|
||||
import { DiffDisplay } from './shared/DiffDisplay.js';
|
||||
import { groupContent } from './shared/utils.js';
|
||||
import { useVSCode } from '../../hooks/useVSCode.js';
|
||||
import { FileLink } from '../ui/FileLink.js';
|
||||
import type { BaseToolCallProps } from '../shared/types.js';
|
||||
import { ToolCallContainer } from '../shared/LayoutComponents.js';
|
||||
import { DiffDisplay } from '../shared/DiffDisplay.js';
|
||||
import { groupContent } from '../shared/utils.js';
|
||||
import { useVSCode } from '../../../hooks/useVSCode.js';
|
||||
import { FileLink } from '../../ui/FileLink.js';
|
||||
|
||||
/**
|
||||
* Calculate diff summary (added/removed lines)
|
||||
@@ -76,11 +76,13 @@ export const EditToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
firstDiff.oldText !== undefined &&
|
||||
firstDiff.newText !== undefined
|
||||
) {
|
||||
// TODO: 暂时注释
|
||||
// Add a small delay to ensure the component is fully rendered
|
||||
const timer = setTimeout(() => {
|
||||
handleOpenDiff(path, firstDiff.oldText, firstDiff.newText);
|
||||
}, 100);
|
||||
return () => clearTimeout(timer);
|
||||
// const timer = setTimeout(() => {
|
||||
// handleOpenDiff(path, firstDiff.oldText, firstDiff.newText);
|
||||
// }, 100);
|
||||
let timer;
|
||||
return () => timer && clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
}, [diffs, locations, handleOpenDiff]);
|
||||
@@ -120,48 +122,47 @@ export const EditToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
handleOpenDiff(path, firstDiff.oldText, firstDiff.newText);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className="relative py-2 select-text cursor-pointer hover:bg-[var(--app-input-background)] toolcall-container toolcall-status-success"
|
||||
onClick={openFirstDiff}
|
||||
title="Open diff in VS Code"
|
||||
>
|
||||
{/* Keep content within overall width: pl-[30px] provides the bullet indent; */}
|
||||
{/* 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 pl-[30px] 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">
|
||||
{/* 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)]">
|
||||
Edit
|
||||
</span>
|
||||
{path && (
|
||||
<FileLink
|
||||
path={path}
|
||||
showFullPath={false}
|
||||
className="text-xs font-mono text-[var(--app-secondary-foreground)] hover:underline"
|
||||
/>
|
||||
)}
|
||||
{/* {toolCallId && (
|
||||
<div
|
||||
className="qwen-message message-item relative py-2 select-text cursor-pointer hover:bg-[var(--app-input-background)] toolcall-container toolcall-status-success"
|
||||
onClick={openFirstDiff}
|
||||
title="Open diff in VS Code"
|
||||
>
|
||||
{/* Keep content within overall width: pl-[30px] provides the bullet indent; */}
|
||||
{/* 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 pl-[30px]">
|
||||
<div className="flex items-center justify-between min-w-0">
|
||||
<div className="flex items-center 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)]">
|
||||
Edit
|
||||
</span>
|
||||
{path && (
|
||||
<FileLink
|
||||
path={path}
|
||||
showFullPath={false}
|
||||
className="text-xs font-mono text-[var(--app-secondary-foreground)] hover:underline"
|
||||
/>
|
||||
)}
|
||||
{/* {toolCallId && (
|
||||
<span className="text-[10px] opacity-30">
|
||||
[{toolCallId.slice(-8)}]
|
||||
</span>
|
||||
)} */}
|
||||
</div>
|
||||
<span className="text-xs opacity-60 ml-2">open</span>
|
||||
</div>
|
||||
<div className="inline-flex text-[var(--app-secondary-foreground)] text-[0.85em] opacity-70 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">{summary}</span>
|
||||
</div>
|
||||
<span className="text-xs opacity-60 ml-2">open</span>
|
||||
</div>
|
||||
<div className="inline-flex text-[var(--app-secondary-foreground)] text-[0.85em] opacity-70 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">{summary}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content area aligned with bullet indent. Do NOT exceed container width. */}
|
||||
{/* For any custom blocks here, keep: min-w-0 max-w-full and avoid extra horizontal padding/margins. */}
|
||||
<div className="pl-[30px] mt-1 min-w-0 max-w-full overflow-hidden">
|
||||
{diffs.map(
|
||||
(
|
||||
item: import('./shared/types.js').ToolCallContent,
|
||||
item: import('../shared/types.js').ToolCallContent,
|
||||
idx: number,
|
||||
) => (
|
||||
<DiffDisplay
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Execute tool call styles - Enhanced styling with semantic class names
|
||||
*/
|
||||
|
||||
/* Root container for execute tool call output */
|
||||
.execute-toolcall-card {
|
||||
border: 0.5px solid var(--app-input-border);
|
||||
border-radius: 5px;
|
||||
background: var(--app-tool-background);
|
||||
margin: 8px 0;
|
||||
max-width: 100%;
|
||||
font-size: 1em;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
/* Content wrapper inside the card */
|
||||
.execute-toolcall-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
/* Individual input/output row */
|
||||
.execute-toolcall-row {
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr;
|
||||
border-top: 0.5px solid var(--app-input-border);
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
/* First row has no top border */
|
||||
.execute-toolcall-row:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
/* Row label (IN/OUT/ERROR) */
|
||||
.execute-toolcall-label {
|
||||
grid-column: 1;
|
||||
color: var(--app-secondary-foreground);
|
||||
text-align: left;
|
||||
opacity: 50%;
|
||||
padding: 4px 8px 4px 4px;
|
||||
font-family: var(--app-monospace-font-family);
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
/* Row content area */
|
||||
.execute-toolcall-row-content {
|
||||
grid-column: 2;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
margin: 0;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
/* Truncated content styling */
|
||||
.execute-toolcall-row-content:not(.execute-toolcall-full) {
|
||||
max-height: 60px;
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
var(--app-primary-background) 40px,
|
||||
transparent 60px
|
||||
);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Preformatted content */
|
||||
.execute-toolcall-pre {
|
||||
margin-block: 0;
|
||||
overflow: hidden;
|
||||
font-family: var(--app-monospace-font-family);
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
/* Code content */
|
||||
.execute-toolcall-code {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: var(--app-monospace-font-family);
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
/* Output content with subtle styling */
|
||||
.execute-toolcall-output-subtle {
|
||||
background-color: var(--app-code-background);
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Error content styling */
|
||||
.execute-toolcall-error-content {
|
||||
color: #c74e39;
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Execute tool call component - specialized for command execution operations
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import type { BaseToolCallProps } from '../shared/types.js';
|
||||
import { ToolCallContainer } from '../shared/LayoutComponents.js';
|
||||
import { safeTitle, groupContent } from '../shared/utils.js';
|
||||
import './Execute.css';
|
||||
|
||||
/**
|
||||
* Specialized component for Execute tool calls
|
||||
* Shows: Execute bullet + description + IN/OUT card
|
||||
*/
|
||||
export const ExecuteToolCall: 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}>
|
||||
{/* 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 card - semantic DOM + Tailwind styles */}
|
||||
<div className="execute-toolcall-card">
|
||||
<div className="execute-toolcall-content">
|
||||
{/* IN row */}
|
||||
<div className="execute-toolcall-row">
|
||||
<div className="execute-toolcall-label">IN</div>
|
||||
<div className="execute-toolcall-row-content">
|
||||
<pre className="execute-toolcall-pre">{inputCommand}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ERROR row */}
|
||||
<div className="execute-toolcall-row">
|
||||
<div className="execute-toolcall-label">Error</div>
|
||||
<div className="execute-toolcall-row-content">
|
||||
<pre className="execute-toolcall-pre execute-toolcall-error-content">
|
||||
{errors.join('\n')}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ToolCallContainer>
|
||||
);
|
||||
}
|
||||
|
||||
// Success with output
|
||||
if (textOutputs.length > 0) {
|
||||
const output = textOutputs.join('\n');
|
||||
const truncatedOutput =
|
||||
output.length > 500 ? output.substring(0, 500) + '...' : output;
|
||||
|
||||
return (
|
||||
<ToolCallContainer
|
||||
label="Execute"
|
||||
status="success"
|
||||
toolCallId={toolCallId}
|
||||
>
|
||||
{/* 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>
|
||||
{/* Output card - semantic DOM + Tailwind styles */}
|
||||
<div className="execute-toolcall-card">
|
||||
<div className="execute-toolcall-content">
|
||||
{/* IN row */}
|
||||
<div className="execute-toolcall-row">
|
||||
<div className="execute-toolcall-label">IN</div>
|
||||
<div className="execute-toolcall-row-content">
|
||||
<pre className="execute-toolcall-pre">{inputCommand}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* OUT row */}
|
||||
<div className="execute-toolcall-row">
|
||||
<div className="execute-toolcall-label">OUT</div>
|
||||
<div className="execute-toolcall-row-content">
|
||||
<div className="execute-toolcall-output-subtle">
|
||||
<pre className="execute-toolcall-pre">{truncatedOutput}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ToolCallContainer>
|
||||
);
|
||||
}
|
||||
|
||||
// Success without output: show command with branch connector
|
||||
return (
|
||||
<ToolCallContainer label="Execute" status="success" 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>
|
||||
</ToolCallContainer>
|
||||
);
|
||||
};
|
||||
@@ -12,8 +12,9 @@ import { shouldShowToolCall } from './shared/utils.js';
|
||||
import { GenericToolCall } from './GenericToolCall.js';
|
||||
import { ReadToolCall } from './ReadToolCall.js';
|
||||
import { WriteToolCall } from './WriteToolCall.js';
|
||||
import { EditToolCall } from './EditToolCall.js';
|
||||
import { ExecuteToolCall } from './Bash/Bash.js';
|
||||
import { EditToolCall } from './Edit/EditToolCall.js';
|
||||
import { ExecuteToolCall as BashExecuteToolCall } from './Bash/Bash.js';
|
||||
import { ExecuteToolCall } from './Execute/Execute.js';
|
||||
import { SearchToolCall } from './SearchToolCall.js';
|
||||
import { ThinkToolCall } from './ThinkToolCall.js';
|
||||
import { TodoWriteToolCall } from './TodoWriteToolCall.js';
|
||||
@@ -38,9 +39,11 @@ export const getToolCallComponent = (
|
||||
return EditToolCall;
|
||||
|
||||
case 'execute':
|
||||
return ExecuteToolCall;
|
||||
|
||||
case 'bash':
|
||||
case 'command':
|
||||
return ExecuteToolCall;
|
||||
return BashExecuteToolCall;
|
||||
|
||||
case 'search':
|
||||
case 'grep':
|
||||
|
||||
@@ -153,8 +153,53 @@
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Flex container with margin bottom */
|
||||
/* ToolCall header with loading indicator */
|
||||
.toolcall-header {
|
||||
/* TODO: 应该不需要? 待删除 */
|
||||
/* margin-bottom: 12px; */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.toolcall-header::before {
|
||||
content: '\25cf';
|
||||
position: absolute;
|
||||
left: -22px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
z-index: 1;
|
||||
color: #e1c08d;
|
||||
animation: toolcallHeaderPulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Loading animation for toolcall header */
|
||||
@keyframes toolcallHeaderPulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
/* In-progress toolcall specific styles */
|
||||
.in-progress-toolcall .toolcall-content-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.in-progress-toolcall .toolcall-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2;
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.in-progress-toolcall .toolcall-content-text {
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -43,11 +43,11 @@ export const ToolCallContainer: React.FC<ToolCallContainerProps> = ({
|
||||
labelSuffix,
|
||||
}) => (
|
||||
<div
|
||||
className={`relative pl-[30px] py-2 select-text toolcall-container toolcall-status-${status}`}
|
||||
className={`qwen-message relative pl-[30px] py-2 select-text toolcall-container toolcall-status-${status}`}
|
||||
>
|
||||
{/* Timeline connector line using ::after pseudo-element */}
|
||||
<div className="toolcall-content-wrapper flex flex-col gap-1 min-w-0 max-w-full">
|
||||
<div className="flex items-center gap-2 relative min-w-0 toolcall-header">
|
||||
<div className="flex items-center gap-2 relative min-w-0">
|
||||
<span className="text-[14px] leading-none font-bold text-[var(--app-primary-foreground)]">
|
||||
{label}
|
||||
</span>
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Simplified timeline styles for testing
|
||||
* Simplified timeline styles for tool calls and messages
|
||||
* Merged version of both SimpleTimeline.css files
|
||||
*/
|
||||
|
||||
/* Basic timeline container */
|
||||
.simple-toolcall-container,
|
||||
.simple-timeline-container {
|
||||
position: relative;
|
||||
padding-left: 30px;
|
||||
@@ -15,6 +17,7 @@
|
||||
}
|
||||
|
||||
/* Timeline connector - simple version */
|
||||
.simple-toolcall-container::after,
|
||||
.simple-timeline-container::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
@@ -26,11 +29,13 @@
|
||||
}
|
||||
|
||||
/* First item connector starts lower */
|
||||
.simple-toolcall-container:first-child::after,
|
||||
.simple-timeline-container:first-child::after {
|
||||
top: 24px;
|
||||
}
|
||||
|
||||
/* Last item connector ends higher */
|
||||
.simple-toolcall-container:last-child::after,
|
||||
.simple-timeline-container:last-child::after {
|
||||
height: calc(100% - 24px);
|
||||
top: 0;
|
||||
@@ -38,6 +43,7 @@
|
||||
}
|
||||
|
||||
/* Bullet point */
|
||||
.simple-toolcall-container::before,
|
||||
.simple-timeline-container::before {
|
||||
content: '\25cf';
|
||||
position: absolute;
|
||||
@@ -46,4 +52,4 @@
|
||||
font-size: 10px;
|
||||
color: var(--app-secondary-foreground);
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Simplified timeline styles for tool calls
|
||||
*/
|
||||
|
||||
/* Basic timeline container */
|
||||
.simple-toolcall-container {
|
||||
position: relative;
|
||||
padding-left: 30px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
/* Timeline connector - simple version */
|
||||
.simple-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 lower */
|
||||
.simple-toolcall-container:first-child::after {
|
||||
top: 24px;
|
||||
}
|
||||
|
||||
/* Last item connector ends higher */
|
||||
.simple-toolcall-container:last-child::after {
|
||||
height: calc(100% - 24px);
|
||||
top: 0;
|
||||
bottom: auto;
|
||||
}
|
||||
|
||||
/* Bullet point */
|
||||
.simple-toolcall-container::before {
|
||||
content: '\25cf';
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
padding-top: 2px;
|
||||
font-size: 10px;
|
||||
color: var(--app-secondary-foreground);
|
||||
z-index: 2;
|
||||
}
|
||||
@@ -5,6 +5,9 @@
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { BaseMessageHandler } from './BaseMessageHandler.js';
|
||||
import { getFileName } from '../utils/webviewUtils.js';
|
||||
|
||||
@@ -20,6 +23,7 @@ export class FileMessageHandler extends BaseMessageHandler {
|
||||
'getWorkspaceFiles',
|
||||
'openFile',
|
||||
'openDiff',
|
||||
'createAndOpenTempFile',
|
||||
].includes(messageType);
|
||||
}
|
||||
|
||||
@@ -47,6 +51,10 @@ export class FileMessageHandler extends BaseMessageHandler {
|
||||
await this.handleOpenDiff(data);
|
||||
break;
|
||||
|
||||
case 'createAndOpenTempFile':
|
||||
await this.handleCreateAndOpenTempFile(data);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(
|
||||
'[FileMessageHandler] Unknown message type:',
|
||||
@@ -347,4 +355,52 @@ export class FileMessageHandler extends BaseMessageHandler {
|
||||
vscode.window.showErrorMessage(`Failed to open diff: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and open temporary file
|
||||
*/
|
||||
private async handleCreateAndOpenTempFile(
|
||||
data: Record<string, unknown> | undefined,
|
||||
): Promise<void> {
|
||||
if (!data) {
|
||||
console.warn(
|
||||
'[FileMessageHandler] No data provided for createAndOpenTempFile',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const content = (data.content as string) || '';
|
||||
const fileName = (data.fileName as string) || 'temp';
|
||||
const fileExtension = (data.fileExtension as string) || '.txt';
|
||||
|
||||
// Create temporary file path
|
||||
const tempDir = os.tmpdir();
|
||||
const tempFileName = `${fileName}-${Date.now()}${fileExtension}`;
|
||||
const tempFilePath = path.join(tempDir, tempFileName);
|
||||
|
||||
// Write content to temporary file
|
||||
await fs.promises.writeFile(tempFilePath, content, 'utf8');
|
||||
|
||||
// Open the temporary file in VS Code
|
||||
const uri = vscode.Uri.file(tempFilePath);
|
||||
await vscode.window.showTextDocument(uri, {
|
||||
preview: false,
|
||||
preserveFocus: false,
|
||||
});
|
||||
|
||||
console.log(
|
||||
'[FileMessageHandler] Created and opened temporary file:',
|
||||
tempFilePath,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'[FileMessageHandler] Failed to create and open temporary file:',
|
||||
error,
|
||||
);
|
||||
vscode.window.showErrorMessage(
|
||||
`Failed to create and open temporary file: ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
||||
'getQwenSessions',
|
||||
'saveSession',
|
||||
'resumeSession',
|
||||
'cancelStreaming',
|
||||
// UI action: open a new chat tab (new WebviewPanel)
|
||||
'openNewChatTab',
|
||||
].includes(messageType);
|
||||
@@ -102,6 +103,11 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'cancelStreaming':
|
||||
// Handle cancel streaming request from webview
|
||||
await this.handleCancelStreaming();
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(
|
||||
'[SessionMessageHandler] Unknown message type:',
|
||||
@@ -910,6 +916,34 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle cancel streaming request
|
||||
*/
|
||||
private async handleCancelStreaming(): Promise<void> {
|
||||
try {
|
||||
console.log('[SessionMessageHandler] Canceling streaming...');
|
||||
|
||||
// Cancel the current streaming operation in the agent manager
|
||||
await this.agentManager.cancelCurrentPrompt();
|
||||
|
||||
// Send streamEnd message to WebView to update UI
|
||||
this.sendToWebView({
|
||||
type: 'streamEnd',
|
||||
data: { timestamp: Date.now(), reason: 'user_cancelled' },
|
||||
});
|
||||
|
||||
console.log('[SessionMessageHandler] Streaming cancelled successfully');
|
||||
} catch (_error) {
|
||||
console.log('[SessionMessageHandler] Streaming cancelled (interrupted)');
|
||||
|
||||
// Always send streamEnd to update UI, regardless of errors
|
||||
this.sendToWebView({
|
||||
type: 'streamEnd',
|
||||
data: { timestamp: Date.now(), reason: 'user_cancelled' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle resume session request
|
||||
*/
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
import { useVSCode } from './useVSCode.js';
|
||||
import type { Conversation } from '../../storage/conversationStore.js';
|
||||
import type {
|
||||
PermissionOption,
|
||||
@@ -108,6 +109,8 @@ export const useWebViewMessages = ({
|
||||
inputFieldRef,
|
||||
setInputText,
|
||||
}: UseWebViewMessagesProps) => {
|
||||
// VS Code API for posting messages back to the extension host
|
||||
const vscode = useVSCode();
|
||||
// Use ref to store callbacks to avoid useEffect dependency issues
|
||||
const handlersRef = useRef({
|
||||
sessionManagement,
|
||||
@@ -469,6 +472,8 @@ export const useWebViewMessages = ({
|
||||
(session.name as string) ||
|
||||
'Past Conversations';
|
||||
handlers.sessionManagement.setCurrentSessionTitle(title);
|
||||
// Update the VS Code webview tab title as well
|
||||
vscode.postMessage({ type: 'updatePanelTitle', data: { title } });
|
||||
}
|
||||
if (message.data.messages) {
|
||||
handlers.messageHandling.setMessages(message.data.messages);
|
||||
@@ -487,6 +492,11 @@ export const useWebViewMessages = ({
|
||||
handlers.sessionManagement.setCurrentSessionTitle(
|
||||
'Past Conversations',
|
||||
);
|
||||
// Reset the VS Code tab title to default label
|
||||
vscode.postMessage({
|
||||
type: 'updatePanelTitle',
|
||||
data: { title: 'Qwen Code' },
|
||||
});
|
||||
lastPlanSnapshotRef.current = null;
|
||||
break;
|
||||
|
||||
@@ -496,6 +506,8 @@ export const useWebViewMessages = ({
|
||||
if (sessionId && title) {
|
||||
handlers.sessionManagement.setCurrentSessionId(sessionId);
|
||||
handlers.sessionManagement.setCurrentSessionTitle(title);
|
||||
// Ask extension host to reflect this title in the tab label
|
||||
vscode.postMessage({ type: 'updatePanelTitle', data: { title } });
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -563,11 +575,23 @@ export const useWebViewMessages = ({
|
||||
break;
|
||||
}
|
||||
|
||||
case 'cancelStreaming':
|
||||
// Handle cancel streaming request from webview
|
||||
handlers.messageHandling.endStreaming();
|
||||
handlers.messageHandling.clearWaitingForResponse();
|
||||
// Add interrupted message
|
||||
handlers.messageHandling.addMessage({
|
||||
role: 'assistant',
|
||||
content: 'Interrupted',
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
[inputFieldRef, setInputText],
|
||||
[inputFieldRef, setInputText, vscode],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
/* Import component styles */
|
||||
@import '../components/toolcalls/shared/DiffDisplay.css';
|
||||
@import '../components/messages/AssistantMessage.css';
|
||||
@import '../components/messages/SimpleTimeline.css';
|
||||
@import '../components/toolcalls/shared/SimpleTimeline.css';
|
||||
@import '../components/toolcalls/shared/MergedSimpleTimeline.css';
|
||||
@import '../components/messages/QwenMessageTimeline.css';
|
||||
|
||||
|
||||
/* ===========================
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Temporary file manager for creating and opening temporary files in webview
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a temporary file with the given content and opens it in VS Code
|
||||
* @param content The content to write to the temporary file
|
||||
* @param fileName Optional file name (without extension)
|
||||
* @param fileExtension Optional file extension (defaults to .txt)
|
||||
*/
|
||||
export async function createAndOpenTempFile(
|
||||
postMessage: (message: {
|
||||
type: string;
|
||||
data: Record<string, unknown>;
|
||||
}) => void,
|
||||
content: string,
|
||||
fileName: string = 'temp',
|
||||
fileExtension: string = '.txt',
|
||||
): Promise<void> {
|
||||
// Send message to VS Code extension to create and open temp file
|
||||
postMessage({
|
||||
type: 'createAndOpenTempFile',
|
||||
data: {
|
||||
content,
|
||||
fileName,
|
||||
fileExtension,
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user