tide-commander 0.84.2 → 0.85.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.
@@ -5,6 +5,7 @@ import { createLogger } from '../utils/logger.js';
5
5
  const log = createLogger('CodexParser');
6
6
  let hasLoggedTurnAbortedLiveWarning = false;
7
7
  const MAX_DIFF_FILE_BYTES = 256 * 1024;
8
+ const MAX_FALLBACK_EVENT_TEXT = 4000;
8
9
  function sanitizeCodexMessageText(text) {
9
10
  const hadTurnAborted = /<turn_aborted>[\s\S]*?<\/turn_aborted>/.test(text);
10
11
  const withoutTurnAborted = text.replace(/<turn_aborted>[\s\S]*?<\/turn_aborted>/g, '').trim();
@@ -71,10 +72,37 @@ function parseEnvelope(value) {
71
72
  type: asString(value.type),
72
73
  item: parseItem(value.item),
73
74
  usage: parseUsage(value.usage),
75
+ payload: parseResponsePayload(value.payload),
76
+ };
77
+ }
78
+ function parseSummaryArray(value) {
79
+ if (!Array.isArray(value))
80
+ return undefined;
81
+ return value
82
+ .filter(isObject)
83
+ .map((entry) => ({
84
+ type: asString(entry.type),
85
+ text: asString(entry.text),
86
+ }));
87
+ }
88
+ function parseResponsePayload(payload) {
89
+ if (!isObject(payload))
90
+ return undefined;
91
+ return {
92
+ type: asString(payload.type),
93
+ status: asString(payload.status),
94
+ action: parseAction(payload.action),
95
+ item: parseItem(payload.item),
96
+ usage: parseUsage(payload.usage),
97
+ call_id: asString(payload.call_id),
98
+ name: asString(payload.name),
99
+ input: asString(payload.input),
100
+ output: asString(payload.output),
101
+ summary: parseSummaryArray(payload.summary),
74
102
  };
75
103
  }
76
104
  /**
77
- * Parses line-delimited JSON events from `codex exec --json` and maps them to
105
+ * Parses line-delimited JSON events from `codex exec --experimental-json` and maps them to
78
106
  * Tide runtime events.
79
107
  */
