tide-commander 1.148.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.
Files changed (89) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +6 -3
  3. package/dist/assets/{BossLogsModal-CZMHgMBE.js → BossLogsModal-DvczRUvP.js} +1 -1
  4. package/dist/assets/BossSpawnModal-B2autFKk.js +1 -0
  5. package/dist/assets/{ControlsModal-DJ8Scm16.js → ControlsModal-BaREMQxc.js} +1 -1
  6. package/dist/assets/{DockerLogsModal-Vb4gqdFz.js → DockerLogsModal-DZ0udMCo.js} +1 -1
  7. package/dist/assets/{EmbeddedEditor-D4sHERAj.js → EmbeddedEditor-CEjr0ZSZ.js} +1 -1
  8. package/dist/assets/{GmailOAuthSetup-COIYIbJS.js → GmailOAuthSetup-76BP8J8C.js} +1 -1
  9. package/dist/assets/{GoogleOAuthSetup-DJS59Soi.js → GoogleOAuthSetup-D45GImPI.js} +1 -1
  10. package/dist/assets/{IframeModal-CFysTkn9.js → IframeModal-De63RfJZ.js} +1 -1
  11. package/dist/assets/{IntegrationsPanel-BK8g9ceY.js → IntegrationsPanel-DYrw1Sqk.js} +2 -2
  12. package/dist/assets/{LogViewerModal-DRxR0HYq.js → LogViewerModal-DW-2lpln.js} +1 -1
  13. package/dist/assets/{MonitoringModal-CL7UMOdX.js → MonitoringModal-B0-XXyd3.js} +1 -1
  14. package/dist/assets/{PM2LogsModal-I3aouUlw.js → PM2LogsModal-DOfOhkCY.js} +1 -1
  15. package/dist/assets/{RestoreArchivedAreaModal-PWAg6w6b.js → RestoreArchivedAreaModal-DNGmIJmM.js} +1 -1
  16. package/dist/assets/Scene2DCanvas-BW1E76CQ.js +1 -0
  17. package/dist/assets/SceneManager-DAvc9-Wp.js +104 -0
  18. package/dist/assets/{SkillsPanel-EQ74sIa6.js → SkillsPanel-Br-gLCLA.js} +3 -3
  19. package/dist/assets/{SlackMultiInstanceSetup-CLMUteI0.js → SlackMultiInstanceSetup-DG_bIfDu.js} +1 -1
  20. package/dist/assets/SpawnModal-lWM8OuZW.js +1 -0
  21. package/dist/assets/{StatisticsModal-D6MgA_o0.js → StatisticsModal-LvA0Fcvn.js} +1 -1
  22. package/dist/assets/{SubordinateAssignmentModal-C6n0pMn3.js → SubordinateAssignmentModal-8YxNXcR3.js} +1 -1
  23. package/dist/assets/{TriggerManagerPanel-Umd8iUg1.js → TriggerManagerPanel-DftIuKUG.js} +1 -1
  24. package/dist/assets/{WorkflowEditorPanel-CxjBxs9O.js → WorkflowEditorPanel-oLwyO_C6.js} +1 -1
  25. package/dist/assets/{area-logos-CThPeqIM.js → area-logos-mcpQqUMZ.js} +1 -1
  26. package/dist/assets/grok.ico +0 -0
  27. package/dist/assets/grok.png +0 -0
  28. package/dist/assets/index-Ay7dKyeR.js +1 -0
  29. package/dist/assets/index-BMw7AAAK.js +5 -0
  30. package/dist/assets/{index-Cz-ypmob.js → index-BQpd34Mc.js} +1 -1
  31. package/dist/assets/{index-BUN7MVf5.js → index-C6-68CXL.js} +3 -3
  32. package/dist/assets/index-CIon0DLf.js +2 -0
  33. package/dist/assets/{index-maZSt2YW.js → index-Cwt-E2td.js} +5 -5
  34. package/dist/assets/{index-DelocTaH.js → index-DFVroqA5.js} +1 -1
  35. package/dist/assets/index-DR6_WeF_.js +1 -0
  36. package/dist/assets/{index-h3Cuh2Zn.js → index-O0t07OB_.js} +2 -2
  37. package/dist/assets/main-D5sqeuwB.js +286 -0
  38. package/dist/assets/main-JBkwGXfE.css +1 -0
  39. package/dist/assets/{web-CfvEypdU.js → web-BkrshujV.js} +1 -1
  40. package/dist/assets/{web-CNjTPilF.js → web-DPETNqOf.js} +1 -1
  41. package/dist/assets/{web-bcYaAwP6.js → web-vG1Smf_N.js} +1 -1
  42. package/dist/index.html +2 -2
  43. package/dist/locales/de/config.json +3 -1
  44. package/dist/locales/en/config.json +2 -0
  45. package/dist/locales/en/terminal.json +6 -0
  46. package/dist/locales/en/tools.json +4 -2
  47. package/dist/locales/es/config.json +2 -0
  48. package/dist/locales/fr/config.json +3 -1
  49. package/dist/locales/hi/config.json +3 -1
  50. package/dist/locales/it/config.json +3 -1
  51. package/dist/locales/ja/config.json +3 -1
  52. package/dist/locales/pt/config.json +3 -1
  53. package/dist/locales/ru/config.json +3 -1
  54. package/dist/locales/zh-CN/config.json +3 -1
  55. package/dist/src/packages/server/claude/backend.js +114 -10
  56. package/dist/src/packages/server/claude/runner/process-lifecycle.js +62 -0
  57. package/dist/src/packages/server/claude/runner/stdout-pipeline.js +148 -7
  58. package/dist/src/packages/server/claude/runner/watchdog.js +12 -0
  59. package/dist/src/packages/server/claude/session-loader.js +295 -0
  60. package/dist/src/packages/server/codex/json-event-parser.js +149 -4
  61. package/dist/src/packages/server/data/index.js +1 -0
  62. package/dist/src/packages/server/grok/backend.js +235 -0
  63. package/dist/src/packages/server/grok/index.js +4 -0
  64. package/dist/src/packages/server/grok/json-event-parser.js +153 -0
  65. package/dist/src/packages/server/grok/session-watcher.js +493 -0
  66. package/dist/src/packages/server/opencode/json-event-parser.js +184 -15
  67. package/dist/src/packages/server/routes/agents.js +19 -11
  68. package/dist/src/packages/server/runtime/grok-runtime-provider.js +11 -0
  69. package/dist/src/packages/server/runtime/index.js +1 -0
  70. package/dist/src/packages/server/services/agent-service.js +26 -5
  71. package/dist/src/packages/server/services/grok-usage-service.js +374 -0
  72. package/dist/src/packages/server/services/runtime-command-execution.js +4 -2
  73. package/dist/src/packages/server/services/runtime-events.js +46 -7
  74. package/dist/src/packages/server/services/runtime-service.js +19 -3
  75. package/dist/src/packages/server/utils/tool-formatting.js +40 -12
  76. package/dist/src/packages/server/websocket/handlers/agent-handler.js +70 -17
  77. package/dist/src/packages/server/websocket/listeners/runtime-listeners.js +73 -14
  78. package/dist/src/packages/shared/agent-types.js +10 -0
  79. package/package.json +1 -1
  80. package/dist/assets/BossSpawnModal-C6DxqtKP.js +0 -1
  81. package/dist/assets/Scene2DCanvas-DYPHkU3i.js +0 -1
  82. package/dist/assets/SceneManager-QqKygyaw.js +0 -104
  83. package/dist/assets/SpawnModal-BOeStihN.js +0 -1
  84. package/dist/assets/index-DOzFBkSH.js +0 -1
  85. package/dist/assets/index-DP7kd91h.js +0 -1
  86. package/dist/assets/index-DRyMARFk.js +0 -5
  87. package/dist/assets/index-DuWmOZdY.js +0 -2
  88. package/dist/assets/main-C2zgPci-.js +0 -256
  89. package/dist/assets/main-C3JO2TRI.css +0 -1
