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.
- package/CHANGELOG.md +33 -0
- package/README.md +6 -3
- package/dist/assets/{BossLogsModal-CZMHgMBE.js → BossLogsModal-DvczRUvP.js} +1 -1
- package/dist/assets/BossSpawnModal-B2autFKk.js +1 -0
- package/dist/assets/{ControlsModal-DJ8Scm16.js → ControlsModal-BaREMQxc.js} +1 -1
- package/dist/assets/{DockerLogsModal-Vb4gqdFz.js → DockerLogsModal-DZ0udMCo.js} +1 -1
- package/dist/assets/{EmbeddedEditor-D4sHERAj.js → EmbeddedEditor-CEjr0ZSZ.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-COIYIbJS.js → GmailOAuthSetup-76BP8J8C.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-DJS59Soi.js → GoogleOAuthSetup-D45GImPI.js} +1 -1
- package/dist/assets/{IframeModal-CFysTkn9.js → IframeModal-De63RfJZ.js} +1 -1
- package/dist/assets/{IntegrationsPanel-BK8g9ceY.js → IntegrationsPanel-DYrw1Sqk.js} +2 -2
- package/dist/assets/{LogViewerModal-DRxR0HYq.js → LogViewerModal-DW-2lpln.js} +1 -1
- package/dist/assets/{MonitoringModal-CL7UMOdX.js → MonitoringModal-B0-XXyd3.js} +1 -1
- package/dist/assets/{PM2LogsModal-I3aouUlw.js → PM2LogsModal-DOfOhkCY.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-PWAg6w6b.js → RestoreArchivedAreaModal-DNGmIJmM.js} +1 -1
- package/dist/assets/Scene2DCanvas-BW1E76CQ.js +1 -0
- package/dist/assets/SceneManager-DAvc9-Wp.js +104 -0
- package/dist/assets/{SkillsPanel-EQ74sIa6.js → SkillsPanel-Br-gLCLA.js} +3 -3
- package/dist/assets/{SlackMultiInstanceSetup-CLMUteI0.js → SlackMultiInstanceSetup-DG_bIfDu.js} +1 -1
- package/dist/assets/SpawnModal-lWM8OuZW.js +1 -0
- package/dist/assets/{StatisticsModal-D6MgA_o0.js → StatisticsModal-LvA0Fcvn.js} +1 -1
- package/dist/assets/{SubordinateAssignmentModal-C6n0pMn3.js → SubordinateAssignmentModal-8YxNXcR3.js} +1 -1
- package/dist/assets/{TriggerManagerPanel-Umd8iUg1.js → TriggerManagerPanel-DftIuKUG.js} +1 -1
- package/dist/assets/{WorkflowEditorPanel-CxjBxs9O.js → WorkflowEditorPanel-oLwyO_C6.js} +1 -1
- package/dist/assets/{area-logos-CThPeqIM.js → area-logos-mcpQqUMZ.js} +1 -1
- package/dist/assets/grok.ico +0 -0
- package/dist/assets/grok.png +0 -0
- package/dist/assets/index-Ay7dKyeR.js +1 -0
- package/dist/assets/index-BMw7AAAK.js +5 -0
- package/dist/assets/{index-Cz-ypmob.js → index-BQpd34Mc.js} +1 -1
- package/dist/assets/{index-BUN7MVf5.js → index-C6-68CXL.js} +3 -3
- package/dist/assets/index-CIon0DLf.js +2 -0
- package/dist/assets/{index-maZSt2YW.js → index-Cwt-E2td.js} +5 -5
- package/dist/assets/{index-DelocTaH.js → index-DFVroqA5.js} +1 -1
- package/dist/assets/index-DR6_WeF_.js +1 -0
- package/dist/assets/{index-h3Cuh2Zn.js → index-O0t07OB_.js} +2 -2
- package/dist/assets/main-D5sqeuwB.js +286 -0
- package/dist/assets/main-JBkwGXfE.css +1 -0
- package/dist/assets/{web-CfvEypdU.js → web-BkrshujV.js} +1 -1
- package/dist/assets/{web-CNjTPilF.js → web-DPETNqOf.js} +1 -1
- package/dist/assets/{web-bcYaAwP6.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/en/tools.json +4 -2
- 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 +62 -0
- package/dist/src/packages/server/claude/runner/stdout-pipeline.js +148 -7
- package/dist/src/packages/server/claude/runner/watchdog.js +12 -0
- package/dist/src/packages/server/claude/session-loader.js +295 -0
- package/dist/src/packages/server/codex/json-event-parser.js +149 -4
- package/dist/src/packages/server/data/index.js +1 -0
- package/dist/src/packages/server/grok/backend.js +235 -0
- package/dist/src/packages/server/grok/index.js +4 -0
- package/dist/src/packages/server/grok/json-event-parser.js +153 -0
- package/dist/src/packages/server/grok/session-watcher.js +493 -0
- package/dist/src/packages/server/opencode/json-event-parser.js +184 -15
- package/dist/src/packages/server/routes/agents.js +19 -11
- package/dist/src/packages/server/runtime/grok-runtime-provider.js +11 -0
- package/dist/src/packages/server/runtime/index.js +1 -0
- package/dist/src/packages/server/services/agent-service.js +26 -5
- package/dist/src/packages/server/services/grok-usage-service.js +374 -0
- package/dist/src/packages/server/services/runtime-command-execution.js +4 -2
- package/dist/src/packages/server/services/runtime-events.js +46 -7
- package/dist/src/packages/server/services/runtime-service.js +19 -3
- package/dist/src/packages/server/utils/tool-formatting.js +40 -12
- package/dist/src/packages/server/websocket/handlers/agent-handler.js +70 -17
- package/dist/src/packages/server/websocket/listeners/runtime-listeners.js +73 -14
- package/dist/src/packages/shared/agent-types.js +10 -0
- package/package.json +1 -1
- package/dist/assets/BossSpawnModal-C6DxqtKP.js +0 -1
- package/dist/assets/Scene2DCanvas-DYPHkU3i.js +0 -1
- package/dist/assets/SceneManager-QqKygyaw.js +0 -104
- package/dist/assets/SpawnModal-BOeStihN.js +0 -1
- package/dist/assets/index-DOzFBkSH.js +0 -1
- package/dist/assets/index-DP7kd91h.js +0 -1
- package/dist/assets/index-DRyMARFk.js +0 -5
- package/dist/assets/index-DuWmOZdY.js +0 -2
- package/dist/assets/main-C2zgPci-.js +0 -256
- 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
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
|
258
|
+
toolInput,
|
|
178
259
|
});
|
|
179
|
-
|
|
180
|
-
|
|
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 = [];
|