mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
refactor(webview): 重构工具调用显示逻辑
- 新增多个工具调用组件,分别处理不同类型的工具调用 - 优化工具调用卡片的样式和布局 - 添加加载状态和随机加载消息 - 重构 App 组件,支持新的工具调用显示逻辑
This commit is contained in:
@@ -735,10 +735,6 @@ export class WebViewProvider {
|
||||
try {
|
||||
console.log('[WebViewProvider] Switching to Qwen session:', sessionId);
|
||||
|
||||
// Set current conversation ID so we can send messages
|
||||
this.currentConversationId = sessionId;
|
||||
console.log('[WebViewProvider] Set currentConversationId to:', sessionId);
|
||||
|
||||
// Get session messages from local files
|
||||
const messages = await this.agentManager.getSessionMessages(sessionId);
|
||||
console.log(
|
||||
@@ -758,44 +754,38 @@ export class WebViewProvider {
|
||||
console.log('[WebViewProvider] Could not get session details:', err);
|
||||
}
|
||||
|
||||
// Try to switch session in ACP (may fail if not supported)
|
||||
// IMPORTANT: CLI doesn't support loading old sessions
|
||||
// So we always create a NEW ACP session for continuation
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
|
||||
|
||||
try {
|
||||
await this.agentManager.switchToSession(sessionId);
|
||||
console.log('[WebViewProvider] Session switched successfully in ACP');
|
||||
} catch (_switchError) {
|
||||
const newAcpSessionId =
|
||||
await this.agentManager.createNewSession(workingDir);
|
||||
console.log(
|
||||
'[WebViewProvider] session/switch not supported or failed, creating new session',
|
||||
'[WebViewProvider] Created new ACP session for conversation:',
|
||||
newAcpSessionId,
|
||||
);
|
||||
// If switch fails, create a new session to continue conversation
|
||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
|
||||
try {
|
||||
const newSessionId =
|
||||
await this.agentManager.createNewSession(workingDir);
|
||||
console.log(
|
||||
'[WebViewProvider] Created new session as fallback:',
|
||||
newSessionId,
|
||||
);
|
||||
if (newSessionId) {
|
||||
// Update to the new session ID so messages can be sent
|
||||
this.currentConversationId = newSessionId;
|
||||
console.log(
|
||||
'[WebViewProvider] Updated currentConversationId to new session:',
|
||||
newSessionId,
|
||||
);
|
||||
}
|
||||
} catch (newSessionError) {
|
||||
console.error(
|
||||
'[WebViewProvider] Failed to create new session:',
|
||||
newSessionError,
|
||||
);
|
||||
vscode.window.showWarningMessage(
|
||||
'Could not switch to session. Created new session instead.',
|
||||
);
|
||||
}
|
||||
|
||||
// Use the NEW ACP session ID for sending messages to CLI
|
||||
this.currentConversationId = newAcpSessionId;
|
||||
console.log(
|
||||
'[WebViewProvider] Set currentConversationId (ACP) to:',
|
||||
newAcpSessionId,
|
||||
);
|
||||
} catch (createError) {
|
||||
console.error(
|
||||
'[WebViewProvider] Failed to create new ACP session:',
|
||||
createError,
|
||||
);
|
||||
vscode.window.showWarningMessage(
|
||||
'Could not switch to session. Created new session instead.',
|
||||
);
|
||||
throw createError;
|
||||
}
|
||||
|
||||
// Send messages and session details to WebView
|
||||
// The historical messages are display-only, not sent to CLI
|
||||
this.sendMessageToWebView({
|
||||
type: 'qwenSessionSwitched',
|
||||
data: { sessionId, messages, session: sessionDetails },
|
||||
|
||||
@@ -206,7 +206,7 @@ button {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 20px 20px 40px;
|
||||
padding: 20px 20px 120px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--app-spacing-medium);
|
||||
@@ -686,3 +686,330 @@ button {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
Tool Call Card Styles (Grid Layout)
|
||||
=========================== */
|
||||
.tool-call-card {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: var(--app-spacing-medium);
|
||||
background: var(--app-input-background);
|
||||
border: 1px solid var(--app-input-border);
|
||||
border-radius: var(--corner-radius-medium);
|
||||
padding: var(--app-spacing-large);
|
||||
margin: var(--app-spacing-medium) 0;
|
||||
animation: fadeIn 0.2s ease-in;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.tool-call-icon {
|
||||
font-size: 20px;
|
||||
grid-row: 1;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.tool-call-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--app-spacing-medium);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tool-call-row {
|
||||
display: grid;
|
||||
grid-template-columns: 80px 1fr;
|
||||
gap: var(--app-spacing-medium);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tool-call-label {
|
||||
font-size: 12px;
|
||||
color: var(--app-secondary-foreground);
|
||||
font-weight: 500;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.tool-call-value {
|
||||
color: var(--app-primary-foreground);
|
||||
min-width: 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.tool-call-status-indicator {
|
||||
display: inline-block;
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tool-call-status-indicator::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
margin-right: 6px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.tool-call-status-indicator.pending::before {
|
||||
background: #ffc107;
|
||||
}
|
||||
|
||||
.tool-call-status-indicator.in_progress::before {
|
||||
background: #2196f3;
|
||||
}
|
||||
|
||||
.tool-call-status-indicator.completed::before {
|
||||
background: #4caf50;
|
||||
}
|
||||
|
||||
.tool-call-status-indicator.failed::before {
|
||||
background: #f44336;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
font-family: var(--app-monospace-font-family);
|
||||
font-size: var(--app-monospace-font-size);
|
||||
background: var(--app-primary-background);
|
||||
border: 1px solid var(--app-input-border);
|
||||
border-radius: var(--corner-radius-small);
|
||||
padding: var(--app-spacing-medium);
|
||||
overflow-x: auto;
|
||||
margin: 4px 0 0 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
Permission Request Card Styles
|
||||
=========================== */
|
||||
.permission-request-card {
|
||||
background: var(--app-input-background);
|
||||
border: 1px solid var(--app-qwen-orange);
|
||||
border-radius: var(--corner-radius-medium);
|
||||
margin: var(--app-spacing-medium) 0;
|
||||
margin-bottom: var(--app-spacing-xlarge);
|
||||
overflow: visible;
|
||||
animation: fadeIn 0.2s ease-in;
|
||||
}
|
||||
|
||||
.permission-card-body {
|
||||
padding: var(--app-spacing-large);
|
||||
min-height: fit-content;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.permission-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--app-spacing-large);
|
||||
margin-bottom: var(--app-spacing-large);
|
||||
}
|
||||
|
||||
.permission-icon-wrapper {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(97, 95, 255, 0.1);
|
||||
border-radius: var(--corner-radius-medium);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.permission-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.permission-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.permission-title {
|
||||
font-weight: 600;
|
||||
color: var(--app-primary-foreground);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.permission-subtitle {
|
||||
font-size: 12px;
|
||||
color: var(--app-secondary-foreground);
|
||||
}
|
||||
|
||||
.permission-command-section {
|
||||
margin-bottom: var(--app-spacing-large);
|
||||
}
|
||||
|
||||
.permission-command-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--app-secondary-foreground);
|
||||
margin-bottom: var(--app-spacing-small);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.permission-command-code {
|
||||
display: block;
|
||||
font-family: var(--app-monospace-font-family);
|
||||
font-size: var(--app-monospace-font-size);
|
||||
color: var(--app-primary-foreground);
|
||||
background: var(--app-primary-background);
|
||||
padding: var(--app-spacing-medium);
|
||||
border-radius: var(--corner-radius-small);
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.permission-locations-section {
|
||||
margin-bottom: var(--app-spacing-large);
|
||||
}
|
||||
|
||||
.permission-locations-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--app-secondary-foreground);
|
||||
margin-bottom: var(--app-spacing-small);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.permission-location-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--app-spacing-small);
|
||||
padding: var(--app-spacing-small) 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.permission-location-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.permission-location-path {
|
||||
color: var(--app-primary-foreground);
|
||||
font-family: var(--app-monospace-font-family);
|
||||
}
|
||||
|
||||
.permission-location-line {
|
||||
color: var(--app-secondary-foreground);
|
||||
}
|
||||
|
||||
.permission-options-section {
|
||||
margin-top: var(--app-spacing-large);
|
||||
}
|
||||
|
||||
.permission-options-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--app-primary-foreground);
|
||||
margin-bottom: var(--app-spacing-medium);
|
||||
}
|
||||
|
||||
.permission-options-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--app-spacing-small);
|
||||
}
|
||||
|
||||
.permission-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--app-spacing-medium);
|
||||
padding: var(--app-spacing-medium) var(--app-spacing-large);
|
||||
background: var(--app-primary-background);
|
||||
border: 1px solid var(--app-input-border);
|
||||
border-radius: var(--corner-radius-small);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.permission-option:hover {
|
||||
background: var(--app-list-hover-background);
|
||||
border-color: var(--app-input-active-border);
|
||||
}
|
||||
|
||||
.permission-option.selected {
|
||||
border-color: var(--app-qwen-orange);
|
||||
background: rgba(97, 95, 255, 0.1);
|
||||
}
|
||||
|
||||
.permission-option.allow {
|
||||
/* Allow options */
|
||||
}
|
||||
|
||||
.permission-option.reject {
|
||||
/* Reject options */
|
||||
}
|
||||
|
||||
.permission-radio {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.permission-option-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--app-spacing-small);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.permission-always-badge {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.permission-no-options {
|
||||
text-align: center;
|
||||
padding: var(--app-spacing-large);
|
||||
color: var(--app-secondary-foreground);
|
||||
}
|
||||
|
||||
.permission-actions {
|
||||
margin-top: var(--app-spacing-large);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.permission-confirm-button {
|
||||
padding: var(--app-spacing-medium) var(--app-spacing-xlarge);
|
||||
background: var(--app-qwen-clay-button-orange);
|
||||
color: var(--app-qwen-ivory);
|
||||
border: none;
|
||||
border-radius: var(--corner-radius-small);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: filter 0.15s ease;
|
||||
}
|
||||
|
||||
.permission-confirm-button:hover:not(:disabled) {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.permission-confirm-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.permission-success {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--app-spacing-medium);
|
||||
padding: var(--app-spacing-large);
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
border-radius: var(--corner-radius-small);
|
||||
margin-top: var(--app-spacing-large);
|
||||
}
|
||||
|
||||
.permission-success-icon {
|
||||
color: #4caf50;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.permission-success-text {
|
||||
color: #4caf50;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
|
||||
@@ -46,11 +46,152 @@ interface TextMessage {
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
// Loading messages from Claude Code CLI
|
||||
// Source: packages/cli/src/ui/hooks/usePhraseCycler.ts
|
||||
const WITTY_LOADING_PHRASES = [
|
||||
"I'm Feeling Lucky",
|
||||
'Shipping awesomeness... ',
|
||||
'Painting the serifs back on...',
|
||||
'Navigating the slime mold...',
|
||||
'Consulting the digital spirits...',
|
||||
'Reticulating splines...',
|
||||
'Warming up the AI hamsters...',
|
||||
'Asking the magic conch shell...',
|
||||
'Generating witty retort...',
|
||||
'Polishing the algorithms...',
|
||||
"Don't rush perfection (or my code)...",
|
||||
'Brewing fresh bytes...',
|
||||
'Counting electrons...',
|
||||
'Engaging cognitive processors...',
|
||||
'Checking for syntax errors in the universe...',
|
||||
'One moment, optimizing humor...',
|
||||
'Shuffling punchlines...',
|
||||
'Untangling neural nets...',
|
||||
'Compiling brilliance...',
|
||||
'Loading wit.exe...',
|
||||
'Summoning the cloud of wisdom...',
|
||||
'Preparing a witty response...',
|
||||
"Just a sec, I'm debugging reality...",
|
||||
'Confuzzling the options...',
|
||||
'Tuning the cosmic frequencies...',
|
||||
'Crafting a response worthy of your patience...',
|
||||
'Compiling the 1s and 0s...',
|
||||
'Resolving dependencies... and existential crises...',
|
||||
'Defragmenting memories... both RAM and personal...',
|
||||
'Rebooting the humor module...',
|
||||
'Caching the essentials (mostly cat memes)...',
|
||||
'Optimizing for ludicrous speed',
|
||||
"Swapping bits... don't tell the bytes...",
|
||||
'Garbage collecting... be right back...',
|
||||
'Assembling the interwebs...',
|
||||
'Converting coffee into code...',
|
||||
'Updating the syntax for reality...',
|
||||
'Rewiring the synapses...',
|
||||
'Looking for a misplaced semicolon...',
|
||||
"Greasin' the cogs of the machine...",
|
||||
'Pre-heating the servers...',
|
||||
'Calibrating the flux capacitor...',
|
||||
'Engaging the improbability drive...',
|
||||
'Channeling the Force...',
|
||||
'Aligning the stars for optimal response...',
|
||||
'So say we all...',
|
||||
'Loading the next great idea...',
|
||||
"Just a moment, I'm in the zone...",
|
||||
'Preparing to dazzle you with brilliance...',
|
||||
"Just a tick, I'm polishing my wit...",
|
||||
"Hold tight, I'm crafting a masterpiece...",
|
||||
"Just a jiffy, I'm debugging the universe...",
|
||||
"Just a moment, I'm aligning the pixels...",
|
||||
"Just a sec, I'm optimizing the humor...",
|
||||
"Just a moment, I'm tuning the algorithms...",
|
||||
'Warp speed engaged...',
|
||||
'Mining for more Dilithium crystals...',
|
||||
"Don't panic...",
|
||||
'Following the white rabbit...',
|
||||
'The truth is in here... somewhere...',
|
||||
'Blowing on the cartridge...',
|
||||
'Loading... Do a barrel roll!',
|
||||
'Waiting for the respawn...',
|
||||
'Finishing the Kessel Run in less than 12 parsecs...',
|
||||
"The cake is not a lie, it's just still loading...",
|
||||
'Fiddling with the character creation screen...',
|
||||
"Just a moment, I'm finding the right meme...",
|
||||
"Pressing 'A' to continue...",
|
||||
'Herding digital cats...',
|
||||
'Polishing the pixels...',
|
||||
'Finding a suitable loading screen pun...',
|
||||
'Distracting you with this witty phrase...',
|
||||
'Almost there... probably...',
|
||||
'Our hamsters are working as fast as they can...',
|
||||
'Giving Cloudy a pat on the head...',
|
||||
'Petting the cat...',
|
||||
'Rickrolling my boss...',
|
||||
'Never gonna give you up, never gonna let you down...',
|
||||
'Slapping the bass...',
|
||||
'Tasting the snozberries...',
|
||||
"I'm going the distance, I'm going for speed...",
|
||||
'Is this the real life? Is this just fantasy?...',
|
||||
"I've got a good feeling about this...",
|
||||
'Poking the bear...',
|
||||
'Doing research on the latest memes...',
|
||||
'Figuring out how to make this more witty...',
|
||||
'Hmmm... let me think...',
|
||||
'What do you call a fish with no eyes? A fsh...',
|
||||
'Why did the computer go to therapy? It had too many bytes...',
|
||||
"Why don't programmers like nature? It has too many bugs...",
|
||||
'Why do programmers prefer dark mode? Because light attracts bugs...',
|
||||
'Why did the developer go broke? Because they used up all their cache...',
|
||||
"What can you do with a broken pencil? Nothing, it's pointless...",
|
||||
'Applying percussive maintenance...',
|
||||
'Searching for the correct USB orientation...',
|
||||
'Ensuring the magic smoke stays inside the wires...',
|
||||
'Rewriting in Rust for no particular reason...',
|
||||
'Trying to exit Vim...',
|
||||
'Spinning up the hamster wheel...',
|
||||
"That's not a bug, it's an undocumented feature...",
|
||||
'Engage.',
|
||||
"I'll be back... with an answer.",
|
||||
'My other process is a TARDIS...',
|
||||
'Communing with the machine spirit...',
|
||||
'Letting the thoughts marinate...',
|
||||
'Just remembered where I put my keys...',
|
||||
'Pondering the orb...',
|
||||
"I've seen things you people wouldn't believe... like a user who reads loading messages.",
|
||||
'Initiating thoughtful gaze...',
|
||||
"What's a computer's favorite snack? Microchips.",
|
||||
"Why do Java developers wear glasses? Because they don't C#.",
|
||||
'Charging the laser... pew pew!',
|
||||
'Dividing by zero... just kidding!',
|
||||
'Looking for an adult superviso... I mean, processing.',
|
||||
'Making it go beep boop.',
|
||||
'Buffering... because even AIs need a moment.',
|
||||
'Entangling quantum particles for a faster response...',
|
||||
'Polishing the chrome... on the algorithms.',
|
||||
'Are you not entertained? (Working on it!)',
|
||||
'Summoning the code gremlins... to help, of course.',
|
||||
'Just waiting for the dial-up tone to finish...',
|
||||
'Recalibrating the humor-o-meter.',
|
||||
'My other loading screen is even funnier.',
|
||||
"Pretty sure there's a cat walking on the keyboard somewhere...",
|
||||
'Enhancing... Enhancing... Still loading.',
|
||||
"It's not a bug, it's a feature... of this loading screen.",
|
||||
'Have you tried turning it off and on again? (The loading screen, not me.)',
|
||||
'Constructing additional pylons...',
|
||||
"New line? That's Ctrl+J.",
|
||||
];
|
||||
|
||||
const getRandomLoadingMessage = () =>
|
||||
WITTY_LOADING_PHRASES[
|
||||
Math.floor(Math.random() * WITTY_LOADING_PHRASES.length)
|
||||
];
|
||||
|
||||
export const App: React.FC = () => {
|
||||
const vscode = useVSCode();
|
||||
const [messages, setMessages] = useState<TextMessage[]>([]);
|
||||
const [inputText, setInputText] = useState('');
|
||||
const [isStreaming, setIsStreaming] = useState(false);
|
||||
const [_isWaitingForResponse, setIsWaitingForResponse] = useState(false);
|
||||
const [_loadingMessage, setLoadingMessage] = useState('');
|
||||
const [currentStreamContent, setCurrentStreamContent] = useState('');
|
||||
const [qwenSessions, setQwenSessions] = useState<
|
||||
Array<Record<string, unknown>>
|
||||
@@ -96,6 +237,17 @@ export const App: React.FC = () => {
|
||||
const newMap = new Map(prev);
|
||||
const existing = newMap.get(update.toolCallId);
|
||||
|
||||
// Helper function to safely convert title to string
|
||||
const safeTitle = (title: unknown): string => {
|
||||
if (typeof title === 'string') {
|
||||
return title;
|
||||
}
|
||||
if (title && typeof title === 'object') {
|
||||
return JSON.stringify(title);
|
||||
}
|
||||
return 'Tool Call';
|
||||
};
|
||||
|
||||
if (update.type === 'tool_call') {
|
||||
// New tool call - cast content to proper type
|
||||
const content = update.content?.map((item) => ({
|
||||
@@ -109,7 +261,7 @@ export const App: React.FC = () => {
|
||||
newMap.set(update.toolCallId, {
|
||||
toolCallId: update.toolCallId,
|
||||
kind: update.kind || 'other',
|
||||
title: update.title || 'Tool Call',
|
||||
title: safeTitle(update.title),
|
||||
status: update.status || 'pending',
|
||||
rawInput: update.rawInput as string | object | undefined,
|
||||
content,
|
||||
@@ -130,7 +282,7 @@ export const App: React.FC = () => {
|
||||
newMap.set(update.toolCallId, {
|
||||
...existing,
|
||||
...(update.kind && { kind: update.kind }),
|
||||
...(update.title && { title: update.title }),
|
||||
...(update.title && { title: safeTitle(update.title) }),
|
||||
...(update.status && { status: update.status }),
|
||||
...(updatedContent && { content: updatedContent }),
|
||||
...(update.locations && { locations: update.locations }),
|
||||
@@ -286,6 +438,10 @@ export const App: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set waiting state with random loading message
|
||||
setIsWaitingForResponse(true);
|
||||
setLoadingMessage(getRandomLoadingMessage());
|
||||
|
||||
vscode.postMessage({
|
||||
type: 'sendMessage',
|
||||
data: { text: inputText },
|
||||
|
||||
@@ -270,4 +270,135 @@
|
||||
--app-menu-foreground: var(--vscode-menu-foreground);
|
||||
--app-menu-selection-background: var(--vscode-menu-selectionBackground);
|
||||
--app-menu-selection-foreground: var(--vscode-menu-selectionForeground);
|
||||
|
||||
/* Tool Call Styles */
|
||||
--app-tool-background: var(--vscode-editor-background);
|
||||
--app-code-background: var(--vscode-textCodeBlock-background);
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
Tool Call Card (from Claude Code .Ne)
|
||||
=========================== */
|
||||
.tool-call-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;
|
||||
}
|
||||
|
||||
/* Tool Call Grid Layout (from Claude Code .Ke) */
|
||||
.tool-call-grid {
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr;
|
||||
}
|
||||
|
||||
/* Tool Call Row (from Claude Code .no) */
|
||||
.tool-call-row {
|
||||
grid-column: 1 / -1;
|
||||
display: grid;
|
||||
grid-template-columns: subgrid;
|
||||
border-top: 0.5px solid var(--app-input-border);
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.tool-call-row:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
/* Tool Call Label (from Claude Code .Je) */
|
||||
.tool-call-label {
|
||||
grid-column: 1;
|
||||
color: var(--app-secondary-foreground);
|
||||
text-align: left;
|
||||
opacity: 0.5;
|
||||
padding: 4px 8px 4px 4px;
|
||||
font-family: var(--app-monospace-font-family);
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
/* Tool Call Value (from Claude Code .m) */
|
||||
.tool-call-value {
|
||||
grid-column: 2;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
margin: 0;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.tool-call-value:not(.expanded) {
|
||||
max-height: 60px;
|
||||
mask-image: linear-gradient(to bottom, var(--app-primary-background) 40px, transparent 60px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tool-call-value pre {
|
||||
margin-block: 0;
|
||||
overflow: hidden;
|
||||
font-family: var(--app-monospace-font-family);
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.tool-call-value code {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: var(--app-monospace-font-family);
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
/* Tool Call Icon (from Claude Code .to) */
|
||||
.tool-call-icon {
|
||||
margin: 8px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Code Block (from Claude Code ._e) */
|
||||
.code-block {
|
||||
background-color: var(--app-code-background);
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
font-family: var(--app-monospace-font-family);
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
/* Status indicators for tool calls */
|
||||
.tool-call-status-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.tool-call-status-indicator::before {
|
||||
content: "●";
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.tool-call-status-indicator.pending::before {
|
||||
color: var(--app-secondary-foreground);
|
||||
}
|
||||
|
||||
.tool-call-status-indicator.in-progress::before {
|
||||
color: #e1c08d;
|
||||
animation: blink 1s linear infinite;
|
||||
}
|
||||
|
||||
.tool-call-status-indicator.completed::before {
|
||||
color: #74c991;
|
||||
}
|
||||
|
||||
.tool-call-status-indicator.failed::before {
|
||||
color: #c74e39;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
|
||||
@@ -2,188 +2,37 @@
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Main ToolCall component - uses factory pattern to route to specialized components
|
||||
*
|
||||
* This file serves as the public API for tool call rendering.
|
||||
* It re-exports the router and types from the toolcalls module.
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { ToolCallRouter } from './toolcalls/index.js';
|
||||
|
||||
export interface ToolCallContent {
|
||||
type: 'content' | 'diff';
|
||||
// For content type
|
||||
content?: {
|
||||
type: string;
|
||||
text?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
// For diff type
|
||||
path?: string;
|
||||
oldText?: string | null;
|
||||
newText?: string;
|
||||
}
|
||||
// Re-export types from the toolcalls module for backward compatibility
|
||||
export type {
|
||||
ToolCallData,
|
||||
BaseToolCallProps as ToolCallProps,
|
||||
} from './toolcalls/shared/types.js';
|
||||
|
||||
export interface ToolCallData {
|
||||
toolCallId: string;
|
||||
kind: string;
|
||||
title: string;
|
||||
status: 'pending' | 'in_progress' | 'completed' | 'failed';
|
||||
rawInput?: string | object;
|
||||
content?: ToolCallContent[];
|
||||
locations?: Array<{
|
||||
path: string;
|
||||
line?: number | null;
|
||||
}>;
|
||||
}
|
||||
// Re-export the content type for external use
|
||||
export type { ToolCallContent } from './toolcalls/shared/types.js';
|
||||
|
||||
export interface ToolCallProps {
|
||||
toolCall: ToolCallData;
|
||||
}
|
||||
|
||||
const StatusTag: React.FC<{ status: string }> = ({ status }) => {
|
||||
const getStatusInfo = () => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return { className: 'status-pending', text: 'Pending', icon: '⏳' };
|
||||
case 'in_progress':
|
||||
return {
|
||||
className: 'status-in-progress',
|
||||
text: 'In Progress',
|
||||
icon: '🔄',
|
||||
};
|
||||
case 'completed':
|
||||
return { className: 'status-completed', text: 'Completed', icon: '✓' };
|
||||
case 'failed':
|
||||
return { className: 'status-failed', text: 'Failed', icon: '✗' };
|
||||
default:
|
||||
return { className: 'status-unknown', text: status, icon: '•' };
|
||||
}
|
||||
};
|
||||
|
||||
const { className, text, icon } = getStatusInfo();
|
||||
return (
|
||||
<span className={`tool-call-status ${className}`}>
|
||||
<span className="status-icon">{icon}</span>
|
||||
{text}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const ContentView: React.FC<{ content: ToolCallContent }> = ({ content }) => {
|
||||
// Handle diff type
|
||||
if (content.type === 'diff') {
|
||||
const fileName =
|
||||
content.path?.split(/[/\\]/).pop() || content.path || 'Unknown file';
|
||||
const oldText = content.oldText || '';
|
||||
const newText = content.newText || '';
|
||||
|
||||
return (
|
||||
<div className="tool-call-diff">
|
||||
<div className="diff-header">
|
||||
<span className="diff-icon">📝</span>
|
||||
<span className="diff-filename">{fileName}</span>
|
||||
</div>
|
||||
<div className="diff-content">
|
||||
<div className="diff-side">
|
||||
<div className="diff-side-label">Before</div>
|
||||
<pre className="diff-code">{oldText || '(empty)'}</pre>
|
||||
</div>
|
||||
<div className="diff-arrow">→</div>
|
||||
<div className="diff-side">
|
||||
<div className="diff-side-label">After</div>
|
||||
<pre className="diff-code">{newText || '(empty)'}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Handle content type with text
|
||||
if (content.type === 'content' && content.content?.text) {
|
||||
return (
|
||||
<div className="tool-call-content">
|
||||
<div className="content-text">{content.content.text}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const getKindDisplayName = (kind: string): { name: string; icon: string } => {
|
||||
const kindMap: Record<string, { name: string; icon: string }> = {
|
||||
edit: { name: 'File Edit', icon: '✏️' },
|
||||
read: { name: 'File Read', icon: '📖' },
|
||||
execute: { name: 'Shell Command', icon: '⚡' },
|
||||
fetch: { name: 'Web Fetch', icon: '🌐' },
|
||||
delete: { name: 'Delete', icon: '🗑️' },
|
||||
move: { name: 'Move/Rename', icon: '📦' },
|
||||
search: { name: 'Search', icon: '🔍' },
|
||||
think: { name: 'Thinking', icon: '💭' },
|
||||
other: { name: 'Other', icon: '🔧' },
|
||||
};
|
||||
|
||||
return kindMap[kind] || { name: kind, icon: '🔧' };
|
||||
};
|
||||
|
||||
const formatRawInput = (rawInput: string | object | undefined): string => {
|
||||
if (rawInput === undefined) {
|
||||
return '';
|
||||
}
|
||||
if (typeof rawInput === 'string') {
|
||||
return rawInput;
|
||||
}
|
||||
return JSON.stringify(rawInput, null, 2);
|
||||
};
|
||||
|
||||
export const ToolCall: React.FC<ToolCallProps> = ({ toolCall }) => {
|
||||
const { kind, title, status, rawInput, content, locations, toolCallId } =
|
||||
toolCall;
|
||||
const kindInfo: { name: string; icon: string } = getKindDisplayName(kind);
|
||||
|
||||
return (
|
||||
<div className="tool-call-card">
|
||||
<div className="tool-call-header">
|
||||
<span className="tool-call-kind-icon">{kindInfo.icon}</span>
|
||||
<span className="tool-call-title">{title || kindInfo.name}</span>
|
||||
<StatusTag status={status} />
|
||||
</div>
|
||||
|
||||
{/* Show raw input if available */}
|
||||
{rawInput !== undefined && rawInput !== null ? (
|
||||
<div className="tool-call-raw-input">
|
||||
<div className="raw-input-label">Input</div>
|
||||
<pre className="raw-input-content">{formatRawInput(rawInput)}</pre>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Show locations if available */}
|
||||
{locations && locations.length > 0 && (
|
||||
<div className="tool-call-locations">
|
||||
<div className="locations-label">Files</div>
|
||||
{locations.map((location, index) => (
|
||||
<div key={index} className="location-item">
|
||||
<span className="location-icon">📄</span>
|
||||
<span className="location-path">{location.path}</span>
|
||||
{location.line !== null && location.line !== undefined && (
|
||||
<span className="location-line">:{location.line}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show content if available */}
|
||||
{content && content.length > 0 && (
|
||||
<div className="tool-call-content-list">
|
||||
{content.map((item, index) => (
|
||||
<ContentView key={index} content={item} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="tool-call-footer">
|
||||
<span className="tool-call-id">
|
||||
ID: {toolCallId.substring(0, 8)}...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
/**
|
||||
* Main ToolCall component
|
||||
* Routes to specialized components based on the tool call kind
|
||||
*
|
||||
* Supported kinds:
|
||||
* - read: File reading operations
|
||||
* - write/edit: File writing and editing operations
|
||||
* - execute/bash/command: Command execution
|
||||
* - search/grep/glob/find: Search operations
|
||||
* - think/thinking: AI reasoning
|
||||
* - All others: Generic display
|
||||
*/
|
||||
export const ToolCall: React.FC<{
|
||||
toolCall: import('./toolcalls/shared/types.js').ToolCallData;
|
||||
}> = ({ toolCall }) => <ToolCallRouter toolCall={toolCall} />;
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @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 {
|
||||
ToolCallCard,
|
||||
ToolCallRow,
|
||||
StatusIndicator,
|
||||
CodeBlock,
|
||||
} from './shared/LayoutComponents.js';
|
||||
import { formatValue, safeTitle, groupContent } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* Specialized component for Execute tool calls
|
||||
* Optimized for displaying command execution with stdout/stderr
|
||||
*/
|
||||
export const ExecuteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
const { title, status, rawInput, content } = toolCall;
|
||||
const titleText = safeTitle(title);
|
||||
|
||||
// Group content by type
|
||||
const { textOutputs, errors, otherData } = groupContent(content);
|
||||
|
||||
return (
|
||||
<ToolCallCard icon="⚡">
|
||||
{/* Title row */}
|
||||
<ToolCallRow label="Execute">
|
||||
<StatusIndicator status={status} text={titleText} />
|
||||
</ToolCallRow>
|
||||
|
||||
{/* Command */}
|
||||
{rawInput && (
|
||||
<ToolCallRow label="Command">
|
||||
<CodeBlock>{formatValue(rawInput)}</CodeBlock>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Standard output */}
|
||||
{textOutputs.length > 0 && (
|
||||
<ToolCallRow label="Output">
|
||||
<CodeBlock>{textOutputs.join('\n')}</CodeBlock>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Standard error / Errors */}
|
||||
{errors.length > 0 && (
|
||||
<ToolCallRow label="Error">
|
||||
<div style={{ color: '#c74e39' }}>
|
||||
<CodeBlock>{errors.join('\n')}</CodeBlock>
|
||||
</div>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Exit code or other execution details */}
|
||||
{otherData.length > 0 && (
|
||||
<ToolCallRow label="Details">
|
||||
<CodeBlock>
|
||||
{otherData.map((data: unknown) => formatValue(data)).join('\n\n')}
|
||||
</CodeBlock>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
</ToolCallCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Generic tool call component - handles all tool call types as fallback
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import type { BaseToolCallProps } from './shared/types.js';
|
||||
import {
|
||||
ToolCallCard,
|
||||
ToolCallRow,
|
||||
StatusIndicator,
|
||||
CodeBlock,
|
||||
LocationsList,
|
||||
DiffDisplay,
|
||||
} from './shared/LayoutComponents.js';
|
||||
import {
|
||||
formatValue,
|
||||
safeTitle,
|
||||
getKindIcon,
|
||||
groupContent,
|
||||
} from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* Generic tool call component that can display any tool call type
|
||||
* Used as fallback for unknown tool call kinds
|
||||
*/
|
||||
export const GenericToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
const { kind, title, status, rawInput, content, locations } = toolCall;
|
||||
const kindIcon = getKindIcon(kind);
|
||||
const titleText = safeTitle(title);
|
||||
|
||||
// Group content by type
|
||||
const { textOutputs, errors, diffs, otherData } = groupContent(content);
|
||||
|
||||
return (
|
||||
<ToolCallCard icon={kindIcon}>
|
||||
{/* Title row */}
|
||||
<ToolCallRow label="Tool">
|
||||
<StatusIndicator status={status} text={titleText} />
|
||||
</ToolCallRow>
|
||||
|
||||
{/* Input row */}
|
||||
{rawInput && (
|
||||
<ToolCallRow label="Input">
|
||||
<CodeBlock>{formatValue(rawInput)}</CodeBlock>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Locations row */}
|
||||
{locations && locations.length > 0 && (
|
||||
<ToolCallRow label="Files">
|
||||
<LocationsList locations={locations} />
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Output row - combined text outputs */}
|
||||
{textOutputs.length > 0 && (
|
||||
<ToolCallRow label="Output">
|
||||
<CodeBlock>{textOutputs.join('\n')}</CodeBlock>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Error row - combined errors */}
|
||||
{errors.length > 0 && (
|
||||
<ToolCallRow label="Error">
|
||||
<div style={{ color: '#c74e39' }}>{errors.join('\n')}</div>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Diff rows */}
|
||||
{diffs.map(
|
||||
(item: import('./shared/types.js').ToolCallContent, idx: number) => (
|
||||
<ToolCallRow key={`diff-${idx}`} label="Diff">
|
||||
<DiffDisplay
|
||||
path={item.path}
|
||||
oldText={item.oldText}
|
||||
newText={item.newText}
|
||||
/>
|
||||
</ToolCallRow>
|
||||
),
|
||||
)}
|
||||
|
||||
{/* Other data rows */}
|
||||
{otherData.length > 0 && (
|
||||
<ToolCallRow label="Data">
|
||||
<CodeBlock>
|
||||
{otherData.map((data: unknown) => formatValue(data)).join('\n\n')}
|
||||
</CodeBlock>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
</ToolCallCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Read tool call component - specialized for file reading operations
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import type { BaseToolCallProps } from './shared/types.js';
|
||||
import {
|
||||
ToolCallCard,
|
||||
ToolCallRow,
|
||||
StatusIndicator,
|
||||
CodeBlock,
|
||||
LocationsList,
|
||||
} from './shared/LayoutComponents.js';
|
||||
import { formatValue, safeTitle, groupContent } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* Specialized component for Read tool calls
|
||||
* Optimized for displaying file reading operations
|
||||
*/
|
||||
export const ReadToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
const { title, status, rawInput, content, locations } = toolCall;
|
||||
const titleText = safeTitle(title);
|
||||
|
||||
// Group content by type
|
||||
const { textOutputs, errors, otherData } = groupContent(content);
|
||||
|
||||
return (
|
||||
<ToolCallCard icon="📖">
|
||||
{/* Title row */}
|
||||
<ToolCallRow label="Read">
|
||||
<StatusIndicator status={status} text={titleText} />
|
||||
</ToolCallRow>
|
||||
|
||||
{/* File path(s) */}
|
||||
{locations && locations.length > 0 && (
|
||||
<ToolCallRow label="File">
|
||||
<LocationsList locations={locations} />
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Input parameters (e.g., line range, offset) */}
|
||||
{rawInput && (
|
||||
<ToolCallRow label="Options">
|
||||
<CodeBlock>{formatValue(rawInput)}</CodeBlock>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* File content output */}
|
||||
{textOutputs.length > 0 && (
|
||||
<ToolCallRow label="Content">
|
||||
<CodeBlock>{textOutputs.join('\n')}</CodeBlock>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Error handling */}
|
||||
{errors.length > 0 && (
|
||||
<ToolCallRow label="Error">
|
||||
<div style={{ color: '#c74e39' }}>{errors.join('\n')}</div>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Other data */}
|
||||
{otherData.length > 0 && (
|
||||
<ToolCallRow label="Details">
|
||||
<CodeBlock>
|
||||
{otherData.map((data: unknown) => formatValue(data)).join('\n\n')}
|
||||
</CodeBlock>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
</ToolCallCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Search tool call component - specialized for search operations
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import type { BaseToolCallProps } from './shared/types.js';
|
||||
import {
|
||||
ToolCallCard,
|
||||
ToolCallRow,
|
||||
StatusIndicator,
|
||||
CodeBlock,
|
||||
LocationsList,
|
||||
} from './shared/LayoutComponents.js';
|
||||
import { formatValue, safeTitle, groupContent } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* Specialized component for Search tool calls
|
||||
* Optimized for displaying search operations and results
|
||||
*/
|
||||
export const SearchToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
const { title, status, rawInput, content, locations } = toolCall;
|
||||
const titleText = safeTitle(title);
|
||||
|
||||
// Group content by type
|
||||
const { textOutputs, errors, otherData } = groupContent(content);
|
||||
|
||||
return (
|
||||
<ToolCallCard icon="🔍">
|
||||
{/* Title row */}
|
||||
<ToolCallRow label="Search">
|
||||
<StatusIndicator status={status} text={titleText} />
|
||||
</ToolCallRow>
|
||||
|
||||
{/* Search query/pattern */}
|
||||
{rawInput && (
|
||||
<ToolCallRow label="Query">
|
||||
<CodeBlock>{formatValue(rawInput)}</CodeBlock>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Search results - files found */}
|
||||
{locations && locations.length > 0 && (
|
||||
<ToolCallRow label="Results">
|
||||
<LocationsList locations={locations} />
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Search output details */}
|
||||
{textOutputs.length > 0 && (
|
||||
<ToolCallRow label="Matches">
|
||||
<CodeBlock>{textOutputs.join('\n')}</CodeBlock>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Error handling */}
|
||||
{errors.length > 0 && (
|
||||
<ToolCallRow label="Error">
|
||||
<div style={{ color: '#c74e39' }}>{errors.join('\n')}</div>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Other search metadata */}
|
||||
{otherData.length > 0 && (
|
||||
<ToolCallRow label="Details">
|
||||
<CodeBlock>
|
||||
{otherData.map((data: unknown) => formatValue(data)).join('\n\n')}
|
||||
</CodeBlock>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
</ToolCallCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Think tool call component - specialized for thinking/reasoning operations
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import type { BaseToolCallProps } from './shared/types.js';
|
||||
import {
|
||||
ToolCallCard,
|
||||
ToolCallRow,
|
||||
StatusIndicator,
|
||||
CodeBlock,
|
||||
} from './shared/LayoutComponents.js';
|
||||
import { formatValue, safeTitle, groupContent } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* Specialized component for Think tool calls
|
||||
* Optimized for displaying AI reasoning and thought processes
|
||||
*/
|
||||
export const ThinkToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
const { title, status, rawInput, content } = toolCall;
|
||||
const titleText = safeTitle(title);
|
||||
|
||||
// Group content by type
|
||||
const { textOutputs, errors, otherData } = groupContent(content);
|
||||
|
||||
return (
|
||||
<ToolCallCard icon="💭">
|
||||
{/* Title row */}
|
||||
<ToolCallRow label="Thinking">
|
||||
<StatusIndicator status={status} text={titleText} />
|
||||
</ToolCallRow>
|
||||
|
||||
{/* Thinking context/prompt */}
|
||||
{rawInput && (
|
||||
<ToolCallRow label="Context">
|
||||
<CodeBlock>{formatValue(rawInput)}</CodeBlock>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Thought content */}
|
||||
{textOutputs.length > 0 && (
|
||||
<ToolCallRow label="Thoughts">
|
||||
<div style={{ fontStyle: 'italic', opacity: 0.95 }}>
|
||||
{textOutputs.join('\n\n')}
|
||||
</div>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Error handling */}
|
||||
{errors.length > 0 && (
|
||||
<ToolCallRow label="Error">
|
||||
<div style={{ color: '#c74e39' }}>{errors.join('\n')}</div>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Other reasoning data */}
|
||||
{otherData.length > 0 && (
|
||||
<ToolCallRow label="Details">
|
||||
<CodeBlock>
|
||||
{otherData.map((data: unknown) => formatValue(data)).join('\n\n')}
|
||||
</CodeBlock>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
</ToolCallCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Write/Edit tool call component - specialized for file writing and editing operations
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import type { BaseToolCallProps } from './shared/types.js';
|
||||
import {
|
||||
ToolCallCard,
|
||||
ToolCallRow,
|
||||
StatusIndicator,
|
||||
CodeBlock,
|
||||
LocationsList,
|
||||
DiffDisplay,
|
||||
} from './shared/LayoutComponents.js';
|
||||
import { formatValue, safeTitle, groupContent } from './shared/utils.js';
|
||||
|
||||
/**
|
||||
* Specialized component for Write/Edit tool calls
|
||||
* Optimized for displaying file writing and editing operations with diffs
|
||||
*/
|
||||
export const WriteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
const { kind, title, status, rawInput, content, locations } = toolCall;
|
||||
const titleText = safeTitle(title);
|
||||
const isEdit = kind.toLowerCase() === 'edit';
|
||||
|
||||
// Group content by type
|
||||
const { textOutputs, errors, diffs, otherData } = groupContent(content);
|
||||
|
||||
return (
|
||||
<ToolCallCard icon="✏️">
|
||||
{/* Title row */}
|
||||
<ToolCallRow label={isEdit ? 'Edit' : 'Write'}>
|
||||
<StatusIndicator status={status} text={titleText} />
|
||||
</ToolCallRow>
|
||||
|
||||
{/* File path(s) */}
|
||||
{locations && locations.length > 0 && (
|
||||
<ToolCallRow label="File">
|
||||
<LocationsList locations={locations} />
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Input parameters (e.g., old_string, new_string for edits) */}
|
||||
{rawInput && (
|
||||
<ToolCallRow label="Changes">
|
||||
<CodeBlock>{formatValue(rawInput)}</CodeBlock>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Diff display - most important for write/edit operations */}
|
||||
{diffs.map(
|
||||
(item: import('./shared/types.js').ToolCallContent, idx: number) => (
|
||||
<ToolCallRow key={`diff-${idx}`} label="Diff">
|
||||
<DiffDisplay
|
||||
path={item.path}
|
||||
oldText={item.oldText}
|
||||
newText={item.newText}
|
||||
/>
|
||||
</ToolCallRow>
|
||||
),
|
||||
)}
|
||||
|
||||
{/* Success message or output */}
|
||||
{textOutputs.length > 0 && (
|
||||
<ToolCallRow label="Result">
|
||||
<CodeBlock>{textOutputs.join('\n')}</CodeBlock>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Error handling */}
|
||||
{errors.length > 0 && (
|
||||
<ToolCallRow label="Error">
|
||||
<div style={{ color: '#c74e39' }}>{errors.join('\n')}</div>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
|
||||
{/* Other data */}
|
||||
{otherData.length > 0 && (
|
||||
<ToolCallRow label="Details">
|
||||
<CodeBlock>
|
||||
{otherData.map((data: unknown) => formatValue(data)).join('\n\n')}
|
||||
</CodeBlock>
|
||||
</ToolCallRow>
|
||||
)}
|
||||
</ToolCallCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Tool call component factory - routes to specialized components by kind
|
||||
*/
|
||||
|
||||
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 './ReadToolCall.js';
|
||||
import { WriteToolCall } from './WriteToolCall.js';
|
||||
import { ExecuteToolCall } from './ExecuteToolCall.js';
|
||||
import { SearchToolCall } from './SearchToolCall.js';
|
||||
import { ThinkToolCall } from './ThinkToolCall.js';
|
||||
|
||||
/**
|
||||
* Factory function that returns the appropriate tool call component based on kind
|
||||
*/
|
||||
export const getToolCallComponent = (
|
||||
kind: string,
|
||||
): React.FC<BaseToolCallProps> => {
|
||||
const normalizedKind = kind.toLowerCase();
|
||||
|
||||
// Route to specialized components
|
||||
switch (normalizedKind) {
|
||||
case 'read':
|
||||
return ReadToolCall;
|
||||
|
||||
case 'write':
|
||||
case 'edit':
|
||||
return WriteToolCall;
|
||||
|
||||
case 'execute':
|
||||
case 'bash':
|
||||
case 'command':
|
||||
return ExecuteToolCall;
|
||||
|
||||
case 'search':
|
||||
case 'grep':
|
||||
case 'glob':
|
||||
case 'find':
|
||||
return SearchToolCall;
|
||||
|
||||
case 'think':
|
||||
case 'thinking':
|
||||
return ThinkToolCall;
|
||||
|
||||
// Add more specialized components as needed
|
||||
// case 'fetch':
|
||||
// return FetchToolCall;
|
||||
// case 'delete':
|
||||
// return DeleteToolCall;
|
||||
|
||||
default:
|
||||
// Fallback to generic component
|
||||
return GenericToolCall;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Main tool call component that routes to specialized implementations
|
||||
*/
|
||||
export const ToolCallRouter: React.FC<BaseToolCallProps> = ({ toolCall }) => {
|
||||
// Check if we should show this tool call (hide internal ones)
|
||||
if (!shouldShowToolCall(toolCall.kind)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the appropriate component for this kind
|
||||
const Component = getToolCallComponent(toolCall.kind);
|
||||
|
||||
// Render the specialized component
|
||||
return <Component toolCall={toolCall} />;
|
||||
};
|
||||
|
||||
// Re-export types for convenience
|
||||
export type { BaseToolCallProps, ToolCallData } from './shared/types.js';
|
||||
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Shared layout components for tool call UI
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
|
||||
/**
|
||||
* Props for ToolCallCard wrapper
|
||||
*/
|
||||
interface ToolCallCardProps {
|
||||
icon: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main card wrapper with icon
|
||||
*/
|
||||
export const ToolCallCard: React.FC<ToolCallCardProps> = ({
|
||||
icon,
|
||||
children,
|
||||
}) => (
|
||||
<div className="tool-call-card">
|
||||
<div className="tool-call-icon">{icon}</div>
|
||||
<div className="tool-call-grid">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* Props for ToolCallRow
|
||||
*/
|
||||
interface ToolCallRowProps {
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* A single row in the tool call grid
|
||||
*/
|
||||
export const ToolCallRow: React.FC<ToolCallRowProps> = ({
|
||||
label,
|
||||
children,
|
||||
}) => (
|
||||
<div className="tool-call-row">
|
||||
<div className="tool-call-label">{label}</div>
|
||||
<div className="tool-call-value">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* Props for StatusIndicator
|
||||
*/
|
||||
interface StatusIndicatorProps {
|
||||
status: 'pending' | 'in_progress' | 'completed' | 'failed';
|
||||
text: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Status indicator with colored dot
|
||||
*/
|
||||
export const StatusIndicator: React.FC<StatusIndicatorProps> = ({
|
||||
status,
|
||||
text,
|
||||
}) => (
|
||||
<div className={`tool-call-status-indicator ${status}`} title={status}>
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* Props for CodeBlock
|
||||
*/
|
||||
interface CodeBlockProps {
|
||||
children: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Code block for displaying formatted code or output
|
||||
*/
|
||||
export const CodeBlock: React.FC<CodeBlockProps> = ({ children }) => (
|
||||
<pre className="code-block">{children}</pre>
|
||||
);
|
||||
|
||||
/**
|
||||
* Props for LocationsList
|
||||
*/
|
||||
interface LocationsListProps {
|
||||
locations: Array<{
|
||||
path: string;
|
||||
line?: number | null;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* List of file locations
|
||||
*/
|
||||
export const LocationsList: React.FC<LocationsListProps> = ({ locations }) => (
|
||||
<>
|
||||
{locations.map((loc, idx) => (
|
||||
<div key={idx}>
|
||||
{loc.path}
|
||||
{loc.line !== null && loc.line !== undefined && `:${loc.line}`}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
/**
|
||||
* Props for DiffDisplay
|
||||
*/
|
||||
interface DiffDisplayProps {
|
||||
path?: string;
|
||||
oldText?: string | null;
|
||||
newText?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display diff with before/after sections
|
||||
*/
|
||||
export const DiffDisplay: React.FC<DiffDisplayProps> = ({
|
||||
path,
|
||||
oldText,
|
||||
newText,
|
||||
}) => (
|
||||
<div>
|
||||
<div>
|
||||
<strong>{path || 'Unknown file'}</strong>
|
||||
</div>
|
||||
{oldText !== undefined && (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
opacity: 0.5,
|
||||
fontSize: '0.85em',
|
||||
marginTop: '4px',
|
||||
}}
|
||||
>
|
||||
Before:
|
||||
</div>
|
||||
<pre className="code-block">{oldText || '(empty)'}</pre>
|
||||
</div>
|
||||
)}
|
||||
{newText !== undefined && (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
opacity: 0.5,
|
||||
fontSize: '0.85em',
|
||||
marginTop: '4px',
|
||||
}}
|
||||
>
|
||||
After:
|
||||
</div>
|
||||
<pre className="code-block">{newText}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Shared types for tool call components
|
||||
*/
|
||||
|
||||
/**
|
||||
* Tool call content types
|
||||
*/
|
||||
export interface ToolCallContent {
|
||||
type: 'content' | 'diff';
|
||||
// For content type
|
||||
content?: {
|
||||
type: string;
|
||||
text?: string;
|
||||
error?: unknown;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
// For diff type
|
||||
path?: string;
|
||||
oldText?: string | null;
|
||||
newText?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool call location type
|
||||
*/
|
||||
export interface ToolCallLocation {
|
||||
path: string;
|
||||
line?: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool call status type
|
||||
*/
|
||||
export type ToolCallStatus = 'pending' | 'in_progress' | 'completed' | 'failed';
|
||||
|
||||
/**
|
||||
* Base tool call data interface
|
||||
*/
|
||||
export interface ToolCallData {
|
||||
toolCallId: string;
|
||||
kind: string;
|
||||
title: string | object;
|
||||
status: ToolCallStatus;
|
||||
rawInput?: string | object;
|
||||
content?: ToolCallContent[];
|
||||
locations?: ToolCallLocation[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Base props for all tool call components
|
||||
*/
|
||||
export interface BaseToolCallProps {
|
||||
toolCall: ToolCallData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Grouped content structure for rendering
|
||||
*/
|
||||
export interface GroupedContent {
|
||||
textOutputs: string[];
|
||||
errors: string[];
|
||||
diffs: ToolCallContent[];
|
||||
otherData: unknown[];
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Shared utility functions for tool call components
|
||||
*/
|
||||
|
||||
import type { ToolCallContent, GroupedContent } from './types.js';
|
||||
|
||||
/**
|
||||
* Format any value to a string for display
|
||||
*/
|
||||
export const formatValue = (value: unknown): string => {
|
||||
if (value === null || value === undefined) {
|
||||
return '';
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
try {
|
||||
return JSON.stringify(value, null, 2);
|
||||
} catch (_e) {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Safely convert title to string, handling object types
|
||||
*/
|
||||
export const safeTitle = (title: unknown): string => {
|
||||
if (typeof title === 'string') {
|
||||
return title;
|
||||
}
|
||||
if (title && typeof title === 'object') {
|
||||
return JSON.stringify(title);
|
||||
}
|
||||
return 'Tool Call';
|
||||
};
|
||||
|
||||
/**
|
||||
* Get icon emoji for a given tool kind
|
||||
*/
|
||||
export const getKindIcon = (kind: string): string => {
|
||||
const kindMap: Record<string, string> = {
|
||||
edit: '✏️',
|
||||
write: '✏️',
|
||||
read: '📖',
|
||||
execute: '⚡',
|
||||
fetch: '🌐',
|
||||
delete: '🗑️',
|
||||
move: '📦',
|
||||
search: '🔍',
|
||||
think: '💭',
|
||||
diff: '📝',
|
||||
};
|
||||
return kindMap[kind.toLowerCase()] || '🔧';
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a tool call should be displayed
|
||||
* Hides internal tool calls
|
||||
*/
|
||||
export const shouldShowToolCall = (kind: string): boolean =>
|
||||
!kind.includes('internal');
|
||||
|
||||
/**
|
||||
* Group tool call content by type to avoid duplicate labels
|
||||
*/
|
||||
export const groupContent = (content?: ToolCallContent[]): GroupedContent => {
|
||||
const textOutputs: string[] = [];
|
||||
const errors: string[] = [];
|
||||
const diffs: ToolCallContent[] = [];
|
||||
const otherData: unknown[] = [];
|
||||
|
||||
content?.forEach((item) => {
|
||||
if (item.type === 'diff') {
|
||||
diffs.push(item);
|
||||
} else if (item.content) {
|
||||
const contentObj = item.content;
|
||||
|
||||
// Handle error content
|
||||
if (contentObj.type === 'error' || 'error' in contentObj) {
|
||||
const errorMsg =
|
||||
formatValue(contentObj.error) ||
|
||||
formatValue(contentObj.text) ||
|
||||
'An error occurred';
|
||||
errors.push(errorMsg);
|
||||
}
|
||||
// Handle text content
|
||||
else if (contentObj.text) {
|
||||
textOutputs.push(formatValue(contentObj.text));
|
||||
}
|
||||
// Handle other content
|
||||
else {
|
||||
otherData.push(contentObj);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { textOutputs, errors, diffs, otherData };
|
||||
};
|
||||
Reference in New Issue
Block a user