@blackbox_ai/blackbox-cli 0.8.2 → 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.
Files changed (43) hide show
  1. package/LICENSE +674 -203
  2. package/dist/package.json +2 -2
  3. package/dist/src/commands/update.js +2 -1
  4. package/dist/src/commands/update.js.map +1 -1
  5. package/dist/src/config/auth.d.ts +4 -1
  6. package/dist/src/config/auth.js +36 -0
  7. package/dist/src/config/auth.js.map +1 -1
  8. package/dist/src/config/modelFetcher.js +1 -0
  9. package/dist/src/config/modelFetcher.js.map +1 -1
  10. package/dist/src/config/settingsSchema.d.ts +58 -0
  11. package/dist/src/config/settingsSchema.js +59 -1
  12. package/dist/src/config/settingsSchema.js.map +1 -1
  13. package/dist/src/generated/git-commit.d.ts +2 -2
  14. package/dist/src/generated/git-commit.js +2 -2
  15. package/dist/src/ui/App.js +18 -46
  16. package/dist/src/ui/App.js.map +1 -1
  17. package/dist/src/ui/commands/chatCommand.js +512 -31
  18. package/dist/src/ui/commands/chatCommand.js.map +1 -1
  19. package/dist/src/ui/commands/quitCommand.js +7 -33
  20. package/dist/src/ui/commands/quitCommand.js.map +1 -1
  21. package/dist/src/ui/components/AuthDialog.js +52 -13
  22. package/dist/src/ui/components/AuthDialog.js.map +1 -1
  23. package/dist/src/ui/components/HistoryBrowserDialog.js +65 -8
  24. package/dist/src/ui/components/HistoryBrowserDialog.js.map +1 -1
  25. package/dist/src/ui/contexts/SessionContext.d.ts +16 -0
  26. package/dist/src/ui/contexts/SessionContext.js +43 -3
  27. package/dist/src/ui/contexts/SessionContext.js.map +1 -1
  28. package/dist/src/ui/hooks/atCommandProcessor.js +5 -69
  29. package/dist/src/ui/hooks/atCommandProcessor.js.map +1 -1
  30. package/dist/src/ui/hooks/slashCommandProcessor.js +16 -53
  31. package/dist/src/ui/hooks/slashCommandProcessor.js.map +1 -1
  32. package/dist/src/ui/hooks/useEncryptedStream.js +58 -1
  33. package/dist/src/ui/hooks/useEncryptedStream.js.map +1 -1
  34. package/dist/src/ui/hooks/useGeminiStream.js +118 -1
  35. package/dist/src/ui/hooks/useGeminiStream.js.map +1 -1
  36. package/dist/src/ui/models/availableModels.d.ts +1 -1
  37. package/dist/src/ui/models/availableModels.js +1 -1
  38. package/dist/src/ui/models/availableModels.js.map +1 -1
  39. package/dist/src/utils/versionStorage.d.ts +3 -1
  40. package/dist/src/utils/versionStorage.js +4 -2
  41. package/dist/src/utils/versionStorage.js.map +1 -1
  42. package/dist/tsconfig.tsbuildinfo +1 -1
  43. 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 "Auto-save (time ago)"
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 auto-save-YYYY-MM-DDTHH-MM-SS format
64
- const autoSaveMatch = tag.match(/^auto-save-(\d{4})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})$/);
65
- if (autoSaveMatch) {
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 auto-save chat YYYY-MM-DDTHH-MM-SS format (with space)
69
- const autoSaveChatMatch = tag.match(/^auto-save chat (\d{4})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})$/);
70
- if (autoSaveChatMatch) {
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 auto-save tags
356
- if (tag.startsWith('auto-save'))
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 <tag-or-task-id>',
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
- return {
373
- type: 'message',
374
- messageType: 'error',
375
- content: 'Missing tag or task ID. Usage: /chat resume <tag-or-task-id>',
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
- const text = item.parts
466
- ?.filter((m) => !!m.text)
467
- .map((m) => m.text)
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
- if (i > 2 || !hasSystemPrompt) {
476
- uiHistory.push({
477
- type: (item.role && rolemap[item.role]) || MessageType.GEMINI,
478
- text,
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 {