@buildaharness/aielia 0.3.4 → 0.3.6

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 (113) hide show
  1. package/README.md +156 -35
  2. package/dist/PlanGraphPane.d.ts +19 -0
  3. package/dist/_memory-core-generated.d.ts +1764 -0
  4. package/dist/action-approval-service.d.ts +34 -2
  5. package/dist/action-record.d.ts +13 -0
  6. package/dist/activity-log.d.ts +4 -0
  7. package/dist/agent-loop.d.ts +106 -9
  8. package/dist/ambiguity-guard-flag.d.ts +12 -0
  9. package/dist/ambiguity-guard.d.ts +20 -0
  10. package/dist/answer-claim.d.ts +13 -1
  11. package/dist/ask-clarification-service.d.ts +12 -1
  12. package/dist/ask-response-format.d.ts +6 -0
  13. package/dist/assistant-session.d.ts +32 -2
  14. package/dist/assistant-source.d.ts +7 -0
  15. package/dist/assistant-types.d.ts +20 -2
  16. package/dist/assistant.d.ts +160 -6
  17. package/dist/benchmark-injected-failure.d.ts +6 -0
  18. package/dist/bin-BYRl4xXM.js +4985 -0
  19. package/dist/bin-BYRl4xXM.js.map +1 -0
  20. package/dist/cli-config.d.ts +3 -0
  21. package/dist/cli-session.d.ts +19 -5
  22. package/dist/cli.d.ts +26 -2
  23. package/dist/cli.js +1 -1
  24. package/dist/code-plan-templates.d.ts +21 -0
  25. package/dist/config.d.ts +31 -0
  26. package/dist/constraint-check.d.ts +35 -0
  27. package/dist/contradiction-checker.d.ts +7 -0
  28. package/dist/debug-log.d.ts +10 -1
  29. package/dist/decomposed-answer.d.ts +13 -0
  30. package/dist/decomposition-classifier.d.ts +2 -0
  31. package/dist/doctor-checks.d.ts +3 -0
  32. package/dist/email.d.ts +5 -3
  33. package/dist/episodic-digest.d.ts +89 -0
  34. package/dist/fact-extraction.d.ts +39 -0
  35. package/dist/file-tools-mcp-server.mjs +288 -37
  36. package/dist/file-tools.d.ts +6 -0
  37. package/dist/first-run.d.ts +0 -2
  38. package/dist/git-exclude.d.ts +9 -0
  39. package/dist/goal-graph-store.d.ts +18 -18
  40. package/dist/grounding-check.d.ts +33 -0
  41. package/dist/harness-bridge.d.ts +75 -2
  42. package/dist/harness-token-budget.d.ts +9 -0
  43. package/dist/index.d.ts +21 -2
  44. package/dist/index.js +166 -94
  45. package/dist/index.js.map +1 -1
  46. package/dist/ink-select-prompt.d.ts +2 -0
  47. package/dist/ink-test-render.d.ts +2 -0
  48. package/dist/layer-policy-flag.d.ts +13 -0
  49. package/dist/layer-policy-wiring.d.ts +77 -0
  50. package/dist/layer-settings.d.ts +49 -0
  51. package/dist/layer-telemetry.d.ts +2 -0
  52. package/dist/lexical/lexical-mode.d.ts +29 -0
  53. package/dist/memory-consolidation.d.ts +80 -0
  54. package/dist/memory-governance.d.ts +30 -0
  55. package/dist/memory-reviewer.d.ts +114 -0
  56. package/dist/memory-service.d.ts +356 -9
  57. package/dist/model-json.d.ts +17 -0
  58. package/dist/next-step-context.d.ts +51 -0
  59. package/dist/next-step-proposer.d.ts +10 -2
  60. package/dist/plan-drafting-service.d.ts +3 -0
  61. package/dist/plan-graph-flag.d.ts +20 -0
  62. package/dist/plan-question.d.ts +60 -0
  63. package/dist/plan-service.d.ts +6 -2
  64. package/dist/plan-step-prompt.d.ts +15 -0
  65. package/dist/plan-store.d.ts +12 -1
  66. package/dist/plan-viz/normalize.d.ts +14 -0
  67. package/dist/plan-viz/plan-detail-lines.d.ts +3 -0
  68. package/dist/plan-viz/plan-layout.d.ts +48 -0
  69. package/dist/plan-viz/plan-nav.d.ts +29 -0
  70. package/dist/plan-viz/plan-raster.d.ts +36 -0
  71. package/dist/plan-viz/plan-routes.d.ts +6 -0
  72. package/dist/plan-viz/plan-snapshot.d.ts +73 -0
  73. package/dist/plan-viz/plan-viewport.d.ts +29 -0
  74. package/dist/plan-viz/plan-width.d.ts +15 -0
  75. package/dist/plan-viz/testkit/display-width.d.ts +12 -0
  76. package/dist/plan-viz/testkit/generators.d.ts +61 -0
  77. package/dist/plan-viz/testkit/grid-oracles.d.ts +38 -0
  78. package/dist/plan-viz/testkit/nav-oracles.d.ts +79 -0
  79. package/dist/plan-viz/testkit/types.d.ts +8 -0
  80. package/dist/plan-viz/types.d.ts +34 -0
  81. package/dist/{provider-setup-CPbB5y_V.js → provider-setup-CV3S_TAS.js} +10792 -5731
  82. package/dist/provider-setup-CV3S_TAS.js.map +1 -0
  83. package/dist/recall-tool.d.ts +43 -0
  84. package/dist/recovery-note.d.ts +22 -0
  85. package/dist/reply-audit.d.ts +67 -0
  86. package/dist/response-service.d.ts +11 -1
  87. package/dist/review-checker.d.ts +10 -4
  88. package/dist/reviewer-revision.d.ts +23 -0
  89. package/dist/risk-classifier.d.ts +7 -0
  90. package/dist/scripted-llm-client.d.ts +26 -1
  91. package/dist/semantic-compaction.d.ts +15 -0
  92. package/dist/semantic-hypotheses.d.ts +40 -0
  93. package/dist/side-call-policy.d.ts +30 -0
  94. package/dist/source-reliability.d.ts +71 -0
  95. package/dist/task-completion-check.d.ts +24 -0
  96. package/dist/tool-control-plane.d.ts +33 -1
  97. package/dist/tool-effect-class.d.ts +9 -0
  98. package/dist/tool-step.d.ts +6 -0
  99. package/dist/trace-events.d.ts +1 -1
  100. package/dist/transcript-compaction.d.ts +9 -0
  101. package/dist/trust-tagging.d.ts +2 -0
  102. package/dist/{tui-app-DeyZ4Pr9.js → tui-app-CJq6YaiP.js} +436 -21
  103. package/dist/tui-app-CJq6YaiP.js.map +1 -0
  104. package/dist/tui-app.d.ts +73 -0
  105. package/dist/turn-abort.d.ts +18 -0
  106. package/dist/turn-intent-classifier.d.ts +81 -14
  107. package/dist/turn-interpreter.d.ts +27 -3
  108. package/dist/version.d.ts +1 -1
  109. package/package.json +6 -6
  110. package/dist/bin-CdVTAe7j.js +0 -2209
  111. package/dist/bin-CdVTAe7j.js.map +0 -1
  112. package/dist/provider-setup-CPbB5y_V.js.map +0 -1
  113. package/dist/tui-app-DeyZ4Pr9.js.map +0 -1
