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 {
|
try {
|
||||||
console.log('[WebViewProvider] Switching to Qwen session:', sessionId);
|
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
|
// Get session messages from local files
|
||||||
const messages = await this.agentManager.getSessionMessages(sessionId);
|
const messages = await this.agentManager.getSessionMessages(sessionId);
|
||||||
console.log(
|
console.log(
|
||||||
@@ -758,44 +754,38 @@ export class WebViewProvider {
|
|||||||
console.log('[WebViewProvider] Could not get session details:', err);
|
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 {
|
try {
|
||||||
await this.agentManager.switchToSession(sessionId);
|
const newAcpSessionId =
|
||||||
console.log('[WebViewProvider] Session switched successfully in ACP');
|
await this.agentManager.createNewSession(workingDir);
|
||||||
} catch (_switchError) {
|
|
||||||
console.log(
|
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];
|
// Use the NEW ACP session ID for sending messages to CLI
|
||||||
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
|
this.currentConversationId = newAcpSessionId;
|
||||||
try {
|
console.log(
|
||||||
const newSessionId =
|
'[WebViewProvider] Set currentConversationId (ACP) to:',
|
||||||
await this.agentManager.createNewSession(workingDir);
|
newAcpSessionId,
|
||||||
console.log(
|
);
|
||||||
'[WebViewProvider] Created new session as fallback:',
|
} catch (createError) {
|
||||||
newSessionId,
|
console.error(
|
||||||
);
|
'[WebViewProvider] Failed to create new ACP session:',
|
||||||
if (newSessionId) {
|
createError,
|
||||||
// Update to the new session ID so messages can be sent
|
);
|
||||||
this.currentConversationId = newSessionId;
|
vscode.window.showWarningMessage(
|
||||||
console.log(
|
'Could not switch to session. Created new session instead.',
|
||||||
'[WebViewProvider] Updated currentConversationId to new session:',
|
);
|
||||||
newSessionId,
|
throw createError;
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (newSessionError) {
|
|
||||||
console.error(
|
|
||||||
'[WebViewProvider] Failed to create new session:',
|
|
||||||
newSessionError,
|
|
||||||
);
|
|
||||||
vscode.window.showWarningMessage(
|
|
||||||
'Could not switch to session. Created new session instead.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send messages and session details to WebView
|
// Send messages and session details to WebView
|
||||||
|
// The historical messages are display-only, not sent to CLI
|
||||||
this.sendMessageToWebView({
|
this.sendMessageToWebView({
|
||||||
type: 'qwenSessionSwitched',
|
type: 'qwenSessionSwitched',
|
||||||
data: { sessionId, messages, session: sessionDetails },
|
data: { sessionId, messages, session: sessionDetails },
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ button {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
padding: 20px 20px 40px;
|
padding: 20px 20px 120px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--app-spacing-medium);
|
gap: var(--app-spacing-medium);
|
||||||
@@ -686,3 +686,330 @@ button {
|
|||||||
height: 20px;
|
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;
|
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 = () => {
|
export const App: React.FC = () => {
|
||||||
const vscode = useVSCode();
|
const vscode = useVSCode();
|
||||||
const [messages, setMessages] = useState<TextMessage[]>([]);
|
const [messages, setMessages] = useState<TextMessage[]>([]);
|
||||||
const [inputText, setInputText] = useState('');
|
const [inputText, setInputText] = useState('');
|
||||||
const [isStreaming, setIsStreaming] = useState(false);
|
const [isStreaming, setIsStreaming] = useState(false);
|
||||||
|
const [_isWaitingForResponse, setIsWaitingForResponse] = useState(false);
|
||||||
|
const [_loadingMessage, setLoadingMessage] = useState('');
|
||||||
const [currentStreamContent, setCurrentStreamContent] = useState('');
|
const [currentStreamContent, setCurrentStreamContent] = useState('');
|
||||||
const [qwenSessions, setQwenSessions] = useState<
|
const [qwenSessions, setQwenSessions] = useState<
|
||||||
Array<Record<string, unknown>>
|
Array<Record<string, unknown>>
|
||||||
@@ -96,6 +237,17 @@ export const App: React.FC = () => {
|
|||||||
const newMap = new Map(prev);
|
const newMap = new Map(prev);
|
||||||
const existing = newMap.get(update.toolCallId);
|
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') {
|
if (update.type === 'tool_call') {
|
||||||
// New tool call - cast content to proper type
|
// New tool call - cast content to proper type
|
||||||
const content = update.content?.map((item) => ({
|
const content = update.content?.map((item) => ({
|
||||||
@@ -109,7 +261,7 @@ export const App: React.FC = () => {
|
|||||||
newMap.set(update.toolCallId, {
|
newMap.set(update.toolCallId, {
|
||||||
toolCallId: update.toolCallId,
|
toolCallId: update.toolCallId,
|
||||||
kind: update.kind || 'other',
|
kind: update.kind || 'other',
|
||||||
title: update.title || 'Tool Call',
|
title: safeTitle(update.title),
|
||||||
status: update.status || 'pending',
|
status: update.status || 'pending',
|
||||||
rawInput: update.rawInput as string | object | undefined,
|
rawInput: update.rawInput as string | object | undefined,
|
||||||
content,
|
content,
|
||||||
@@ -130,7 +282,7 @@ export const App: React.FC = () => {
|
|||||||
newMap.set(update.toolCallId, {
|
newMap.set(update.toolCallId, {
|
||||||
...existing,
|
...existing,
|
||||||
...(update.kind && { kind: update.kind }),
|
...(update.kind && { kind: update.kind }),
|
||||||
...(update.title && { title: update.title }),
|
...(update.title && { title: safeTitle(update.title) }),
|
||||||
...(update.status && { status: update.status }),
|
...(update.status && { status: update.status }),
|
||||||
...(updatedContent && { content: updatedContent }),
|
...(updatedContent && { content: updatedContent }),
|
||||||
...(update.locations && { locations: update.locations }),
|
...(update.locations && { locations: update.locations }),
|
||||||
@@ -286,6 +438,10 @@ export const App: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set waiting state with random loading message
|
||||||
|
setIsWaitingForResponse(true);
|
||||||
|
setLoadingMessage(getRandomLoadingMessage());
|
||||||
|
|
||||||
vscode.postMessage({
|
vscode.postMessage({
|
||||||
type: 'sendMessage',
|
type: 'sendMessage',
|
||||||
data: { text: inputText },
|
data: { text: inputText },
|
||||||
|
|||||||
@@ -270,4 +270,135 @@
|
|||||||
--app-menu-foreground: var(--vscode-menu-foreground);
|
--app-menu-foreground: var(--vscode-menu-foreground);
|
||||||
--app-menu-selection-background: var(--vscode-menu-selectionBackground);
|
--app-menu-selection-background: var(--vscode-menu-selectionBackground);
|
||||||
--app-menu-selection-foreground: var(--vscode-menu-selectionForeground);
|
--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
|
* @license
|
||||||
* Copyright 2025 Qwen Team
|
* Copyright 2025 Qwen Team
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* 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 type React from 'react';
|
||||||
|
import { ToolCallRouter } from './toolcalls/index.js';
|
||||||
|
|
||||||
export interface ToolCallContent {
|
// Re-export types from the toolcalls module for backward compatibility
|
||||||
type: 'content' | 'diff';
|
export type {
|
||||||
// For content type
|
ToolCallData,
|
||||||
content?: {
|
BaseToolCallProps as ToolCallProps,
|
||||||
type: string;
|
} from './toolcalls/shared/types.js';
|
||||||
text?: string;
|
|
||||||
[key: string]: unknown;
|
|
||||||
};
|
|
||||||
// For diff type
|
|
||||||
path?: string;
|
|
||||||
oldText?: string | null;
|
|
||||||
newText?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ToolCallData {
|
// Re-export the content type for external use
|
||||||
toolCallId: string;
|
export type { ToolCallContent } from './toolcalls/shared/types.js';
|
||||||
kind: string;
|
|
||||||
title: string;
|
|
||||||
status: 'pending' | 'in_progress' | 'completed' | 'failed';
|
|
||||||
rawInput?: string | object;
|
|
||||||
content?: ToolCallContent[];
|
|
||||||
locations?: Array<{
|
|
||||||
path: string;
|
|
||||||
line?: number | null;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ToolCallProps {
|
/**
|
||||||
toolCall: ToolCallData;
|
* Main ToolCall component
|
||||||
}
|
* Routes to specialized components based on the tool call kind
|
||||||
|
*
|
||||||
const StatusTag: React.FC<{ status: string }> = ({ status }) => {
|
* Supported kinds:
|
||||||
const getStatusInfo = () => {
|
* - read: File reading operations
|
||||||
switch (status) {
|
* - write/edit: File writing and editing operations
|
||||||
case 'pending':
|
* - execute/bash/command: Command execution
|
||||||
return { className: 'status-pending', text: 'Pending', icon: '⏳' };
|
* - search/grep/glob/find: Search operations
|
||||||
case 'in_progress':
|
* - think/thinking: AI reasoning
|
||||||
return {
|
* - All others: Generic display
|
||||||
className: 'status-in-progress',
|
*/
|
||||||
text: 'In Progress',
|
export const ToolCall: React.FC<{
|
||||||
icon: '🔄',
|
toolCall: import('./toolcalls/shared/types.js').ToolCallData;
|
||||||
};
|
}> = ({ toolCall }) => <ToolCallRouter toolCall={toolCall} />;
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -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