@phren/agent 0.1.3 → 0.1.5

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 (42) hide show
  1. package/dist/agent-loop/index.js +214 -0
  2. package/dist/agent-loop/stream.js +124 -0
  3. package/dist/agent-loop/types.js +13 -0
  4. package/dist/agent-loop.js +7 -333
  5. package/dist/commands/info.js +146 -0
  6. package/dist/commands/memory.js +165 -0
  7. package/dist/commands/model.js +138 -0
  8. package/dist/commands/session.js +213 -0
  9. package/dist/commands.js +24 -643
  10. package/dist/index.js +9 -4
  11. package/dist/mcp-client.js +11 -7
  12. package/dist/multi/multi-commands.js +170 -0
  13. package/dist/multi/multi-events.js +81 -0
  14. package/dist/multi/multi-render.js +146 -0
  15. package/dist/multi/pane.js +28 -0
  16. package/dist/multi/tui-multi.js +39 -454
  17. package/dist/permissions/allowlist.js +2 -2
  18. package/dist/providers/anthropic.js +4 -2
  19. package/dist/providers/codex.js +9 -4
  20. package/dist/providers/openai-compat.js +6 -1
  21. package/dist/tools/glob.js +30 -6
  22. package/dist/tui/ansi.js +48 -0
  23. package/dist/tui/components/AgentMessage.js +5 -0
  24. package/dist/tui/components/App.js +68 -0
  25. package/dist/tui/components/Banner.js +44 -0
  26. package/dist/tui/components/ChatMessage.js +23 -0
  27. package/dist/tui/components/InputArea.js +23 -0
  28. package/dist/tui/components/Separator.js +7 -0
  29. package/dist/tui/components/StatusBar.js +25 -0
  30. package/dist/tui/components/SteerQueue.js +7 -0
  31. package/dist/tui/components/StreamingText.js +5 -0
  32. package/dist/tui/components/ThinkingIndicator.js +26 -0
  33. package/dist/tui/components/ToolCall.js +11 -0
  34. package/dist/tui/components/UserMessage.js +5 -0
  35. package/dist/tui/hooks/useKeyboardShortcuts.js +89 -0
  36. package/dist/tui/hooks/useSlashCommands.js +52 -0
  37. package/dist/tui/index.js +5 -0
  38. package/dist/tui/ink-entry.js +287 -0
  39. package/dist/tui/menu-mode.js +86 -0
  40. package/dist/tui/tool-render.js +43 -0
  41. package/dist/tui.js +149 -280
  42. package/package.json +9 -2
@@ -1,104 +1,9 @@
1
- /**
2
- * Multiplexed TUI for multi-agent orchestration.
3
- *
4
- * Full alternate-screen terminal with:
5
- * - Top bar: agent tabs with status color coding
6
- * - Main area: scrollback buffer for the selected agent
7
- * - Bottom bar: input line + keyboard hints
8
- *
9
- * Keyboard:
10
- * 1-9 Select agent pane by index
11
- * Ctrl+Left/Right Cycle between agent panes
12
- * Enter Send input / execute command
13
- * /spawn <n> <t> Spawn a new agent
14
- * /list List all agents
15
- * /kill <name> Terminate an agent
16
- * /broadcast <msg> Send message to all agents
17
- * Ctrl+D Exit (kills all agents)
18
- */
1
+ /** Multiplexed TUI for multi-agent orchestration. */
19
2
  import * as readline from "node:readline";