@@ -16,11 +16,30 @@ export class RunnerStdoutPipeline {
16
16
  // loops (respond → notify → respond → notify → ...). Once the notification is sent,
17
17
  // suppress all further text/tool output until a new user message arrives.
18
18
  notificationSent = new Set();
19
+ // Streaming thinking blocks share a uuid across token deltas. The UI marks a
20
+ // row as thinking via a single leading `[thinking]` prefix — only the first
21
+ // chunk of each stream should get it, otherwise merges become
22
+ // `[thinking] a[thinking] b…`.
23
+ thinkingStreamPrefixed = new Set();
24
+ // High-frequency token streams (Grok headless) produce dozens of tiny deltas
25
+ // per second. Coalesce them into ~one flush every STREAM_COALESCE_MS so the
26
+ // client doesn't remeasure/redraw the virtual list on every token.
27
+ streamCoalesce = new Map();
19
28
  constructor(deps) {
20
29
  this.backend = deps.backend;
21
30
  this.callbacks = deps.callbacks;
22
31
  this.bus = deps.bus;
23
32
  }
33
+ /** Backends that emit token-sized streaming-json deltas. */
34
+ shouldCoalesceStreaming() {
35
+ // Grok always streams token-sized NDJSON; Claude does with
36
+ // --include-partial-messages. Codex/OpenCode stream when their CLI
37
+ // surfaces item.updated / message.part.delta (stable-uuid parsers ready).
38
+ return (this.backend.name === 'grok' ||
39
+ this.backend.name === 'claude' ||
40
+ this.backend.name === 'codex' ||
41
+ this.backend.name === 'opencode');
42
+ }
24
43
  handleStdout(agentId, process) {
25
44
  return new Promise((resolve) => {
26
45
  const decoder = new StringDecoder('utf8');
@@ -134,25 +153,75 @@ export class RunnerStdoutPipeline {
134
153
  case 'init':
135
154
  this.lastEmittedText.delete(agentId);
136
155
  this.notificationSent.delete(agentId);
156
+ this.clearThinkingPrefixState(agentId);
137
157
  this.callbacks.onOutput(agentId, `Session started: ${event.sessionId} (${event.model})`);
138
158
  break;
139
159
  case 'text':
140
160
  if (event.text) {
141
161
  // Suppress consecutive identical text (extra safety for OpenCode agentic loop)
162
+ // Skip for streaming chunks — token-level deltas are intentionally small and
163
+ // may legitimately repeat, and they are merged client-side by uuid.
142
164
  const prevText = this.lastEmittedText.get(agentId);
143
- if (prevText && prevText === event.text.trim()) {
165
+ if (!event.isStreaming && prevText && prevText === event.text.trim()) {
144
166
  log.log(`[text] Suppressing duplicate text for agent ${agentId.slice(0, 4)}`);
145
167
  this.textEmittedInTurn.add(agentId);
146
168
  break;
147
169
  }
148
- this.lastEmittedText.set(agentId, event.text.trim());
149
- this.callbacks.onOutput(agentId, event.text, event.isStreaming, undefined, event.uuid);
170
+ if (event.isStreaming && event.uuid && this.shouldCoalesceStreaming()) {
171
+ this.enqueueStreamChunk(agentId, event.uuid, 'text', event.text);
172
+ }
173
+ else {
174
+ // Flush any pending coalesced deltas before a final/non-stream emit
175
+ // so the client never applies final full-text over a stale partial.
176
+ if (event.uuid) {
177
+ this.flushStreamCoalesce(`${agentId}:text:${event.uuid}`);
178
+ }
179
+ if (!event.isStreaming) {
180
+ this.lastEmittedText.set(agentId, event.text.trim());
181
+ }
182
+ this.callbacks.onOutput(agentId, event.text, event.isStreaming, undefined, event.uuid);
183
+ }
150
184
  this.textEmittedInTurn.add(agentId);
151
185
  }
152
186
  break;
153
187
  case 'thinking':
154
188
  if (event.text) {
155
- this.callbacks.onOutput(agentId, `[thinking] ${event.text}`, event.isStreaming, undefined, event.uuid);
189
+ const streamKey = event.uuid ? `${agentId}:${event.uuid}` : undefined;
190
+ if (!event.isStreaming) {
191
+ if (event.uuid) {
192
+ this.flushStreamCoalesce(`${agentId}:thinking:${event.uuid}`);
193
+ }
194
+ const thinkingOut = event.text.startsWith('[thinking]')
195
+ ? event.text
196
+ : `[thinking] ${event.text}`;
197
+ this.callbacks.onOutput(agentId, thinkingOut, false, undefined, event.uuid);
198
+ if (streamKey) {
199
+ this.thinkingStreamPrefixed.delete(streamKey);
200
+ }
201
+ break;
202
+ }
203
+ // Streaming path
204
+ if (event.uuid && this.shouldCoalesceStreaming()) {
205
+ let chunk = event.text;
206
+ if (streamKey && !this.thinkingStreamPrefixed.has(streamKey)) {
207
+ this.thinkingStreamPrefixed.add(streamKey);
208
+ chunk = `[thinking] ${event.text}`;
209
+ }
210
+ this.enqueueStreamChunk(agentId, event.uuid, 'thinking', chunk);
211
+ }
212
+ else {
213
+ let thinkingOut = event.text;
214
+ if (streamKey) {
215
+ if (!this.thinkingStreamPrefixed.has(streamKey)) {
216
+ this.thinkingStreamPrefixed.add(streamKey);
217
+ thinkingOut = `[thinking] ${event.text}`;
218
+ }
219
+ }
220
+ else if (!event.text.startsWith('[thinking]')) {
221
+ thinkingOut = `[thinking] ${event.text}`;
222
+ }
223
+ this.callbacks.onOutput(agentId, thinkingOut, true, undefined, event.uuid);
224
+ }
156
225
  }
157
226
  break;
158
227
  case 'tool_start': {
@@ -171,13 +240,27 @@ export class RunnerStdoutPipeline {
171
240
  if (event.parentToolUseId) {
172
241
  break;
173
242
  }
243
+ const toolInput = event.toolInput;
244
+ const hasToolInput = !!toolInput
245
+ && typeof toolInput === 'object'
246
+ && !Array.isArray(toolInput)
247
+ && Object.keys(toolInput).length > 0;
248
+ // Grok events.jsonl fires tool_started with name only (empty {}). Emitting
249
+ // those as terminal cards creates bare "LIST FILES" / "TASK OUTPUT" /
250
+ // "TODOWRITE" rows; chat_history re-emits the same uuid with full args.
251
+ // Still forward onEvent (above) for activity badges — only skip terminal text.
252
+ if (!hasToolInput && this.backend.name === 'grok') {
253
+ break;
254
+ }
174
255
  const toolStartSubName = event.subagentName || this.activeSubagentName.get(agentId);
175
256
  this.callbacks.onOutput(agentId, `Using tool: ${event.toolName}`, false, toolStartSubName, event.uuid, {
176
257
  toolName: event.toolName,
177
- toolInput: event.toolInput,
258
+ toolInput,
178
259
  });
179
- if (event.toolInput) {
180
- this.callbacks.onOutput(agentId, `Tool input: ${JSON.stringify(event.toolInput)}`, false, toolStartSubName, event.uuid);
260
+ // Never emit "Tool input: {}" — empty object is truthy in JS and was
261
+ // producing useless sibling rows for every Grok early tool_start.
262
+ if (hasToolInput) {
263
+ this.callbacks.onOutput(agentId, `Tool input: ${JSON.stringify(toolInput)}`, false, toolStartSubName, event.uuid);
181
264
  }
182
265
  break;
183
266
  }
@@ -195,6 +278,8 @@ export class RunnerStdoutPipeline {
195
278
  break;
196
279
  }
197
280
  case 'step_complete': {
281
+ // Flush any pending coalesced stream chunks before finalizing the turn
282
+ this.flushAllStreamCoalesceForAgent(agentId);
198
283
  const hasErrorResultText = this.isLikelyErrorResultText(event.resultText);
199
284
  if (event.resultText && (!this.textEmittedInTurn.has(agentId) || hasErrorResultText)) {
200
285
  log.log(`[step_complete] Emitting resultText as fallback (no prior text events) for agent ${agentId.slice(0, 4)}`);
@@ -203,6 +288,8 @@ export class RunnerStdoutPipeline {
203
288
  else if (event.resultText) {
204
289
  log.log(`[step_complete] Skipping resultText (already emitted via text events) for agent ${agentId.slice(0, 4)}`);
205
290
  }
291
+ this.textEmittedInTurn.delete(agentId);
292
+ this.clearThinkingPrefixState(agentId);
206
293
  if (event.permissionDenials && event.permissionDenials.length > 0) {
207
294
  for (const denial of event.permissionDenials) {
208
295
  // Suppress the "[System] Permission denied" line for the two tools
@@ -245,6 +332,60 @@ export class RunnerStdoutPipeline {
245
332
  break;
246
333
  }
247
334
  }
335
+ clearThinkingPrefixState(agentId) {
336
+ const prefix = `${agentId}:`;
337
+ for (const key of this.thinkingStreamPrefixed) {
338
+ if (key.startsWith(prefix)) {
339
+ this.thinkingStreamPrefixed.delete(key);
340
+ }
341
+ }
342
+ // Drop any pending coalesced chunks for this agent
343
+ for (const [key, buf] of this.streamCoalesce) {
344
+ if (buf.agentId === agentId) {
345
+ if (buf.timer)
346
+ clearTimeout(buf.timer);
347
+ this.streamCoalesce.delete(key);
348
+ }
349
+ }
350
+ }
351
+ static STREAM_COALESCE_MS = 80;
352
+ enqueueStreamChunk(agentId, uuid, kind, chunk) {
353
+ const key = `${agentId}:${kind}:${uuid}`;
354
+ let buf = this.streamCoalesce.get(key);
355
+ if (!buf) {
356
+ buf = { agentId, uuid, kind, chunks: [], timer: null };
357
+ this.streamCoalesce.set(key, buf);
358
+ }
359
+ buf.chunks.push(chunk);
360
+ if (buf.timer)
361
+ return;
362
+ buf.timer = setTimeout(() => {
363
+ this.flushStreamCoalesce(key);
364
+ }, RunnerStdoutPipeline.STREAM_COALESCE_MS);
365
+ }
366
+ flushStreamCoalesce(key) {
367
+ const buf = this.streamCoalesce.get(key);
368
+ if (!buf)
369
+ return;
370
+ if (buf.timer) {
371
+ clearTimeout(buf.timer);
372
+ buf.timer = null;
373
+ }
374
+ if (buf.chunks.length === 0) {
375
+ this.streamCoalesce.delete(key);
376
+ return;
377
+ }
378
+ const text = buf.chunks.join('');
379
+ buf.chunks = [];
380
+ this.streamCoalesce.delete(key);
381
+ this.callbacks.onOutput(buf.agentId, text, true, undefined, buf.uuid);
382
+ }
383
+ flushAllStreamCoalesceForAgent(agentId) {
384
+ const keys = [...this.streamCoalesce.keys()].filter((k) => k.startsWith(`${agentId}:`));
385
+ for (const key of keys) {
386
+ this.flushStreamCoalesce(key);
387
+ }
388
+ }
248
389
  isNotificationCurl(toolInput) {
249
390
  const cmd = typeof toolInput?.command === 'string' ? toolInput.command : '';
250
391
  return cmd.includes('/api/notify');
@@ -71,6 +71,12 @@ export class RunnerWatchdog {
71
71
  log.error(`🐕 [WATCHDOG] Agent ${agentId}: tailer drain failed: ${String(err)}`);
72
72
  }
73
73
  activeProcess.tmuxTailer?.stop();
74
+ try {
75
+ activeProcess.sideChannelStop?.();
76
+ }
77
+ catch {
78
+ // ignore
79
+ }
74
80
  this.recordDeath({
75
81
  agentId,
76
82
  pid: activeProcess.process.pid ?? 0,
@@ -125,6 +131,12 @@ export class RunnerWatchdog {
125
131
  timestamp: Date.now(),
126
132
  stderr: this.lastStderr.get(agentId),
127
133
  });
134
+ try {
135
+ activeProcess.sideChannelStop?.();
136
+ }
137
+ catch {
138
+ // ignore
139
+ }
128
140
  this.activeProcesses.delete(agentId);
129
141
  this.lastStderr.delete(agentId);
130
142
  const remaining = this.activeProcesses.size;
@@ -52,6 +52,27 @@ const OPENCODE_TOOL_NAME_MAP = {
52
52
  function normalizeOpencodeToolName(raw) {
53
53
  return OPENCODE_TOOL_NAME_MAP[raw.toLowerCase()] || raw;
54
54
  }
55
+ // Grok CLI tool names → Tide UI names (keep in sync with grok/session-watcher.ts)
56
+ const GROK_TOOL_NAME_MAP = {
57
+ list_dir: 'ListFiles',
58
+ read_file: 'Read',
59
+ search_replace: 'Edit',
60
+ write: 'Write',
61
+ run_terminal_cmd: 'Bash',
62
+ run_terminal_command: 'Bash',
63
+ grep: 'Grep',
64
+ web_search: 'WebSearch',
65
+ web_fetch: 'WebFetch',
66
+ open_page: 'WebFetch',
67
+ open_page_with_find: 'WebFetch',
68
+ spawn_subagent: 'Task',
69
+ todo_write: 'TodoWrite',
70
+ };
71
+ function normalizeGrokToolName(raw) {
72
+ return GROK_TOOL_NAME_MAP[raw.toLowerCase()] || raw;
73
+ }
74
+ const GROK_DIR = path.join(os.homedir(), '.grok');
75
+ const GROK_SESSIONS_DIR = path.join(GROK_DIR, 'sessions');
55
76
  const codexSessionFileById = new Map();
56
77
  const opencodeSessionFileById = new Map();
57
78
  let cachedOpencodeDb = null;
@@ -452,6 +473,49 @@ function findOpencodeSessionFile(sessionId) {
452
473
  }
453
474
  return null;
454
475
  }
476
+ /**
477
+ * Locate a Grok session directory for (cwd, sessionId).
478
+ * Grok keys projects by encodeURIComponent(absolute cwd) under ~/.grok/sessions/.
479
+ */
480
+ function findGrokSessionDir(cwd, sessionId) {
481
+ if (!sessionId)
482
+ return null;
483
+ const candidates = new Set();
484
+ try {
485
+ candidates.add(path.resolve(cwd));
486
+ }
487
+ catch {
488
+ // ignore
489
+ }
490
+ candidates.add(cwd.replace(/\/+$/, ''));
491
+ candidates.add(cwd);
492
+ for (const c of candidates) {
493
+ if (!c)
494
+ continue;
495
+ const dir = path.join(GROK_SESSIONS_DIR, encodeURIComponent(c), sessionId);
496
+ const chatPath = path.join(dir, 'chat_history.jsonl');
497
+ if (fs.existsSync(chatPath)) {
498
+ return dir;
499
+ }
500
+ }
501
+ // Fallback: scan all project keys for this session id (slower, rare)
502
+ if (!fs.existsSync(GROK_SESSIONS_DIR))
503
+ return null;
504
+ try {
505
+ for (const entry of fs.readdirSync(GROK_SESSIONS_DIR, { withFileTypes: true })) {
506
+ if (!entry.isDirectory())
507
+ continue;
508
+ const dir = path.join(GROK_SESSIONS_DIR, entry.name, sessionId);
509
+ if (fs.existsSync(path.join(dir, 'chat_history.jsonl'))) {
510
+ return dir;
511
+ }
512
+ }
513
+ }
514
+ catch {
515
+ // ignore
516
+ }
517
+ return null;
518
+ }
455
519
  function resolveSessionFile(cwd, sessionId) {
456
520
  const claudeFile = path.join(getProjectDir(cwd), `${sessionId}.jsonl`);
457
521
  if (fs.existsSync(claudeFile)) {
@@ -473,6 +537,14 @@ function resolveSessionFile(cwd, sessionId) {
473
537
  if (opencodeFile && fs.existsSync(opencodeFile)) {
474
538
  return { provider: 'opencode', filePath: opencodeFile };
475
539
  }
540
+ const grokDir = findGrokSessionDir(cwd, sessionId);
541
+ if (grokDir) {
542
+ return {
543
+ provider: 'grok',
544
+ filePath: path.join(grokDir, 'chat_history.jsonl'),
545
+ grokSessionDir: grokDir,
546
+ };
547
+ }
476
548
  return null;
477
549
  }
478
550
  /**
@@ -1374,6 +1446,206 @@ async function parseOpencodeSessionMessages(sessionFilePath) {
1374
1446
  lastMessageTimestamp: last?.timestamp ? new Date(last.timestamp) : null,
1375
1447
  };
1376
1448
  }
1449
+ function extractGrokTextContent(content) {
1450
+ if (typeof content === 'string')
1451
+ return content;
1452
+ if (!Array.isArray(content))
1453
+ return '';
1454
+ const parts = [];
1455
+ for (const block of content) {
1456
+ if (!block || typeof block !== 'object')
1457
+ continue;
1458
+ const b = block;
1459
+ if (b.type === 'text' && typeof b.text === 'string') {
1460
+ parts.push(b.text);
1461
+ }
1462
+ }
1463
+ return parts.join('\n');
1464
+ }
1465
+ /** Prefer the Tide-injected <user_query> body when present. */
1466
+ function extractGrokUserDisplayText(content) {
1467
+ const raw = extractGrokTextContent(content).trim();
1468
+ if (!raw)
1469
+ return '';
1470
+ const queryMatch = raw.match(/<user_query>\s*([\s\S]*?)\s*<\/user_query>/i);
1471
+ if (queryMatch?.[1]) {
1472
+ return queryMatch[1].trim();
1473
+ }
1474
+ return raw;
1475
+ }
1476
+ function parseGrokToolArguments(args) {
1477
+ if (!args)
1478
+ return {};
1479
+ if (typeof args === 'object' && args !== null && !Array.isArray(args)) {
1480
+ return args;
1481
+ }
1482
+ if (typeof args === 'string') {
1483
+ try {
1484
+ const parsed = JSON.parse(args);
1485
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
1486
+ return parsed;
1487
+ }
1488
+ return { raw: args };
1489
+ }
1490
+ catch {
1491
+ return { raw: args };
1492
+ }
1493
+ }
1494
+ return {};
1495
+ }
1496
+ /**
1497
+ * Parse Grok chat_history.jsonl into Tide SessionMessages.
1498
+ * chat_history has no per-line timestamps — we synthesize monotonic ISO times
1499
+ * so the client sort order matches file order.
1500
+ */
1501
+ function parseGrokSessionMessages(chatHistoryPath) {
1502
+ const messages = [];
1503
+ if (!fs.existsSync(chatHistoryPath)) {
1504
+ return { messages: [], lastMessageType: null, lastMessageTimestamp: null };
1505
+ }
1506
+ // Timestamps: assign AFTER parse so the LAST message is file mtime and each
1507
+ // prior message is 1s earlier. Using (mtime - 1h + lineIndex) put the whole
1508
+ // history page ~50min behind wall-clock live events, so the merged
1509
+ // history+live list buried all tools ABOVE the live stream (user pin-to-
1510
+ // bottom only saw thinking + final text).
1511
+ let fileMtimeMs = Date.now();
1512
+ try {
1513
+ fileMtimeMs = fs.statSync(chatHistoryPath).mtimeMs;
1514
+ }
1515
+ catch {
1516
+ // ignore
1517
+ }
1518
+ const content = fs.readFileSync(chatHistoryPath, 'utf-8');
1519
+ const lines = content.split('\n');
1520
+ let lineIndex = 0;
1521
+ const toolUseIdToName = new Map();
1522
+ for (const line of lines) {
1523
+ if (!line.trim())
1524
+ continue;
1525
+ lineIndex += 1;
1526
+ let entry;
1527
+ try {
1528
+ entry = JSON.parse(line);
1529
+ }
1530
+ catch {
1531
+ continue;
1532
+ }
1533
+ const type = typeof entry.type === 'string' ? entry.type : '';
1534
+ // Placeholder — rewritten below from file mtime once we know message count.
1535
+ const ts = new Date(fileMtimeMs).toISOString();
1536
+ // Skip system prompt and synthetic MCP/system-reminder injections
1537
+ if (type === 'system')
1538
+ continue;
1539
+ if (type === 'user' && entry.synthetic_reason === 'system_reminder')
1540
+ continue;
1541
+ if (type === 'user') {
1542
+ const text = extractGrokUserDisplayText(entry.content);
1543
+ if (!text)
1544
+ continue;
1545
+ // Skip pure scaffolding (user_info / system-reminder without user_query)
1546
+ if (text.startsWith('<user_info>') ||
1547
+ text.startsWith('<system-reminder>') ||
1548
+ (text.includes('<system-reminder>') && !text.includes('<user_query>'))) {
1549
+ continue;
1550
+ }
1551
+ messages.push({
1552
+ type: 'user',
1553
+ content: text,
1554
+ timestamp: ts,
1555
+ uuid: `grok-user-${lineIndex}`,
1556
+ });
1557
+ continue;
1558
+ }
1559
+ if (type === 'reasoning') {
1560
+ // Optional thinking summaries — surface as assistant thinking lines
1561
+ const summary = entry.summary;
1562
+ let thinking = '';
1563
+ if (Array.isArray(summary)) {
1564
+ thinking = summary
1565
+ .map((s) => {
1566
+ if (s && typeof s === 'object' && typeof s.text === 'string') {
1567
+ return s.text;
1568
+ }
1569
+ return '';
1570
+ })
1571
+ .filter(Boolean)
1572
+ .join('\n');
1573
+ }
1574
+ if (thinking) {
1575
+ messages.push({
1576
+ type: 'assistant',
1577
+ content: `[thinking] ${thinking}`,
1578
+ timestamp: ts,
1579
+ uuid: typeof entry.id === 'string' ? entry.id : `grok-reasoning-${lineIndex}`,
1580
+ });
1581
+ }
1582
+ continue;
1583
+ }
1584
+ if (type === 'assistant') {
1585
+ const text = typeof entry.content === 'string' ? entry.content : extractGrokTextContent(entry.content);
1586
+ if (text.trim()) {
1587
+ messages.push({
1588
+ type: 'assistant',
1589
+ content: text,
1590
+ timestamp: ts,
1591
+ uuid: `grok-assistant-${lineIndex}`,
1592
+ });
1593
+ }
1594
+ const toolCalls = entry.tool_calls;
1595
+ if (Array.isArray(toolCalls)) {
1596
+ for (let ti = 0; ti < toolCalls.length; ti++) {
1597
+ const call = toolCalls[ti];
1598
+ const callId = call.id || `grok-call-${lineIndex}-${ti}`;
1599
+ const toolName = normalizeGrokToolName(call.name || 'unknown');
1600
+ toolUseIdToName.set(callId, toolName);
1601
+ messages.push({
1602
+ type: 'tool_use',
1603
+ content: '',
1604
+ timestamp: ts,
1605
+ uuid: callId,
1606
+ toolName,
1607
+ toolInput: parseGrokToolArguments(call.arguments),
1608
+ toolUseId: callId,
1609
+ });
1610
+ }
1611
+ }
1612
+ continue;
1613
+ }
1614
+ if (type === 'tool_result') {
1615
+ const callId = typeof entry.tool_call_id === 'string' ? entry.tool_call_id : `grok-result-${lineIndex}`;
1616
+ const content = typeof entry.content === 'string'
1617
+ ? entry.content
1618
+ : entry.content != null
1619
+ ? JSON.stringify(entry.content)
1620
+ : '';
1621
+ messages.push({
1622
+ type: 'tool_result',
1623
+ content,
1624
+ timestamp: ts,
1625
+ uuid: `${callId}-result`,
1626
+ toolUseId: callId,
1627
+ toolName: toolUseIdToName.get(callId) || 'unknown',
1628
+ });
1629
+ }
1630
+ }
1631
+ const dedupedMessages = deduplicateSessionMessages(messages);
1632
+ // Restamp so last message ≈ file mtime and order is preserved (1s steps).
1633
+ const n = dedupedMessages.length;
1634
+ if (n > 0) {
1635
+ for (let i = 0; i < n; i++) {
1636
+ dedupedMessages[i] = {
1637
+ ...dedupedMessages[i],
1638
+ timestamp: new Date(fileMtimeMs - (n - 1 - i) * 1000).toISOString(),
1639
+ };
1640
+ }
1641
+ }
1642
+ const last = n > 0 ? dedupedMessages[n - 1] : null;
1643
+ return {
1644
+ messages: dedupedMessages,
1645
+ lastMessageType: last?.type ?? null,
1646
+ lastMessageTimestamp: last?.timestamp ? new Date(last.timestamp) : null,
1647
+ };
1648
+ }
1377
1649
  async function parseSessionMessages(resolved) {
1378
1650
  if (resolved.provider === 'opencode') {
1379
1651
  if (resolved.opencodeDbSessionId) {
@@ -1381,6 +1653,9 @@ async function parseSessionMessages(resolved) {
1381
1653
  }
1382
1654
  return parseOpencodeSessionMessages(resolved.filePath);
1383
1655
  }
1656
+ if (resolved.provider === 'grok') {
1657
+ return parseGrokSessionMessages(resolved.filePath);
1658
+ }
1384
1659
  const messages = [];
1385
1660
  const toolUseIdToName = new Map();
1386
1661
  const fileStream = fs.createReadStream(resolved.filePath);
@@ -1685,12 +1960,16 @@ const PROVIDER_PROCESS_PATTERNS = {
1685
1960
  claude: '(claude$|/claude( |$)|claude\\.cmd|claude\\.exe)',
1686
1961
  codex: '(codex($| )|/codex( |$)|codex\\.cmd|codex\\.exe|codex\\.js|@openai/codex)',
1687
1962
  opencode: '(opencode($| )|/opencode( |$)|opencode\\.cmd|opencode\\.exe)',
1963
+ // Match `grok` CLI but not unrelated tools with "grok" in the path/name as a substring of a longer token.
1964
+ grok: '(^|/)grok($| )|grok\\.cmd|grok\\.exe',
1688
1965
  };
1689
1966
  function providerDisplayName(provider) {
1690
1967
  if (provider === 'codex')
1691
1968
  return 'Codex';
1692
1969
  if (provider === 'opencode')
1693
1970
  return 'OpenCode';
1971
+ if (provider === 'grok')
1972
+ return 'Grok';
1694
1973
  return 'Claude';
1695
1974
  }
1696
1975
  function getProviderProcessPids(provider, execSync) {
@@ -1891,6 +2170,22 @@ export async function findOpencodeProcessPidInCwd(cwd) {
1891
2170
  export async function killOpencodeProcessInCwd(cwd) {
1892
2171
  return killProviderProcessInCwd(cwd, 'opencode');
1893
2172
  }
2173
+ /**
2174
+ * Check if there's a Grok process running in a specific directory.
2175
+ */
2176
+ export async function isGrokProcessRunningInCwd(cwd) {
2177
+ return isProviderProcessRunningInCwd(cwd, 'grok');
2178
+ }
2179
+ export async function findGrokProcessPidInCwd(cwd) {
2180
+ return findProviderProcessPidInCwd(cwd, 'grok');
2181
+ }
2182
+ /**
2183
+ * Kill any Grok process running in the specified directory.
2184
+ * Returns true if a process was found and killed.
2185
+ */
2186
+ export async function killGrokProcessInCwd(cwd) {
2187
+ return killProviderProcessInCwd(cwd, 'grok');
2188
+ }
1894
2189
  export async function loadToolHistory(cwd, sessionId, agentId, agentName, limit = 100) {
1895
2190
  const toolExecutions = [];
1896
2191
  const fileChanges = [];