80
108
  export class CodexJsonEventParser {
@@ -108,6 +136,12 @@ export class CodexJsonEventParser {
108
136
  const event = parseEnvelope(rawEvent);
109
137
  if (!event?.type)
110
138
  return [];
139
+ if (event.type === 'event_msg') {
140
+ return this.parseEventMsg(rawEvent);
141
+ }
142
+ if (event.type === 'response_item') {
143
+ return this.parseResponseItem(event.payload);
144
+ }
111
145
  if (event.type === 'item.started') {
112
146
  return this.parseItemStarted(event.item);
113
147
  }
@@ -121,7 +155,171 @@ export class CodexJsonEventParser {
121
155
  if (event.type === 'turn.completed') {
122
156
  return this.parseTurnCompleted(event.usage);
123
157
  }
124
- return [];
158
+ // Informational envelope events that don't need terminal display
159
+ if (event.type === 'turn.started' || event.type === 'thread.started') {
160
+ return [];
161
+ }
162
+ return [this.buildUnknownEventFallback(`Unhandled Codex event type: ${event.type}`, rawEvent)];
163
+ }
164
+ parseEventMsg(rawEvent) {
165
+ if (!isObject(rawEvent))
166
+ return [];
167
+ const payload = rawEvent.payload;
168
+ if (!isObject(payload))
169
+ return [];
170
+ const payloadType = asString(payload.type);
171
+ // token_count: Silent. Token accounting is handled by turn.completed.
172
+ if (payloadType === 'token_count') {
173
+ return [];
174
+ }
175
+ // agent_reasoning: Map to thinking event
176
+ if (payloadType === 'agent_reasoning') {
177
+ const text = asString(payload.text);
178
+ if (!text)
179
+ return [];
180
+ return [{ type: 'thinking', text, isStreaming: false }];
181
+ }
182
+ // turn_aborted: Silent. The interruption is already visible from the agent stopping.
183
+ if (payloadType === 'turn_aborted') {
184
+ return [];
185
+ }
186
+ // user_message / agent_message are handled via item.completed, skip if seen here
187
+ if (payloadType === 'user_message' || payloadType === 'agent_message') {
188
+ return [];
189
+ }
190
+ // task_complete: Display the final agent message as formatted text
191
+ if (payloadType === 'task_complete') {
192
+ const lastMsg = asString(payload.last_agent_message);
193
+ if (lastMsg) {
194
+ return [{ type: 'text', text: lastMsg, isStreaming: false }];
195
+ }
196
+ return [];
197
+ }
198
+ return [this.buildUnknownEventFallback(`Unhandled Codex event_msg type: ${payloadType}`, payload)];
199
+ }
200
+ parseResponseItem(payload) {
201
+ if (!payload?.type)
202
+ return [];
203
+ // Codex can emit web search activity wrapped in response_item payloads.
204
+ // Emit synthetic start/result so terminal streaming and history stay consistent.
205
+ if (payload.type === 'web_search_call') {
206
+ const toolName = 'web_search';
207
+ const toolInput = {
208
+ actionType: payload.action?.type,
209
+ actionQuery: payload.action?.query,
210
+ actionQueries: payload.action?.queries,
211
+ actionUrl: payload.action?.url,
212
+ status: payload.status,
213
+ };
214
+ if (payload.status === 'completed') {
215
+ return [
216
+ { type: 'tool_start', toolName, toolInput },
217
+ { type: 'tool_result', toolName, toolOutput: JSON.stringify(toolInput) },
218
+ ];
219
+ }
220
+ if (payload.status === 'in_progress' || payload.status === 'started') {
221
+ return [{ type: 'tool_start', toolName, toolInput }];
222
+ }
223
+ return [{ type: 'tool_result', toolName, toolOutput: JSON.stringify(toolInput) }];
224
+ }
225
+ // reasoning: Map summary text to thinking event
226
+ if (payload.type === 'reasoning') {
227
+ const text = this.extractReasoningSummaryText(payload);
228
+ if (!text)
229
+ return [];
230
+ return [{ type: 'thinking', text, isStreaming: false }];
231
+ }
232
+ // custom_tool_call: Map to tool_start (with special handling for apply_patch)
233
+ if (payload.type === 'custom_tool_call') {
234
+ return this.parseCustomToolCall(payload);
235
+ }
236
+ // custom_tool_call_output: Map to tool_result
237
+ if (payload.type === 'custom_tool_call_output') {
238
+ return this.parseCustomToolCallOutput(payload);
239
+ }
240
+ return [this.buildUnknownEventFallback(`Unhandled Codex response_item payload type: ${payload.type}`, payload)];
241
+ }
242
+ extractReasoningSummaryText(payload) {
243
+ if (!payload.summary || !Array.isArray(payload.summary))
244
+ return undefined;
245
+ const texts = payload.summary
246
+ .map((s) => s.text)
247
+ .filter((t) => Boolean(t));
248
+ return texts.length > 0 ? texts.join('\n') : undefined;
249
+ }
250
+ parseCustomToolCall(payload) {
251
+ const toolName = payload.name || 'unknown_tool';
252
+ const callId = payload.call_id;
253
+ const input = payload.input || '';
254
+ // Track tool name by call_id for correlating with custom_tool_call_output
255
+ if (callId) {
256
+ this.activeToolByItemId.set(callId, toolName);
257
+ }
258
+ // Special handling for apply_patch: generate synthetic Edit/Write events
259
+ if (toolName === 'apply_patch' && input) {
260
+ const inferredCalls = this.extractApplyPatchOperations(input);
261
+ if (inferredCalls.length > 0) {
262
+ const events = [];
263
+ for (const call of inferredCalls) {
264
+ const filePath = this.stringField(call.toolInput.file_path);
265
+ if (filePath) {
266
+ const uiPath = this.normalizePathForUi(filePath);
267
+ if (uiPath) {
268
+ call.toolInput.file_path = uiPath;
269
+ }
270
+ }
271
+ events.push({
272
+ type: 'tool_start',
273
+ toolName: call.toolName,
274
+ toolInput: call.toolInput,
275
+ });
276
+ if (call.toolOutput) {
277
+ events.push({
278
+ type: 'tool_result',
279
+ toolName: call.toolName,
280
+ toolOutput: call.toolOutput,
281
+ });
282
+ }
283
+ }
284
+ return events;
285
+ }
286
+ }
287
+ // Generic tool_start for non-apply_patch or when patch parsing yields nothing
288
+ return [{
289
+ type: 'tool_start',
290
+ toolName,
291
+ toolInput: { input, call_id: callId },
292
+ }];
293
+ }
294
+ parseCustomToolCallOutput(payload) {
295
+ const callId = payload.call_id;
296
+ const toolName = callId
297
+ ? (this.activeToolByItemId.get(callId) ?? 'unknown_tool')
298
+ : 'unknown_tool';
299
+ if (callId) {
300
+ this.activeToolByItemId.delete(callId);
301
+ }
302
+ let toolOutput = payload.output || '';
303
+ // For apply_patch, extract the meaningful output from the JSON wrapper
304
+ if (toolName === 'apply_patch' && toolOutput) {
305
+ try {
306
+ const parsed = JSON.parse(toolOutput);
307
+ if (isObject(parsed)) {
308
+ const outputText = asString(parsed.output);
309
+ if (outputText) {
310
+ toolOutput = outputText;
311
+ }
312
+ }
313
+ }
314
+ catch {
315
+ // Use raw output string as-is
316
+ }
317
+ }
318
+ return [{
319
+ type: 'tool_result',
320
+ toolName: toolName === 'apply_patch' ? 'Edit' : toolName,
321
+ toolOutput,
322
+ }];
125
323
  }
126
324
  parseItemStarted(item) {
127
325
  if (!item?.type)
@@ -152,7 +350,7 @@ export class CodexJsonEventParser {
152
350
  },
153
351
  ];
154
352
  }
155
- return [];
353
+ return [this.buildUnknownEventFallback(`Unhandled Codex item.started type: ${item.type}`, item)];
156
354
  }