@@ -3,11 +3,11 @@ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { en
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
4
  import { jsxs, jsx } from "react/jsx-runtime";
5
5
  import { Readable, Writable } from "node:stream";
6
- import { useState, useRef, useCallback, useSyncExternalStore, useEffect } from "react";
6
+ import { useState, useRef, useEffect, useMemo, useCallback, useSyncExternalStore } from "react";
7
7
  import { useWindowSize, usePaste, useInput, Box, Text, render, Static } from "ink";
8
- import { r as runCli, P as PLAN_LINE_PREFIX, I as ICONS } from "./bin-CdVTAe7j.js";
8
+ import { s as sanitizeLabel, r as renderPlan, t as toLines, a as runCli, P as PLAN_LINE_PREFIX, I as ICONS } from "./bin-BYRl4xXM.js";
9
9
  import { format } from "node:util";
10
- import { j as DIFF_INDENT } from "./provider-setup-CPbB5y_V.js";
10
+ import { n as DIFF_INDENT, bL as stripTerminalControls } from "./provider-setup-CV3S_TAS.js";
11
11
  function classify(raw, stream) {
12
12
  if (raw.startsWith("\r")) {
13
13
  return { type: "progress", text: raw.replace(/\r/g, "").trimEnd() };
@@ -99,6 +99,15 @@ function TuiInput(props) {
99
99
  const [history, setHistory] = useState([]);
100
100
  const [historyIndex, setHistoryIndex] = useState(null);
101
101
  const [pendingDraft, setPendingDraft] = useState("");
102
+ const previousPromptLabel = useRef(promptLabel);
103
+ useEffect(() => {
104
+ if (previousPromptLabel.current === promptLabel) return;
105
+ previousPromptLabel.current = promptLabel;
106
+ setValue("");
107
+ setCursor(0);
108
+ setHistoryIndex(null);
109
+ setPendingDraft("");
110
+ }, [promptLabel]);
102
111
  const windowSize = useWindowSize();
103
112
  const terminalColumns = props.columns ?? windowSize.columns;
104
113
  const wrapWidth = Math.max(1, terminalColumns - BOX_CHROME_WIDTH);
@@ -212,10 +221,20 @@ function TuiInput(props) {
212
221
  ] });
213
222
  }
214
223
  function SelectPrompt(props) {
215
- const { question, options, onSubmit, isActive = true } = props;
224
+ const { question, options, onSubmit, isActive = true, armDelayMs = 0 } = props;
216
225
  const [highlighted, setHighlighted] = useState(0);
226
+ const armed = useRef(armDelayMs <= 0);
227
+ useEffect(() => {
228
+ if (armDelayMs <= 0) return;
229
+ armed.current = false;
230
+ const timer = setTimeout(() => {
231
+ armed.current = true;
232
+ }, armDelayMs);
233
+ return () => clearTimeout(timer);
234
+ }, [armDelayMs, question, options]);
217
235
  useInput(
218
236
  (input, key) => {
237
+ if (!armed.current) return;
219
238
  if (key.upArrow) {
220
239
  setHighlighted((h) => (h - 1 + options.length) % options.length);
221
240
  return;
@@ -244,6 +263,195 @@ function SelectPrompt(props) {
244
263
  ] }, option.key)) })
245
264
  ] });
246
265
  }