20
- import { getAgentStyle, formatAgentName } from "./agent-colors.js";
21
- import { decodeDiffPayload, renderInlineDiff, DIFF_MARKER } from "./diff-renderer.js";
22
- // ── ANSI helpers (mirrors tui.ts pattern) ────────────────────────────────────
23
- const ESC = "\x1b[";
24
- const s = {
25
- reset: `${ESC}0m`,
26
- bold: (t) => `${ESC}1m${t}${ESC}0m`,
27
- dim: (t) => `${ESC}2m${t}${ESC}0m`,
28
- cyan: (t) => `${ESC}36m${t}${ESC}0m`,
29
- green: (t) => `${ESC}32m${t}${ESC}0m`,
30
- yellow: (t) => `${ESC}33m${t}${ESC}0m`,
31
- red: (t) => `${ESC}31m${t}${ESC}0m`,
32
- gray: (t) => `${ESC}90m${t}${ESC}0m`,
33
- white: (t) => `${ESC}37m${t}${ESC}0m`,
34
- bgGreen: (t) => `${ESC}42m${t}${ESC}0m`,
35
- bgRed: (t) => `${ESC}41m${t}${ESC}0m`,
36
- bgGray: (t) => `${ESC}100m${t}${ESC}0m`,
37
- bgCyan: (t) => `${ESC}46m${t}${ESC}0m`,
38
- bgYellow: (t) => `${ESC}43m${t}${ESC}0m`,
39
- invert: (t) => `${ESC}7m${t}${ESC}0m`,
40
- };
41
- function stripAnsi(t) {
42
- return t.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "");
43
- }
44
- function cols() {
45
- return process.stdout.columns || 80;
46
- }
47
- function rows() {
48
- return process.stdout.rows || 24;
49
- }
50
- // ── Pane buffer ──────────────────────────────────────────────────────────────
51
- const MAX_SCROLLBACK = 1000;
52
- let nextPaneIndex = 0;
53
- function createPane(agentId, name) {
54
- return { agentId, name, index: nextPaneIndex++, lines: [], partial: "" };
55
- }
56
- function appendToPane(pane, text) {
57
- // Merge with partial line buffer
58
- const combined = pane.partial + text;
59
- const parts = combined.split("\n");
60
- // Everything except the last segment is a complete line
61
- for (let i = 0; i < parts.length - 1; i++) {
62
- pane.lines.push(parts[i]);
63
- }
64
- pane.partial = parts[parts.length - 1];
65
- // Enforce scrollback cap
66
- if (pane.lines.length > MAX_SCROLLBACK) {
67
- pane.lines.splice(0, pane.lines.length - MAX_SCROLLBACK);
68
- }
69
- }
70
- function flushPartial(pane) {
71
- if (pane.partial) {
72
- pane.lines.push(pane.partial);
73
- pane.partial = "";
74
- }
75
- }
76
- // ── Status color ─────────────────────────────────────────────────────────────
77
- function statusColor(status) {
78
- switch (status) {
79
- case "starting": return s.yellow;
80
- case "running": return s.green;
81
- case "done": return s.gray;
82
- case "error": return s.red;
83
- case "cancelled": return s.gray;
84
- }
85
- }
86
- // ── Tool call formatting ─────────────────────────────────────────────────────
87
- function formatToolStart(toolName, input) {
88
- const preview = JSON.stringify(input).slice(0, 60);
89
- return s.dim(` > ${toolName}(${preview})...`);
90
- }
91
- function formatToolEnd(toolName, input, output, isError, durationMs) {
92
- const dur = durationMs < 1000 ? `${durationMs}ms` : `${(durationMs / 1000).toFixed(1)}s`;
93
- const icon = isError ? s.red("x") : s.green("ok");
94
- const preview = JSON.stringify(input).slice(0, 50);
95
- const header = s.dim(` ${toolName}(${preview})`) + ` ${icon} ${s.dim(dur)}`;
96
- const allLines = output.split("\n");
97
- const w = cols();
98
- const body = allLines.slice(0, 4).map((l) => s.dim(` | ${l.slice(0, w - 6)}`)).join("\n");
99
- const more = allLines.length > 4 ? `\n${s.dim(` | ... (${allLines.length} lines)`)}` : "";
100
- return `${header}\n${body}${more}`;
101
- }
3
+ import { MAX_SCROLLBACK, createPane, appendToPane } from "./pane.js";
4
+ import { ESC, s, render } from "./multi-render.js";
5
+ import { handleSlashCommand } from "./multi-commands.js";
6
+ import { wireSpawnerEvents } from "./multi-events.js";
102
7
  // ── Main TUI ─────────────────────────────────────────────────────────────────