157
355
  parseItemCompleted(item) {
158
356
  if (!item?.type)
@@ -203,7 +401,12 @@ export class CodexJsonEventParser {
203
401
  },
204
402
  ];
205
403
  }
206
- return [];
404
+ // file_change: Silent. File change info is already captured from the
405
+ // command_execution or custom_tool_call that caused it.
406
+ if (item.type === 'file_change') {
407
+ return [];
408
+ }
409
+ return [this.buildUnknownEventFallback(`Unhandled Codex item.completed type: ${item.type}`, item)];
207
410
  }
208
411
  parseTurnCompleted(usage) {
209
412
  if (!usage)
@@ -223,6 +426,23 @@ export class CodexJsonEventParser {
223
426
  }
224
427
  return [event];
225
428
  }
429
+ buildUnknownEventFallback(prefix, payload) {
430
+ let serialized = '';
431
+ try {
432
+ serialized = JSON.stringify(payload);
433
+ }
434
+ catch {
435
+ serialized = String(payload);
436
+ }
437
+ if (serialized.length > MAX_FALLBACK_EVENT_TEXT) {
438
+ serialized = `${serialized.slice(0, MAX_FALLBACK_EVENT_TEXT)}...`;
439
+ }
440
+ return {
441
+ type: 'text',
442
+ text: `[codex-event] ${prefix}\n${serialized}`,
443
+ isStreaming: false,
444
+ };
445
+ }
226
446
  buildWebSearchToolInput(item) {
227
447
  return {
228
448
  query: item.query,
@@ -2,7 +2,7 @@ export const fullNotifications = {
2
2
  slug: 'full-notifications',
3
3
  name: 'Full Notifications',
4
4
  description: 'Send notification via browser, android or in-app',
5
- allowedTools: ['Bash(curl:*)', 'Bash(gdbus:*)'],
5
+ allowedTools: ['Bash(curl:*)'],
6
6
  content: `# Task Completion Notifications (MANDATORY)
7
7
 
8
8
  **IMPORTANT: You MUST send a notification automatically whenever you complete a task. This is not optional.**
@@ -14,31 +14,26 @@ export const fullNotifications = {
14
14
 
15
15
  ## Command Template
16
16
  \`\`\`bash
17
- curl -s -X POST http://localhost:5174/api/notify -H "Content-Type: application/json" -d '{"agentId":"YOUR_AGENT_ID","title":"TITLE","message":"MESSAGE"}' & gdbus call --session --dest=org.freedesktop.Notifications --object-path=/org/freedesktop/Notifications --method=org.freedesktop.Notifications.Notify 'Tide Commander' 0 'ICON' 'TITLE' 'MESSAGE' '[]' '{}' 5000
17
+ curl -s -X POST http://localhost:5174/api/notify -H "Content-Type: application/json" -d '{"agentId":"YOUR_AGENT_ID","title":"TITLE","message":"MESSAGE"}'
18
18
  \`\`\`
19
19
 
20
20
  ## Examples by Type
21
21
 
22
22
  **Task Complete:**
23
23
  \`\`\`bash
24
- curl -s -X POST http://localhost:5174/api/notify -H "Content-Type: application/json" -d '{"agentId":"YOUR_AGENT_ID","title":"Task Complete","message":"Build succeeded"}' & gdbus call --session --dest=org.freedesktop.Notifications --object-path=/org/freedesktop/Notifications --method=org.freedesktop.Notifications.Notify 'Tide Commander' 0 'dialog-information' 'Task Complete' 'Build succeeded' '[]' '{}' 5000
24
+ curl -s -X POST http://localhost:5174/api/notify -H "Content-Type: application/json" -d '{"agentId":"YOUR_AGENT_ID","title":"Task Complete","message":"Build succeeded"}'
25
25
  \`\`\`
26
26
 
27
27
  **Error/Attention Needed:**
28
28
  \`\`\`bash
29
- curl -s -X POST http://localhost:5174/api/notify -H "Content-Type: application/json" -d '{"agentId":"YOUR_AGENT_ID","title":"Error","message":"Build failed"}' & gdbus call --session --dest=org.freedesktop.Notifications --object-path=/org/freedesktop/Notifications --method=org.freedesktop.Notifications.Notify 'Tide Commander' 0 'dialog-warning' 'Error' 'Build failed' '[]' '{}' 5000
29
+ curl -s -X POST http://localhost:5174/api/notify -H "Content-Type: application/json" -d '{"agentId":"YOUR_AGENT_ID","title":"Error","message":"Build failed"}'
30
30
  \`\`\`
31
31
 
32
32
  **Input Required:**
33
33
  \`\`\`bash
34
- curl -s -X POST http://localhost:5174/api/notify -H "Content-Type: application/json" -d '{"agentId":"YOUR_AGENT_ID","title":"Input Needed","message":"Which database?"}' & gdbus call --session --dest=org.freedesktop.Notifications --object-path=/org/freedesktop/Notifications --method=org.freedesktop.Notifications.Notify 'Tide Commander' 0 'dialog-question' 'Input Needed' 'Which database?' '[]' '{}' 5000
34
+ curl -s -X POST http://localhost:5174/api/notify -H "Content-Type: application/json" -d '{"agentId":"YOUR_AGENT_ID","title":"Input Needed","message":"Which database?"}'
35
35
  \`\`\`
36
36
 
37
- ## Icons (gdbus)
38
- - \`dialog-information\` - Task complete
39
- - \`dialog-warning\` - Error/attention needed
40
- - \`dialog-question\` - Input required
41
-
42
37
  ## Rules
43
38
  - Replace \`YOUR_AGENT_ID\` with your actual agent ID from the system prompt
44
39
  - Keep messages under 50 characters
@@ -49,6 +44,5 @@ curl -s -X POST http://localhost:5174/api/notify -H "Content-Type: application/j
49
44
  - If task involves waiting for other agents to finish, do NOT notify until they confirm completion
50
45
  - Only notify when YOU have nothing more to do on this task
51
46
  - Send notification as your FINAL action after completing work
52
- - Do NOT skip this step - the user relies on notifications
53
- - The \`&\` runs both commands in parallel (curl for mobile/browser, gdbus for Linux desktop)`,
47
+ - Do NOT skip this step - the user relies on notifications`,
54
48
  };
@@ -12,7 +12,7 @@ import { getClaudeProjectDir } from '../data/index.js';
12
12
  // Session listing is done inline for performance
13
13
  import { createLogger } from '../utils/logger.js';
14
14
  import { buildCustomAgentConfig } from '../websocket/handlers/command-handler.js';
15
- import { getSystemPrompt, setSystemPrompt, clearSystemPrompt, isEchoPromptEnabled, setEchoPromptEnabled } from '../services/system-prompt-service.js';
15
+ import { getSystemPrompt, setSystemPrompt, clearSystemPrompt, isEchoPromptEnabled, setEchoPromptEnabled, getCodexBinaryPath, setCodexBinaryPath } from '../services/system-prompt-service.js';
16
16
  const log = createLogger('Routes');
17
17
  const router = Router();
18
18
  function runCommandWithTimeout(command, args, timeoutMs, cwd) {
@@ -482,4 +482,32 @@ router.post('/system-settings/echo-prompt', (req, res) => {
482
482
  res.status(500).json({ error: err.message });
483
483
  }
484
484
  });
485
+ // GET /api/system-settings/codex-binary - Get the codex binary path
486
+ router.get('/system-settings/codex-binary', (_req, res) => {
487
+ try {
488
+ const binaryPath = getCodexBinaryPath();
489
+ res.json({ path: binaryPath });
490
+ }
491
+ catch (err) {
492
+ log.error(' Failed to get codex binary path:', err);
493
+ res.status(500).json({ error: err.message });
494
+ }
495
+ });
496
+ // POST /api/system-settings/codex-binary - Set the codex binary path
497
+ router.post('/system-settings/codex-binary', (req, res) => {
498
+ try {
499
+ const { path: binaryPath } = req.body;
500
+ if (typeof binaryPath !== 'string') {
501
+ res.status(400).json({ error: 'path must be a string' });
502
+ return;
503
+ }
504
+ setCodexBinaryPath(binaryPath);
505
+ log.log(` Codex binary path updated: ${binaryPath || '(cleared)'}`);
506
+ res.json({ success: true, path: binaryPath });
507
+ }
508
+ catch (err) {
509
+ log.error(' Failed to set codex binary path:', err);
510
+ res.status(500).json({ error: err.message });
511
+ }
512
+ });
485
513
  export default router;
@@ -97,7 +97,7 @@ const CONFIG_CATEGORIES = [
97
97
  id: 'system-settings',
98
98
  name: 'System Settings',
99
99
  description: 'Global system prompt and settings',
100
- files: ['system-prompt.json', 'echo-prompt-setting.json'],
100
+ files: ['system-prompt.json', 'echo-prompt-setting.json', 'codex-binary-path.json'],
101
101
  sourceDir: 'data',
102
102
  },
103
103
  ];
@@ -115,3 +115,58 @@ export function setEchoPromptEnabled(enabled) {
115
115
  throw error;
116
116
  }
117
117
  }
118
+ // ============================================================================
119
+ // Codex Binary Path Setting
120
+ // ============================================================================
121
+ const CODEX_BINARY_FILE = path.join(DATA_DIR, 'codex-binary-path.json');
122
+ /**
123
+ * Get the configured codex binary path (empty string if not set)
124
+ */
125
+ export function getCodexBinaryPath() {
126
+ ensureDataDir();
127
+ try {
128
+ if (fs.existsSync(CODEX_BINARY_FILE)) {
129
+ const data = JSON.parse(fs.readFileSync(CODEX_BINARY_FILE, 'utf-8'));
130
+ return data.path;
131
+ }
132
+ }
133
+ catch (error) {
134
+ log.error(` Failed to load codex binary path: ${error.message}`);
135
+ }
136
+ return '';
137
+ }
138
+ /**
139
+ * Set the codex binary path
140
+ */
141
+ export function setCodexBinaryPath(binaryPath) {
142
+ ensureDataDir();
143
+ const trimmed = binaryPath.trim();
144
+ if (trimmed) {
145
+ const data = {
146
+ path: trimmed,
147
+ updatedAt: Date.now(),
148
+ };
149
+ fs.writeFileSync(CODEX_BINARY_FILE, JSON.stringify(data, null, 2), 'utf-8');
150
+ log.log(` Codex binary path set: ${trimmed}`);
151
+ }
152
+ else {
153
+ // Empty means clear
154
+ clearCodexBinaryPath();
155
+ }
156
+ }
157
+ /**
158
+ * Clear the codex binary path (revert to auto-detect)
159
+ */
160
+ export function clearCodexBinaryPath() {
161
+ ensureDataDir();
162
+ try {
163
+ if (fs.existsSync(CODEX_BINARY_FILE)) {
164
+ fs.unlinkSync(CODEX_BINARY_FILE);
165
+ log.log(` Codex binary path cleared (will auto-detect)`);
166
+ }
167
+ }
168
+ catch (error) {
169
+ log.error(` Failed to clear codex binary path: ${error.message}`);
170
+ throw error;
171
+ }
172
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tide-commander",
3
- "version": "0.84.2",
3
+ "version": "0.85.0",
4
4
  "description": "Visual multi-agent orchestrator and manager for Claude Code with 3D/2D interface",
5
5
  "repository": {
6
6
  "type": "git",