@noir-ai/cli 1.14.0 → 1.15.0-beta.1

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/dist/tui/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  import {
2
+ INTERRUPT_GRACE_MS,
3
+ RunInterrupt,
4
+ UsageReducer,
2
5
  buildPaletteCommandsForTui,
3
6
  describeContext,
4
7
  describeDaemon,
@@ -6,24 +9,37 @@ import {
6
9
  describeStore,
7
10
  describeWorkflow,
8
11
  gatherStatusPayload,
12
+ humanizeElapsed,
13
+ interruptedNotice,
9
14
  isDestructive,
10
- resolveSections
11
- } from "./chunk-6LO4LXEH.js";
12
- import "./chunk-2RK6AFZQ.js";
15
+ loadRunConfig,
16
+ mergeEnv,
17
+ normalizeStreamEvent,
18
+ parseStreamLine,
19
+ performPostRunAction,
20
+ postRunActions,
21
+ resolveRunProfile,
22
+ resolveSections,
23
+ runHost,
24
+ runPromptFrom,
25
+ writeTranscript
26
+ } from "./chunk-5KWETEMF.js";
27
+ import "./chunk-TPNJEN4B.js";
13
28
  import {
14
29
  c,
15
30
  contentWidth,
16
31
  divider,
17
32
  useColor
18
- } from "./chunk-ESZTOQEC.js";
33
+ } from "./chunk-KETMM3BA.js";
19
34
 
20
35
  // src/tui/index.tsx
21
- import { loadProjectInfo } from "@noir-ai/core";
36
+ import { SUPPORTED_HOSTS } from "@noir-ai/adapters";
37
+ import { loadProjectInfo, parseConfig } from "@noir-ai/core";
22
38
  import { render } from "ink";
23
39
 
24
40
  // src/tui/App.tsx
25
- import { Box as Box5, Text as Text8, useApp, useInput } from "ink";
26
- import { useEffect, useMemo, useState as useState2 } from "react";
41
+ import { Box as Box8, Text as Text11, useApp, useInput as useInput3 } from "ink";
42
+ import { useEffect as useEffect3, useMemo as useMemo2, useState as useState4 } from "react";
27
43
 
28
44
  // src/tui/CommandInput.tsx
29
45
  import { Box, Text } from "ink";
