tide-commander 1.149.0 → 1.150.0
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/CHANGELOG.md +16 -0
- package/dist/assets/{BossLogsModal-Cs4kWugD.js → BossLogsModal-DvczRUvP.js} +1 -1
- package/dist/assets/{BossSpawnModal-BudMgJqj.js → BossSpawnModal-B2autFKk.js} +1 -1
- package/dist/assets/{ControlsModal-Bh6_3qtH.js → ControlsModal-BaREMQxc.js} +1 -1
- package/dist/assets/{DockerLogsModal-Nf4wQFfz.js → DockerLogsModal-DZ0udMCo.js} +1 -1
- package/dist/assets/{EmbeddedEditor-BfRXRX50.js → EmbeddedEditor-CEjr0ZSZ.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-CXUtjA0r.js → GmailOAuthSetup-76BP8J8C.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-D-_rwicD.js → GoogleOAuthSetup-D45GImPI.js} +1 -1
- package/dist/assets/{IframeModal-CdaXsQK0.js → IframeModal-De63RfJZ.js} +1 -1
- package/dist/assets/{IntegrationsPanel-ZsSVxcWr.js → IntegrationsPanel-DYrw1Sqk.js} +2 -2
- package/dist/assets/{LogViewerModal-BAf-klB2.js → LogViewerModal-DW-2lpln.js} +1 -1
- package/dist/assets/{MonitoringModal-BjRbh5z4.js → MonitoringModal-B0-XXyd3.js} +1 -1
- package/dist/assets/{PM2LogsModal-SaywDZVL.js → PM2LogsModal-DOfOhkCY.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-jru2Ez9a.js → RestoreArchivedAreaModal-DNGmIJmM.js} +1 -1
- package/dist/assets/{Scene2DCanvas-NJHoe2Qg.js → Scene2DCanvas-BW1E76CQ.js} +1 -1
- package/dist/assets/{SceneManager-i9wVMa_V.js → SceneManager-DAvc9-Wp.js} +1 -1
- package/dist/assets/{SkillsPanel-BHfKLzWe.js → SkillsPanel-Br-gLCLA.js} +1 -1
- package/dist/assets/{SlackMultiInstanceSetup-BfVevCSo.js → SlackMultiInstanceSetup-DG_bIfDu.js} +1 -1
- package/dist/assets/{SpawnModal-29x5z53B.js → SpawnModal-lWM8OuZW.js} +1 -1
- package/dist/assets/{StatisticsModal-MbIByjWD.js → StatisticsModal-LvA0Fcvn.js} +1 -1
- package/dist/assets/{SubordinateAssignmentModal-DF0vYaM6.js → SubordinateAssignmentModal-8YxNXcR3.js} +1 -1
- package/dist/assets/{TriggerManagerPanel-EGcKS1Pt.js → TriggerManagerPanel-DftIuKUG.js} +1 -1
- package/dist/assets/{WorkflowEditorPanel-BLmejfqe.js → WorkflowEditorPanel-oLwyO_C6.js} +1 -1
- package/dist/assets/{area-logos-C4fgpLgO.js → area-logos-mcpQqUMZ.js} +1 -1
- package/dist/assets/{index-BiiLWTXJ.js → index-Ay7dKyeR.js} +1 -1
- package/dist/assets/{index-DThK5_yS.js → index-BMw7AAAK.js} +3 -3
- package/dist/assets/{index-C8CGfmXP.js → index-BQpd34Mc.js} +1 -1
- package/dist/assets/{index-BZ4wZwRu.js → index-C6-68CXL.js} +3 -3
- package/dist/assets/index-CIon0DLf.js +2 -0
- package/dist/assets/{index-C6Kf4ewu.js → index-Cwt-E2td.js} +1 -1
- package/dist/assets/{index-B9xpLAcH.js → index-DFVroqA5.js} +1 -1
- package/dist/assets/index-DR6_WeF_.js +1 -0
- package/dist/assets/{index-B-2H7O_Y.js → index-O0t07OB_.js} +2 -2
- package/dist/assets/{main-B0UlZnfk.js → main-D5sqeuwB.js} +151 -125
- package/dist/assets/main-JBkwGXfE.css +1 -0
- package/dist/assets/{web-CXC8P25z.js → web-BkrshujV.js} +1 -1
- package/dist/assets/{web-LGEDa25c.js → web-DPETNqOf.js} +1 -1
- package/dist/assets/{web-NM4dUqWg.js → web-vG1Smf_N.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/locales/de/config.json +3 -1
- package/dist/locales/en/config.json +2 -0
- package/dist/locales/en/terminal.json +6 -0
- package/dist/locales/es/config.json +2 -0
- package/dist/locales/fr/config.json +3 -1
- package/dist/locales/hi/config.json +3 -1
- package/dist/locales/it/config.json +3 -1
- package/dist/locales/ja/config.json +3 -1
- package/dist/locales/pt/config.json +3 -1
- package/dist/locales/ru/config.json +3 -1
- package/dist/locales/zh-CN/config.json +3 -1
- package/dist/src/packages/server/claude/backend.js +114 -10
- package/dist/src/packages/server/claude/runner/process-lifecycle.js +9 -0
- package/dist/src/packages/server/claude/runner/stdout-pipeline.js +24 -4
- package/dist/src/packages/server/claude/session-loader.js +20 -5
- package/dist/src/packages/server/codex/json-event-parser.js +149 -4
- package/dist/src/packages/server/grok/backend.js +4 -0
- package/dist/src/packages/server/grok/json-event-parser.js +29 -14
- package/dist/src/packages/server/grok/session-watcher.js +67 -34
- package/dist/src/packages/server/opencode/json-event-parser.js +184 -15
- package/dist/src/packages/server/routes/agents.js +12 -9
- package/dist/src/packages/server/services/grok-usage-service.js +374 -0
- package/dist/src/packages/server/websocket/listeners/runtime-listeners.js +10 -7
- package/package.json +1 -1
- package/dist/assets/index-DYgVP0G3.js +0 -2
- package/dist/assets/index-DhD3m2Qx.js +0 -1
- package/dist/assets/main-DeiAIdzy.css +0 -1
|
@@ -156,6 +156,15 @@ export class CodexJsonEventParser {
|
|
|
156
156
|
enableFileDiffEnrichment;
|
|
157
157
|
workingDirectory;
|
|
158
158
|
gitRootCache = new Map();
|
|
159
|
+
/**
|
|
160
|
+
* Progressive agent_message text per item.id (for item.updated / delta streams).
|
|
161
|
+
* Current `codex exec --json` only emits agent_message on item.completed, but
|
|
162
|
+
* app-server / newer CLIs can stream via item.updated or agentMessage deltas.
|
|
163
|
+
*/
|
|
164
|
+
streamedAgentTextByItemId = new Map();
|
|
165
|
+
openAgentTextUuids = new Set();
|
|
166
|
+
streamedThinkingByItemId = new Map();
|
|
167
|
+
openThinkingUuids = new Set();
|
|
159
168
|
constructor(options = {}) {
|
|
160
169
|
this.enableFileDiffEnrichment = options.enableFileDiffEnrichment === true;
|
|
161
170
|
this.workingDirectory = options.workingDirectory || process.cwd();
|
|
@@ -190,6 +199,9 @@ export class CodexJsonEventParser {
|
|
|
190
199
|
if (event.type === 'item.started') {
|
|
191
200
|
return this.parseItemStarted(event.item);
|
|
192
201
|
}
|
|
202
|
+
if (event.type === 'item.updated') {
|
|
203
|
+
return this.parseItemUpdated(event.item);
|
|
204
|
+
}
|
|
193
205
|
if (event.type === 'item.completed') {
|
|
194
206
|
// Capture agent message text for later use in step_complete
|
|
195
207
|
if (event.item?.type === 'agent_message' && event.item?.text) {
|
|
@@ -197,6 +209,12 @@ export class CodexJsonEventParser {
|
|
|
197
209
|
}
|
|
198
210
|
return this.parseItemCompleted(event.item);
|
|
199
211
|
}
|
|
212
|
+
// App-server style notification (when present on exec stream): streaming text delta.
|
|
213
|
+
if (event.type === 'item/agentMessage/delta' ||
|
|
214
|
+
event.type === 'item.agentMessage.delta' ||
|
|
215
|
+
event.type === 'agent_message_delta') {
|
|
216
|
+
return this.parseAgentMessageDelta(rawEvent);
|
|
217
|
+
}
|
|
200
218
|
if (event.type === 'turn.completed') {
|
|
201
219
|
return this.parseTurnCompleted(event.usage);
|
|
202
220
|
}
|
|
@@ -228,12 +246,27 @@ export class CodexJsonEventParser {
|
|
|
228
246
|
};
|
|
229
247
|
return [];
|
|
230
248
|
}
|
|
231
|
-
// agent_reasoning: Map to thinking event
|
|
249
|
+
// agent_reasoning: Map to thinking event (stable uuid when item id present)
|
|
232
250
|
if (payloadType === 'agent_reasoning') {
|
|
233
251
|
const text = asString(payload.text);
|
|
234
252
|
if (!text)
|
|
235
253
|
return [];
|
|
236
|
-
|
|
254
|
+
const itemId = asString(payload.item_id) || asString(payload.id);
|
|
255
|
+
const uuid = itemId ? `codex-thinking-${itemId}` : undefined;
|
|
256
|
+
return [{ type: 'thinking', text, isStreaming: false, uuid }];
|
|
257
|
+
}
|
|
258
|
+
// Streaming agent message delta (app-server / experimental envelopes)
|
|
259
|
+
if (payloadType === 'agent_message_delta' ||
|
|
260
|
+
payloadType === 'agent_message/delta' ||
|
|
261
|
+
payloadType === 'output_text_delta') {
|
|
262
|
+
const delta = asString(payload.text) ||
|
|
263
|
+
asString(payload.delta);
|
|
264
|
+
if (!delta)
|
|
265
|
+
return [];
|
|
266
|
+
const itemId = asString(payload.item_id) ||
|
|
267
|
+
asString(payload.id) ||
|
|
268
|
+
'anon';
|
|
269
|
+
return this.appendAgentTextDelta(itemId, delta);
|
|
237
270
|
}
|
|
238
271
|
// turn_aborted: Silent. The interruption is already visible from the agent stopping.
|
|
239
272
|
if (payloadType === 'turn_aborted') {
|
|
@@ -391,9 +424,109 @@ export class CodexJsonEventParser {
|
|
|
391
424
|
toolOutput,
|
|
392
425
|
}];
|
|
393
426
|
}
|
|
427
|
+
/** Stable uuid for a Codex agent_message item so streaming chunks merge. */
|
|
428
|
+
agentTextUuid(itemId) {
|
|
429
|
+
return `codex-text-${itemId}`;
|
|
430
|
+
}
|
|
431
|
+
appendAgentTextDelta(itemId, delta) {
|
|
432
|
+
if (!delta)
|
|
433
|
+
return [];
|
|
434
|
+
const prev = this.streamedAgentTextByItemId.get(itemId) || '';
|
|
435
|
+
const next = prev + delta;
|
|
436
|
+
this.streamedAgentTextByItemId.set(itemId, next);
|
|
437
|
+
this.lastAgentMessageText = next;
|
|
438
|
+
const uuid = this.agentTextUuid(itemId);
|
|
439
|
+
this.openAgentTextUuids.add(uuid);
|
|
440
|
+
return [{ type: 'text', text: delta, isStreaming: true, uuid }];
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Progressive snapshot for agent_message / reasoning on item.updated.
|
|
444
|
+
* Emits only the new suffix when text grows (prefix match).
|
|
445
|
+
*/
|
|
446
|
+
emitProgressiveItemText(kind, itemId, fullText, isFinal) {
|
|
447
|
+
if (!fullText)
|
|
448
|
+
return [];
|
|
449
|
+
const id = itemId || 'anon';
|
|
450
|
+
const map = kind === 'text' ? this.streamedAgentTextByItemId : this.streamedThinkingByItemId;
|
|
451
|
+
const openSet = kind === 'text' ? this.openAgentTextUuids : this.openThinkingUuids;
|
|
452
|
+
const uuid = kind === 'text' ? this.agentTextUuid(id) : `codex-thinking-${id}`;
|
|
453
|
+
const prev = map.get(id) || '';
|
|
454
|
+
if (kind === 'text')
|
|
455
|
+
this.lastAgentMessageText = fullText;
|
|
456
|
+
if (fullText === prev) {
|
|
457
|
+
if (isFinal && openSet.has(uuid)) {
|
|
458
|
+
openSet.delete(uuid);
|
|
459
|
+
map.delete(id);
|
|
460
|
+
return [{ type: kind === 'text' ? 'text' : 'thinking', text: fullText, isStreaming: false, uuid }];
|
|
461
|
+
}
|
|
462
|
+
return [];
|
|
463
|
+
}
|
|
464
|
+
map.set(id, fullText);
|
|
465
|
+
if (!isFinal && fullText.startsWith(prev)) {
|
|
466
|
+
const delta = fullText.slice(prev.length);
|
|
467
|
+
if (!delta)
|
|
468
|
+
return [];
|
|
469
|
+
openSet.add(uuid);
|
|
470
|
+
return [{ type: kind === 'text' ? 'text' : 'thinking', text: delta, isStreaming: true, uuid }];
|
|
471
|
+
}
|
|
472
|
+
openSet.delete(uuid);
|
|
473
|
+
if (isFinal)
|
|
474
|
+
map.delete(id);
|
|
475
|
+
return [{ type: kind === 'text' ? 'text' : 'thinking', text: fullText, isStreaming: false, uuid }];
|
|
476
|
+
}
|
|
477
|
+
parseItemUpdated(item) {
|
|
478
|
+
if (!item?.type)
|
|
479
|
+
return [];
|
|
480
|
+
// Progressive agent_message text (when CLI emits item.updated for messages).
|
|
481
|
+
if (item.type === 'agent_message' && item.text) {
|
|
482
|
+
const sanitized = sanitizeCodexMessageText(item.text);
|
|
483
|
+
if (!sanitized.text)
|
|
484
|
+
return [];
|
|
485
|
+
return this.emitProgressiveItemText('text', item.id, sanitized.text, false);
|
|
486
|
+
}
|
|
487
|
+
if (item.type === 'reasoning' && item.text) {
|
|
488
|
+
return this.emitProgressiveItemText('thinking', item.id, item.text, false);
|
|
489
|
+
}
|
|
490
|
+
// todo_list updates are already handled elsewhere as non-text progress;
|
|
491
|
+
// leave other item.updated types silent to avoid raw dumps.
|
|
492
|
+
if (item.type === 'todo_list') {
|
|
493
|
+
return [];
|
|
494
|
+
}
|
|
495
|
+
return [];
|
|
496
|
+
}
|
|
497
|
+
parseAgentMessageDelta(rawEvent) {
|
|
498
|
+
if (!isObject(rawEvent))
|
|
499
|
+
return [];
|
|
500
|
+
const item = isObject(rawEvent.item) ? rawEvent.item : undefined;
|
|
501
|
+
const params = isObject(rawEvent.params) ? rawEvent.params : undefined;
|
|
502
|
+
const delta = asString(rawEvent.delta) ||
|
|
503
|
+
asString(rawEvent.text) ||
|
|
504
|
+
asString(item?.text) ||
|
|
505
|
+
(params ? asString(params.delta) || asString(params.text) : undefined);
|
|
506
|
+
if (!delta)
|
|
507
|
+
return [];
|
|
508
|
+
const itemId = asString(item?.id) ||
|
|
509
|
+
asString(rawEvent.item_id) ||
|
|
510
|
+
asString(rawEvent.id) ||
|
|
511
|
+
(params ? asString(params.item_id) || asString(params.id) : undefined) ||
|
|
512
|
+
'anon';
|
|
513
|
+
return this.appendAgentTextDelta(itemId, delta);
|
|
514
|
+
}
|
|
394
515
|
parseItemStarted(item) {
|
|
395
516
|
if (!item?.type)
|
|
396
517
|
return [];
|
|
518
|
+
// Some Codex builds open agent_message with empty text on item.started;
|
|
519
|
+
// pin the stream uuid so later updates merge into one row.
|
|
520
|
+
if (item.type === 'agent_message') {
|
|
521
|
+
if (item.id) {
|
|
522
|
+
const uuid = this.agentTextUuid(item.id);
|
|
523
|
+
this.openAgentTextUuids.add(uuid);
|
|
524
|
+
if (item.text) {
|
|
525
|
+
return this.emitProgressiveItemText('text', item.id, item.text, false);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return [];
|
|
529
|
+
}
|
|
397
530
|
if (item.type === 'web_search') {
|
|
398
531
|
const toolName = 'web_search';
|
|
399
532
|
if (item.id) {
|
|
@@ -446,7 +579,12 @@ export class CodexJsonEventParser {
|
|
|
446
579
|
return [{ type: 'error', errorMessage }];
|
|
447
580
|
}
|
|
448
581
|
if (item.type === 'reasoning' && item.text) {
|
|
449
|
-
|
|
582
|
+
const uuid = item.id ? `codex-thinking-${item.id}` : undefined;
|
|
583
|
+
if (item.id) {
|
|
584
|
+
this.streamedThinkingByItemId.delete(item.id);
|
|
585
|
+
this.openThinkingUuids.delete(uuid);
|
|
586
|
+
}
|
|
587
|
+
return [{ type: 'thinking', text: item.text, isStreaming: false, uuid }];
|
|
450
588
|
}
|
|
451
589
|
// Handle agent_message regardless of whether `text` is present. An empty
|
|
452
590
|
// agent_message (text:"" or missing) must be suppressed — NOT allowed to
|
|
@@ -467,7 +605,14 @@ export class CodexJsonEventParser {
|
|
|
467
605
|
// Empty message → render nothing (no empty bubble, no raw debug line).
|
|
468
606
|
if (!sanitized.text)
|
|
469
607
|
return [];
|
|
470
|
-
|
|
608
|
+
const uuid = item.id ? `codex-text-${item.id}` : undefined;
|
|
609
|
+
if (item.id) {
|
|
610
|
+
this.streamedAgentTextByItemId.delete(item.id);
|
|
611
|
+
if (uuid)
|
|
612
|
+
this.openAgentTextUuids.delete(uuid);
|
|
613
|
+
}
|
|
614
|
+
// isStreaming:false finalizes any prior streaming row with the same uuid.
|
|
615
|
+
return [{ type: 'text', text: sanitized.text, isStreaming: false, uuid }];
|
|
471
616
|
}
|
|
472
617
|
if (item.type === 'web_search') {
|
|
473
618
|
const toolName = item.id ? (this.activeToolByItemId.get(item.id) ?? 'web_search') : 'web_search';
|
|
@@ -141,6 +141,10 @@ export class GrokBackend {
|
|
|
141
141
|
return null;
|
|
142
142
|
return events.length === 1 ? events[0] : events;
|
|
143
143
|
}
|
|
144
|
+
/** See CLIBackend.breakOpenStreams — tool-boundary stream split for Grok. */
|
|
145
|
+
breakOpenStreams() {
|
|
146
|
+
return this.parser.breakOpenStreams();
|
|
147
|
+
}
|
|
144
148
|
extractSessionId(rawEvent) {
|
|
145
149
|
const event = rawEvent;
|
|
146
150
|
if (event?.sessionId && typeof event.sessionId === 'string') {
|
|
@@ -85,18 +85,23 @@ export class GrokJsonEventParser {
|
|
|
85
85
|
sessionId: event.sessionId,
|
|
86
86
|
}];
|
|
87
87
|
}
|
|
88
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Finalize open text/thinking streams and mint new uuids for the next
|
|
90
|
+
* generation — WITHOUT step_complete (agent stays "working").
|
|
91
|
+
*
|
|
92
|
+
* Grok keeps one agentic turn across many tool rounds and does not emit
|
|
93
|
+
* `end` between them. Without this break, every intermediate status line
|
|
94
|
+
* concatenates into a single mashed bubble until the final end.
|
|
95
|
+
*/
|
|
96
|
+
breakOpenStreams(sessionId) {
|
|
89
97
|
const events = [];
|
|
90
|
-
// Finalize open streams so the client stops treating the row as "live"
|
|
91
|
-
// and stops re-mounting/blinking. Final payload replaces the merged row
|
|
92
|
-
// (same uuid, isStreaming=false) with the full accumulated text.
|
|
93
98
|
if (this.thinkingStreamUuid && this.lastThinkingContent) {
|
|
94
99
|
events.push({
|
|
95
100
|
type: 'thinking',
|
|
96
101
|
text: this.lastThinkingContent,
|
|
97
102
|
isStreaming: false,
|
|
98
103
|
uuid: this.thinkingStreamUuid,
|
|
99
|
-
sessionId
|
|
104
|
+
sessionId,
|
|
100
105
|
});
|
|
101
106
|
}
|
|
102
107
|
if (this.textStreamUuid && this.lastTextContent) {
|
|
@@ -105,27 +110,37 @@ export class GrokJsonEventParser {
|
|
|
105
110
|
text: this.lastTextContent,
|
|
106
111
|
isStreaming: false,
|
|
107
112
|
uuid: this.textStreamUuid,
|
|
108
|
-
sessionId
|
|
113
|
+
sessionId,
|
|
109
114
|
});
|
|
110
115
|
}
|
|
116
|
+
this.lastTextContent = '';
|
|
117
|
+
this.lastThinkingContent = '';
|
|
118
|
+
// Keep textEventEmittedInTurn so a later end/step_complete still knows
|
|
119
|
+
// we produced text this turn (avoids empty-thinking fallback).
|
|
120
|
+
this.textStreamUuid = undefined;
|
|
121
|
+
this.thinkingStreamUuid = undefined;
|
|
122
|
+
return events;
|
|
123
|
+
}
|
|
124
|
+
parseEnd(event) {
|
|
125
|
+
// Finalize open streams (same as a tool-boundary break) then step_complete.
|
|
126
|
+
const events = this.breakOpenStreams(event.sessionId);
|
|
127
|
+
// breakOpenStreams cleared accumulators — re-read is not possible. Capture
|
|
128
|
+
// resultText from the finalize events we just built.
|
|
129
|
+
const finalizedText = events.find((e) => e.type === 'text' && e.text)?.text;
|
|
130
|
+
const finalizedThinking = events.find((e) => e.type === 'thinking' && e.text)?.text;
|
|
111
131
|
const stepComplete = {
|
|
112
132
|
type: 'step_complete',
|
|
113
133
|
sessionId: event.sessionId,
|
|
114
134
|
};
|
|
115
|
-
if (
|
|
116
|
-
stepComplete.resultText =
|
|
135
|
+
if (finalizedText) {
|
|
136
|
+
stepComplete.resultText = finalizedText;
|
|
117
137
|
}
|
|
118
|
-
else if (
|
|
138
|
+
else if (finalizedThinking && !this.textEventEmittedInTurn) {
|
|
119
139
|
// Thinking-only turn — surface a marker so the terminal isn't blank
|
|
120
140
|
stepComplete.resultText = `(Empty response: thinking only, stopReason=${event.stopReason || 'EndTurn'})`;
|
|
121
141
|
}
|
|
122
142
|
events.push(stepComplete);
|
|
123
|
-
// Reset per-turn accumulators / stream ids
|
|
124
|
-
this.lastTextContent = '';
|
|
125
|
-
this.lastThinkingContent = '';
|
|
126
143
|
this.textEventEmittedInTurn = false;
|
|
127
|
-
this.textStreamUuid = undefined;
|
|
128
|
-
this.thinkingStreamUuid = undefined;
|
|
129
144
|
return events;
|
|
130
145
|
}
|
|
131
146
|
parseError(event) {
|
|
@@ -186,10 +186,43 @@ export function startGrokSessionWatcher(opts) {
|
|
|
186
186
|
let lastSignalsUsageKey = '';
|
|
187
187
|
const emittedToolStarts = new Set();
|
|
188
188
|
const emittedToolResults = new Set();
|
|
189
|
-
/**
|
|
189
|
+
/**
|
|
190
|
+
* raw tool_name → early synthetic ids still running (events tool_started,
|
|
191
|
+
* no tool_completed yet). chat_history prefers these first.
|
|
192
|
+
*/
|
|
190
193
|
const pendingEarlyByName = new Map();
|
|
194
|
+
/**
|
|
195
|
+
* raw tool_name → early ids that already got tool_completed BEFORE
|
|
196
|
+
* chat_history wrote the real call. Fast tools (TodoWrite) often complete
|
|
197
|
+
* in <100ms; if we only kept them in pending and shifted on complete, the
|
|
198
|
+
* upgrade path was empty and we emitted a second call-* chip — leaving the
|
|
199
|
+
* original early card stuck with empty toolInput (bare "TODOWRITE").
|
|
200
|
+
*/
|
|
201
|
+
const completedEarlyByName = new Map();
|
|
191
202
|
const earlyToReal = new Map();
|
|
192
203
|
const realToEarly = new Map();
|
|
204
|
+
/** Pop the next early id eligible for upgrade (still running, else completed). */
|
|
205
|
+
const takeEarlyForUpgrade = (rawName) => {
|
|
206
|
+
const pending = pendingEarlyByName.get(rawName);
|
|
207
|
+
if (pending && pending.length > 0) {
|
|
208
|
+
const id = pending.shift();
|
|
209
|
+
if (pending.length === 0)
|
|
210
|
+
pendingEarlyByName.delete(rawName);
|
|
211
|
+
else
|
|
212
|
+
pendingEarlyByName.set(rawName, pending);
|
|
213
|
+
return id;
|
|
214
|
+
}
|
|
215
|
+
const completed = completedEarlyByName.get(rawName);
|
|
216
|
+
if (completed && completed.length > 0) {
|
|
217
|
+
const id = completed.shift();
|
|
218
|
+
if (completed.length === 0)
|
|
219
|
+
completedEarlyByName.delete(rawName);
|
|
220
|
+
else
|
|
221
|
+
completedEarlyByName.set(rawName, completed);
|
|
222
|
+
return id;
|
|
223
|
+
}
|
|
224
|
+
return undefined;
|
|
225
|
+
};
|
|
193
226
|
const emit = (event) => {
|
|
194
227
|
if (stopped)
|
|
195
228
|
return;
|
|
@@ -263,24 +296,22 @@ export function startGrokSessionWatcher(opts) {
|
|
|
263
296
|
pendingEarlyByName.delete(rawName);
|
|
264
297
|
else
|
|
265
298
|
pendingEarlyByName.set(rawName, queue);
|
|
266
|
-
//
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
return;
|
|
299
|
+
// Keep the early id available for chat_history upgrade even after complete.
|
|
300
|
+
// TodoWrite / list_dir often finish before the assistant tool_calls line
|
|
301
|
+
// is flushed — without this, upgrade fails and the UI keeps an empty chip.
|
|
302
|
+
if (!earlyToReal.has(synthId)) {
|
|
303
|
+
const completedQ = completedEarlyByName.get(rawName) || [];
|
|
304
|
+
completedQ.push(synthId);
|
|
305
|
+
completedEarlyByName.set(rawName, completedQ);
|
|
274
306
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
// Tools change context fill — re-read signals after completions.
|
|
307
|
+
// Do NOT emit tool_result from events.jsonl.
|
|
308
|
+
// events only has outcome (success/error) — no stdout. Emitting
|
|
309
|
+
// "Bash output:\n(ok)" here races ahead of chat_history, marks the early
|
|
310
|
+
// uuid as done, and blocks the real dual-emit with full stdout. The UI
|
|
311
|
+
// then pairs the Bash chip to "(ok)" forever ("No output available").
|
|
312
|
+
// Real tool_result content always comes from chat_history.jsonl below.
|
|
313
|
+
//
|
|
314
|
+
// Still refresh context usage — tools change fill.
|
|
284
315
|
pollSignalsUsage();
|
|
285
316
|
return;
|
|
286
317
|
}
|
|
@@ -301,14 +332,11 @@ export function startGrokSessionWatcher(opts) {
|
|
|
301
332
|
const rawName = call.name || 'unknown';
|
|
302
333
|
const toolName = normalizeGrokToolName(rawName);
|
|
303
334
|
const toolInput = parseToolInput(call.arguments);
|
|
304
|
-
// Link to
|
|
305
|
-
const
|
|
306
|
-
if (
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
pendingEarlyByName.delete(rawName);
|
|
310
|
-
earlyToReal.set(earlyId, callId);
|
|
311
|
-
realToEarly.set(callId, earlyId);
|
|
335
|
+
// Link to an early card for this tool name (running or already completed).
|
|
336
|
+
const linkedEarlyId = takeEarlyForUpgrade(rawName);
|
|
337
|
+
if (linkedEarlyId) {
|
|
338
|
+
earlyToReal.set(linkedEarlyId, callId);
|
|
339
|
+
realToEarly.set(callId, linkedEarlyId);
|
|
312
340
|
}
|
|
313
341
|
emittedToolStarts.add(callId);
|
|
314
342
|
// Prefer upgrading the early card (same uuid) with full args so the UI
|
|
@@ -340,26 +368,31 @@ export function startGrokSessionWatcher(opts) {
|
|
|
340
368
|
const callId = line.tool_call_id;
|
|
341
369
|
if (emittedToolResults.has(callId))
|
|
342
370
|
return;
|
|
343
|
-
emittedToolResults.add(callId);
|
|
344
371
|
const content = typeof line.content === 'string'
|
|
345
372
|
? line.content
|
|
346
373
|
: line.content != null
|
|
347
374
|
? JSON.stringify(line.content)
|
|
348
375
|
: '';
|
|
349
376
|
const out = truncateOutput(content);
|
|
377
|
+
// Prefer the live card uuid (early synth id) so Bash output pairs with the
|
|
378
|
+
// "Using tool: Bash" row the UI already rendered under that uuid.
|
|
379
|
+
const earlyId = realToEarly.get(callId);
|
|
380
|
+
const cardUuid = earlyId || callId;
|
|
381
|
+
emittedToolResults.add(callId);
|
|
382
|
+
if (earlyId)
|
|
383
|
+
emittedToolResults.add(earlyId);
|
|
350
384
|
emit({
|
|
351
385
|
type: 'tool_result',
|
|
352
|
-
toolUseId:
|
|
353
|
-
uuid:
|
|
386
|
+
toolUseId: cardUuid,
|
|
387
|
+
uuid: cardUuid,
|
|
354
388
|
toolOutput: out,
|
|
355
389
|
});
|
|
356
|
-
|
|
357
|
-
if (
|
|
358
|
-
emittedToolResults.add(earlyId);
|
|
390
|
+
// Also emit under callId when different so any call-id-keyed consumers match.
|
|
391
|
+
if (cardUuid !== callId) {
|
|
359
392
|
emit({
|
|
360
393
|
type: 'tool_result',
|
|
361
|
-
toolUseId:
|
|
362
|
-
uuid:
|
|
394
|
+
toolUseId: callId,
|
|
395
|
+
uuid: callId,
|
|
363
396
|
toolOutput: out,
|
|
364
397
|
});
|
|
365
398
|
}
|