266
+ function navModelFrom(nodes, boxes) {
267
+ const byId = new Map(boxes.map((b) => [b.id, b]));
268
+ const ids = nodes.map((n) => n.id).filter((id) => byId.has(id));
269
+ const dependents = Object.fromEntries(ids.map((id) => [id, []]));
270
+ for (const n of nodes) if (byId.has(n.id)) {
271
+ for (const d of n.deps) if (dependents[d]) dependents[d].push(n.id);
272
+ }
273
+ return {
274
+ ids,
275
+ taskOrder: ids,
276
+ x: Object.fromEntries(ids.map((id) => [id, byId.get(id).x + byId.get(id).w / 2])),
277
+ y: Object.fromEntries(ids.map((id) => [id, byId.get(id).y])),
278
+ deps: Object.fromEntries(nodes.map((n) => [n.id, n.deps.filter((d) => byId.has(d))])),
279
+ dependents
280
+ };
281
+ }
282
+ const cmp = (a, b) => a < b ? -1 : a > b ? 1 : 0;
283
+ const byX = (m, ids) => [...ids].sort((a, b) => m.x[a] - m.x[b] || cmp(a, b));
284
+ const planNavigator = {
285
+ start: (_m, id) => ({ cur: id, k: 0 }),
286
+ press(m, st, key) {
287
+ if (key === "tab" || key === "shiftTab") {
288
+ const n = m.taskOrder.length;
289
+ if (!n) return st;
290
+ const i2 = m.taskOrder.indexOf(st.cur);
291
+ return { cur: m.taskOrder[(i2 + (key === "tab" ? 1 : -1) + n) % n], k: 0 };
292
+ }
293
+ if (key === "up" || key === "down") {
294
+ let origin = st.lastKey === key && st.lastFrom !== void 0 ? st.lastFrom : st.cur;
295
+ let cands = byX(m, (key === "up" ? m.deps[origin] : m.dependents[origin]) ?? []);
296
+ let repeating = st.lastKey === key;
297
+ if (repeating && cands.length < 2) {
298
+ origin = st.cur;
299
+ cands = byX(m, (key === "up" ? m.deps[origin] : m.dependents[origin]) ?? []);
300
+ repeating = false;
301
+ }
302
+ if (!cands.length) return st;
303
+ const nearest = [...cands].sort((a, b) => Math.abs(m.x[a] - m.x[origin]) - Math.abs(m.x[b] - m.x[origin]) || cmp(a, b))[0];
304
+ const k = repeating ? (st.k + 1) % cands.length : 0;
305
+ return { cur: cands[(cands.indexOf(nearest) + k) % cands.length], lastKey: key, lastFrom: origin, k };
306
+ }
307
+ const rank = byX(m, m.ids.filter((id) => m.y[id] === m.y[st.cur]));
308
+ const i = rank.indexOf(st.cur);
309
+ if (i < 0) return st;
310
+ return { cur: rank[(i + (key === "left" ? -1 : 1) + rank.length) % rank.length], lastKey: key, k: 0 };
311
+ }
312
+ };
313
+ function followSelection(prev, box, grid, term) {
314
+ const width = Math.min(term.cols, grid.width);
315
+ const height = Math.min(term.rows, grid.height);
316
+ const fit = (start, size, bStart, bSize, total) => {
317
+ let s = start;
318
+ if (bStart < s) s = bStart;
319
+ if (bStart + bSize > s + size) s = bStart + bSize - size;
320
+ return Math.max(0, Math.min(s, total - size));
321
+ };
322
+ return { width, height, left: fit(prev.left, width, box.x, box.w, grid.width), top: fit(prev.top, height, box.y, box.h, grid.height) };
323
+ }
324
+ function scrollIndicators(v, grid) {
325
+ return { up: v.top > 0, down: v.top + v.height < grid.height, left: v.left > 0, right: v.left + v.width < grid.width };
326
+ }
327
+ const STATUS_WORD = {
328
+ pending: "pending",
329
+ ready: "ready to run",
330
+ running: "running",
331
+ done: "done",
332
+ failed: "failed",
333
+ awaiting_user: "waiting for you",
334
+ awaiting_input: "blocked, waiting for input",
335
+ cancelled: "cancelled"
336
+ };
337
+ function wrap(text, width) {
338
+ const out = [];
339
+ let line = "";
340
+ for (const word of text.split(/\s+/).filter(Boolean)) {
341
+ let w = word;
342
+ while (w.length > width) {
343
+ if (line) {
344
+ out.push(line);
345
+ line = "";
346
+ }
347
+ out.push(w.slice(0, width));
348
+ w = w.slice(width);
349
+ }
350
+ if (!line) line = w;
351
+ else if (line.length + 1 + w.length <= width) line += ` ${w}`;
352
+ else {
353
+ out.push(line);
354
+ line = w;
355
+ }
356
+ }
357
+ if (line) out.push(line);
358
+ return out;
359
+ }
360
+ function planDetailLines(nodes, id, width) {
361
+ const node = nodes.find((n) => n.id === id);
362
+ if (!node) return ["No task selected."];
363
+ const w = Math.max(10, width);
364
+ const name = (i) => {
365
+ var _a;
366
+ return sanitizeLabel(((_a = nodes.find((n) => n.id === i)) == null ? void 0 : _a.id) ?? i);
367
+ };
368
+ const dependents = nodes.filter((n) => n.deps.includes(node.id)).map((n) => n.id);
369
+ return [
370
+ ...wrap(sanitizeLabel(node.id), w),
371
+ "",
372
+ ...wrap(sanitizeLabel(node.label), w),
373
+ "",
374
+ `Status: ${STATUS_WORD[node.status] ?? node.status}`,
375
+ ...wrap(`Depends on: ${node.deps.length ? node.deps.map(name).join(", ") : "nothing"}`, w),
376
+ ...wrap(`Needed by: ${dependents.length ? dependents.map(sanitizeLabel).join(", ") : "nothing"}`, w)
377
+ ];
378
+ }
379
+ const DRAWER_DOCK_COLUMNS = 120;
380
+ const DRAWER_WIDTH = 36;
381
+ function PlanGraphPane({ nodes, columns, rows, active = true, color = true, ascii = false, onClose }) {
382
+ const docked = columns >= DRAWER_DOCK_COLUMNS;
383
+ const [drawerOpen, setDrawerOpen] = useState(false);
384
+ const showDrawer = docked || drawerOpen;
385
+ const graphCols = showDrawer ? Math.max(20, columns - DRAWER_WIDTH - 1) : columns;
386
+ const graphRows = Math.max(3, rows - 2);
387
+ const [nav, setNav] = useState(void 0);
388
+ const lastIndex = useRef(0);
389
+ const viewport = useRef({ left: 0, top: 0, width: 0, height: 0 });
390
+ const base = useMemo(() => renderPlan(nodes, { maxCols: graphCols, ascii, color: false }), [nodes, graphCols, ascii]);
391
+ const model = useMemo(() => base.ok ? navModelFrom(nodes, base.boxes) : void 0, [nodes, base]);
392
+ let selectedId;
393
+ if (model && model.ids.length) {
394
+ if (nav && model.ids.includes(nav.cur)) selectedId = nav.cur;
395
+ else selectedId = model.taskOrder[Math.min(lastIndex.current, model.taskOrder.length - 1)];
396
+ }
397
+ if (model && selectedId) lastIndex.current = model.taskOrder.indexOf(selectedId);
398
+ const detail = useMemo(() => planDetailLines(nodes, selectedId, DRAWER_WIDTH - 2), [nodes, selectedId]);
399
+ const [scroll, setScroll] = useState({ id: void 0, offset: 0 });
400
+ const drawerRows = graphRows;
401
+ const maxScroll = Math.max(0, detail.length - drawerRows);
402
+ const drawerOffset = scroll.id === selectedId ? Math.min(scroll.offset, maxScroll) : 0;
403
+ const scrollDrawer = (delta) => setScroll({ id: selectedId, offset: Math.max(0, Math.min(maxScroll, drawerOffset + delta)) });
404
+ useInput(
405
+ (input, key) => {
406
+ if (key.escape || input === "q") return onClose();
407
+ if (key.return || input === "d") return setDrawerOpen((open) => !open);
408
+ if (showDrawer) {
409
+ if (input === "j") return scrollDrawer(1);
410
+ if (input === "k") return scrollDrawer(-1);
411
+ if (key.pageDown) return scrollDrawer(drawerRows);
412
+ if (key.pageUp) return scrollDrawer(-drawerRows);
413
+ }
414
+ if (!model || !selectedId) return;
415
+ const k = key.upArrow || input === "[" ? "up" : key.downArrow || input === "]" ? "down" : key.leftArrow ? "left" : key.rightArrow ? "right" : key.tab ? key.shift ? "shiftTab" : "tab" : void 0;
416
+ if (!k) return;
417
+ const from = nav && nav.cur === selectedId ? nav : planNavigator.start(model, selectedId);
418
+ setNav(planNavigator.press(model, from, k));
419
+ },
420
+ { isActive: active }
421
+ );
422
+ if (!base.ok || !model) {
423
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", height: rows, children: [
424
+ /* @__PURE__ */ jsxs(Text, { children: [
425
+ "Plan graph unavailable: ",
426
+ base.ok ? "nothing to draw" : base.message
427
+ ] }),
428
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "q / Esc closes" })
429
+ ] });
430
+ }
431
+ const r = renderPlan(nodes, { maxCols: graphCols, ascii, color, selectedId });
432
+ if (!r.ok) return /* @__PURE__ */ jsxs(Text, { children: [
433
+ "Plan graph unavailable: ",
434
+ r.message
435
+ ] });
436
+ const box = r.boxes.find((b) => b.id === selectedId);
437
+ const vp = box ? followSelection(viewport.current, box, r, { cols: graphCols, rows: graphRows }) : { left: 0, top: 0, width: Math.min(graphCols, r.width), height: Math.min(graphRows, r.height) };
438
+ viewport.current = vp;
439
+ const lines = toLines(r.cells, r.styles, [vp.top, vp.top + vp.height], [vp.left, vp.left + vp.width]);
440
+ const more = scrollIndicators(vp, r);
441
+ const arrows = `${more.up ? "↑" : " "}${more.down ? "↓" : " "}${more.left ? "←" : " "}${more.right ? "→" : " "}`;
442
+ const done = nodes.filter((n) => n.status === "done").length;
443
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", height: rows, children: [
444
+ /* @__PURE__ */ jsxs(Text, { bold: true, children: [
445
+ `Plan graph — ${done}/${nodes.length} done `,
446
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: arrows })
447
+ ] }),
448
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", flexGrow: 1, children: [
449
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", width: graphCols, children: lines.map((line, i) => /* @__PURE__ */ jsx(Text, { wrap: "truncate", children: line.length ? line : " " }, i)) }),
450
+ showDrawer && /* @__PURE__ */ jsx(Box, { flexDirection: "column", width: DRAWER_WIDTH + 1, paddingLeft: 1, borderStyle: "single", borderTop: false, borderBottom: false, borderRight: false, children: detail.slice(drawerOffset, drawerOffset + drawerRows).map((line, i) => /* @__PURE__ */ jsx(Text, { wrap: "truncate", children: line.length ? line : " " }, i)) })
451
+ ] }),
452
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: docked ? "↑↓←→ move · Tab next · j/k PgUp/PgDn scroll details · q close" : "↑↓←→ move · Tab next · Enter details · q close" })
453
+ ] });
454
+ }
247
455
  const HEADER_RE = /^(#{1,6})\s+(.*)$/;
248
456
  const CHECKBOX_RE = /^(\s*[-*]\s)\[([ xX])\]\s+(.*)$/;
249
457
  const DIFF_LINE_RE = /^(\s*\d+) ([+\- ])(.*)$/;
@@ -323,7 +531,7 @@ const ASSISTANT_INDENT_WIDTH = 2;
323
531
  function useStore(store) {
324
532
  return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
325
533
  }
326
- const EMPTY_LOG = { lines: [], progressText: "", transientText: "", waitingForOutput: false };
534
+ const EMPTY_LOG = { lines: [], progressText: "", transientText: "", waitingForOutput: false, turnInFlight: false };
327
535
  const TOOL_STEP_PREFIXES = [ICONS.toolStep, ICONS.proposalStep, ICONS.deniedStep];
328
536
  const ASSISTANT_REPLY_PREFIX = "Aielia>";
329
537
  function classifyLineKind(mergedText, streamedReplyWasOpen, stream) {
@@ -345,6 +553,8 @@ class EventLogBridge {
345
553
  __publicField(this, "progressText", "");
346
554
  __publicField(this, "transientText", "");
347
555
  __publicField(this, "waitingForOutput", false);
556
+ __publicField(this, "turnInFlight", false);
557
+ __publicField(this, "lastActivityAt");
348
558
  __publicField(this, "snapshot", EMPTY_LOG);
349
559
  __publicField(this, "listeners", /* @__PURE__ */ new Set());
350
560
  __publicField(this, "subscribe", (listener) => {
@@ -356,12 +566,41 @@ class EventLogBridge {
356
566
  __publicField(this, "getSnapshot", () => this.snapshot);
357
567
  }
358
568
  commit() {
359
- this.snapshot = { lines: this.lines, progressText: this.progressText, transientText: this.transientText, waitingForOutput: this.waitingForOutput };
569
+ this.snapshot = {
570
+ lines: this.lines,
571
+ progressText: this.progressText,
572
+ transientText: this.transientText,
573
+ waitingForOutput: this.waitingForOutput,
574
+ turnInFlight: this.turnInFlight,
575
+ lastActivityAt: this.lastActivityAt
576
+ };
360
577
  for (const listener of this.listeners) listener();
361
578
  }
362
579
  /** Marks the start of a turn's wait for its first output (see `TuiLogState.waitingForOutput`'s doc comment) — called by `TuiApp` right after a chat line or resolved prompt answer is submitted. */
363
580
  beginTurn() {
364
581
  this.waitingForOutput = true;
582
+ this.turnInFlight = true;
583
+ this.lastActivityAt = Date.now();
584
+ this.commit();
585
+ }
586
+ /** A resolved approval/clarification answer: output is expected again, but it says nothing about whether a turn is in flight — `TurnActivityTracker` owns that, so a prompt answer can never leave the indicator stuck on. */
587
+ expectOutput() {
588
+ this.waitingForOutput = true;
589
+ this.commit();
590
+ }
591
+ /** Marks a turn in flight (idempotent); the elapsed counter counts from the first call. */
592
+ markBusy() {
593
+ if (this.turnInFlight) return;
594
+ this.turnInFlight = true;
595
+ this.lastActivityAt = Date.now();
596
+ this.commit();
597
+ }
598
+ /** Marks the turn finished (its `dispatchLine()` settled) — the liveness line stops. Safe to call when no turn is in flight. */
599
+ endTurn() {
600
+ if (!this.turnInFlight && !this.waitingForOutput) return;
601
+ this.turnInFlight = false;
602
+ this.waitingForOutput = false;
603
+ this.lastActivityAt = void 0;
365
604
  this.commit();
366
605
  }
367
606
  /**
@@ -387,18 +626,19 @@ class EventLogBridge {
387
626
  }
388
627
  handleEvent(event) {
389
628
  this.waitingForOutput = false;
629
+ if (this.turnInFlight) this.lastActivityAt = Date.now();
390
630
  if (event.type === "progress") {
391
- this.progressText = event.text;
631
+ this.progressText = stripTerminalControls(event.text);
392
632
  this.commit();
393
633
  return;
394
634
  }
395
635
  if (event.type === "token") {
396
- this.transientText += event.text;
636
+ this.transientText += stripTerminalControls(event.text);
397
637
  this.commit();
398
638
  return;
399
639
  }
400
640
  const streamedReplyWasOpen = this.transientText.length > 0;
401
- const merged = this.transientText + event.lines.join("\n");
641
+ const merged = this.transientText + stripTerminalControls(event.lines.join("\n"));
402
642
  const kind = classifyLineKind(merged, streamedReplyWasOpen, event.stream);
403
643
  const displayText = kind === "assistant" ? stripAssistantLabel(merged) : kind === "plan" ? stripPlanLabel(merged) : merged;
404
644
  this.lines = kind === "plan" ? [...this.lines, { text: displayText, kind }] : (
@@ -413,6 +653,31 @@ class EventLogBridge {
413
653
  if (kind === "assistant") this.pushTurnMargin();
414
654
  }
415
655
  }
656
+ class TurnActivityTracker {
657
+ constructor(eventLog) {
658
+ __publicField(this, "dispatches", 0);
659
+ __publicField(this, "cliBusy", false);
660
+ this.eventLog = eventLog;
661
+ }
662
+ /** Wraps one `dispatchLine()` call. Always settles, whether it resolves or throws. */
663
+ async track(run) {
664
+ this.dispatches++;
665
+ try {
666
+ return await run();
667
+ } finally {
668
+ this.dispatches--;
669
+ this.settle();
670
+ }
671
+ }
672
+ setCliBusy(busy) {
673
+ this.cliBusy = busy;
674
+ if (busy) this.eventLog.markBusy();
675
+ else this.settle();
676
+ }
677
+ settle() {
678
+ if (this.dispatches === 0 && !this.cliBusy) this.eventLog.endTurn();
679
+ }
680
+ }
416
681
  class PromptBridge {
417
682
  constructor() {
418
683
  __publicField(this, "pending");
@@ -433,7 +698,10 @@ class PromptBridge {
433
698
  }
434
699
  ask(question, options) {
435
700
  return new Promise((resolve) => {
436
- this.pending = { question, options };
701
+ this.pending = {
702
+ question: stripTerminalControls(question),
703
+ options: options == null ? void 0 : options.map((o) => ({ ...o, label: stripTerminalControls(o.label) }))
704
+ };
437
705
  this.resolve = resolve;
438
706
  for (const listener of this.listeners) listener();
439
707
  });
@@ -463,6 +731,108 @@ class StatusBridge {
463
731
  for (const listener of this.listeners) listener();
464
732
  }
465
733
  }
734
+ const ALT_SCREEN_ON = "\x1B[?1049h";
735
+ const ALT_SCREEN_OFF = "\x1B[?1049l";
736
+ const ALT_SCREEN_SETTLE_MS = 80;
737
+ const SELECT_ARM_DELAY_MS = 400;
738
+ const PLAN_GRAPH_MIN_RENDER_MS = 100;
739
+ class PlanGraphBridge {
740
+ constructor(writeRaw, settleMs = ALT_SCREEN_SETTLE_MS, minRenderMs = PLAN_GRAPH_MIN_RENDER_MS) {
741
+ __publicField(this, "view", { mode: "chat", nodes: [] });
742
+ __publicField(this, "listeners", /* @__PURE__ */ new Set());
743
+ __publicField(this, "timer");
744
+ __publicField(this, "pendingNodes");
745
+ __publicField(this, "transition", Promise.resolve());
746
+ __publicField(this, "subscribe", (listener) => {
747
+ this.listeners.add(listener);
748
+ return () => {
749
+ this.listeners.delete(listener);
750
+ };
751
+ });
752
+ __publicField(this, "getSnapshot", () => this.view);
753
+ this.writeRaw = writeRaw;
754
+ this.settleMs = settleMs;
755
+ this.minRenderMs = minRenderMs;
756
+ }
757
+ isOpen() {
758
+ return this.view.mode !== "chat";
759
+ }
760
+ set(view) {
761
+ this.view = view;
762
+ for (const listener of this.listeners) listener();
763
+ }
764
+ sleep() {
765
+ return new Promise((resolve) => setTimeout(resolve, this.settleMs));
766
+ }
767
+ open(nodes) {
768
+ this.transition = this.transition.then(async () => {
769
+ if (this.view.mode !== "chat") {
770
+ this.update(nodes);
771
+ return;
772
+ }
773
+ this.set({ mode: "blank", nodes });
774
+ await this.sleep();
775
+ this.writeRaw(ALT_SCREEN_ON);
776
+ this.set({ mode: "pane", nodes: this.pendingNodes ?? nodes });
777
+ this.pendingNodes = void 0;
778
+ });
779
+ return this.transition;
780
+ }
781
+ close() {
782
+ this.transition = this.transition.then(async () => {
783
+ if (this.view.mode === "chat") return;
784
+ this.clearTimer();
785
+ this.set({ mode: "blank", nodes: this.view.nodes });
786
+ await this.sleep();
787
+ this.writeRaw(ALT_SCREEN_OFF);
788
+ this.set({ mode: "chat", nodes: [] });
789
+ });
790
+ return this.transition;
791
+ }
792
+ /** Synchronous leave for process exit: restores the main screen without waiting. */
793
+ restore() {
794
+ this.clearTimer();
795
+ if (this.view.mode === "pane") this.writeRaw(ALT_SCREEN_OFF);
796
+ this.view = { mode: "chat", nodes: [] };
797
+ }
798
+ /** Throttled node update: the first goes through at once, later ones coalesce to the newest within `minRenderMs`. Identical content is ignored. */
799
+ update(nodes) {
800
+ if (this.view.mode === "chat") return;
801
+ if (this.view.mode === "blank") {
802
+ this.pendingNodes = nodes;
803
+ return;
804
+ }
805
+ if (this.timer) {
806
+ this.pendingNodes = nodes;
807
+ return;
808
+ }
809
+ this.apply(nodes);
810
+ this.timer = setTimeout(() => this.flush(), this.minRenderMs);
811
+ }
812
+ apply(nodes) {
813
+ if (this.view.mode !== "pane" || JSON.stringify(nodes) === JSON.stringify(this.view.nodes)) return;
814
+ this.set({ mode: "pane", nodes });
815
+ }
816
+ flush() {
817
+ this.timer = void 0;
818
+ const next = this.pendingNodes;
819
+ this.pendingNodes = void 0;
820
+ if (next) {
821
+ this.apply(next);
822
+ this.timer = setTimeout(() => this.flush(), this.minRenderMs);
823
+ }
824
+ }
825
+ clearTimer() {
826
+ if (this.timer) clearTimeout(this.timer);
827
+ this.timer = void 0;
828
+ this.pendingNodes = void 0;
829
+ }
830
+ }
831
+ const NO_PLAN_GRAPH = { subscribe: () => () => {
832
+ }, getSnapshot: /* @__PURE__ */ (() => {
833
+ const v = { mode: "chat", nodes: [] };
834
+ return () => v;
835
+ })() };
466
836
  function UserMessageBox({ text, width, borderColor = "gray" }) {
467
837
  return /* @__PURE__ */ jsx(Box, { borderStyle: "round", borderColor, flexDirection: "column", paddingX: 1, width, children: text.split("\n").map((line, i) => /* @__PURE__ */ jsx(Text, { children: line.length > 0 ? line : " " }, i)) });
468
838
  }
@@ -491,15 +861,23 @@ function LogLineText({ line, width }) {
491
861
  }
492
862
  const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
493
863
  const SPINNER_INTERVAL_MS = 80;
494
- function Spinner() {
864
+ const LIVENESS_COUNTER_AFTER_S = 3;
865
+ function formatLivenessLabel(elapsedMs, hasProgress) {
866
+ const seconds = Math.max(0, Math.floor(elapsedMs / 1e3));
867
+ if (seconds < LIVENESS_COUNTER_AFTER_S) return hasProgress ? "Working…" : "Thinking…";
868
+ return `Working… ${seconds}s`;
869
+ }
870
+ function Spinner({ since, hasProgress, quietOnly = false }) {
495
871
  const [frame, setFrame] = useState(0);
496
872
  useEffect(() => {
497
873
  const id = setInterval(() => setFrame((f) => (f + 1) % SPINNER_FRAMES.length), SPINNER_INTERVAL_MS);
498
874
  return () => clearInterval(id);
499
875
  }, []);
876
+ if (quietOnly && since !== void 0 && Date.now() - since < LIVENESS_COUNTER_AFTER_S * 1e3) return null;
500
877
  return /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
501
878
  SPINNER_FRAMES[frame],
502
- " Thinking…"
879
+ " ",
880
+ formatLivenessLabel(since === void 0 ? 0 : Date.now() - since, hasProgress)
503
881
  ] });
504
882
  }
505
883
  function StatusLine({ indicators }) {
@@ -509,12 +887,15 @@ function StatusLine({ indicators }) {
509
887
  ] }, indicator)) });