@@ -81,9 +97,13 @@ async function captureProcessOutput(fn) {
81
97
  import { Text as Text2 } from "ink";
82
98
 
83
99
  // src/tui/hints.ts
84
- var FOOTER_HINT = "?/h/Ctrl+K palette \xB7 Ctrl+F find \xB7 \u2191/\u2193 scroll \xB7 Enter run \xB7 q/Esc quit \xB7 Ctrl+C exit";
100
+ var FOOTER_HINT = "?/h/Ctrl+K palette \xB7 Ctrl+F find \xB7 Ctrl+T transcripts \xB7 \u2191/\u2193 scroll \xB7 Enter run \xB7 q/Esc quit \xB7 Ctrl+C exit";
85
101
  var RUNNING_HINT = "running\u2026 (Ctrl+C to force exit)";
86
102
  var LIST_NAV_HINT = "\u2191/\u2193 navigate \xB7 Enter run \xB7 Esc close \xB7 Tab view";
103
+ var RUN_HINT = "streaming \u2014 Esc cancels the host";
104
+ var RUN_CANCEL_HINT = "cancelling\u2026";
105
+ var TRANSCRIPT_LIST_HINT = "\u2191/\u2193 choose \xB7 Enter open \xB7 Esc back";
106
+ var TRANSCRIPT_READ_HINT = "\u2191/\u2193 scroll \xB7 Esc back to list \xB7 read-only";
87
107
  var HELP_ENTRIES = [
88
108
  { keys: "/command", desc: "run a Noir sub-command (e.g. /status, /sync, /task next)" },
89
109
  { keys: "Enter", desc: "run the typed /command" },
@@ -94,8 +114,10 @@ var HELP_ENTRIES = [
94
114
  { keys: "h", desc: "palette \u2014 commands (quick actions + all commands)" },
95
115
  { keys: "Ctrl+K", desc: "palette \u2014 all commands (fuzzy search)" },
96
116
  { keys: "Ctrl+F", desc: "palette \u2014 find in the dispatched output" },
117
+ { keys: "Ctrl+T", desc: "transcripts \u2014 reopen a past run, read-only" },
97
118
  { keys: "Tab", desc: "switch between commands, output, and help" },
98
119
  { keys: "y / n", desc: "approve / decline a destructive command prompt" },
120
+ { keys: "Esc (in a run)", desc: "cancel the host and return to the dashboard" },
99
121
  { keys: "Ctrl+C", desc: "force exit" }
100
122
  ];
101
123
 
@@ -168,6 +190,10 @@ function useInputBuffer() {
168
190
  return { buffer, setBuffer, pushHistory, recall, clear, seed };
169
191
  }
170
192
 
193
+ // src/tui/modes/run.tsx
194
+ import { Box as Box4, Text as Text7, useInput } from "ink";
195
+ import { useCallback as useCallback2, useEffect, useMemo, useRef as useRef2, useState as useState2 } from "react";
196
+
171
197
  // src/tui/OutputPane.tsx
172
198
  import { Text as Text4 } from "ink";
173
199
  import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
@@ -186,14 +212,15 @@ function OutputPane({
186
212
  lines,
187
213
  scrollOffset,
188
214
  height = 12,
189
- title
215
+ title,
216
+ followTail = false
190
217
  }) {
191
218
  const width = contentWidth();
192
219
  if (lines.length === 0) {
193
220
  return /* @__PURE__ */ jsx4(Text4, { children: c.dim("(no output \u2014 type a /command, or ? for help)") });
194
221
  }
195
222
  const maxOffset = Math.max(0, lines.length - height);
196
- const offset = Math.min(Math.max(0, scrollOffset), maxOffset);
223
+ const offset = followTail ? maxOffset : Math.min(Math.max(0, scrollOffset), maxOffset);
197
224
  const rows = toRows(lines.slice(offset, offset + height));
198
225
  return /* @__PURE__ */ jsxs3(Fragment, { children: [
199
226
  title !== void 0 ? /* @__PURE__ */ jsx4(Text4, { wrap: "truncate-end", children: c.dim(`\u2500\u2500 ${title} \u2500\u2500`) }) : null,
@@ -204,7 +231,7 @@ function OutputPane({
204
231
  ] });
205
232
  }
206
233
 
207
- // src/tui/overlays/ConfirmOverlay.tsx
234
+ // src/tui/overlays/PostRunOverlay.tsx
208
235
  import { Box as Box3, Text as Text5 } from "ink";
209
236
 
210
237
  // src/tui/Panel.tsx
@@ -227,12 +254,665 @@ function Panel({ children, maxWidth }) {
227
254
  );
228
255
  }
229
256
 
230
- // src/tui/overlays/ConfirmOverlay.tsx
257
+ // src/tui/overlays/PostRunOverlay.tsx
231
258
  import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
259
+ var OVERLAY_WIDTH = 64;
260
+ function truncate(text, max) {
261
+ return text.length > max ? `${text.slice(0, Math.max(1, max - 1))}\u2026` : text;
262
+ }
263
+ function PostRunOverlay({
264
+ options,
265
+ active,
266
+ value,
267
+ working = false,
268
+ result
269
+ }) {
270
+ const listFocused = value === void 0 && !working && result === void 0;
271
+ const elements = [
272
+ /* @__PURE__ */ jsx6(Box3, { paddingX: 1, children: /* @__PURE__ */ jsxs4(Text5, { children: [
273
+ c.bold("\u25B8 run finished "),
274
+ /* @__PURE__ */ jsx6(Text5, { children: c.dim("\xB7 what next?") })
275
+ ] }) }, "header")
276
+ ];
277
+ for (let i = 0; i < options.length; i++) {
278
+ const row = options[i];
279
+ if (!row) continue;
280
+ const focused = listFocused && i === active;
281
+ const body = focused ? `${truncate(row.label, 28).padEnd(30)}${truncate(row.hint, 30)}` : truncate(row.label, 60);
282
+ elements.push(
283
+ /* @__PURE__ */ jsx6(Box3, { paddingX: 1, children: /* @__PURE__ */ jsx6(Text5, { children: focused ? c.inverse(`\u25B8 ${body}`) : c.dim(listFocused ? ` ${body}` : `\u25B8 ${body}`) }) }, row.value)
284
+ );
285
+ }
286
+ if (value !== void 0) {
287
+ elements.push(
288
+ /* @__PURE__ */ jsx6(Box3, { paddingX: 1, children: /* @__PURE__ */ jsxs4(Text5, { children: [
289
+ c.accent("\u25B8 "),
290
+ value.value.length > 0 ? value.value : c.dim(value.placeholder),
291
+ /* @__PURE__ */ jsx6(Text5, { children: c.dim("\u258C") })
292
+ ] }) }, "value"),
293
+ /* @__PURE__ */ jsx6(Box3, { paddingX: 1, children: /* @__PURE__ */ jsx6(Text5, { children: c.dim(`${value.label} \u2014 Enter to continue \xB7 Esc to go back`) }) }, "value-label")
294
+ );
295
+ }
296
+ if (value?.notice !== void 0) {
297
+ elements.push(
298
+ /* @__PURE__ */ jsx6(Box3, { paddingX: 1, children: /* @__PURE__ */ jsx6(Text5, { children: c.warn(value.notice) }) }, "value-notice")
299
+ );
300
+ }
301
+ if (working) {
302
+ elements.push(
303
+ /* @__PURE__ */ jsx6(Box3, { paddingX: 1, children: /* @__PURE__ */ jsx6(Text5, { children: c.dim("working\u2026") }) }, "working")
304
+ );
305
+ }
306
+ if (result !== void 0) {
307
+ elements.push(
308
+ /* @__PURE__ */ jsx6(Box3, { paddingX: 1, children: /* @__PURE__ */ jsx6(Text5, { children: result.ok ? c.ok(result.message) : c.warn(result.message) }) }, "result")
309
+ );
310
+ elements.push(
311
+ /* @__PURE__ */ jsx6(Box3, { paddingX: 1, children: /* @__PURE__ */ jsx6(Text5, { children: c.dim("Enter or Esc to close") }) }, "result-hint")
312
+ );
313
+ }
314
+ return /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", children: [
315
+ /* @__PURE__ */ jsx6(Panel, { maxWidth: OVERLAY_WIDTH, children: elements }),
316
+ /* @__PURE__ */ jsx6(Text5, { children: c.dim(
317
+ result !== void 0 ? "Enter close" : value !== void 0 ? "type the value \xB7 Enter continue \xB7 Esc back" : "\u2191/\u2193 choose \xB7 Enter run \xB7 Esc dismiss"
318
+ ) })
319
+ ] });
320
+ }
321
+
322
+ // src/tui/run-stream.ts
323
+ var TOOL_MARK = "\u25CF";
324
+ var RunStream = class {
325
+ reducer = new UsageReducer();
326
+ /** The pane, in arrival order. */
327
+ rows = [];
328
+ sessionId;
329
+ model;
330
+ /** Fold one normalized host event in. */
331
+ apply(event) {
332
+ switch (event.kind) {
333
+ case "init":
334
+ if (event.sessionId !== void 0) this.sessionId = event.sessionId;
335
+ if (event.model !== void 0) this.model = event.model;
336
+ break;
337
+ case "assistant":
338
+ this.commitMessage(event);
339
+ break;
340
+ case "tool":
341
+ this.recordTool(event.name, event.messageId !== void 0);
342
+ break;
343
+ case "delta":
344
+ this.appendText(event.text);
345
+ break;
346
+ default:
347
+ break;
348
+ }
349
+ this.reducer.add(event);
350
+ }
351
+ /**
352
+ * Settle what is still provisional: called once the run has ended, so a run
353
+ * that stopped mid-block keeps the words it had already produced and the
354
+ * tools it had already started instead of losing them (and so a transcript
355
+ * read back from disk renders completely).
356
+ */
357
+ finalize() {
358
+ for (const row of this.rows) row.pending = false;
359
+ }
360
+ /** Everything the run screen renders, as of now. */
361
+ snapshot() {
362
+ const usage = this.reducer.snapshot();
363
+ const lines = [];
364
+ const answer = [];
365
+ let toolCount = 0;
366
+ for (const row of this.rows) {
367
+ if (row.kind === "tool") {
368
+ toolCount += 1;
369
+ lines.push(`${TOOL_MARK} ${row.text}`);
370
+ continue;
371
+ }
372
+ answer.push(row.text);
373
+ lines.push(...splitRows(row.text));
374
+ }
375
+ return {
376
+ lines,
377
+ // Provisional text counts: a cancelled run's answer is what it managed to
378
+ // say, and `finalize` will have committed it anyway.
379
+ answer: answer.join(""),
380
+ toolCount,
381
+ ...this.sessionId === void 0 ? {} : { sessionId: this.sessionId },
382
+ ...this.model === void 0 ? {} : { model: this.model },
383
+ inputTokens: usage.inputTokens,
384
+ outputTokens: usage.outputTokens
385
+ };
386
+ }
387
+ /**
388
+ * Commit a completed assistant message.
389
+ *
390
+ * Its text supersedes the deltas that spelled it out — the host reports the
391
+ * same words twice — and its tool calls are the authoritative word on the
392
+ * calls the incremental feed opened, each taking over the row its own
393
+ * announcement opened. The text lands where its deltas were, which is above
394
+ * the calls it went on to make: a message that says something and then calls
395
+ * a tool reads in that order, rather than the call floating above the
396
+ * sentence that introduced it.
397
+ */
398
+ commitMessage(event) {
399
+ const opened = this.rows.findIndex((row) => row.kind === "text" && row.pending);
400
+ const at = opened === -1 ? this.rows.length : opened;
401
+ for (let i = this.rows.length - 1; i >= 0; i--) {
402
+ const row = this.rows[i];
403
+ if (row !== void 0 && row.kind === "text" && row.pending) this.rows.splice(i, 1);
404
+ }
405
+ if (event.isError !== true && event.text !== void 0 && event.text.length > 0) {
406
+ this.rows.splice(at, 0, { kind: "text", text: event.text, pending: false });
407
+ }
408
+ for (const name of event.tools ?? []) this.recordTool(name, true);
409
+ }
410
+ /**
411
+ * Record a tool call, confirmed by a completed message or announced by the
412
+ * frame that opened its block. A confirmed call takes over the row the
413
+ * announcement left open; with nothing to take over — the first word on the
414
+ * call, which is all some hosts ever send — it opens a row of its own.
415
+ */
416
+ recordTool(name, confirmed) {
417
+ if (confirmed) {
418
+ const opened = this.rows.findIndex(
419
+ (row2) => row2.kind === "tool" && row2.pending && row2.text === name
420
+ );
421
+ const row = this.rows[opened];
422
+ if (row !== void 0) {
423
+ row.pending = false;
424
+ return;
425
+ }
426
+ }
427
+ this.rows.push({ kind: "tool", text: name, pending: !confirmed });
428
+ }
429
+ /** Extend the stretch of text the host is writing, or start a new one. */
430
+ appendText(text) {
431
+ const last = this.rows[this.rows.length - 1];
432
+ if (last !== void 0 && last.kind === "text" && last.pending) {
433
+ this.rows[this.rows.length - 1] = { kind: "text", text: last.text + text, pending: true };
434
+ return;
435
+ }
436
+ this.rows.push({ kind: "text", text, pending: true });
437
+ }
438
+ };
439
+ function splitRows(text) {
440
+ const rows = text.split("\n");
441
+ while (rows.length > 0 && rows[rows.length - 1] === "") rows.pop();
442
+ return rows;
443
+ }
444
+ function renderTranscript(rawLines) {
445
+ const stream = new RunStream();
446
+ for (const line of rawLines) {
447
+ const raw = parseStreamLine(line);
448
+ if (raw === null) continue;
449
+ const event = normalizeStreamEvent(raw);
450
+ if (event !== null) stream.apply(event);
451
+ }
452
+ stream.finalize();
453
+ return stream.snapshot().lines;
454
+ }
455
+
456
+ // src/tui/StatusBar.tsx
457
+ import { Text as Text6 } from "ink";
458
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
459
+ function Cell({
460
+ label,
461
+ value,
462
+ accent
463
+ }) {
464
+ return /* @__PURE__ */ jsxs5(Text6, { children: [
465
+ /* @__PURE__ */ jsx7(Text6, { children: c.dim(`${label}:`) }),
466
+ " ",
467
+ /* @__PURE__ */ jsx7(Text6, { children: accent ? c.accent(value) : value })
468
+ ] });
469
+ }
470
+ function StatusBar({ payload, loading, run }) {
471
+ if (run !== void 0) {
472
+ const tools = run.tools !== void 0 && run.tools > 0 ? ` \xB7 ${run.tools} tool${run.tools === 1 ? "" : "s"}` : "";
473
+ return /* @__PURE__ */ jsx7(Text6, { children: /* @__PURE__ */ jsx7(Cell, { label: "run", value: `${run.model} \xB7 ${run.elapsed} \xB7 ${run.tokens}${tools}`, accent: true }) });
474
+ }
475
+ const host = payload?.host ?? "\u2014";
476
+ const mode = payload?.workflow?.mode ?? "\u2014";
477
+ const phase = payload?.workflow?.phase ?? (loading ? "\u2026" : "idle");
478
+ const daemonRunning = payload?.daemon.running === true;
479
+ const daemon = payload === null && loading ? "\u2026" : daemonRunning ? "up" : "down";
480
+ return /* @__PURE__ */ jsxs5(Text6, { children: [
481
+ /* @__PURE__ */ jsx7(Cell, { label: "host", value: host, accent: true }),
482
+ /* @__PURE__ */ jsx7(Text6, { children: c.dim(" \xB7 ") }),
483
+ /* @__PURE__ */ jsx7(Cell, { label: "mode", value: mode }),
484
+ /* @__PURE__ */ jsx7(Text6, { children: c.dim(" \xB7 ") }),
485
+ /* @__PURE__ */ jsx7(Cell, { label: "phase", value: phase }),
486
+ /* @__PURE__ */ jsx7(Text6, { children: c.dim(" \xB7 ") }),
487
+ /* @__PURE__ */ jsxs5(Text6, { children: [
488
+ c.dim("daemon:"),
489
+ " ",
490
+ daemonRunning ? c.ok(daemon) : c.warn(daemon)
491
+ ] })
492
+ ] });
493
+ }
494
+
495
+ // src/tui/modes/run.tsx
496
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
497
+ var EMPTY_STREAM = {
498
+ lines: [],
499
+ answer: "",
500
+ toolCount: 0,
501
+ inputTokens: 0,
502
+ outputTokens: 0
503
+ };
504
+ function tokenCell(input, output) {
505
+ return `\u2193${input.toLocaleString()}\u2191${output.toLocaleString()} tokens`;
506
+ }
507
+ function RunMode({
508
+ prompt,
509
+ deps,
510
+ onExit,
511
+ onQuit = onExit,
512
+ height = 14,
513
+ tickMs = 1e3,
514
+ now = Date.now,
515
+ graceMs = INTERRUPT_GRACE_MS
516
+ }) {
517
+ const [stream, setStream] = useState2(EMPTY_STREAM);
518
+ const [binary, setBinary] = useState2("host");
519
+ const [phase, setPhase] = useState2({ kind: "running" });
520
+ const [elapsedMs, setElapsedMs] = useState2(0);
521
+ const [turn, setTurn] = useState2({ prompt });
522
+ const [valueBuffer, setValueBuffer] = useState2("");
523
+ const [valueNotice, setValueNotice] = useState2(null);
524
+ const [postRun, setPostRun] = useState2(null);
525
+ const [statusHint, setStatusHint] = useState2(null);
526
+ const interruptRef = useRef2(null);
527
+ const rawLinesRef = useRef2([]);
528
+ const startedAtRef = useRef2(now());
529
+ const exitRef = useRef2(onExit);
530
+ useEffect(() => {
531
+ exitRef.current = onExit;
532
+ }, [onExit]);
533
+ useEffect(() => {
534
+ const streamState = new RunStream();
535
+ const interrupt = new RunInterrupt({ graceMs });
536
+ interruptRef.current = interrupt;
537
+ rawLinesRef.current = [];
538
+ startedAtRef.current = now();
539
+ let live = true;
540
+ setStream(streamState.snapshot());
541
+ setElapsedMs(0);
542
+ setPostRun(null);
543
+ setPhase({ kind: "running" });
544
+ setStatusHint(null);
545
+ const tick = setInterval(() => {
546
+ if (live) setElapsedMs(now() - startedAtRef.current);
547
+ }, tickMs);
548
+ let session;
549
+ try {
550
+ session = deps.start(
551
+ turn.prompt,
552
+ {
553
+ onEvent: (event) => {
554
+ streamState.apply(event);
555
+ if (live) setStream(streamState.snapshot());
556
+ },
557
+ onLine: (line) => {
558
+ rawLinesRef.current.push(line);
559
+ },
560
+ signal: interrupt.signal,
561
+ onChild: (child) => interrupt.track(child)
562
+ },
563
+ turn.sessionId
564
+ );
565
+ } catch (err) {
566
+ clearInterval(tick);
567
+ setPhase({
568
+ kind: "ended",
569
+ ok: false,
570
+ message: err instanceof Error ? err.message : String(err)
571
+ });
572
+ return () => {
573
+ live = false;
574
+ };
575
+ }
576
+ setBinary(session.binary);
577
+ void session.done.then(async (result) => {
578
+ streamState.finalize();
579
+ const snapshot = streamState.snapshot();
580
+ const cancelled = interrupt.signal.aborted;
581
+ const transcript = await deps.transcripts?.write(rawLinesRef.current) ?? "(not persisted)";
582
+ if (!live) return;
583
+ setStream(snapshot);
584
+ if (cancelled) {
585
+ exitRef.current(interruptedNotice(transcript));
586
+ return;
587
+ }
588
+ if (result.isError || result.exitCode !== 0) {
589
+ setPhase({ kind: "ended", ok: false, message: failureText(result, session.binary) });
590
+ return;
591
+ }
592
+ setPostRun({
593
+ answer: snapshot.answer,
594
+ transcript,
595
+ host: session.binary,
596
+ ...snapshot.sessionId === void 0 ? {} : { sessionId: snapshot.sessionId },
597
+ canContinue: snapshot.sessionId !== void 0
598
+ });
599
+ setPhase({ kind: "menu", active: 0 });
600
+ }).catch((err) => {
601
+ if (!live) return;
602
+ setPhase({
603
+ kind: "ended",
604
+ ok: false,
605
+ message: `could not run ${session.binary}: ${err instanceof Error ? err.message : String(err)}`
606
+ });
607
+ });
608
+ return () => {
609
+ live = false;
610
+ clearInterval(tick);
611
+ interrupt.terminate();
612
+ };
613
+ }, [turn, deps, now, tickMs, graceMs]);
614
+ const options = useMemo(() => postRun === null ? [] : deps.actions(postRun), [postRun, deps]);
615
+ const choose = useCallback2(
616
+ (choice, value) => {
617
+ if (postRun === null) return;
618
+ const label = options.find((o) => o.value === choice)?.label ?? "That action";
619
+ if (choice === "dismiss") {
620
+ onExit("run finished");
621
+ return;
622
+ }
623
+ if (choice === "resume") {
624
+ const sessionId = postRun.sessionId;
625
+ if (sessionId === void 0 || value === void 0 || value.trim().length === 0) {
626
+ setValueNotice("a follow-up prompt is required");
627
+ return;
628
+ }
629
+ setValueBuffer("");
630
+ setValueNotice(null);
631
+ setTurn({ prompt: value.trim(), sessionId });
632
+ return;
633
+ }
634
+ setPhase({ kind: "working", message: label });
635
+ void (async () => {
636
+ let outcome = { ok: true, message: "" };
637
+ const captured = await captureProcessOutput(async () => {
638
+ outcome = await deps.perform(postRun, choice, value);
639
+ });
640
+ const written = `${captured.stdout}${captured.stderr}`.trim();
641
+ setPhase({
642
+ kind: "result",
643
+ ok: outcome.ok,
644
+ message: [outcome.message, outcome.detail, written].filter((part) => part !== void 0 && part.length > 0).join(" \u2014 ").replace(/\s+/g, " ")
645
+ });
646
+ })();
647
+ },
648
+ [deps, onExit, options, postRun]
649
+ );
650
+ const submitValue = useCallback2(
651
+ (option) => {
652
+ const value = valueBuffer;
653
+ if (option.needsValue !== void 0 && value.trim().length === 0 && option.value !== "save") {
654
+ setValueNotice(`a ${option.needsValue.label} is required`);
655
+ return;
656
+ }
657
+ setValueBuffer("");
658
+ setValueNotice(null);
659
+ choose(option.value, value);
660
+ },
661
+ [choose, valueBuffer]
662
+ );
663
+ useInput((input, key) => {
664
+ if (key.ctrl && input === "c") {
665
+ if (phase.kind === "running") {
666
+ const interrupt = interruptRef.current;
667
+ if (interrupt === null) return;
668
+ if (interrupt.signal.aborted) {
669
+ interrupt.forceNow();
670
+ return;
671
+ }
672
+ interrupt.terminate();
673
+ setStatusHint(RUN_CANCEL_HINT);
674
+ return;
675
+ }
676
+ onQuit();
677
+ return;
678
+ }
679
+ if (phase.kind === "running") {
680
+ if (key.escape) {
681
+ interruptRef.current?.terminate();
682
+ setStatusHint(RUN_CANCEL_HINT);
683
+ }
684
+ return;
685
+ }
686
+ if (phase.kind === "ended") {
687
+ if (key.return || key.escape) onExit();
688
+ return;
689
+ }
690
+ if (phase.kind === "result") {
691
+ if (key.return || key.escape) setPhase({ kind: "menu", active: 0 });
692
+ return;
693
+ }
694
+ if (phase.kind === "working") return;
695
+ if (phase.kind === "value") {
696
+ if (key.escape) {
697
+ setValueBuffer("");
698
+ setValueNotice(null);
699
+ setPhase({ kind: "menu", active: 0 });
700
+ return;
701
+ }
702
+ if (key.return) {
703
+ submitValue(phase.option);
704
+ return;
705
+ }
706
+ if (key.backspace || key.delete) {
707
+ setValueBuffer((b) => b.slice(0, -1));
708
+ setValueNotice(null);
709
+ return;
710
+ }
711
+ if (key.ctrl || key.tab || key.upArrow || key.downArrow) return;
712
+ if (input.length > 0) {
713
+ setValueBuffer((b) => b + input);
714
+ setValueNotice(null);
715
+ }
716
+ return;
717
+ }
718
+ if (key.escape) {
719
+ onExit("run finished");
720
+ return;
721
+ }
722
+ if (key.return) {
723
+ const option = options[phase.active];
724
+ if (option === void 0) return;
725
+ if (option.needsValue === void 0) {
726
+ choose(option.value);
727
+ return;
728
+ }
729
+ setValueBuffer("");
730
+ setValueNotice(null);
731
+ setPhase({ kind: "value", option });
732
+ return;
733
+ }
734
+ if (key.upArrow) {
735
+ setPhase({ kind: "menu", active: Math.max(0, phase.active - 1) });
736
+ return;
737
+ }
738
+ if (key.downArrow) {
739
+ setPhase({
740
+ kind: "menu",
741
+ active: Math.min(Math.max(0, options.length - 1), phase.active + 1)
742
+ });
743
+ }
744
+ });
745
+ const valueStep = phase.kind === "value" && phase.option.needsValue !== void 0 ? {
746
+ label: phase.option.needsValue.label,
747
+ placeholder: phase.option.needsValue.placeholder,
748
+ value: valueBuffer,
749
+ ...valueNotice === null ? {} : { notice: valueNotice }
750
+ } : void 0;
751
+ const overlay = phase.kind === "menu" || phase.kind === "value" || phase.kind === "working" || phase.kind === "result" ? /* @__PURE__ */ jsx8(
752
+ PostRunOverlay,
753
+ {
754
+ options,
755
+ active: phase.kind === "menu" ? phase.active : 0,
756
+ ...valueStep === void 0 ? {} : { value: valueStep },
757
+ ...phase.kind === "working" ? { working: true } : {},
758
+ ...phase.kind === "result" ? { result: { ok: phase.ok, message: phase.message } } : {}
759
+ }
760
+ ) : null;
761
+ return /* @__PURE__ */ jsxs6(Box4, { flexDirection: "column", children: [
762
+ /* @__PURE__ */ jsx8(Header, { tagline: "run" }),
763
+ /* @__PURE__ */ jsxs6(Panel, { children: [
764
+ /* @__PURE__ */ jsx8(Box4, { paddingX: 1, children: /* @__PURE__ */ jsx8(
765
+ StatusBar,
766
+ {
767
+ payload: null,
768
+ run: {
769
+ model: stream.model ?? binary,
770
+ elapsed: humanizeElapsed(elapsedMs),
771
+ tokens: tokenCell(stream.inputTokens, stream.outputTokens),
772
+ tools: stream.toolCount
773
+ }
774
+ }
775
+ ) }),
776
+ /* @__PURE__ */ jsx8(Box4, { paddingX: 1, children: /* @__PURE__ */ jsx8(Text7, { children: divider() }) }),
777
+ /* @__PURE__ */ jsx8(Box4, { flexDirection: "column", paddingX: 1, children: /* @__PURE__ */ jsx8(
778
+ OutputPane,
779
+ {
780
+ lines: stream.lines,
781
+ scrollOffset: 0,
782
+ height,
783
+ title: prompt,
784
+ followTail: true
785
+ }
786
+ ) })
787
+ ] }),
788
+ phase.kind === "running" ? /* @__PURE__ */ jsx8(Text7, { children: c.dim(statusHint ?? RUN_HINT) }) : null,
789
+ phase.kind === "ended" ? /* @__PURE__ */ jsx8(Text7, { children: phase.ok ? c.ok(phase.message) : c.warn(phase.message) }) : null,
790
+ overlay
791
+ ] });
792
+ }
793
+ function failureText(result, binary) {
794
+ const reason = result.errorText !== void 0 && result.errorText.trim().length > 0 ? result.errorText.trim() : `exit code ${result.exitCode}`;
795
+ return `${binary} failed: ${reason}`;
796
+ }
797
+
798
+ // src/tui/modes/transcripts.tsx
799
+ import { Box as Box5, Text as Text8, useInput as useInput2 } from "ink";
800
+ import { useEffect as useEffect2, useState as useState3 } from "react";
801
+ import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
802
+ function TranscriptPicker({
803
+ transcripts,
804
+ onExit,
805
+ height = 14
806
+ }) {
807
+ const [entries, setEntries] = useState3([]);
808
+ const [loading, setLoading] = useState3(true);
809
+ const [active, setActive] = useState3(0);
810
+ const [view, setView] = useState3({ kind: "list" });
811
+ const [scroll, setScroll] = useState3(0);
812
+ useEffect2(() => {
813
+ let live = true;
814
+ void transcripts.list().then((found) => {
815
+ if (live) setEntries(found);
816
+ }).catch(() => {
817
+ if (live) setEntries([]);
818
+ }).finally(() => {
819
+ if (live) setLoading(false);
820
+ });
821
+ return () => {
822
+ live = false;
823
+ };
824
+ }, [transcripts]);
825
+ useInput2((_input, key) => {
826
+ if (loading) {
827
+ if (key.escape) onExit();
828
+ return;
829
+ }
830
+ if (view.kind === "list") {
831
+ if (key.escape) {
832
+ onExit();
833
+ return;
834
+ }
835
+ if (key.return) {
836
+ const entry = entries[active];
837
+ if (entry === void 0) return;
838
+ void transcripts.read(entry.path).then((lines) => {
839
+ setView({ kind: "open", name: entry.name, lines: renderTranscript(lines) });
840
+ setScroll(0);
841
+ }).catch(() => {
842
+ });
843
+ return;
844
+ }
845
+ if (key.upArrow) {
846
+ setActive((a) => Math.max(0, a - 1));
847
+ return;
848
+ }
849
+ if (key.downArrow) {
850
+ setActive((a) => Math.min(Math.max(0, entries.length - 1), a + 1));
851
+ return;
852
+ }
853
+ return;
854
+ }
855
+ if (key.escape) {
856
+ setView({ kind: "list" });
857
+ return;
858
+ }
859
+ if (key.upArrow) setScroll((s) => Math.max(0, s - 1));
860
+ if (key.downArrow) setScroll((s) => s + 1);
861
+ });
862
+ if (loading) {
863
+ return /* @__PURE__ */ jsxs7(Box5, { flexDirection: "column", children: [
864
+ /* @__PURE__ */ jsx9(Header, { tagline: "transcripts" }),
865
+ /* @__PURE__ */ jsx9(Panel, { children: /* @__PURE__ */ jsx9(Box5, { paddingX: 1, children: /* @__PURE__ */ jsx9(Text8, { children: c.dim("loading transcripts\u2026") }) }) }),
866
+ /* @__PURE__ */ jsx9(Text8, { children: c.dim("Esc back") })
867
+ ] });
868
+ }
869
+ if (view.kind === "open") {
870
+ return /* @__PURE__ */ jsxs7(Box5, { flexDirection: "column", children: [
871
+ /* @__PURE__ */ jsx9(Header, { tagline: `transcripts \xB7 ${view.name}` }),
872
+ /* @__PURE__ */ jsx9(Panel, { children: /* @__PURE__ */ jsx9(Box5, { flexDirection: "column", paddingX: 1, children: /* @__PURE__ */ jsx9(
873
+ OutputPane,
874
+ {
875
+ lines: view.lines.length > 0 ? view.lines : ["(empty transcript)"],
876
+ scrollOffset: scroll,
877
+ height,
878
+ title: view.name
879
+ }
880
+ ) }) }),
881
+ /* @__PURE__ */ jsx9(Text8, { children: c.dim(TRANSCRIPT_READ_HINT) })
882
+ ] });
883
+ }
884
+ if (entries.length === 0) {
885
+ return /* @__PURE__ */ jsxs7(Box5, { flexDirection: "column", children: [
886
+ /* @__PURE__ */ jsx9(Header, { tagline: "transcripts" }),
887
+ /* @__PURE__ */ jsx9(Panel, { children: /* @__PURE__ */ jsx9(Box5, { paddingX: 1, children: /* @__PURE__ */ jsx9(Text8, { children: c.dim("no transcripts yet \u2014 a run leaves one here") }) }) }),
888
+ /* @__PURE__ */ jsx9(Text8, { children: c.dim("Esc back") })
889
+ ] });
890
+ }
891
+ return /* @__PURE__ */ jsxs7(Box5, { flexDirection: "column", children: [
892
+ /* @__PURE__ */ jsx9(Header, { tagline: "transcripts" }),
893
+ /* @__PURE__ */ jsxs7(Panel, { children: [
894
+ /* @__PURE__ */ jsx9(Box5, { paddingX: 1, children: /* @__PURE__ */ jsxs7(Text8, { children: [
895
+ c.bold("\u25B8 recent transcripts "),
896
+ /* @__PURE__ */ jsx9(Text8, { children: c.dim("\xB7 newest first") })
897
+ ] }) }),
898
+ entries.map((entry, i) => {
899
+ const focused = i === active;
900
+ const when = new Date(entry.mtimeMs).toISOString().replace("T", " ").slice(0, 19);
901
+ const row = `${entry.name} \xB7 ${when} \xB7 ${entry.sizeBytes} bytes`;
902
+ return /* @__PURE__ */ jsx9(Box5, { paddingX: 1, children: /* @__PURE__ */ jsx9(Text8, { children: focused ? c.inverse(`\u25B8 ${row}`) : c.dim(` ${row}`) }) }, entry.path);
903
+ })
904
+ ] }),
905
+ /* @__PURE__ */ jsx9(Text8, { children: c.dim(TRANSCRIPT_LIST_HINT) })
906
+ ] });
907
+ }
908
+
909
+ // src/tui/overlays/ConfirmOverlay.tsx
910
+ import { Box as Box6, Text as Text9 } from "ink";
911
+ import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
232
912
  function ConfirmOverlay({ argv }) {
233
- return /* @__PURE__ */ jsx6(Panel, { children: /* @__PURE__ */ jsx6(Box3, { paddingX: 1, children: /* @__PURE__ */ jsxs4(Text5, { children: [
913
+ return /* @__PURE__ */ jsx10(Panel, { children: /* @__PURE__ */ jsx10(Box6, { paddingX: 1, children: /* @__PURE__ */ jsxs8(Text9, { children: [
234
914
  c.warn(`! run /${argv.join(" ")}? (y/N)`),
235
- /* @__PURE__ */ jsx6(Text5, { children: c.dim(" [y approve \xB7 n/Esc back to palette]") })
915
+ /* @__PURE__ */ jsx10(Text9, { children: c.dim(" [y approve \xB7 n/Esc back to palette]") })
236
916
  ] }) }) });
237
917
  }
238
918
 
@@ -329,7 +1009,7 @@ var handRolledMatcher = {
329
1009
  };
330
1010
 
331
1011
  // src/tui/palette/Palette.tsx
332
- import { Box as Box4, Text as Text6 } from "ink";
1012
+ import { Box as Box7, Text as Text10 } from "ink";
333
1013
 
334
1014
  // src/tui/palette/output.ts
335
1015
  function computeMatches(lines, query) {
@@ -357,6 +1037,20 @@ function recentDeduped(recent, commands) {
357
1037
  const known = new Set(commands.map((cmd) => cmd.id));
358
1038
  return recent.filter((cmd) => known.has(cmd.id));
359
1039
  }
1040
+ function argRequirements(commands, homeSections) {
1041
+ const out = /* @__PURE__ */ new Map();
1042
+ for (const cmd of commands) {
1043
+ if (cmd.needsArg !== void 0) out.set(cmd.argv.join(" "), cmd.needsArg);
1044
+ }
1045
+ for (const { action } of flattenHome(homeSections)) {
1046
+ const label = action.needsArg?.label;
1047
+ if (label !== void 0) out.set((action.dispatch ?? [action.id]).join(" "), label);
1048
+ }
1049
+ return out;
1050
+ }
1051
+ function requirementFor(requirements, argv, id) {
1052
+ return requirements.get(argv.join(" ")) ?? requirements.get(id);
1053
+ }
360
1054
  function buildPaletteRows(input) {
361
1055
  const { corpus, query, commands, matcher, recent, homeSections, outputLines } = input;
362
1056
  if (corpus === "help") {
@@ -414,6 +1108,7 @@ function buildPaletteRows(input) {
414
1108
  group: "matches"
415
1109
  }));
416
1110
  }
1111
+ const args = argRequirements(commands, homeSections);
417
1112
  if (query.length === 0) {
418
1113
  const rows = [];
419
1114
  const recents = recentDeduped(recent, commands);
@@ -425,7 +1120,8 @@ function buildPaletteRows(input) {
425
1120
  secondary: cmd.description,
426
1121
  argv: cmd.argv,
427
1122
  destructive: cmd.destructive,
428
- group: "recent"
1123
+ group: "recent",
1124
+ needsArg: requirementFor(args, cmd.argv, cmd.id)
429
1125
  });
430
1126
  }
431
1127
  const homeIds = /* @__PURE__ */ new Set();
@@ -439,7 +1135,8 @@ function buildPaletteRows(input) {
439
1135
  secondary: action.hint,
440
1136
  argv,
441
1137
  destructive: action.destructive ?? false,
442
- group
1138
+ group,
1139
+ needsArg: requirementFor(args, argv, action.id)
443
1140
  });
444
1141
  }
445
1142
  for (const cmd of commands) {
@@ -450,7 +1147,8 @@ function buildPaletteRows(input) {
450
1147
  secondary: cmd.description,
451
1148
  argv: cmd.argv,
452
1149
  destructive: cmd.destructive,
453
- group: cmd.category
1150
+ group: cmd.category,
1151
+ needsArg: requirementFor(args, cmd.argv, cmd.id)
454
1152
  });
455
1153
  }
456
1154
  return rows;
@@ -461,12 +1159,13 @@ function buildPaletteRows(input) {
461
1159
  secondary: m.item.description,
462
1160
  argv: m.item.argv,
463
1161
  destructive: m.item.destructive,
464
- group: null
1162
+ group: null,
1163
+ needsArg: requirementFor(args, m.item.argv, m.item.id)
465
1164
  }));
466
1165
  }