103
8
  export async function startMultiTui(spawner, config) {
104
9
  const w = process.stdout;
@@ -130,7 +35,7 @@ export async function startMultiTui(spawner, config) {
130
35
  if (panes.has(agentId)) {
131
36
  selectedId = agentId;
132
37
  scrollOffset = 0;
133
- render();
38
+ doRender();
134
39
  }
135
40
  }
136
41
  function selectByIndex(index) {
@@ -149,349 +54,29 @@ export async function startMultiTui(spawner, config) {
149
54
  next = 0;
150
55
  selectAgent(agentOrder[next]);
151
56
  }
152
- // ── Rendering ──────────────────────────────────────────────────────────
153
- function renderTopBar() {
154
- const w_ = cols();
155
- const agents = spawner.listAgents();
156
- const tabs = [];
157
- for (let i = 0; i < agents.length; i++) {
158
- const a = agents[i];
159
- const isSel = a.id === selectedId;
160
- const pane = panes.get(a.id);
161
- const paneIdx = pane?.index ?? i;
162
- const stColor = statusColor(a.status);
163
- const agentLabel = formatAgentName(pane?.name ?? a.task.slice(0, 12), paneIdx);
164
- const statusTag = stColor(a.status);
165
- const raw = ` ${i + 1}:${stripAnsi(agentLabel)} [${stripAnsi(statusTag)}] `;
166
- const colored = ` ${i + 1}:${agentLabel} [${statusTag}] `;
167
- const tab = isSel ? s.invert(raw) : colored;
168
- tabs.push(tab);
169
- }
170
- if (agents.length === 0) {
171
- tabs.push(s.dim(" no agents "));
172
- }
173
- const title = s.bold(" phren-multi ");
174
- const tabStr = tabs.join(s.dim("|"));
175
- const line = title + s.dim("|") + tabStr;
176
- const pad = Math.max(0, w_ - stripAnsi(line).length);
177
- return s.invert(stripAnsi(title)) + s.dim("|") + tabStr + " ".repeat(pad);
178
- }
179
- function renderMainArea() {
180
- const availRows = rows() - 3; // top bar + bottom bar + input line
181
- if (availRows < 1)
182
- return [];
183
- if (!selectedId || !panes.has(selectedId)) {
184
- const emptyMsg = s.dim(" No agent selected. Use /spawn <name> <task> to create one.");
185
- const lines = [emptyMsg];
186
- while (lines.length < availRows)
187
- lines.push("");
188
- return lines;
189
- }
190
- const pane = panes.get(selectedId);
191
- // Include partial line if any
192
- const allLines = [...pane.lines];
193
- if (pane.partial)
194
- allLines.push(pane.partial);
195
- // Apply scroll offset
196
- const totalLines = allLines.length;
197
- let start = Math.max(0, totalLines - availRows - scrollOffset);
198
- let end = start + availRows;
199
- if (end > totalLines) {
200
- end = totalLines;
201
- start = Math.max(0, end - availRows);
202
- }
203
- const visible = allLines.slice(start, end);
204
- const w_ = cols();
205
- const output = [];
206
- const paneStyle = getAgentStyle(pane.index);
207
- const linePrefix = paneStyle.color(paneStyle.icon) + " ";
208
- const prefixLen = 2; // icon + space
209
- for (const line of visible) {
210
- output.push(linePrefix + line.slice(0, w_ - prefixLen));
211
- }
212
- // Pad remaining rows
213
- while (output.length < availRows)
214
- output.push("");
215
- return output;
216
- }
217
- function renderBottomBar() {
218
- const w_ = cols();
219
- const agentCount = spawner.listAgents().length;
220
- const runningCount = spawner.getAgentsByStatus("running").length;
221
- const left = ` Agents: ${agentCount} (${runningCount} running)`;
222
- const right = `1-9:select Ctrl+</>:cycle /spawn /list /kill /broadcast Ctrl+D:exit `;
223
- const pad = Math.max(0, w_ - left.length - right.length);
224
- return s.invert(left + " ".repeat(pad) + right);
225
- }
226
- function renderInputLine() {
227
- const prompt = s.cyan("multi> ");
228
- return prompt + inputLine;
229
- }
230
- function render() {
231
- // Hide cursor, move to top, clear screen
232
- w.write(`${ESC}?25l${ESC}H${ESC}2J`);
233
- // Top bar
234
- w.write(renderTopBar());
235
- w.write("\n");
236
- // Main area
237
- const mainLines = renderMainArea();
238
- for (const line of mainLines) {
239
- w.write(line + "\n");
240
- }
241
- // Bottom bar
242
- w.write(renderBottomBar());
243
- w.write("\n");
244
- // Input line
245
- w.write(renderInputLine());
246
- // Show cursor
247
- w.write(`${ESC}?25h`);
57
+ function doRender() {
58
+ render(w, spawner, panes, selectedId, scrollOffset, inputLine);
59
+ }
60
+ // ── Command context for slash commands ────────────────────────────────
61
+ function getCmdCtx() {
62
+ return {
63
+ spawner,
64
+ config,
65
+ panes,
66
+ agentOrder,
67
+ selectedId,
68
+ setSelectedId: (id) => { selectedId = id; },
69
+ getOrCreatePane,
70
+ render: doRender,
71
+ };
248
72
  }
249
73
  // ── Spawner event wiring ───────────────────────────────────────────────
250
- spawner.on("text_delta", (agentId, text) => {
251
- const pane = getOrCreatePane(agentId);
252
- appendToPane(pane, text);
253
- if (agentId === selectedId)
254
- render();
255
- });
256
- spawner.on("text_block", (agentId, text) => {
257
- const pane = getOrCreatePane(agentId);
258
- appendToPane(pane, text + "\n");
259
- if (agentId === selectedId)
260
- render();
261
- });
262
- spawner.on("tool_start", (agentId, toolName, input) => {
263
- const pane = getOrCreatePane(agentId);
264
- flushPartial(pane);
265
- appendToPane(pane, formatToolStart(toolName, input) + "\n");
266
- if (agentId === selectedId)
267
- render();
268
- });
269
- spawner.on("tool_end", (agentId, toolName, input, output, isError, durationMs) => {
270
- const pane = getOrCreatePane(agentId);
271
- flushPartial(pane);
272
- const diffData = (toolName === "edit_file" || toolName === "write_file") ? decodeDiffPayload(output) : null;
273
- const cleanOutput = diffData ? output.slice(0, output.indexOf(DIFF_MARKER)) : output;
274
- appendToPane(pane, formatToolEnd(toolName, input, cleanOutput, isError, durationMs) + "\n");
275
- if (diffData) {
276
- appendToPane(pane, renderInlineDiff(diffData.oldContent, diffData.newContent, diffData.filePath) + "\n");
277
- }
278
- if (agentId === selectedId)
279
- render();
280
- });
281
- spawner.on("status", (agentId, message) => {
282
- const pane = getOrCreatePane(agentId);
283
- appendToPane(pane, s.dim(message) + "\n");
284
- if (agentId === selectedId)
285
- render();
286
- });
287
- spawner.on("done", (agentId, result) => {
288
- const pane = getOrCreatePane(agentId);
289
- flushPartial(pane);
290
- const style = getAgentStyle(pane.index);
291
- appendToPane(pane, "\n" + style.color(`--- ${style.icon} Agent completed ---`) + "\n");
292
- appendToPane(pane, s.dim(` Turns: ${result.turns} Tool calls: ${result.toolCalls}${result.totalCost ? ` Cost: ${result.totalCost}` : ""}`) + "\n");
293
- render();
294
- });
295
- spawner.on("error", (agentId, error) => {
296
- const pane = getOrCreatePane(agentId);
297
- flushPartial(pane);
298
- const style = getAgentStyle(pane.index);
299
- appendToPane(pane, "\n" + style.color(`--- ${style.icon} Error: ${error} ---`) + "\n");
300
- render();
301
- });
302
- spawner.on("exit", (agentId, code) => {
303
- const pane = getOrCreatePane(agentId);
304
- if (code !== null && code !== 0) {
305
- appendToPane(pane, s.dim(` Process exited with code ${code}`) + "\n");
306
- }
307
- render();
74
+ wireSpawnerEvents(spawner, {
75
+ panes,
76
+ getOrCreatePane,
77
+ getSelectedId: () => selectedId,
78
+ render: doRender,
308
79
  });
309
- spawner.on("message", (from, to, content) => {
310
- // Show in sender's pane
311
- const senderPane = panes.get(from);
312
- if (senderPane) {
313
- flushPartial(senderPane);
314
- const toName = panes.get(to)?.name ?? to;
315
- appendToPane(senderPane, s.yellow(`[${senderPane.name} -> ${toName}] ${content}`) + "\n");
316
- }
317
- // Show in recipient's pane
318
- const recipientPane = panes.get(to);
319
- if (recipientPane) {
320
- flushPartial(recipientPane);
321
- const fromName = senderPane?.name ?? from;
322
- appendToPane(recipientPane, s.yellow(`[${fromName} -> ${recipientPane.name}] ${content}`) + "\n");
323
- }
324
- if (from === selectedId || to === selectedId)
325
- render();
326
- });
327
- // ── Slash command handling ─────────────────────────────────────────────
328
- function handleSlashCommand(line) {
329
- const parts = line.split(/\s+/);
330
- const cmd = parts[0].toLowerCase();
331
- if (cmd === "/spawn") {
332
- const name = parts[1];
333
- const task = parts.slice(2).join(" ");
334
- if (!name || !task) {
335
- appendToSystem("Usage: /spawn <name> <task>");
336
- return true;
337
- }
338
- const opts = {
339
- task,
340
- cwd: process.cwd(),
341
- provider: config.provider.name,
342
- permissions: "auto-confirm",
343
- verbose: config.verbose,
344
- };
345
- const agentId = spawner.spawn(opts);
346
- const pane = getOrCreatePane(agentId);
347
- pane.name = name;
348
- appendToPane(pane, s.cyan(`Spawned agent "${name}" (${agentId}): ${task}`) + "\n");
349
- selectAgent(agentId);
350
- return true;
351
- }
352
- if (cmd === "/list") {
353
- const agents = spawner.listAgents();
354
- if (agents.length === 0) {
355
- appendToSystem("No agents.");
356
- }
357
- else {
358
- const lines = ["Agents:"];
359
- for (let i = 0; i < agents.length; i++) {
360
- const a = agents[i];
361
- const pane = panes.get(a.id);
362
- const name = pane?.name ?? a.id;
363
- const color = statusColor(a.status);
364
- const elapsed = a.finishedAt
365
- ? `${((a.finishedAt - a.startedAt) / 1000).toFixed(1)}s`
366
- : `${((Date.now() - a.startedAt) / 1000).toFixed(0)}s`;
367
- lines.push(` ${i + 1}. ${name} [${color(a.status)}] ${s.dim(elapsed)} — ${a.task.slice(0, 50)}`);
368
- }
369
- appendToSystem(lines.join("\n"));
370
- }
371
- return true;
372
- }
373
- if (cmd === "/kill") {
374
- const target = parts[1];
375
- if (!target) {
376
- appendToSystem("Usage: /kill <name|index>");
377
- return true;
378
- }
379
- const agentId = resolveAgentTarget(target);
380
- if (!agentId) {
381
- appendToSystem(`Agent "${target}" not found.`);
382
- return true;
383
- }
384
- const ok = spawner.cancel(agentId);
385
- const pane = getOrCreatePane(agentId);
386
- if (ok) {
387
- appendToPane(pane, s.yellow("\n--- Cancelled ---\n"));
388
- }
389
- else {
390
- appendToSystem(`Agent "${target}" is not running.`);
391
- }
392
- render();
393
- return true;
394
- }
395
- if (cmd === "/broadcast") {
396
- const msg = parts.slice(1).join(" ");
397
- if (!msg) {
398
- appendToSystem("Usage: /broadcast <message>");
399
- return true;
400
- }
401
- const agents = spawner.listAgents();
402
- let sent = 0;
403
- for (const a of agents) {
404
- if (a.status === "running") {
405
- const pane = getOrCreatePane(a.id);
406
- appendToPane(pane, s.yellow(`[broadcast] ${msg}`) + "\n");
407
- sent++;
408
- }
409
- }
410
- appendToSystem(`Broadcast sent to ${sent} running agent(s).`);
411
- return true;
412
- }
413
- if (cmd === "/msg") {
414
- const target = parts[1];
415
- const msg = parts.slice(2).join(" ");
416
- if (!target || !msg) {
417
- appendToSystem("Usage: /msg <agent> <text>");
418
- return true;
419
- }
420
- const agentId = resolveAgentTarget(target);
421
- if (!agentId) {
422
- appendToSystem(`Agent "${target}" not found.`);
423
- return true;
424
- }
425
- const ok = spawner.sendToAgent(agentId, msg, "user");
426
- if (ok) {
427
- const recipientPane = getOrCreatePane(agentId);
428
- flushPartial(recipientPane);
429
- appendToPane(recipientPane, s.yellow(`[user -> ${recipientPane.name}] ${msg}`) + "\n");
430
- if (selectedId && selectedId !== agentId && panes.has(selectedId)) {
431
- const curPane = panes.get(selectedId);
432
- flushPartial(curPane);
433
- appendToPane(curPane, s.yellow(`[user -> ${recipientPane.name}] ${msg}`) + "\n");
434
- }
435
- }
436
- else {
437
- appendToSystem(`Agent "${target}" is not running.`);
438
- }
439
- render();
440
- return true;
441
- }
442
- if (cmd === "/help") {
443
- appendToSystem([
444
- "Commands:",
445
- " /spawn <name> <task> — Spawn a new agent",
446
- " /list — List all agents",
447
- " /kill <name|index> — Terminate an agent",
448
- " /msg <agent> <text> — Send direct message to an agent",
449
- " /broadcast <msg> — Send to all running agents",
450
- " /help — Show this help",
451
- "",
452
- "Keys:",
453
- " 1-9 — Select agent by number",
454
- " Ctrl+Left/Right — Cycle agents",
455
- " PageUp/PageDown — Scroll output",
456
- " Ctrl+D — Exit (kills all)",
457
- ].join("\n"));
458
- return true;
459
- }
460
- return false;
461
- }
462
- function resolveAgentTarget(target) {
463
- // Try numeric index (1-based)
464
- const idx = parseInt(target, 10);
465
- if (!isNaN(idx) && idx >= 1 && idx <= agentOrder.length) {
466
- return agentOrder[idx - 1];
467
- }
468
- // Try name match
469
- for (const [id, pane] of panes) {
470
- if (pane.name === target)
471
- return id;
472
- }
473
- // Try agent ID
474
- if (spawner.getAgent(target))
475
- return target;
476
- return null;
477
- }
478
- function appendToSystem(text) {
479
- if (!selectedId || !panes.has(selectedId)) {
480
- // Create a virtual system pane
481
- const pane = createPane("_system", "system");
482
- panes.set("_system", pane);
483
- if (!agentOrder.includes("_system"))
484
- agentOrder.push("_system");
485
- selectedId = "_system";
486
- appendToPane(pane, text + "\n");
487
- }
488
- else {
489
- const pane = panes.get(selectedId);
490
- flushPartial(pane);
491
- appendToPane(pane, text + "\n");
492
- }
493
- render();
494
- }
495
80
  // ── Terminal setup ─────────────────────────────────────────────────────
496
81
  // Enter alternate screen
497
82
  w.write("\x1b[?1049h");
@@ -531,7 +116,7 @@ export async function startMultiTui(spawner, config) {
531
116
  if (key.ctrl && key.name === "c") {
532
117
  if (inputLine.length > 0) {
533
118
  inputLine = "";
534
- render();
119
+ doRender();
535
120
  }
536
121
  else {
537
122
  shutdown();
@@ -554,15 +139,15 @@ export async function startMultiTui(spawner, config) {
554
139
  }
555
140
  // Page Up/Down — scroll
556
141
  if (key.name === "pageup") {
557
- const availRows = rows() - 3;
142
+ const availRows = (process.stdout.rows || 24) - 3;
558
143
  scrollOffset = Math.min(scrollOffset + Math.floor(availRows / 2), MAX_SCROLLBACK);
559
- render();
144
+ doRender();
560
145
  return;
561
146
  }
562
147
  if (key.name === "pagedown") {
563
- const availRows = rows() - 3;
148
+ const availRows = (process.stdout.rows || 24) - 3;
564
149
  scrollOffset = Math.max(0, scrollOffset - Math.floor(availRows / 2));
565
- render();
150
+ doRender();
566
151
  return;
567
152
  }
568
153
  // Enter — submit input
@@ -570,12 +155,12 @@ export async function startMultiTui(spawner, config) {
570
155
  const line = inputLine.trim();
571
156
  inputLine = "";
572
157
  if (!line) {
573
- render();
158
+ doRender();
574
159
  return;
575
160
  }
576
161
  if (line.startsWith("/")) {
577
- if (handleSlashCommand(line)) {
578
- render();
162
+ if (handleSlashCommand(line, getCmdCtx())) {
163
+ doRender();
579
164
  return;
580
165
  }
581
166
  }
@@ -584,25 +169,25 @@ export async function startMultiTui(spawner, config) {
584
169
  const pane = panes.get(selectedId);
585
170
  appendToPane(pane, s.cyan(`> ${line}`) + "\n");
586
171
  }
587
- render();
172
+ doRender();
588
173
  return;
589
174
  }
590
175
  // Backspace
591
176
  if (key.name === "backspace") {
592
177
  if (inputLine.length > 0) {
593
178
  inputLine = inputLine.slice(0, -1);
594
- render();
179
+ doRender();
595
180
  }
596
181
  return;
597
182
  }
598
183
  // Regular character input
599
184
  if (key.sequence && !key.ctrl && !key.meta) {
600
185
  inputLine += key.sequence;
601
- render();
186
+ doRender();
602
187
  }
603
188
  });
604
189
  // Handle terminal resize
605
- process.stdout.on("resize", () => render());
190
+ process.stdout.on("resize", () => doRender());
606
191
  // Register panes for any agents that already exist
607
192
  for (const agent of spawner.listAgents()) {
608
193
  getOrCreatePane(agent.id);
@@ -610,6 +195,6 @@ export async function startMultiTui(spawner, config) {
610
195
  if (agentOrder.length > 0) {
611
196
  selectedId = agentOrder[0];
612
197
  }
613
- render();
198
+ doRender();
614
199
  });
615
200
  }
@@ -33,8 +33,8 @@ export function isAllowed(toolName, input) {
33
33
  return false;
34
34
  if (entry.pattern === "*")
35
35
  return true;
36
- // For file paths: exact match or child path
37
- if (pattern.startsWith(entry.pattern))
36
+ // For file paths: exact match or child path (boundary-aware to prevent prefix collisions)
37
+ if (pattern === entry.pattern || pattern.startsWith(entry.pattern.endsWith("/") ? entry.pattern : entry.pattern + "/"))
38
38
  return true;
39
39
  // For shell commands: match the binary name
40
40
  return entry.pattern === pattern;
@@ -94,21 +94,23 @@ export class AnthropicProvider {
94
94
  stopReason = "tool_use";
95
95
  else if (delta.stop_reason === "max_tokens")
96
96
  stopReason = "max_tokens";
97
+ // message_delta carries output_tokens — merge with existing input_tokens from message_start
97
98
  const u = data.usage;
98
99
  if (u) {
99
100
  usage = {
100
- input_tokens: u.input_tokens ?? 0,
101
+ input_tokens: usage?.input_tokens ?? 0,
101
102
  output_tokens: u.output_tokens ?? 0,
102
103
  };
103
104
  }
104
105
  }
105
106
  else if (type === "message_start") {
107
+ // message_start carries input_tokens — initialize usage
106
108
  const u = data.message?.usage;
107
109
  if (u) {
108
110
  logCacheUsage(u);
109
111
  usage = {
110
112
  input_tokens: u.input_tokens ?? 0,
111
- output_tokens: u.output_tokens ?? 0,
113
+ output_tokens: usage?.output_tokens ?? 0,
112
114
  };
113
115
  }
114
116
  }
@@ -10,7 +10,7 @@ function toResponsesTools(tools) {
10
10
  }));
11
11
  }
12
12
  /** Convert our messages to Responses API input format. */
13
- function toResponsesInput(system, messages) {
13
+ function toResponsesInput(messages) {
14
14
  const input = [];
15
15
  for (const msg of messages) {
16
16
  if (msg.role === "user") {
@@ -91,11 +91,16 @@ function parseResponsesOutput(data) {
91
91
  }
92
92
  else if (item.type === "function_call") {
93
93
  hasToolUse = true;
94
+ let input = {};
95
+ try {
96
+ input = JSON.parse(item.arguments);
97
+ }
98
+ catch { /* malformed arguments */ }
94
99
  content.push({
95
100
  type: "tool_use",
96
101
  id: item.call_id,
97
102
  name: item.name,
98
- input: JSON.parse(item.arguments),
103
+ input,
99
104
  });
100
105
  }
101
106
  }
@@ -125,7 +130,7 @@ export class CodexProvider {
125
130
  const body = {
126
131
  model: this.model,
127
132
  instructions: system,
128
- input: toResponsesInput(system, messages),
133
+ input: toResponsesInput(messages),
129
134
  store: false,
130
135
  stream: true,
131
136
  };
@@ -185,7 +190,7 @@ export class CodexProvider {
185
190
  const body = {
186
191
  model: this.model,
187
192
  instructions: system,
188
- input: toResponsesInput(system, messages),
193
+ input: toResponsesInput(messages),
189
194
  store: false,
190
195
  stream: true,
191
196
  include: ["reasoning.encrypted_content"],
@@ -58,11 +58,16 @@ export function parseOpenAiResponse(data) {
58
58
  if (toolCalls) {
59
59
  for (const tc of toolCalls) {
60
60
  const fn = tc.function;
61
+ let input = {};
62
+ try {
63
+ input = JSON.parse(fn.arguments);
64
+ }
65
+ catch { /* malformed arguments */ }
61
66
  content.push({
62
67
  type: "tool_use",
63
68
  id: tc.id,
64
69
  name: fn.name,
65
- input: JSON.parse(fn.arguments),
70
+ input,
66
71
  });
67
72
  }
68
73
  }