510
888
  }
511
889
  function TuiApp(props) {
512
- const { eventLog, prompt, status, onSubmitChat, onExit, columns } = props;
890
+ const { eventLog, prompt, status, planGraph, onSubmitChat, onExit, columns } = props;
513
891
  const log = useStore(eventLog);
514
892
  const pending = useStore(prompt);
515
893
  const statusIndicators = useStore(status);
516
894
  const windowSize = useWindowSize();
517
895
  const width = columns ?? windowSize.columns;
896
+ const graphView = useStore(planGraph ?? NO_PLAN_GRAPH);
897
+ const frozenLines = useRef(0);
898
+ if (graphView.mode === "chat") frozenLines.current = log.lines.length;
518
899
  useInput((input, key) => {
519
900
  if (key.ctrl && input === "c") onExit();
520
901
  });
@@ -533,22 +914,36 @@ function TuiApp(props) {
533
914
  var _a, _b;
534
915
  const displayAnswer = ((_b = (_a = pending == null ? void 0 : pending.options) == null ? void 0 : _a.find((option) => option.key === line)) == null ? void 0 : _b.label) ?? line;
535
916
  eventLog.pushEchoLine("> ", (pending == null ? void 0 : pending.question) !== void 0 ? `${pending.question.trimEnd()} → ${displayAnswer}` : line);
536
- eventLog.beginTurn();
917
+ eventLog.expectOutput();
537
918
  prompt.submit(line);
538
919
  },
539
920
  [eventLog, prompt, pending]
540
921
  );