467
1166
 
468
1167
  // src/tui/palette/Palette.tsx
469
- import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
1168
+ import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
470
1169
  var PALETTE_WIDTH = 64;
471
1170
  var ROW_TEXT_WIDTH = PALETTE_WIDTH - 6;
472
1171
  var LABEL_WIDTH = 26;
@@ -495,22 +1194,31 @@ function placeholder(corpus) {
495
1194
  if (corpus === "output") return "type to filter output";
496
1195
  return "keybindings";
497
1196
  }
498
- function Palette({ corpus, query, active, rows }) {
1197
+ function Palette({ corpus, query, active, rows, arg }) {
499
1198
  const total = rows.length;
500
1199
  const scroll = Math.max(0, Math.min(active - (VISIBLE_ROWS - 1), total - VISIBLE_ROWS));
501
1200
  const visible = rows.slice(scroll, scroll + VISIBLE_ROWS);
502
1201
  const activeIndex = Math.min(Math.max(active - scroll, 0), Math.max(0, visible.length - 1));
1202
+ const input = arg === void 0 ? { marker: c.dim("> "), body: query.length > 0 ? query : c.dim(placeholder(corpus)) } : {
1203
+ marker: c.accent("\u25B8 "),
1204
+ body: arg.value.length > 0 ? arg.value : c.dim(arg.placeholder)
1205
+ };
503
1206
  const elements = [
504
- /* @__PURE__ */ jsx7(Box4, { paddingX: 1, children: /* @__PURE__ */ jsxs5(Text6, { children: [
1207
+ /* @__PURE__ */ jsx11(Box7, { paddingX: 1, children: /* @__PURE__ */ jsxs9(Text10, { children: [
505
1208
  c.bold("\u25B8 palette "),
506
- /* @__PURE__ */ jsx7(Text6, { children: c.dim(`\xB7 ${corpus}`) })
1209
+ /* @__PURE__ */ jsx11(Text10, { children: c.dim(`\xB7 ${corpus}`) })
507
1210
  ] }) }, "header"),
508
- /* @__PURE__ */ jsx7(Box4, { paddingX: 1, children: /* @__PURE__ */ jsxs5(Text6, { children: [
509
- c.dim("> "),
510
- query.length > 0 ? query : c.dim(placeholder(corpus)),
511
- /* @__PURE__ */ jsx7(Text6, { children: c.dim("\u258C") })
1211
+ /* @__PURE__ */ jsx11(Box7, { paddingX: 1, children: /* @__PURE__ */ jsxs9(Text10, { children: [
1212
+ input.marker,
1213
+ input.body,
1214
+ /* @__PURE__ */ jsx11(Text10, { children: c.dim("\u258C") })
512
1215
  ] }) }, "query")
513
1216
  ];
1217
+ if (arg?.hint !== void 0) {
1218
+ elements.push(
1219
+ /* @__PURE__ */ jsx11(Box7, { paddingX: 1, children: /* @__PURE__ */ jsx11(Text10, { children: c.warn(arg.hint) }) }, "arg-hint")
1220
+ );
1221
+ }
514
1222
  let lastGroup = null;
515
1223
  for (let i = 0; i < visible.length; i++) {
516
1224
  const row = visible[i];
@@ -518,27 +1226,32 @@ function Palette({ corpus, query, active, rows }) {
518
1226
  if (row.group !== null && row.group !== lastGroup) {
519
1227
  if (lastGroup !== null) {
520
1228
  elements.push(
521
- /* @__PURE__ */ jsx7(Box4, { children: /* @__PURE__ */ jsx7(Text6, { children: " " }) }, `gap:${row.group}`)
1229
+ /* @__PURE__ */ jsx11(Box7, { children: /* @__PURE__ */ jsx11(Text10, { children: " " }) }, `gap:${row.group}`)
522
1230
  );
523
1231
  }
524
1232
  elements.push(
525
- /* @__PURE__ */ jsx7(Box4, { paddingX: 1, children: /* @__PURE__ */ jsx7(Text6, { children: c.bold(`\u2500\u2500 ${row.group} \u2500\u2500`) }) }, `cat:${row.group}`)
1233
+ /* @__PURE__ */ jsx11(Box7, { paddingX: 1, children: /* @__PURE__ */ jsx11(Text10, { children: c.bold(`\u2500\u2500 ${row.group} \u2500\u2500`) }) }, `cat:${row.group}`)
526
1234
  );
527
1235
  lastGroup = row.group;
528
1236
  }
529
1237
  const isActive = i === activeIndex;
530
- const prefix = isActive ? "\u25B8 " : " ";
1238
+ const focused = isActive && arg === void 0;
1239
+ const prefix = focused ? "\u25B8 " : " ";
531
1240
  const label = truncateLabel(row.primary);
532
1241
  const labelCol = (prefix + label).padEnd(LABEL_WIDTH);
533
1242
  const hintRaw = row.argv ? `/${row.argv.join(" ")}` : row.secondary;
534
1243
  const hint = hintRaw.length > HINT_WIDTH ? `${hintRaw.slice(0, HINT_WIDTH - 1)}\u2026` : hintRaw;
535
- if (isActive) {
1244
+ if (focused) {
536
1245
  elements.push(
537
- /* @__PURE__ */ jsx7(Box4, { paddingX: 1, children: /* @__PURE__ */ jsx7(Text6, { children: c.inverse(`${labelCol}${hint}`) }) }, row.key)
1246
+ /* @__PURE__ */ jsx11(Box7, { paddingX: 1, children: /* @__PURE__ */ jsx11(Text10, { children: c.inverse(`${labelCol}${hint}`) }) }, row.key)
1247
+ );
1248
+ } else if (arg !== void 0) {
1249
+ elements.push(
1250
+ /* @__PURE__ */ jsx11(Box7, { paddingX: 1, children: /* @__PURE__ */ jsx11(Text10, { children: c.dim(`${labelCol}${hint}`) }) }, row.key)
538
1251
  );
539
1252
  } else {
540
1253
  elements.push(
541
- /* @__PURE__ */ jsx7(Box4, { paddingX: 1, children: /* @__PURE__ */ jsxs5(Text6, { children: [
1254
+ /* @__PURE__ */ jsx11(Box7, { paddingX: 1, children: /* @__PURE__ */ jsxs9(Text10, { children: [
542
1255
  c.bold(labelCol),
543
1256
  hint.length > 0 ? c.dim(hint) : null
544
1257
  ] }) }, row.key)
@@ -550,54 +1263,19 @@ function Palette({ corpus, query, active, rows }) {
550
1263
  const line = detailLines[di] ?? "";
551
1264
  const prefix2 = di === 0 ? " \u21B3 " : " ";
552
1265
  elements.push(
553
- /* @__PURE__ */ jsx7(Box4, { paddingX: 1, children: /* @__PURE__ */ jsx7(Text6, { children: c.dim(`${prefix2}${line}`) }) }, `${row.key}:d:${di}`)
1266
+ /* @__PURE__ */ jsx11(Box7, { paddingX: 1, children: /* @__PURE__ */ jsx11(Text10, { children: c.dim(`${prefix2}${line}`) }) }, `${row.key}:d:${di}`)
554
1267
  );
555
1268
  }
556
1269
  }
557
1270
  }
558
- return /* @__PURE__ */ jsxs5(Box4, { flexDirection: "column", children: [
559
- /* @__PURE__ */ jsx7(Panel, { maxWidth: PALETTE_WIDTH, children: elements }),
560
- /* @__PURE__ */ jsx7(Text6, { children: c.dim(LIST_NAV_HINT) })
561
- ] });
562
- }
563
-
564
- // src/tui/StatusBar.tsx
565
- import { Text as Text7 } from "ink";
566
- import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
567
- function Cell({
568
- label,
569
- value,
570
- accent
571
- }) {
572
- return /* @__PURE__ */ jsxs6(Text7, { children: [
573
- /* @__PURE__ */ jsx8(Text7, { children: c.dim(`${label}:`) }),
574
- " ",
575
- /* @__PURE__ */ jsx8(Text7, { children: accent ? c.accent(value) : value })
576
- ] });
577
- }
578
- function StatusBar({ payload, loading }) {
579
- const host = payload?.host ?? "\u2014";
580
- const mode = payload?.workflow?.mode ?? "\u2014";
581
- const phase = payload?.workflow?.phase ?? (loading ? "\u2026" : "idle");
582
- const daemonRunning = payload?.daemon.running === true;
583
- const daemon = payload === null && loading ? "\u2026" : daemonRunning ? "up" : "down";
584
- return /* @__PURE__ */ jsxs6(Text7, { children: [
585
- /* @__PURE__ */ jsx8(Cell, { label: "host", value: host, accent: true }),
586
- /* @__PURE__ */ jsx8(Text7, { children: c.dim(" \xB7 ") }),
587
- /* @__PURE__ */ jsx8(Cell, { label: "mode", value: mode }),
588
- /* @__PURE__ */ jsx8(Text7, { children: c.dim(" \xB7 ") }),
589
- /* @__PURE__ */ jsx8(Cell, { label: "phase", value: phase }),
590
- /* @__PURE__ */ jsx8(Text7, { children: c.dim(" \xB7 ") }),
591
- /* @__PURE__ */ jsxs6(Text7, { children: [
592
- c.dim("daemon:"),
593
- " ",
594
- daemonRunning ? c.ok(daemon) : c.warn(daemon)
595
- ] })
1271
+ return /* @__PURE__ */ jsxs9(Box7, { flexDirection: "column", children: [
1272
+ /* @__PURE__ */ jsx11(Panel, { maxWidth: PALETTE_WIDTH, children: elements }),
1273
+ /* @__PURE__ */ jsx11(Text10, { children: c.dim(LIST_NAV_HINT) })
596
1274
  ] });
597
1275
  }
598
1276
 
599
1277
  // src/tui/App.tsx
600
- import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
1278
+ import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
601
1279
  function App({
602
1280
  deps,
603
1281
  initialPayload = null,
@@ -606,20 +1284,22 @@ function App({
606
1284
  }) {
607
1285
  const { exit } = useApp();
608
1286
  const { buffer, setBuffer, pushHistory, recall, clear, seed } = useInputBuffer();
609
- const [mode, setMode] = useState2(initialMode);
610
- const [payload, setPayload] = useState2(initialPayload);
611
- const [loading, setLoading] = useState2(initialPayload === null);
612
- const [output, setOutput] = useState2(null);
613
- const [searchLines, setSearchLines] = useState2([]);
614
- const [scrollOffset, setScrollOffset] = useState2(0);
615
- const [running, setRunning] = useState2(false);
616
- const [notice, setNotice] = useState2(null);
617
- const [pending, setPending] = useState2(null);
618
- const [recent, setRecent] = useState2([]);
619
- const [homeSections, setHomeSections] = useState2([]);
620
- const [paletteQuery, setPaletteQuery] = useState2("");
621
- const [paletteActive, setPaletteActive] = useState2(0);
622
- useEffect(() => {
1287
+ const [mode, setMode] = useState4(initialMode);
1288
+ const [payload, setPayload] = useState4(initialPayload);
1289
+ const [loading, setLoading] = useState4(initialPayload === null);
1290
+ const [output, setOutput] = useState4(null);
1291
+ const [searchLines, setSearchLines] = useState4([]);
1292
+ const [scrollOffset, setScrollOffset] = useState4(0);
1293
+ const [running, setRunning] = useState4(false);
1294
+ const [notice, setNotice] = useState4(null);
1295
+ const [pending, setPending] = useState4(null);
1296
+ const [recent, setRecent] = useState4([]);
1297
+ const [homeSections, setHomeSections] = useState4([]);
1298
+ const [paletteQuery, setPaletteQuery] = useState4("");
1299
+ const [paletteActive, setPaletteActive] = useState4(0);
1300
+ const [argBuffer, setArgBuffer] = useState4("");
1301
+ const [argNotice, setArgNotice] = useState4(null);
1302
+ useEffect3(() => {
623
1303
  if (!deps.loadRecent) return;
624
1304
  const loader = deps.loadRecent;
625
1305
  let cancelled = false;
@@ -634,7 +1314,7 @@ function App({
634
1314
  cancelled = true;
635
1315
  };
636
1316
  }, [deps.loadRecent, seed]);
637
- useEffect(() => {
1317
+ useEffect3(() => {
638
1318
  let cancelled = false;
639
1319
  void resolveSections(deps.commands ?? []).then((sections) => {
640
1320
  if (!cancelled) setHomeSections(sections);
@@ -643,7 +1323,7 @@ function App({
643
1323
  cancelled = true;
644
1324
  };
645
1325
  }, [deps.commands]);
646
- useEffect(() => {
1326
+ useEffect3(() => {
647
1327
  if (running || mode.kind !== "dashboard") return;
648
1328
  let cancelled = false;
649
1329
  let inFlight = false;
@@ -668,7 +1348,7 @@ function App({
668
1348
  clearInterval(id);
669
1349
  };
670
1350
  }, [running, mode.kind, refreshMs, deps.fetchStatus]);
671
- useEffect(() => {
1351
+ useEffect3(() => {
672
1352
  if (pending === null) return;
673
1353
  const argv = pending;
674
1354
  let active = true;
@@ -700,6 +1380,14 @@ function App({
700
1380
  setMode({ kind: "confirm", argv: [...argv] });
701
1381
  return;
702
1382
  }
1383
+ enterRunOrDispatch(argv);
1384
+ }
1385
+ function enterRunOrDispatch(argv) {
1386
+ const prompt = deps.run === void 0 ? null : runPromptFrom(argv);
1387
+ if (prompt !== null) {
1388
+ setMode({ kind: "run", prompt });
1389
+ return;
1390
+ }
703
1391
  dispatchCmd([...argv]);
704
1392
  void recordRuns([...argv]);
705
1393
  pushHistory(`/${argv.join(" ")}`);
@@ -777,6 +1465,10 @@ function App({
777
1465
  openOutputSearch();
778
1466
  return;
779
1467
  }
1468
+ if (input === "t" && deps.run?.transcripts !== void 0) {
1469
+ setMode({ kind: "transcripts" });
1470
+ return;
1471
+ }
780
1472
  return;
781
1473
  }
782
1474
  if (input.length === 1) {
@@ -807,6 +1499,10 @@ function App({
807
1499
  }
808
1500
  function handlePaletteInput(input, key) {
809
1501
  if (mode.kind !== "palette") return;
1502
+ if (mode.collecting !== void 0) {
1503
+ handleArgInput(input, key, mode.collecting);
1504
+ return;
1505
+ }
810
1506
  if (key.escape) {
811
1507
  setPaletteQuery("");
812
1508
  setPaletteActive(0);
@@ -827,7 +1523,22 @@ function App({
827
1523
  if (key.return) {
828
1524
  if (mode.corpus === "commands") {
829
1525
  const row = paletteRows[paletteActive];
830
- if (row?.argv) handleRun(row.argv, row.destructive);
1526
+ if (!row?.argv) return;
1527
+ if (row.needsArg !== void 0) {
1528
+ setArgBuffer("");
1529
+ setArgNotice(null);
1530
+ setMode({
1531
+ kind: "palette",
1532
+ corpus: mode.corpus,
1533
+ collecting: {
1534
+ argv: [...row.argv],
1535
+ label: row.needsArg,
1536
+ destructive: row.destructive
1537
+ }
1538
+ });
1539
+ return;
1540
+ }
1541
+ handleRun(row.argv, row.destructive);
831
1542
  } else {
832
1543
  moveActive(1);
833
1544
  }
@@ -852,6 +1563,37 @@ function App({
852
1563
  setPaletteActive(0);
853
1564
  }
854
1565
  }
1566
+ function handleArgInput(input, key, collecting) {
1567
+ if (mode.kind !== "palette") return;
1568
+ const corpus = mode.corpus;
1569
+ if (key.escape) {
1570
+ setArgBuffer("");
1571
+ setArgNotice(null);
1572
+ setMode({ kind: "palette", corpus });
1573
+ return;
1574
+ }
1575
+ if (key.return) {
1576
+ const value = argBuffer.trim();
1577
+ if (value.length === 0) {
1578
+ setArgNotice(`a ${collecting.label} is required`);
1579
+ return;
1580
+ }
1581
+ setArgBuffer("");
1582
+ setArgNotice(null);
1583
+ handleRun([...collecting.argv, value], collecting.destructive);
1584
+ return;
1585
+ }
1586
+ if (key.backspace || key.delete) {
1587
+ setArgBuffer((b) => b.slice(0, -1));
1588
+ setArgNotice(null);
1589
+ return;
1590
+ }
1591
+ if (key.ctrl || key.tab) return;
1592
+ if (input.length > 0) {
1593
+ setArgBuffer((b) => b + input);
1594
+ setArgNotice(null);
1595
+ }
1596
+ }
855
1597
  function handleConfirmInput(input, key) {
856
1598
  if (key.escape || input === "n" || input === "N") {
857
1599
  setPaletteQuery("");
@@ -861,13 +1603,14 @@ function App({
861
1603
  }
862
1604
  if (input === "y" || input === "Y") {
863
1605
  if (mode.kind !== "confirm") return;
864
- dispatchCmd([...mode.argv]);
865
- void recordRuns([...mode.argv]);
866
- pushHistory(`/${mode.argv.join(" ")}`);
867
- setMode({ kind: "dashboard" });
1606
+ enterRunOrDispatch(mode.argv);
868
1607
  }
869
1608
  }
870
- useInput((input, key) => {
1609
+ useInput3((input, key) => {
1610
+ if (key.ctrl && input === "c") {
1611
+ if (mode.kind !== "run") exit();
1612
+ return;
1613
+ }
871
1614
  switch (mode.kind) {
872
1615
  case "dashboard":
873
1616
  handleDashboardInput(input, key);
@@ -878,6 +1621,9 @@ function App({
878
1621
  case "confirm":
879
1622
  handleConfirmInput(input, key);
880
1623
  return;
1624
+ case "run":
1625
+ case "transcripts":
1626
+ return;
881
1627
  }
882
1628
  });
883
1629
  function submit() {
@@ -901,7 +1647,7 @@ function App({
901
1647
  dispatchCmd(argv);
902
1648
  void recordRuns(argv);
903
1649
  }
904
- const paletteRows = useMemo(
1650
+ const paletteRows = useMemo2(
905
1651
  () => buildPaletteRows({
906
1652
  corpus: mode.kind === "palette" ? mode.corpus : "commands",
907
1653
  query: paletteQuery,
@@ -914,40 +1660,64 @@ function App({
914
1660
  [mode, paletteQuery, deps.commands, deps.matcher, recent, homeSections, searchLines]
915
1661
  );
916
1662
  if (mode.kind === "palette") {
917
- return /* @__PURE__ */ jsxs7(Box5, { flexDirection: "column", children: [
918
- /* @__PURE__ */ jsx9(Header, { tagline: `palette \xB7 ${mode.corpus}` }),
919
- /* @__PURE__ */ jsx9(
1663
+ const collecting = mode.collecting;
1664
+ return /* @__PURE__ */ jsxs10(Box8, { flexDirection: "column", children: [
1665
+ /* @__PURE__ */ jsx12(Header, { tagline: `palette \xB7 ${mode.corpus}` }),
1666
+ /* @__PURE__ */ jsx12(
920
1667
  Palette,
921
1668
  {
922
1669
  corpus: mode.corpus,
923
1670
  query: paletteQuery,
924
1671
  active: paletteActive,
925
- rows: paletteRows
1672
+ rows: paletteRows,
1673
+ arg: collecting === void 0 ? void 0 : {
1674
+ value: argBuffer,
1675
+ placeholder: `${collecting.label} for ${collecting.argv.join(" ")}\u2026`,
1676
+ hint: argNotice ?? void 0
1677
+ }
926
1678
  }
927
1679
  ),
928
- /* @__PURE__ */ jsx9(Footer, { running: false })
1680
+ /* @__PURE__ */ jsx12(Footer, { running: false })
929
1681
  ] });
930
1682
  }
931
1683
  if (mode.kind === "confirm") {
932
- return /* @__PURE__ */ jsxs7(Box5, { flexDirection: "column", children: [
933
- /* @__PURE__ */ jsx9(Header, { tagline: "confirm" }),
934
- /* @__PURE__ */ jsx9(ConfirmOverlay, { argv: mode.argv }),
935
- /* @__PURE__ */ jsx9(Footer, { running: false })
1684
+ return /* @__PURE__ */ jsxs10(Box8, { flexDirection: "column", children: [
1685
+ /* @__PURE__ */ jsx12(Header, { tagline: "confirm" }),
1686
+ /* @__PURE__ */ jsx12(ConfirmOverlay, { argv: mode.argv }),
1687
+ /* @__PURE__ */ jsx12(Footer, { running: false })
936
1688
  ] });
937
1689
  }
1690
+ if (mode.kind === "run" && deps.run !== void 0) {
1691
+ return /* @__PURE__ */ jsx12(
1692
+ RunMode,
1693
+ {
1694
+ prompt: mode.prompt,
1695
+ deps: deps.run,
1696
+ onQuit: exit,
1697
+ onExit: (notice2) => {
1698
+ if (notice2 !== void 0) setNotice(notice2);
1699
+ setMode({ kind: "dashboard" });
1700
+ }
1701
+ }
1702
+ );
1703
+ }
1704
+ const transcripts = deps.run?.transcripts;
1705
+ if (mode.kind === "transcripts" && transcripts !== void 0) {
1706
+ return /* @__PURE__ */ jsx12(TranscriptPicker, { transcripts, onExit: () => setMode({ kind: "dashboard" }) });
1707
+ }
938
1708
  const snapshotLines = formatStatusPayload(payload);
939
1709
  const paneLines = output?.lines ?? snapshotLines ?? [];
940
1710
  const paneTitle = output?.title ?? "snapshot";
941
- return /* @__PURE__ */ jsxs7(Box5, { flexDirection: "column", children: [
942
- /* @__PURE__ */ jsx9(Header, { tagline: "dashboard" }),
943
- /* @__PURE__ */ jsxs7(Panel, { children: [
944
- /* @__PURE__ */ jsx9(Box5, { paddingX: 1, children: /* @__PURE__ */ jsx9(StatusBar, { payload, loading }) }),
945
- /* @__PURE__ */ jsx9(Box5, { paddingX: 1, children: /* @__PURE__ */ jsx9(Text8, { children: divider() }) }),
946
- /* @__PURE__ */ jsx9(Box5, { flexDirection: "column", paddingX: 1, children: /* @__PURE__ */ jsx9(OutputPane, { lines: paneLines, scrollOffset, title: paneTitle }) })
1711
+ return /* @__PURE__ */ jsxs10(Box8, { flexDirection: "column", children: [
1712
+ /* @__PURE__ */ jsx12(Header, { tagline: "dashboard" }),
1713
+ /* @__PURE__ */ jsxs10(Panel, { children: [
1714
+ /* @__PURE__ */ jsx12(Box8, { paddingX: 1, children: /* @__PURE__ */ jsx12(StatusBar, { payload, loading }) }),
1715
+ /* @__PURE__ */ jsx12(Box8, { paddingX: 1, children: /* @__PURE__ */ jsx12(Text11, { children: divider() }) }),
1716
+ /* @__PURE__ */ jsx12(Box8, { flexDirection: "column", paddingX: 1, children: /* @__PURE__ */ jsx12(OutputPane, { lines: paneLines, scrollOffset, title: paneTitle }) })
947
1717
  ] }),
948
- /* @__PURE__ */ jsx9(CommandInput, { buffer, running }),
949
- notice !== null ? /* @__PURE__ */ jsx9(Text8, { children: c.dim(notice) }) : null,
950
- /* @__PURE__ */ jsx9(Footer, { running })
1718
+ /* @__PURE__ */ jsx12(CommandInput, { buffer, running }),
1719
+ notice !== null ? /* @__PURE__ */ jsx12(Text11, { children: c.dim(notice) }) : null,
1720
+ /* @__PURE__ */ jsx12(Footer, { running })
951
1721
  ] });
952
1722
  }
953
1723
 
@@ -1006,8 +1776,57 @@ function recordRecent(projectId, argv) {
1006
1776
  }
1007
1777
  }
1008
1778
 
1779
+ // src/tui/transcripts.ts
1780
+ import { readdirSync, readFileSync as readFileSync2, statSync } from "fs";
1781
+ import { join as join2, resolve, sep } from "path";
1782
+ import { paths } from "@noir-ai/core";
1783
+ var TRANSCRIPT_LIST_LIMIT = 20;
1784
+ function transcriptDir(root = process.cwd()) {
1785
+ return paths.transcriptsDir(root);
1786
+ }
1787
+ function createTranscriptStore(opts) {
1788
+ const root = opts.root ?? process.cwd();
1789
+ const dir = transcriptDir(root);
1790
+ return {
1791
+ list: async (limit = TRANSCRIPT_LIST_LIMIT) => {
1792
+ let names;
1793
+ try {
1794
+ names = readdirSync(dir);
1795
+ } catch {
1796
+ return [];
1797
+ }
1798
+ const entries = [];
1799
+ for (const name of names) {
1800
+ if (!name.endsWith(".jsonl")) continue;
1801
+ const path = join2(dir, name);
1802
+ try {
1803
+ const info = statSync(path);
1804
+ if (!info.isFile()) continue;
1805
+ entries.push({ path, name, sizeBytes: info.size, mtimeMs: info.mtimeMs });
1806
+ } catch {
1807
+ }
1808
+ }
1809
+ entries.sort((a, b) => b.mtimeMs - a.mtimeMs || a.name.localeCompare(b.name));
1810
+ return entries.slice(0, Math.max(0, limit));
1811
+ },
1812
+ read: async (path) => {
1813
+ const resolved = resolve(path);
1814
+ if (resolved !== resolve(dir) && !resolved.startsWith(resolve(dir) + sep)) return [];
1815
+ try {
1816
+ return readFileSync2(resolved, "utf8").split("\n");
1817
+ } catch {
1818
+ return [];
1819
+ }
1820
+ },
1821
+ write: async (lines) => {
1822
+ return writeTranscript(opts.host, lines, root);
1823
+ }
1824
+ };
1825
+ }
1826
+
1009
1827
  // src/tui/index.tsx
1010
- import { jsx as jsx10 } from "react/jsx-runtime";
1828
+ import { jsx as jsx13 } from "react/jsx-runtime";
1829
+ var DEFAULT_RUN_HOST = "claude";
1011
1830
  function defaultFetchStatus(opts) {
1012
1831
  return async () => {
1013
1832
  try {
@@ -1017,17 +1836,68 @@ function defaultFetchStatus(opts) {
1017
1836
  }
1018
1837
  };
1019
1838
  }
1839
+ function startHostRun(prompt, handlers, resumeSessionId) {
1840
+ const host = DEFAULT_RUN_HOST;
1841
+ if (!SUPPORTED_HOSTS.includes(host)) {
1842
+ throw new Error(`unknown host '${host}' (supported: ${SUPPORTED_HOSTS.join(", ")})`);
1843
+ }
1844
+ const profile = resolveProfile();
1845
+ const extraArgs = resumeSessionId === void 0 ? profile.args : [...profile.args ?? [], "--resume", resumeSessionId];
1846
+ const env = profile.env === void 0 ? void 0 : mergeEnv(process.env, profile.env);
1847
+ const binary = profile.binary !== void 0 && profile.binary.length > 0 ? profile.binary : host;
1848
+ return {
1849
+ binary,
1850
+ done: runHost({
1851
+ host,
1852
+ prompt,
1853
+ ...profile.binary === void 0 ? {} : { customBinary: profile.binary },
1854
+ ...extraArgs === void 0 ? {} : { extraArgs },
1855
+ ...env === void 0 ? {} : { env },
1856
+ onLine: handlers.onLine,
1857
+ onEvent: handlers.onEvent,
1858
+ signal: handlers.signal,
1859
+ ...handlers.onChild === void 0 ? {} : { onChild: handlers.onChild }
1860
+ })
1861
+ };
1862
+ }
1863
+ function resolveProfile() {
1864
+ const config = loadRunConfig(process.cwd()) ?? parseConfig({});
1865
+ const resolved = resolveRunProfile(void 0, config, process.env);
1866
+ if (!resolved.ok) throw new Error(resolved.message);
1867
+ return resolved.profile;
1868
+ }
1869
+ function defaultRunDeps(opts) {
1870
+ const toInput = (request) => ({
1871
+ answer: request.answer,
1872
+ transcript: request.transcript,
1873
+ host: request.host,
1874
+ opts,
1875
+ ...request.sessionId === void 0 ? {} : { sessionId: request.sessionId },
1876
+ ...request.canContinue === void 0 ? {} : { canContinue: request.canContinue }
1877
+ });
1878
+ return {
1879
+ start: (prompt, handlers, resumeSessionId) => startHostRun(prompt, handlers, resumeSessionId),
1880
+ actions: (request) => postRunActions(toInput(request)),
1881
+ perform: (request, choice, value) => performPostRunAction(choice, toInput(request), value),
1882
+ // Named by host, exactly as the CLI names its own transcripts, so a run
1883
+ // started from the dashboard lands beside one started from a shell.
1884
+ transcripts: createTranscriptStore({ host: DEFAULT_RUN_HOST })
1885
+ };
1886
+ }
1020
1887
  async function runTui(opts, dispatch) {
1021
1888
  process.stdout.write("\x1B[2J\x1B[H");
1022
1889
  const deps = await buildTuiDeps(opts, dispatch);
1023
- const instance = render(/* @__PURE__ */ jsx10(App, { deps }));
1890
+ const instance = render(/* @__PURE__ */ jsx13(App, { deps }), { exitOnCtrlC: false });
1024
1891
  await instance.waitUntilExit();
1025
1892
  }
1026
1893
  async function runPalette(opts, dispatch) {
1027
1894
  process.stdout.write("\x1B[2J\x1B[H");
1028
1895
  const deps = await buildTuiDeps(opts, dispatch);
1029
1896
  const instance = render(
1030
- /* @__PURE__ */ jsx10(App, { deps, initialMode: { kind: "palette", corpus: "commands" } })
1897
+ /* @__PURE__ */ jsx13(App, { deps, initialMode: { kind: "palette", corpus: "commands" } }),
1898
+ {
1899
+ exitOnCtrlC: false
1900
+ }
1031
1901
  );
1032
1902
  await instance.waitUntilExit();
1033
1903
  }
@@ -1041,9 +1911,13 @@ async function buildTuiDeps(opts, dispatch) {
1041
1911
  const deps = {
1042
1912
  dispatch,
1043
1913
  fetchStatus: defaultFetchStatus(opts),
1044
- // The palette source, derived from a fresh commander program at launch (B3).
1914
+ // The live host-run screen. Present here (not optional in practice) is what
1915
+ // routes a `run <prompt>` selection to the screen instead of dispatching it
1916
+ // through the captured-command path — see `enterRunOrDispatch` in App.tsx.
1917
+ run: defaultRunDeps(opts),
1918
+ // The palette source, derived from a fresh commander program at launch.
1045
1919
  commands: buildPaletteCommandsForTui(),
1046
- // C3 — persistent recent commands (projectId-keyed). recordRecent is async
1920
+ // Persistent recent commands (projectId-keyed). recordRecent is async
1047
1921
  // only to match the TuiDeps seam; it never rejects.
1048
1922
  record: (argv) => {
1049
1923
  if (projectId) recordRecent(projectId, argv);