@blackbox_ai/blackbox-cli 0.8.1 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -203
- package/README.md +14 -16
- package/dist/package.json +2 -2
- package/dist/src/commands/update.js +2 -1
- package/dist/src/commands/update.js.map +1 -1
- package/dist/src/config/auth.d.ts +4 -1
- package/dist/src/config/auth.js +36 -0
- package/dist/src/config/auth.js.map +1 -1
- package/dist/src/config/modelFetcher.js +1 -0
- package/dist/src/config/modelFetcher.js.map +1 -1
- package/dist/src/config/settingsSchema.d.ts +58 -0
- package/dist/src/config/settingsSchema.js +59 -1
- package/dist/src/config/settingsSchema.js.map +1 -1
- package/dist/src/generated/git-commit.d.ts +2 -2
- package/dist/src/generated/git-commit.js +2 -2
- package/dist/src/ui/App.js +18 -46
- package/dist/src/ui/App.js.map +1 -1
- package/dist/src/ui/commands/chatCommand.js +512 -31
- package/dist/src/ui/commands/chatCommand.js.map +1 -1
- package/dist/src/ui/commands/quitCommand.js +7 -33
- package/dist/src/ui/commands/quitCommand.js.map +1 -1
- package/dist/src/ui/components/AuthDialog.js +52 -13
- package/dist/src/ui/components/AuthDialog.js.map +1 -1
- package/dist/src/ui/components/HistoryBrowserDialog.js +65 -8
- package/dist/src/ui/components/HistoryBrowserDialog.js.map +1 -1
- package/dist/src/ui/contexts/SessionContext.d.ts +16 -0
- package/dist/src/ui/contexts/SessionContext.js +43 -3
- package/dist/src/ui/contexts/SessionContext.js.map +1 -1
- package/dist/src/ui/hooks/atCommandProcessor.js +5 -69
- package/dist/src/ui/hooks/atCommandProcessor.js.map +1 -1
- package/dist/src/ui/hooks/slashCommandProcessor.js +16 -53
- package/dist/src/ui/hooks/slashCommandProcessor.js.map +1 -1
- package/dist/src/ui/hooks/useEncryptedStream.js +58 -1
- package/dist/src/ui/hooks/useEncryptedStream.js.map +1 -1
- package/dist/src/ui/hooks/useGeminiStream.js +118 -1
- package/dist/src/ui/hooks/useGeminiStream.js.map +1 -1
- package/dist/src/ui/models/availableModels.d.ts +1 -1
- package/dist/src/ui/models/availableModels.js +1 -1
- package/dist/src/ui/models/availableModels.js.map +1 -1
- package/dist/src/utils/versionStorage.d.ts +3 -1
- package/dist/src/utils/versionStorage.js +4 -2
- package/dist/src/utils/versionStorage.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
|
@@ -8,9 +8,229 @@ import React from 'react';
|
|
|
8
8
|
import { Text } from 'ink';
|
|
9
9
|
import { Colors } from '../colors.js';
|
|
10
10
|
import { CommandKind } from './types.js';
|
|
11
|
-
import { decodeTagName, CheckpointApiService } from '@blackbox_ai/blackbox-cli-core';
|
|
11
|
+
import { decodeTagName, CheckpointApiService, formatThinkContent, detectModelType, createEncryptedToolParser, } from '@blackbox_ai/blackbox-cli-core';
|
|
12
12
|
import path from 'node:path';
|
|
13
|
-
import { MessageType } from '../types.js';
|
|
13
|
+
import { MessageType, ToolCallStatus } from '../types.js';
|
|
14
|
+
/**
|
|
15
|
+
* Check if text contains encrypted provider tool call markup
|
|
16
|
+
* This includes MiniMax, DeepSeek, Qwen formats and think tags
|
|
17
|
+
*/
|
|
18
|
+
function hasEncryptedProviderMarkup(text) {
|
|
19
|
+
// Check for various encrypted provider tool call formats
|
|
20
|
+
const patterns = [
|
|
21
|
+
/<minimax:tool_call>/,
|
|
22
|
+
/<invoke name=/,
|
|
23
|
+
/<|tool▁calls▁begin|>/,
|
|
24
|
+
/<|tool▁call▁begin|>/,
|
|
25
|
+
/<|tool▁sep|>/,
|
|
26
|
+
/<tool_call>/,
|
|
27
|
+
/<think>/,
|
|
28
|
+
// Bare tool call patterns
|
|
29
|
+
/<read_file[\s>]/,
|
|
30
|
+
/<glob[\s>]/,
|
|
31
|
+
/<search_files[\s>]/,
|
|
32
|
+
/<list_files[\s>]/,
|
|
33
|
+
/<create_file[\s>]/,
|
|
34
|
+
/<edit_file[\s>]/,
|
|
35
|
+
/<execute_command[\s>]/,
|
|
36
|
+
/<browser_action[\s>]/,
|
|
37
|
+
/<ask_followup_question[\s>]/,
|
|
38
|
+
/<attempt_completion[\s>]/,
|
|
39
|
+
/<new_task[\s>]/,
|
|
40
|
+
/<retrieve_knowledge[\s>]/,
|
|
41
|
+
/<web_fetch[\s>]/,
|
|
42
|
+
// Pipe-delimited patterns
|
|
43
|
+
/<\|read_file\|>/,
|
|
44
|
+
/<\|glob\|>/,
|
|
45
|
+
/<\|search_files\|>/,
|
|
46
|
+
/<\|list_files\|>/,
|
|
47
|
+
/<\|create_file\|>/,
|
|
48
|
+
/<\|edit_file\|>/,
|
|
49
|
+
/<\|execute_command\|>/,
|
|
50
|
+
/<\|browser_action\|>/,
|
|
51
|
+
/<\|ask_followup_question\|>/,
|
|
52
|
+
/<\|attempt_completion\|>/,
|
|
53
|
+
/<\|new_task\|>/,
|
|
54
|
+
/<\|retrieve_knowledge\|>/,
|
|
55
|
+
/<\|web_fetch\|>/,
|
|
56
|
+
];
|
|
57
|
+
return patterns.some(pattern => pattern.test(text));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Extract think content from text and return formatted think content and remaining text
|
|
61
|
+
*/
|
|
62
|
+
function extractAndFormatThinkContent(text) {
|
|
63
|
+
const thinkRegex = /<think>([\s\S]*?)<\/think>/g;
|
|
64
|
+
let thinkContent = null;
|
|
65
|
+
let remainingText = text;
|
|
66
|
+
const matches = text.match(thinkRegex);
|
|
67
|
+
if (matches && matches.length > 0) {
|
|
68
|
+
// Extract all think content
|
|
69
|
+
const thinkParts = [];
|
|
70
|
+
let match;
|
|
71
|
+
while ((match = thinkRegex.exec(text)) !== null) {
|
|
72
|
+
thinkParts.push(match[1].trim());
|
|
73
|
+
}
|
|
74
|
+
if (thinkParts.length > 0) {
|
|
75
|
+
thinkContent = formatThinkContent(thinkParts.join('\n\n'));
|
|
76
|
+
}
|
|
77
|
+
// Remove think tags from remaining text
|
|
78
|
+
remainingText = text.replace(thinkRegex, '').trim();
|
|
79
|
+
}
|
|
80
|
+
return { thinkContent, remainingText };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Default tool declarations for parsing encrypted provider tool calls
|
|
84
|
+
* These are the common tools used in the CLI
|
|
85
|
+
*/
|
|
86
|
+
const DEFAULT_TOOL_DECLARATIONS = [
|
|
87
|
+
{ name: 'read_file' },
|
|
88
|
+
{ name: 'glob' },
|
|
89
|
+
{ name: 'search_files' },
|
|
90
|
+
{ name: 'list_files' },
|
|
91
|
+
{ name: 'create_file' },
|
|
92
|
+
{ name: 'edit_file' },
|
|
93
|
+
{ name: 'execute_command' },
|
|
94
|
+
{ name: 'browser_action' },
|
|
95
|
+
{ name: 'ask_followup_question' },
|
|
96
|
+
{ name: 'attempt_completion' },
|
|
97
|
+
{ name: 'new_task' },
|
|
98
|
+
{ name: 'retrieve_knowledge' },
|
|
99
|
+
{ name: 'web_search' },
|
|
100
|
+
{ name: 'web_fetch' },
|
|
101
|
+
{ name: 'shell' },
|
|
102
|
+
{ name: 'write_file' },
|
|
103
|
+
{ name: 'edit' },
|
|
104
|
+
{ name: 'todo_write' },
|
|
105
|
+
{ name: 'skill' },
|
|
106
|
+
];
|
|
107
|
+
/**
|
|
108
|
+
* Extract tool result from encrypted provider user message
|
|
109
|
+
* Format: "Function <tool_name> returned: {...}"
|
|
110
|
+
*/
|
|
111
|
+
function extractToolResultFromUserMessage(text) {
|
|
112
|
+
const results = new Map();
|
|
113
|
+
// Pattern to match "Function <tool_name> returned: {...}"
|
|
114
|
+
// The result can be JSON or plain text
|
|
115
|
+
const functionReturnPattern = /Function\s+(\w+)\s+returned:\s*(\{[\s\S]*?\}(?=\s*(?:Function\s+\w+\s+returned:|$))|[^\n]+)/gi;
|
|
116
|
+
let match;
|
|
117
|
+
while ((match = functionReturnPattern.exec(text)) !== null) {
|
|
118
|
+
const toolName = match[1];
|
|
119
|
+
let resultText = match[2].trim();
|
|
120
|
+
let isError = false;
|
|
121
|
+
// Try to parse as JSON to extract the actual output
|
|
122
|
+
try {
|
|
123
|
+
const parsed = JSON.parse(resultText);
|
|
124
|
+
if (parsed.output) {
|
|
125
|
+
resultText = parsed.output;
|
|
126
|
+
}
|
|
127
|
+
else if (parsed.error) {
|
|
128
|
+
resultText = parsed.error;
|
|
129
|
+
isError = true;
|
|
130
|
+
}
|
|
131
|
+
else if (parsed.result) {
|
|
132
|
+
resultText = parsed.result;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// Not JSON, use as-is
|
|
137
|
+
}
|
|
138
|
+
// Check for error indicators
|
|
139
|
+
if (resultText.toLowerCase().includes('error:') || resultText.toLowerCase().includes('failed')) {
|
|
140
|
+
isError = true;
|
|
141
|
+
}
|
|
142
|
+
results.set(toolName, { output: resultText, isError });
|
|
143
|
+
}
|
|
144
|
+
return results;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Parse encrypted provider text for tool calls and create tool_group UI items
|
|
148
|
+
* Returns the parsed tool calls as IndividualToolCallDisplay items and the remaining text
|
|
149
|
+
*/
|
|
150
|
+
function parseEncryptedProviderToolCalls(text, toolResults) {
|
|
151
|
+
const toolDisplays = [];
|
|
152
|
+
// First extract think content
|
|
153
|
+
const { thinkContent, remainingText: textAfterThink } = extractAndFormatThinkContent(text);
|
|
154
|
+
// Detect model type and parse tool calls
|
|
155
|
+
const modelType = detectModelType(textAfterThink);
|
|
156
|
+
const parser = createEncryptedToolParser();
|
|
157
|
+
const parseResult = parser.parseComplete(textAfterThink, DEFAULT_TOOL_DECLARATIONS, modelType);
|
|
158
|
+
if (parseResult.success && parseResult.toolCalls.length > 0) {
|
|
159
|
+
for (const toolCall of parseResult.toolCalls) {
|
|
160
|
+
const toolName = toolCall.name;
|
|
161
|
+
const args = toolCall.arguments;
|
|
162
|
+
// Create a description based on tool name and args
|
|
163
|
+
let description = toolName;
|
|
164
|
+
if (toolName === 'web_search' && args['query']) {
|
|
165
|
+
description = `Searching the web for: "${args['query']}"`;
|
|
166
|
+
}
|
|
167
|
+
else if (toolName === 'web_fetch' && args['url']) {
|
|
168
|
+
description = `Fetching URL: ${args['url']}`;
|
|
169
|
+
}
|
|
170
|
+
else if (toolName === 'read_file' && (args['file_path'] || args['path'])) {
|
|
171
|
+
description = `Reading file: ${args['file_path'] || args['path']}`;
|
|
172
|
+
}
|
|
173
|
+
else if ((toolName === 'write_file' || toolName === 'create_file') && (args['file_path'] || args['path'])) {
|
|
174
|
+
description = `Writing to file: ${args['file_path'] || args['path']}`;
|
|
175
|
+
}
|
|
176
|
+
else if ((toolName === 'shell' || toolName === 'execute_command') && args['command']) {
|
|
177
|
+
description = `Running command: ${args['command']}`;
|
|
178
|
+
}
|
|
179
|
+
else if (toolName === 'edit' && (args['file_path'] || args['path'])) {
|
|
180
|
+
description = `Editing file: ${args['file_path'] || args['path']}`;
|
|
181
|
+
}
|
|
182
|
+
else if (toolName === 'edit_file' && (args['file_path'] || args['path'])) {
|
|
183
|
+
description = `Editing file: ${args['file_path'] || args['path']}`;
|
|
184
|
+
}
|
|
185
|
+
else if (toolName === 'glob' && args['pattern']) {
|
|
186
|
+
description = `Finding files matching: ${args['pattern']}`;
|
|
187
|
+
}
|
|
188
|
+
else if (toolName === 'search_files' && args['regex']) {
|
|
189
|
+
description = `Searching for: ${args['regex']}`;
|
|
190
|
+
}
|
|
191
|
+
else if (toolName === 'list_files' && args['path']) {
|
|
192
|
+
description = `Listing files in: ${args['path']}`;
|
|
193
|
+
}
|
|
194
|
+
else if (toolName === 'ask_followup_question' && args['question']) {
|
|
195
|
+
description = `Asking: ${args['question'].substring(0, 50)}...`;
|
|
196
|
+
}
|
|
197
|
+
else if (toolName === 'attempt_completion' && args['result']) {
|
|
198
|
+
description = `Completing task`;
|
|
199
|
+
}
|
|
200
|
+
else if (toolName === 'browser_action' && args['action']) {
|
|
201
|
+
description = `Browser: ${args['action']}`;
|
|
202
|
+
}
|
|
203
|
+
else if (toolName === 'todo_write') {
|
|
204
|
+
description = 'Update todos';
|
|
205
|
+
}
|
|
206
|
+
else if (toolName === 'skill' && args['skill_name']) {
|
|
207
|
+
description = `Using skill: ${args['skill_name']}`;
|
|
208
|
+
}
|
|
209
|
+
// Get the tool result if available
|
|
210
|
+
let resultDisplay = '(Tool call from resumed session - result not available)';
|
|
211
|
+
let isError = false;
|
|
212
|
+
if (toolResults && toolResults.has(toolName)) {
|
|
213
|
+
const result = toolResults.get(toolName);
|
|
214
|
+
resultDisplay = result.output;
|
|
215
|
+
isError = result.isError;
|
|
216
|
+
}
|
|
217
|
+
toolDisplays.push({
|
|
218
|
+
callId: toolCall.id || `restored-${toolName}-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
219
|
+
name: toolName,
|
|
220
|
+
description,
|
|
221
|
+
resultDisplay,
|
|
222
|
+
status: isError ? ToolCallStatus.Error : ToolCallStatus.Success,
|
|
223
|
+
confirmationDetails: undefined,
|
|
224
|
+
renderOutputAsMarkdown: true, // Enable markdown rendering for tool results
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
toolDisplays,
|
|
230
|
+
remainingText: parseResult.normalText,
|
|
231
|
+
thinkContent,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
14
234
|
/**
|
|
15
235
|
* Formats a date into a human-readable relative time or date string.
|
|
16
236
|
*/
|
|
@@ -42,7 +262,7 @@ function formatRelativeTime(date) {
|
|
|
42
262
|
/**
|
|
43
263
|
* Formats a tag name into a human-readable display name.
|
|
44
264
|
* Converts ISO timestamps to readable dates and extracts meaningful names.
|
|
45
|
-
* Format: "message preview (time ago)" or "
|
|
265
|
+
* Format: "message preview (time ago)" or "Session (time ago)"
|
|
46
266
|
*/
|
|
47
267
|
function formatTagForDisplay(tag, firstMessagePreview, mtime) {
|
|
48
268
|
const timeStr = mtime ? ` (${formatRelativeTime(mtime)})` : '';
|
|
@@ -60,15 +280,13 @@ function formatTagForDisplay(tag, firstMessagePreview, mtime) {
|
|
|
60
280
|
}
|
|
61
281
|
return `${preview}${timeStr}`;
|
|
62
282
|
}
|
|
63
|
-
// Check for
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return `Auto-save${timeStr}`;
|
|
283
|
+
// Check for session-* format (progressive save)
|
|
284
|
+
if (tag.startsWith('session-')) {
|
|
285
|
+
return `Session${timeStr}`;
|
|
67
286
|
}
|
|
68
|
-
// Check for
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return `Auto-save${timeStr}`;
|
|
287
|
+
// Check for cloud-task-* format
|
|
288
|
+
if (tag.startsWith('cloud-task-')) {
|
|
289
|
+
return `Cloud Task${timeStr}`;
|
|
72
290
|
}
|
|
73
291
|
// Check for timestamp format like "2025-11-24T18-34-38-727Z"
|
|
74
292
|
const timestampMatch = tag.match(/(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})/);
|
|
@@ -352,8 +570,10 @@ function formatCloudTaskStatusCard(taskInfo, commandPrefix = '/chat resume') {
|
|
|
352
570
|
*/
|
|
353
571
|
function looksLikeCloudTaskId(tag) {
|
|
354
572
|
// Cloud task IDs are typically alphanumeric with dashes/underscores, 10-30 chars
|
|
355
|
-
// They don't look like timestamps or
|
|
356
|
-
if (tag.startsWith('
|
|
573
|
+
// They don't look like timestamps or session tags
|
|
574
|
+
if (tag.startsWith('session-'))
|
|
575
|
+
return false;
|
|
576
|
+
if (tag.startsWith('cloud-task-'))
|
|
357
577
|
return false;
|
|
358
578
|
if (tag.match(/^\d{4}-\d{2}-\d{2}/))
|
|
359
579
|
return false; // Timestamp format
|
|
@@ -364,16 +584,29 @@ function looksLikeCloudTaskId(tag) {
|
|
|
364
584
|
const resumeCommand = {
|
|
365
585
|
name: 'resume',
|
|
366
586
|
altNames: ['load'],
|
|
367
|
-
description: 'Resume a conversation checkpoint. Usage: /chat resume
|
|
587
|
+
description: 'Resume a conversation checkpoint. Usage: /chat resume [tag-or-task-id]. Without arguments, resumes the most recent session.',
|
|
368
588
|
kind: CommandKind.BUILT_IN,
|
|
369
589
|
action: async (context, args) => {
|
|
370
590
|
let tag = args.trim();
|
|
591
|
+
// If no tag provided, try to find the most recent session checkpoint
|
|
371
592
|
if (!tag) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
593
|
+
const savedChats = await getSavedChatTags(context, true); // sorted by mtime desc (newest first)
|
|
594
|
+
if (savedChats.length === 0) {
|
|
595
|
+
return {
|
|
596
|
+
type: 'message',
|
|
597
|
+
messageType: 'info',
|
|
598
|
+
content: 'No saved conversation checkpoints found. Use /chat resume <tag> to resume a specific checkpoint.',
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
// Find the most recent session checkpoint (session-*)
|
|
602
|
+
const sessionCheckpoint = savedChats.find(chat => chat.name.startsWith('session-'));
|
|
603
|
+
if (sessionCheckpoint) {
|
|
604
|
+
tag = sessionCheckpoint.name;
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
// Fall back to the most recent checkpoint of any type
|
|
608
|
+
tag = savedChats[0].name;
|
|
609
|
+
}
|
|
377
610
|
}
|
|
378
611
|
// Normalize cloud-task prefix if present
|
|
379
612
|
const originalTag = tag;
|
|
@@ -460,23 +693,271 @@ const resumeCommand = {
|
|
|
460
693
|
const uiHistory = [];
|
|
461
694
|
let hasSystemPrompt = false;
|
|
462
695
|
let i = 0;
|
|
696
|
+
// Track pending function calls to match with their responses
|
|
697
|
+
const pendingFunctionCalls = new Map();
|
|
463
698
|
for (const item of conversation) {
|
|
464
699
|
i += 1;
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
.join('') || '';
|
|
469
|
-
if (!text) {
|
|
470
|
-
continue;
|
|
471
|
-
}
|
|
700
|
+
// Check for system prompt in first message
|
|
701
|
+
const textParts = item.parts?.filter((m) => !!m.text) || [];
|
|
702
|
+
const text = textParts.map((m) => m.text).join('') || '';
|
|
472
703
|
if (i === 1 && text.match(/context for our chat/)) {
|
|
473
704
|
hasSystemPrompt = true;
|
|
705
|
+
continue;
|
|
474
706
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
707
|
+
// Skip the model acknowledgment of system prompt
|
|
708
|
+
if (i === 2 && hasSystemPrompt) {
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
// Process function calls from model responses
|
|
712
|
+
const functionCallParts = item.parts?.filter((m) => !!m.functionCall) || [];
|
|
713
|
+
// Process function responses from user messages
|
|
714
|
+
const functionResponseParts = item.parts?.filter((m) => !!m.functionResponse) || [];
|
|
715
|
+
// Handle function calls (from model)
|
|
716
|
+
if (functionCallParts.length > 0 && item.role === 'model') {
|
|
717
|
+
for (const part of functionCallParts) {
|
|
718
|
+
const fc = part.functionCall;
|
|
719
|
+
if (fc) {
|
|
720
|
+
// Store the function call for later matching with its response
|
|
721
|
+
pendingFunctionCalls.set(fc.name, { name: fc.name, args: fc.args || {} });
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
// If there's also text content, add it as a gemini message
|
|
725
|
+
if (text) {
|
|
726
|
+
uiHistory.push({
|
|
727
|
+
type: MessageType.GEMINI,
|
|
728
|
+
text,
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
// Handle function responses (from user) - create tool_group display
|
|
734
|
+
if (functionResponseParts.length > 0 && item.role === 'user') {
|
|
735
|
+
const toolDisplays = [];
|
|
736
|
+
for (const part of functionResponseParts) {
|
|
737
|
+
const fr = part.functionResponse;
|
|
738
|
+
if (fr) {
|
|
739
|
+
const toolName = fr.name;
|
|
740
|
+
const response = fr.response;
|
|
741
|
+
// Extract the result text - check for common response formats
|
|
742
|
+
// Tools may return: { result: "..." }, { output: "..." }, { error: "..." }, or other formats
|
|
743
|
+
let resultText = '';
|
|
744
|
+
let isError = false;
|
|
745
|
+
if (response) {
|
|
746
|
+
if (typeof response === 'string') {
|
|
747
|
+
resultText = response;
|
|
748
|
+
}
|
|
749
|
+
else if (response.result) {
|
|
750
|
+
resultText = response.result;
|
|
751
|
+
}
|
|
752
|
+
else if (response.output) {
|
|
753
|
+
resultText = response.output;
|
|
754
|
+
}
|
|
755
|
+
else if (response.error) {
|
|
756
|
+
resultText = response.error;
|
|
757
|
+
isError = true;
|
|
758
|
+
}
|
|
759
|
+
else {
|
|
760
|
+
// For other formats, try to extract meaningful content
|
|
761
|
+
const responseObj = response;
|
|
762
|
+
// Check for common field names
|
|
763
|
+
const commonFields = ['content', 'data', 'text', 'message', 'value'];
|
|
764
|
+
for (const field of commonFields) {
|
|
765
|
+
if (responseObj[field] && typeof responseObj[field] === 'string') {
|
|
766
|
+
resultText = responseObj[field];
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
// If still no result, stringify but try to make it readable
|
|
771
|
+
if (!resultText) {
|
|
772
|
+
resultText = JSON.stringify(response, null, 2);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
// Get the original args from pending calls if available
|
|
777
|
+
const pendingCall = pendingFunctionCalls.get(toolName);
|
|
778
|
+
const args = pendingCall?.args || {};
|
|
779
|
+
// Create a description based on tool name and args
|
|
780
|
+
let description = toolName;
|
|
781
|
+
// Use ToolResultDisplay type which includes FileDiff and TodoResultDisplay
|
|
782
|
+
let resultDisplayValue = resultText;
|
|
783
|
+
if (toolName === 'web_search' && args['query']) {
|
|
784
|
+
description = `Searching the web for: "${args['query']}"`;
|
|
785
|
+
}
|
|
786
|
+
else if (toolName === 'web_fetch' && (args['url'] || args['urls'])) {
|
|
787
|
+
const url = args['url'] || (Array.isArray(args['urls']) ? args['urls'][0] : args['urls']);
|
|
788
|
+
description = `Fetching URL: ${url}`;
|
|
789
|
+
}
|
|
790
|
+
else if (toolName === 'read_file' && args['file_path']) {
|
|
791
|
+
description = `Reading file: ${args['file_path']}`;
|
|
792
|
+
}
|
|
793
|
+
else if (toolName === 'write_file' && args['file_path']) {
|
|
794
|
+
const filePath = args['file_path'];
|
|
795
|
+
const content = args['content'];
|
|
796
|
+
description = `Writing to file: ${filePath}`;
|
|
797
|
+
// Generate a diff display for write_file
|
|
798
|
+
if (content) {
|
|
799
|
+
const fileName = filePath.split('/').pop() || filePath;
|
|
800
|
+
// Create a unified diff showing the new content
|
|
801
|
+
const diffLines = [
|
|
802
|
+
`--- /dev/null`,
|
|
803
|
+
`+++ ${fileName}`,
|
|
804
|
+
`@@ -0,0 +1,${content.split('\n').length} @@`,
|
|
805
|
+
...content.split('\n').map(line => `+${line}`),
|
|
806
|
+
];
|
|
807
|
+
resultDisplayValue = {
|
|
808
|
+
fileDiff: diffLines.join('\n'),
|
|
809
|
+
fileName,
|
|
810
|
+
originalContent: null,
|
|
811
|
+
newContent: content,
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
else if ((toolName === 'shell' || toolName === 'execute_command') && args['command']) {
|
|
816
|
+
description = `Running command: ${args['command']}`;
|
|
817
|
+
}
|
|
818
|
+
else if (toolName === 'edit' && args['file_path']) {
|
|
819
|
+
const filePath = args['file_path'];
|
|
820
|
+
const oldString = args['old_string'];
|
|
821
|
+
const newString = args['new_string'];
|
|
822
|
+
description = `Editing file: ${filePath}`;
|
|
823
|
+
// Generate a diff from old_string and new_string for display
|
|
824
|
+
if (oldString !== undefined && newString !== undefined) {
|
|
825
|
+
const fileName = filePath.split('/').pop() || filePath;
|
|
826
|
+
// Create a simple unified diff format
|
|
827
|
+
const oldLines = (oldString || '').split('\n');
|
|
828
|
+
const newLines = (newString || '').split('\n');
|
|
829
|
+
const diffLines = [
|
|
830
|
+
`--- a/${fileName}`,
|
|
831
|
+
`+++ b/${fileName}`,
|
|
832
|
+
`@@ -1,${oldLines.length} +1,${newLines.length} @@`,
|
|
833
|
+
...oldLines.map(line => `-${line}`),
|
|
834
|
+
...newLines.map(line => `+${line}`),
|
|
835
|
+
];
|
|
836
|
+
resultDisplayValue = {
|
|
837
|
+
fileDiff: diffLines.join('\n'),
|
|
838
|
+
fileName,
|
|
839
|
+
originalContent: oldString,
|
|
840
|
+
newContent: newString,
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
else if (toolName === 'todo_write') {
|
|
845
|
+
description = 'Update todos';
|
|
846
|
+
// Parse the todo_write response to create a TodoResultDisplay
|
|
847
|
+
try {
|
|
848
|
+
// The response could be the object directly or a string that needs parsing
|
|
849
|
+
let todoResponse = null;
|
|
850
|
+
// First try to parse from resultText (which may be the JSON string)
|
|
851
|
+
if (typeof resultText === 'string' && resultText.trim().startsWith('{')) {
|
|
852
|
+
try {
|
|
853
|
+
todoResponse = JSON.parse(resultText);
|
|
854
|
+
}
|
|
855
|
+
catch {
|
|
856
|
+
// Ignore parse errors
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
// If that didn't work, try the response object directly
|
|
860
|
+
if (!todoResponse && typeof response === 'object' && response !== null) {
|
|
861
|
+
// Check if response has todos property (it might be nested differently)
|
|
862
|
+
const responseAny = response;
|
|
863
|
+
if ('todos' in responseAny && Array.isArray(responseAny['todos'])) {
|
|
864
|
+
todoResponse = responseAny;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
if (todoResponse && todoResponse.todos && Array.isArray(todoResponse.todos)) {
|
|
868
|
+
// Return a TodoResultDisplay object
|
|
869
|
+
resultDisplayValue = {
|
|
870
|
+
type: 'todo_list',
|
|
871
|
+
todos: todoResponse.todos.map((todo) => ({
|
|
872
|
+
id: todo.id || '',
|
|
873
|
+
content: todo.content || '',
|
|
874
|
+
status: todo.status || 'pending',
|
|
875
|
+
})),
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
catch {
|
|
880
|
+
// If parsing fails, keep the original resultText
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
toolDisplays.push({
|
|
884
|
+
callId: `restored-${toolName}-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
885
|
+
name: toolName,
|
|
886
|
+
description,
|
|
887
|
+
resultDisplay: resultDisplayValue,
|
|
888
|
+
status: isError ? ToolCallStatus.Error : ToolCallStatus.Success,
|
|
889
|
+
confirmationDetails: undefined,
|
|
890
|
+
renderOutputAsMarkdown: typeof resultDisplayValue === 'string',
|
|
891
|
+
});
|
|
892
|
+
// Remove from pending
|
|
893
|
+
pendingFunctionCalls.delete(toolName);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
if (toolDisplays.length > 0) {
|
|
897
|
+
uiHistory.push({
|
|
898
|
+
type: 'tool_group',
|
|
899
|
+
tools: toolDisplays,
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
// Handle regular text messages
|
|
905
|
+
if (text) {
|
|
906
|
+
// Check if this is model content with encrypted provider markup
|
|
907
|
+
// (MiniMax, DeepSeek, Qwen tool call formats or think tags)
|
|
908
|
+
if (item.role === 'model' && hasEncryptedProviderMarkup(text)) {
|
|
909
|
+
// Look ahead to find tool results in the next user message
|
|
910
|
+
// For encrypted providers, tool results are in user messages like:
|
|
911
|
+
// "Function <tool_name> returned: {...}"
|
|
912
|
+
let toolResults;
|
|
913
|
+
// Find the next user message in the conversation
|
|
914
|
+
const currentIndex = conversation.indexOf(item);
|
|
915
|
+
if (currentIndex >= 0 && currentIndex < conversation.length - 1) {
|
|
916
|
+
const nextItem = conversation[currentIndex + 1];
|
|
917
|
+
if (nextItem && nextItem.role === 'user') {
|
|
918
|
+
const nextTextParts = nextItem.parts?.filter((m) => !!m.text) || [];
|
|
919
|
+
const nextText = nextTextParts.map((m) => m.text).join('') || '';
|
|
920
|
+
if (nextText && nextText.includes('Function') && nextText.includes('returned:')) {
|
|
921
|
+
toolResults = extractToolResultFromUserMessage(nextText);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
// Parse tool calls and create tool_group UI items
|
|
926
|
+
const { toolDisplays, remainingText, thinkContent } = parseEncryptedProviderToolCalls(text, toolResults);
|
|
927
|
+
// Add think content as a separate message if present
|
|
928
|
+
if (thinkContent && thinkContent.trim()) {
|
|
929
|
+
uiHistory.push({
|
|
930
|
+
type: MessageType.GEMINI,
|
|
931
|
+
text: thinkContent,
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
// Add tool_group if there are tool calls
|
|
935
|
+
if (toolDisplays.length > 0) {
|
|
936
|
+
uiHistory.push({
|
|
937
|
+
type: 'tool_group',
|
|
938
|
+
tools: toolDisplays,
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
// Add remaining text as a gemini message if present
|
|
942
|
+
if (remainingText && remainingText.trim()) {
|
|
943
|
+
uiHistory.push({
|
|
944
|
+
type: MessageType.GEMINI,
|
|
945
|
+
text: remainingText,
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
else if (item.role === 'user' && text.includes('Function') && text.includes('returned:')) {
|
|
950
|
+
// Skip user messages that contain tool results - they were already processed
|
|
951
|
+
// when we looked ahead from the model message
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
else {
|
|
955
|
+
// Regular text message without encrypted provider markup
|
|
956
|
+
uiHistory.push({
|
|
957
|
+
type: (item.role && rolemap[item.role]) || MessageType.GEMINI,
|
|
958
|
+
text,
|
|
959
|
+
});
|
|
960
|
+
}
|
|
480
961
|
}
|
|
481
962
|
}
|
|
482
963
|
return {
|