mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-22 09:47:47 +00:00
chore(vscode-ide-companion): refactor directory structure
This commit is contained in:
@@ -24,15 +24,15 @@ import type {
|
||||
ToolCall as PermissionToolCall,
|
||||
} from './components/PermissionDrawer/PermissionRequest.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 { ToolCall } from './components/ToolCall.js';
|
||||
import { hasToolCallOutput } from './components/toolcalls/shared/utils.js';
|
||||
import { EmptyState } from './components/ui/EmptyState.js';
|
||||
import { type CompletionItem } from './types/CompletionTypes.js';
|
||||
import { ToolCall } from './components/messages/toolcalls/ToolCall.js';
|
||||
import { hasToolCallOutput } from './components/messages/toolcalls/shared/utils.js';
|
||||
import { EmptyState } from './components/layout/EmptyState.js';
|
||||
import { type CompletionItem } from '../types/completionItemTypes.js';
|
||||
import { useCompletionTrigger } from './hooks/useCompletionTrigger.js';
|
||||
import { InfoBanner } from './components/ui/InfoBanner.js';
|
||||
import { ChatHeader } from './components/ui/layouts/ChatHeader.js';
|
||||
import { InfoBanner } from './components/layout/InfoBanner.js';
|
||||
import { ChatHeader } from './components/layout/ChatHeader.js';
|
||||
import {
|
||||
UserMessage,
|
||||
AssistantMessage,
|
||||
@@ -40,11 +40,11 @@ import {
|
||||
WaitingMessage,
|
||||
InterruptedMessage,
|
||||
} from './components/messages/index.js';
|
||||
import { InputForm } from './components/InputForm.js';
|
||||
import { SessionSelector } from './components/session/SessionSelector.js';
|
||||
import { InputForm } from './components/layout/InputForm.js';
|
||||
import { SessionSelector } from './components/layout/SessionSelector.js';
|
||||
import { FileIcon, UserIcon } from './components/icons/index.js';
|
||||
import type { EditMode } from './types/toolCall.js';
|
||||
import type { PlanEntry } from '../agents/qwenTypes.js';
|
||||
import type { EditMode } from '../types/qwenTypes.js';
|
||||
import type { PlanEntry } from '../types/qwenTypes.js';
|
||||
|
||||
export const App: React.FC = () => {
|
||||
const vscode = useVSCode();
|
||||
@@ -609,7 +609,7 @@ export const App: React.FC = () => {
|
||||
const isToolCallType = (
|
||||
x: unknown,
|
||||
): x is { type: 'in-progress-tool-call' | 'completed-tool-call' } =>
|
||||
x &&
|
||||
!!x &&
|
||||
typeof x === 'object' &&
|
||||
'type' in (x as Record<string, unknown>) &&
|
||||
((x as { type: string }).type === 'in-progress-tool-call' ||
|
||||
@@ -782,8 +782,6 @@ export const App: React.FC = () => {
|
||||
onClose={() => setPermissionRequest(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Claude-style dropdown is rendered inside InputForm for proper anchoring */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { QwenAgentManager } from '../agents/qwenAgentManager.js';
|
||||
import type { ConversationStore } from '../storage/conversationStore.js';
|
||||
import type { QwenAgentManager } from '../services/qwenAgentManager.js';
|
||||
import type { ConversationStore } from '../services/conversationStore.js';
|
||||
import { MessageRouter } from './handlers/MessageRouter.js';
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,17 +5,17 @@
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { QwenAgentManager } from '../agents/qwenAgentManager.js';
|
||||
import { ConversationStore } from '../storage/conversationStore.js';
|
||||
import type { AcpPermissionRequest } from '../constants/acpTypes.js';
|
||||
import { QwenAgentManager } from '../services/qwenAgentManager.js';
|
||||
import { ConversationStore } from '../services/conversationStore.js';
|
||||
import type { AcpPermissionRequest } from '../types/acpTypes.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 { MessageHandler } from '../webview/MessageHandler.js';
|
||||
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 { authMethod } from '../constants/auth.js';
|
||||
import { runQwenCodeCommand } from '../commands/index.js';
|
||||
|
||||
export class WebViewProvider {
|
||||
|
||||
@@ -29,7 +29,7 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
|
||||
const [focusedIndex, setFocusedIndex] = useState(0);
|
||||
const [customMessage, setCustomMessage] = useState('');
|
||||
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);
|
||||
|
||||
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 ||
|
||||
'cancel';
|
||||
onResponse(rejectOptionId);
|
||||
if (onClose) onClose();
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -196,7 +198,7 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
|
||||
toolCall.kind === 'bash') &&
|
||||
toolCall.title && (
|
||||
<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"
|
||||
style={{
|
||||
fontSize: '.9em',
|
||||
@@ -249,7 +251,9 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
|
||||
setCustomMessage={setCustomMessage}
|
||||
onFocusRow={() => setFocusedIndex(options.length)}
|
||||
onSubmitReject={() => {
|
||||
if (rejectOptionId) onResponse(rejectOptionId);
|
||||
if (rejectOptionId) {
|
||||
onResponse(rejectOptionId);
|
||||
}
|
||||
}}
|
||||
inputRef={customInputRef}
|
||||
/>
|
||||
@@ -264,14 +268,14 @@ export const PermissionDrawer: React.FC<PermissionDrawerProps> = ({
|
||||
};
|
||||
|
||||
/**
|
||||
* CustomMessageInputRow: 复用的自定义输入行组件(无 hooks)
|
||||
* CustomMessageInputRow: Reusable custom input row component (without hooks)
|
||||
*/
|
||||
interface CustomMessageInputRowProps {
|
||||
isFocused: boolean;
|
||||
customMessage: string;
|
||||
setCustomMessage: (val: string) => void;
|
||||
onFocusRow: () => void; // 鼠标移入或输入框 focus 时设置焦点
|
||||
onSubmitReject: () => void; // Enter 提交时触发(选择 reject 选项)
|
||||
onFocusRow: () => void; // Set focus when mouse enters or input box is focused
|
||||
onSubmitReject: () => void; // Triggered when Enter is pressed (selecting reject option)
|
||||
inputRef: React.RefObject<HTMLInputElement>;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,10 +16,6 @@ interface ThinkingIconProps extends IconProps {
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Thinking/brain wave icon (16x16)
|
||||
* Used for thinking mode toggle
|
||||
*/
|
||||
export const ThinkingIcon: React.FC<ThinkingIconProps> = ({
|
||||
size = 16,
|
||||
className,
|
||||
@@ -53,10 +49,6 @@ export const ThinkingIcon: React.FC<ThinkingIconProps> = ({
|
||||
</svg>
|
||||
);
|
||||
|
||||
/**
|
||||
* Terminal/code editor icon (20x20)
|
||||
* Used for terminal preference info banner
|
||||
*/
|
||||
export const TerminalIcon: React.FC<IconProps> = ({
|
||||
size = 20,
|
||||
className,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { ChevronDownIcon, PlusIcon } from '../../icons/index.js';
|
||||
import { ChevronDownIcon, PlusIcon } from '../icons/index.js';
|
||||
|
||||
interface ChatHeaderProps {
|
||||
currentSessionTitle: string;
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import type React from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import type { CompletionItem } from '../../types/CompletionTypes.js';
|
||||
import type { CompletionItem } from '../../../types/completionItemTypes.js';
|
||||
|
||||
interface CompletionMenuProps {
|
||||
items: CompletionItem[];
|
||||
@@ -16,11 +16,10 @@ import {
|
||||
LinkIcon,
|
||||
ArrowUpIcon,
|
||||
StopIcon,
|
||||
} from './icons/index.js';
|
||||
import { CompletionMenu } from './ui/CompletionMenu.js';
|
||||
import type { CompletionItem } from '../types/CompletionTypes.js';
|
||||
|
||||
type EditMode = 'ask' | 'auto' | 'plan' | 'yolo';
|
||||
} from '../icons/index.js';
|
||||
import { CompletionMenu } from '../layout/CompletionMenu.js';
|
||||
import type { CompletionItem } from '../../../types/completionItemTypes.js';
|
||||
import type { EditMode } from '../../../types/qwenTypes.js';
|
||||
|
||||
interface InputFormProps {
|
||||
inputText: string;
|
||||
@@ -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>
|
||||
);
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { MessageContent } from '../../MessageContent.js';
|
||||
import { MessageContent } from '../MessageContent.js';
|
||||
import './AssistantMessage.css';
|
||||
|
||||
interface AssistantMessageProps {
|
||||
|
||||
@@ -135,8 +135,8 @@
|
||||
border: 1px solid var(--app-primary-border-color);
|
||||
border-radius: var(--corner-radius-small, 4px);
|
||||
padding: 0.2em 0.4em;
|
||||
white-space: pre-wrap; /* 支持自动换行 */
|
||||
word-break: break-word; /* 在必要时断词 */
|
||||
white-space: pre-wrap; /* Support automatic line wrapping */
|
||||
word-break: break-word; /* Break words when necessary */
|
||||
}
|
||||
|
||||
.markdown-content pre {
|
||||
@@ -207,8 +207,8 @@
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
white-space: pre-wrap; /* 支持自动换行 */
|
||||
word-break: break-word; /* 在必要时断词 */
|
||||
white-space: pre-wrap; /* Support automatic line wrapping */
|
||||
word-break: break-word; /* Break words when necessary */
|
||||
}
|
||||
|
||||
.markdown-content .file-path-link {
|
||||
@@ -126,7 +126,9 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
|
||||
*/
|
||||
const processFilePaths = (html: string): string => {
|
||||
// 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
|
||||
const FILE_PATH_NO_G = new RegExp(
|
||||
@@ -193,7 +195,9 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -289,7 +293,9 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
|
||||
e: React.MouseEvent<HTMLDivElement, MouseEvent>,
|
||||
) => {
|
||||
const target = e.target as HTMLElement | null;
|
||||
if (!target) return;
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle copy button clicks for fenced code blocks
|
||||
const copyBtn = (target.closest &&
|
||||
@@ -322,10 +328,14 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
|
||||
// Find nearest anchor with our marker class
|
||||
const anchor = (target.closest &&
|
||||
target.closest('a.file-path-link')) as HTMLAnchorElement | null;
|
||||
if (!anchor) return;
|
||||
if (!anchor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filePath = anchor.getAttribute('data-file-path');
|
||||
if (!filePath) return;
|
||||
if (!filePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { MessageContent } from '../MessageContent.js';
|
||||
import { MessageContent } from './MessageContent.js';
|
||||
|
||||
interface ThinkingMessageProps {
|
||||
content: string;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { MessageContent } from '../MessageContent.js';
|
||||
import { MessageContent } from './MessageContent.js';
|
||||
|
||||
interface FileContext {
|
||||
fileName: string;
|
||||
|
||||
@@ -7,6 +7,5 @@
|
||||
export { UserMessage } from './UserMessage.js';
|
||||
export { AssistantMessage } from './Assistant/AssistantMessage.js';
|
||||
export { ThinkingMessage } from './ThinkingMessage.js';
|
||||
export { StreamingMessage } from './StreamingMessage.js';
|
||||
export { WaitingMessage } from './Waiting/WaitingMessage.js';
|
||||
export { InterruptedMessage } from './Waiting/InterruptedMessage.js';
|
||||
|
||||
@@ -10,8 +10,8 @@ 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 { useVSCode } from '../../../hooks/useVSCode.js';
|
||||
import { createAndOpenTempFile } from '../../../utils/tempFileManager.js';
|
||||
import { useVSCode } from '../../../../hooks/useVSCode.js';
|
||||
import { createAndOpenTempFile } from '../../../../utils/tempFileManager.js';
|
||||
import './Bash.css';
|
||||
|
||||
/**
|
||||
@@ -7,16 +7,15 @@
|
||||
*/
|
||||
|
||||
import { useEffect, useCallback, useMemo } from 'react';
|
||||
import type { BaseToolCallProps } from '../../shared/types.js';
|
||||
import type { BaseToolCallProps } from '../shared/types.js';
|
||||
import {
|
||||
groupContent,
|
||||
mapToolStatusToContainerStatus,
|
||||
} from '../../shared/utils.js';
|
||||
import { FileLink } from '../../../ui/FileLink.js';
|
||||
import type { ToolCallContainerProps } from '../../shared/LayoutComponents.js';
|
||||
} from '../shared/utils.js';
|
||||
import { FileLink } from '../../../layout/FileLink.js';
|
||||
import type { ToolCallContainerProps } from '../shared/LayoutComponents.js';
|
||||
import { useVSCode } from '../../../../hooks/useVSCode.js';
|
||||
import { handleOpenDiff } from '../../../../utils/diffUtils.js';
|
||||
import { DiffDisplay } from '../../shared/DiffDisplay.js';
|
||||
|
||||
export const ToolCallContainer: React.FC<ToolCallContainerProps> = ({
|
||||
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">
|
||||
<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>
|
||||
);
|
||||
@@ -7,10 +7,10 @@
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import type { BaseToolCallProps } from '../../shared/types.js';
|
||||
import { safeTitle, groupContent } from '../../shared/utils.js';
|
||||
import type { BaseToolCallProps } from '../shared/types.js';
|
||||
import { safeTitle, groupContent } from '../shared/utils.js';
|
||||
import './Execute.css';
|
||||
import type { ToolCallContainerProps } from '../../shared/LayoutComponents.js';
|
||||
import type { ToolCallContainerProps } from '../shared/LayoutComponents.js';
|
||||
|
||||
export const ToolCallContainer: React.FC<ToolCallContainerProps> = ({
|
||||
label,
|
||||
@@ -14,10 +14,7 @@ import {
|
||||
ToolCallRow,
|
||||
LocationsList,
|
||||
} from './shared/LayoutComponents.js';
|
||||
import { DiffDisplay } from './shared/DiffDisplay.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
|
||||
@@ -27,10 +24,9 @@ import { handleOpenDiff } from '../../utils/diffUtils.js';
|
||||
export const GenericToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
const { kind, title, content, locations, toolCallId } = toolCall;
|
||||
const operationText = safeTitle(title);
|
||||
const vscode = useVSCode();
|
||||
|
||||
// Group content by type
|
||||
const { textOutputs, errors, diffs } = groupContent(content);
|
||||
const { textOutputs, errors } = groupContent(content);
|
||||
|
||||
// Error case: show operation + error in card layout
|
||||
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
|
||||
if (textOutputs.length > 0) {
|
||||
const output = textOutputs.join('\n');
|
||||
@@ -8,15 +8,15 @@
|
||||
|
||||
import type React from 'react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import type { BaseToolCallProps } from '../../shared/types.js';
|
||||
import type { BaseToolCallProps } from '../shared/types.js';
|
||||
import {
|
||||
groupContent,
|
||||
mapToolStatusToContainerStatus,
|
||||
} from '../../shared/utils.js';
|
||||
import { FileLink } from '../../../ui/FileLink.js';
|
||||
} from '../shared/utils.js';
|
||||
import { FileLink } from '../../../layout/FileLink.js';
|
||||
import { useVSCode } from '../../../../hooks/useVSCode.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> = ({
|
||||
label,
|
||||
@@ -7,15 +7,15 @@
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import type { BaseToolCallProps } from '../../shared/types.js';
|
||||
import { FileLink } from '../../../ui/FileLink.js';
|
||||
import type { BaseToolCallProps } from '../shared/types.js';
|
||||
import { FileLink } from '../../../layout/FileLink.js';
|
||||
import {
|
||||
safeTitle,
|
||||
groupContent,
|
||||
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> = ({
|
||||
label,
|
||||
@@ -10,31 +10,18 @@
|
||||
*/
|
||||
|
||||
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
|
||||
export type {
|
||||
ToolCallData,
|
||||
BaseToolCallProps as ToolCallProps,
|
||||
} from './toolcalls/shared/types.js';
|
||||
} from './shared/types.js';
|
||||
|
||||
// Re-export the content type for external use
|
||||
export type { ToolCallContent } from './toolcalls/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 type { ToolCallContent } from './shared/types.js';
|
||||
export const ToolCall: React.FC<{
|
||||
toolCall: import('./toolcalls/shared/types.js').ToolCallData;
|
||||
toolCall: import('./shared/types.js').ToolCallData;
|
||||
isFirst?: boolean;
|
||||
isLast?: boolean;
|
||||
}> = ({ toolCall, isFirst, isLast }) => (
|
||||
@@ -10,8 +10,8 @@ import type React from 'react';
|
||||
import type { BaseToolCallProps } from '../shared/types.js';
|
||||
import type { ToolCallContainerProps } from '../shared/LayoutComponents.js';
|
||||
import { groupContent, safeTitle } from '../shared/utils.js';
|
||||
import { CheckboxDisplay } from '../../ui/CheckboxDisplay.js';
|
||||
import type { PlanEntry } from '../../../../agents/qwenTypes.js';
|
||||
import { CheckboxDisplay } from './CheckboxDisplay.js';
|
||||
import type { PlanEntry } from '../../../../../types/qwenTypes.js';
|
||||
|
||||
type EntryStatus = 'pending' | 'in_progress' | 'completed';
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
groupContent,
|
||||
mapToolStatusToContainerStatus,
|
||||
} from '../shared/utils.js';
|
||||
import { FileLink } from '../../ui/FileLink.js';
|
||||
import { FileLink } from '../../../layout/FileLink.js';
|
||||
|
||||
/**
|
||||
* Specialized component for Write tool calls
|
||||
@@ -10,14 +10,13 @@ import type React from 'react';
|
||||
import type { BaseToolCallProps } from './shared/types.js';
|
||||
import { shouldShowToolCall } from './shared/utils.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 { EditToolCall } from './done/Edit/EditToolCall.js';
|
||||
import { EditToolCall } from './Edit/EditToolCall.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 { ExecuteNodeToolCall } from './ExecuteNode/ExecuteNodeToolCall.js';
|
||||
import { SearchToolCall } from './done/Search/SearchToolCall.js';
|
||||
import { SearchToolCall } from './Search/SearchToolCall.js';
|
||||
import { ThinkToolCall } from './Think/ThinkToolCall.js';
|
||||
|
||||
/**
|
||||
@@ -25,7 +24,6 @@ import { ThinkToolCall } from './Think/ThinkToolCall.js';
|
||||
*/
|
||||
export const getToolCallComponent = (
|
||||
kind: string,
|
||||
toolCall?: import('./shared/types.js').ToolCallData,
|
||||
): React.FC<BaseToolCallProps> => {
|
||||
const normalizedKind = kind.toLowerCase();
|
||||
|
||||
@@ -41,24 +39,6 @@ export const getToolCallComponent = (
|
||||
return EditToolCall;
|
||||
|
||||
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;
|
||||
|
||||
case 'bash':
|
||||
@@ -71,7 +51,6 @@ export const getToolCallComponent = (
|
||||
case 'update_todos':
|
||||
case 'todowrite':
|
||||
return UpdatedPlanToolCall;
|
||||
// return TodoWriteToolCall;
|
||||
|
||||
case 'search':
|
||||
case 'grep':
|
||||
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { FileLink } from '../../ui/FileLink.js';
|
||||
import { FileLink } from '../../../layout/FileLink.js';
|
||||
import './LayoutComponents.css';
|
||||
|
||||
/**
|
||||
@@ -20,7 +20,7 @@ export const formatValue = (value: unknown): string => {
|
||||
return '';
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
// TODO: 尝试从 string 取出 Output 部分
|
||||
// TODO: Trying to take out the Output part from the string
|
||||
try {
|
||||
value = (JSON.parse(value) as { output?: unknown }).output ?? value;
|
||||
} catch (_error) {
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -4,8 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { QwenAgentManager } from '../../agents/qwenAgentManager.js';
|
||||
import type { ConversationStore } from '../../storage/conversationStore.js';
|
||||
import type { QwenAgentManager } from '../../services/qwenAgentManager.js';
|
||||
import type { ConversationStore } from '../../services/conversationStore.js';
|
||||
|
||||
/**
|
||||
* Base message handler interface
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
*/
|
||||
|
||||
import type { IMessageHandler } from './BaseMessageHandler.js';
|
||||
import type { QwenAgentManager } from '../../agents/qwenAgentManager.js';
|
||||
import type { ConversationStore } from '../../storage/conversationStore.js';
|
||||
import type { QwenAgentManager } from '../../services/qwenAgentManager.js';
|
||||
import type { ConversationStore } from '../../services/conversationStore.js';
|
||||
import { SessionMessageHandler } from './SessionMessageHandler.js';
|
||||
import { FileMessageHandler } from './FileMessageHandler.js';
|
||||
import { EditorMessageHandler } from './EditorMessageHandler.js';
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { BaseMessageHandler } from './BaseMessageHandler.js';
|
||||
import type { ChatMessage } from '../../agents/qwenAgentManager.js';
|
||||
import type { ChatMessage } from '../../services/qwenAgentManager.js';
|
||||
|
||||
/**
|
||||
* Session message handler
|
||||
@@ -581,10 +581,11 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
||||
let sessionDetails: Record<string, unknown> | null = null;
|
||||
try {
|
||||
const allSessions = await this.agentManager.getSessionList();
|
||||
sessionDetails = allSessions.find(
|
||||
(s: { id?: string; sessionId?: string }) =>
|
||||
s.id === sessionId || s.sessionId === sessionId,
|
||||
);
|
||||
sessionDetails =
|
||||
allSessions.find(
|
||||
(s: { id?: string; sessionId?: string }) =>
|
||||
s.id === sessionId || s.sessionId === sessionId,
|
||||
) || null;
|
||||
} catch (err) {
|
||||
console.log(
|
||||
'[SessionMessageHandler] Could not get session details:',
|
||||
|
||||
@@ -73,7 +73,9 @@ export const useMessageHandling = () => {
|
||||
const appendStreamChunk = useCallback(
|
||||
(chunk: string) => {
|
||||
// Ignore late chunks after user cancelled streaming (until next streamStart)
|
||||
if (!isStreaming) return;
|
||||
if (!isStreaming) {
|
||||
return;
|
||||
}
|
||||
|
||||
setMessages((prev) => {
|
||||
let idx = streamingMessageIndexRef.current;
|
||||
@@ -157,7 +159,9 @@ export const useMessageHandling = () => {
|
||||
// Thought handling
|
||||
appendThinkingChunk: (chunk: string) => {
|
||||
// Ignore late thoughts after user cancelled streaming
|
||||
if (!isStreaming) return;
|
||||
if (!isStreaming) {
|
||||
return;
|
||||
}
|
||||
setMessages((prev) => {
|
||||
let idx = thinkingMessageIndexRef.current;
|
||||
const next = prev.slice();
|
||||
|
||||
@@ -59,7 +59,9 @@ export const useSessionManagement = (vscode: VSCodeAPI) => {
|
||||
}, [vscode]);
|
||||
|
||||
const handleLoadMoreSessions = useCallback(() => {
|
||||
if (!hasMore || isLoading || nextCursor === undefined) return;
|
||||
if (!hasMore || isLoading || nextCursor === undefined) {
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
vscode.postMessage({
|
||||
type: 'getQwenSessions',
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import type { RefObject } 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 {
|
||||
isOpen: boolean;
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import type { ToolCallData } from '../components/ToolCall.js';
|
||||
import type { ToolCallUpdate } from '../types/toolCall.js';
|
||||
import type { ToolCallData } from '../components/messages/toolcalls/ToolCall.js';
|
||||
import type { ToolCallUpdate } from '../../types/qwenTypes.js';
|
||||
|
||||
/**
|
||||
* Tool call management Hook
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
import { useVSCode } from './useVSCode.js';
|
||||
import type { Conversation } from '../../storage/conversationStore.js';
|
||||
import type { Conversation } from '../../services/conversationStore.js';
|
||||
import type {
|
||||
PermissionOption,
|
||||
ToolCall as PermissionToolCall,
|
||||
} from '../components/PermissionDrawer/PermissionRequest.js';
|
||||
import type { ToolCallUpdate, EditMode } from '../types/toolCall.js';
|
||||
import type { PlanEntry } from '../../agents/qwenTypes.js';
|
||||
import type { ToolCallUpdate, EditMode } from '../../types/qwenTypes.js';
|
||||
import type { PlanEntry } from '../../types/qwenTypes.js';
|
||||
|
||||
interface UseWebViewMessagesProps {
|
||||
// Session management
|
||||
|
||||
@@ -11,7 +11,7 @@ import './styles/tailwind.css';
|
||||
// eslint-disable-next-line import/no-internal-modules
|
||||
import './styles/App.css';
|
||||
// eslint-disable-next-line import/no-internal-modules
|
||||
import './styles/ClaudeCodeStyles.css';
|
||||
import './styles/styles.css';
|
||||
|
||||
const container = document.getElementById('root');
|
||||
if (container) {
|
||||
|
||||
@@ -8,12 +8,9 @@
|
||||
*/
|
||||
|
||||
/* Import component styles */
|
||||
@import '../components/toolcalls/shared/DiffDisplay.css';
|
||||
@import '../components/messages/Assistant/AssistantMessage.css';
|
||||
@import '../components/toolcalls/shared/SimpleTimeline.css';
|
||||
@import '../components/messages/QwenMessageTimeline.css';
|
||||
@import '../components/MarkdownRenderer/MarkdownRenderer.css';
|
||||
|
||||
@import './timeline.css';
|
||||
@import '../components/messages/MarkdownRenderer/MarkdownRenderer.css';
|
||||
|
||||
/* ===========================
|
||||
CSS Variables (from Claude Code root styles)
|
||||
@@ -56,7 +53,10 @@
|
||||
--app-code-background: var(--vscode-textCodeBlock-background);
|
||||
|
||||
/* 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-foreground: var(--vscode-editorWarning-foreground, #ffcc00);
|
||||
}
|
||||
126
packages/vscode-ide-companion/src/webview/styles/timeline.css
Normal file
126
packages/vscode-ide-companion/src/webview/styles/timeline.css
Normal 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;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
|
||||
// Shared type for completion items used by the input completion system
|
||||
export interface CompletionItem {
|
||||
id: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
icon?: React.ReactNode;
|
||||
type: 'file' | 'folder' | 'symbol' | 'command' | 'variable' | 'info';
|
||||
// Value inserted into the input when selected (e.g., filename or command)
|
||||
value?: string;
|
||||
// Optional full path for files (used to build @filename -> full path mapping)
|
||||
path?: string;
|
||||
}
|
||||
@@ -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';
|
||||
@@ -6,7 +6,7 @@
|
||||
* 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
|
||||
@@ -16,7 +16,7 @@ import type { WebviewApi } from 'vscode-webview';
|
||||
* @param newText New content (right side)
|
||||
*/
|
||||
export const handleOpenDiff = (
|
||||
vscode: WebviewApi<unknown>,
|
||||
vscode: VSCodeAPI,
|
||||
path: string | undefined,
|
||||
oldText: string | null | undefined,
|
||||
newText: string | undefined,
|
||||
|
||||
Reference in New Issue
Block a user