922
+ const closePane = useCallback(() => {
923
+ void (planGraph == null ? void 0 : planGraph.close());
924
+ }, [planGraph]);
541
925
  const hasTransient = log.progressText.length > 0 || log.transientText.length > 0;
542
- const showSpinner = log.waitingForOutput && !hasTransient;
926
+ const showSpinner = (log.turnInFlight || log.waitingForOutput) && !pending;
927
+ const staticItems = graphView.mode === "chat" ? log.lines : log.lines.slice(0, frozenLines.current);
928
+ const paneRows = Math.max(3, (windowSize.rows ?? 24) - 1);
929
+ if (graphView.mode !== "chat") {
930
+ const promptRows = pending ? Math.min(10, Math.floor(paneRows / 2)) : 0;
931
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
932
+ /* @__PURE__ */ jsx(Static, { items: staticItems, children: (line, index) => /* @__PURE__ */ jsx(LogLineText, { line, width }, index) }),
933
+ graphView.mode === "pane" && /* @__PURE__ */ jsx(PlanGraphPane, { nodes: graphView.nodes, columns: width, rows: paneRows - promptRows, active: !pending, color: !process.env.NO_COLOR, onClose: closePane }),
934
+ graphView.mode === "pane" && (pending == null ? void 0 : pending.options) && /* @__PURE__ */ jsx(SelectPrompt, { question: pending.question, options: pending.options, onSubmit: handleSubmitPrompt, armDelayMs: SELECT_ARM_DELAY_MS }),
935
+ graphView.mode === "pane" && pending && !pending.options && /* @__PURE__ */ jsx(TuiInput, { promptLabel: pending.question, onSubmitChat: handleSubmitChat, onSubmitPrompt: handleSubmitPrompt, columns })
936
+ ] });
937
+ }
543
938
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
544
- /* @__PURE__ */ jsx(Static, { items: log.lines, children: (line, index) => /* @__PURE__ */ jsx(LogLineText, { line, width }, index) }),
545
- showSpinner && /* @__PURE__ */ jsx(Spinner, {}),
939
+ /* @__PURE__ */ jsx(Static, { items: staticItems, children: (line, index) => /* @__PURE__ */ jsx(LogLineText, { line, width }, index) }),
546
940
  hasTransient && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
547
941
  log.progressText.length > 0 && /* @__PURE__ */ jsx(Text, { dimColor: true, children: log.progressText }),
548
942
  log.transientText.length > 0 && /* @__PURE__ */ jsx(Text, { children: stripAssistantLabel(log.transientText) })
549
943
  ] }),
944
+ showSpinner && /* @__PURE__ */ jsx(Spinner, { since: log.lastActivityAt, hasProgress: log.progressText.length > 0, quietOnly: log.transientText.length > 0 }),
550
945
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "─".repeat(Math.max(1, width)) }),
551
- (pending == null ? void 0 : pending.options) ? /* @__PURE__ */ jsx(SelectPrompt, { question: pending.question, options: pending.options, onSubmit: handleSubmitPrompt }) : /* @__PURE__ */ jsx(
946
+ (pending == null ? void 0 : pending.options) ? /* @__PURE__ */ jsx(SelectPrompt, { question: pending.question, options: pending.options, onSubmit: handleSubmitPrompt, armDelayMs: SELECT_ARM_DELAY_MS }) : /* @__PURE__ */ jsx(
552
947
  TuiInput,
553
948
  {
554
949
  promptLabel: pending == null ? void 0 : pending.question,
@@ -582,12 +977,20 @@ async function runTuiApp(options = {}) {
582
977
  const prompt = new PromptBridge();
583
978
  const status = new StatusBridge();
584
979
  const originalWrite = process.stdout.write.bind(process.stdout);
980
+ const activity = new TurnActivityTracker(eventLog);
585
981
  const restoreCapture = startCapture((event) => eventLog.handleEvent(event));
982
+ const planGraph = new PlanGraphBridge((chunk) => void originalWrite(chunk));
983
+ let instanceRef;
586
984
  let instance;
587
985
  try {
588
986
  const inert = createInertStreams();
589
987
  instance = await runCli({
590
988
  ...options,
989
+ onTurnBusyChange: (busy) => activity.setCliBusy(busy),
990
+ openPlanGraph: (nodes) => void planGraph.open(nodes),
991
+ onPlanProgress: (tasks) => {
992
+ if (planGraph.isOpen()) void (instanceRef == null ? void 0 : instanceRef.getPlanGraphNodes(tasks).then((nodes) => nodes && planGraph.update(nodes)));
993
+ },
591
994
  input: options.input ?? inert.input,
592
995
  output: options.output ?? inert.output,
593
996
  askYesNo: prompt.askYesNo,
@@ -598,23 +1001,32 @@ async function runTuiApp(options = {}) {
598
1001
  restoreCapture();
599
1002
  throw err;
600
1003
  }
1004
+ instanceRef = instance;
601
1005
  const refreshStatus = async () => {
602
1006
  status.set(await instance.getStatusIndicators());
1007
+ if (planGraph.isOpen()) {
1008
+ const nodes = await instance.getPlanGraphNodes();
1009
+ if (nodes) planGraph.update(nodes);
1010
+ }
603
1011
  };
604
1012
  await refreshStatus();
605
1013
  const handleSubmitChat = (line) => {
606
- void instance.dispatchLine(line).then(refreshStatus);
1014
+ void activity.track(async () => {
1015
+ await instance.dispatchLine(line);
1016
+ await refreshStatus();
1017
+ });
607
1018
  };
608
1019
  let exiting = false;
609
1020
  const exit = () => {
610
1021
  if (exiting) return;
611
1022
  exiting = true;
1023
+ planGraph.restore();
612
1024
  restoreCapture();
613
1025
  instance.close();
614
1026
  app.unmount();
615
1027
  process.exit(0);
616
1028
  };
617
- const app = render(/* @__PURE__ */ jsx(TuiApp, { eventLog, prompt, status, onSubmitChat: handleSubmitChat, onExit: exit }), {
1029
+ const app = render(/* @__PURE__ */ jsx(TuiApp, { eventLog, prompt, status, planGraph, onSubmitChat: handleSubmitChat, onExit: exit }), {
618
1030
  // Phase 1's startCapture already routes every console.log/process.stdout.write call into
619
1031
  // eventLog — ink's own patchConsole would double-intercept the same calls with a competing
620
1032
  // mechanism (writing them above its own Static area independently), not compose with it.
@@ -629,9 +1041,12 @@ async function runTuiApp(options = {}) {
629
1041
  }
630
1042
  export {
631
1043
  EventLogBridge,
1044
+ PlanGraphBridge,
632
1045
  PromptBridge,
633
1046
  StatusBridge,
634
1047
  TuiApp,
1048
+ TurnActivityTracker,
1049
+ formatLivenessLabel,
635
1050
  runTuiApp
636
1051
  };
637
- //# sourceMappingURL=tui-app-DeyZ4Pr9.js.map
1052
+ //# sourceMappingURL=tui-app-CJq6YaiP.js.map