@namzu/cli 18.0.0 → 18.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/cli.d.ts.map +1 -1
  2. package/dist/cli.js +1 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/acp.d.ts.map +1 -1
  5. package/dist/commands/acp.js +1 -0
  6. package/dist/commands/acp.js.map +1 -1
  7. package/dist/commands/run-stream.d.ts.map +1 -1
  8. package/dist/commands/run-stream.js +1 -0
  9. package/dist/commands/run-stream.js.map +1 -1
  10. package/dist/commands/run.d.ts.map +1 -1
  11. package/dist/commands/run.js +1 -0
  12. package/dist/commands/run.js.map +1 -1
  13. package/dist/config/load.d.ts.map +1 -1
  14. package/dist/config/load.js +15 -0
  15. package/dist/config/load.js.map +1 -1
  16. package/dist/config/schema.d.ts +17 -0
  17. package/dist/config/schema.d.ts.map +1 -1
  18. package/dist/config/schema.js.map +1 -1
  19. package/dist/context/doctrine.d.ts +44 -0
  20. package/dist/context/doctrine.d.ts.map +1 -0
  21. package/dist/context/doctrine.js +81 -0
  22. package/dist/context/doctrine.js.map +1 -0
  23. package/dist/context/turn-snapshot.d.ts +59 -0
  24. package/dist/context/turn-snapshot.d.ts.map +1 -0
  25. package/dist/context/turn-snapshot.js +122 -0
  26. package/dist/context/turn-snapshot.js.map +1 -0
  27. package/dist/integrations/subagents/runtime.d.ts +13 -0
  28. package/dist/integrations/subagents/runtime.d.ts.map +1 -1
  29. package/dist/integrations/subagents/runtime.js +71 -11
  30. package/dist/integrations/subagents/runtime.js.map +1 -1
  31. package/dist/permissions/mode.d.ts +25 -0
  32. package/dist/permissions/mode.d.ts.map +1 -1
  33. package/dist/permissions/mode.js +11 -1
  34. package/dist/permissions/mode.js.map +1 -1
  35. package/dist/tui/App.d.ts.map +1 -1
  36. package/dist/tui/App.js +243 -7
  37. package/dist/tui/App.js.map +1 -1
  38. package/dist/tui/Composer.d.ts +6 -1
  39. package/dist/tui/Composer.d.ts.map +1 -1
  40. package/dist/tui/Composer.js +18 -2
  41. package/dist/tui/Composer.js.map +1 -1
  42. package/dist/tui/PermissionOverlay.d.ts.map +1 -1
  43. package/dist/tui/PermissionOverlay.js +16 -1
  44. package/dist/tui/PermissionOverlay.js.map +1 -1
  45. package/dist/tui/TaskList.d.ts +30 -0
  46. package/dist/tui/TaskList.d.ts.map +1 -0
  47. package/dist/tui/TaskList.js +58 -0
  48. package/dist/tui/TaskList.js.map +1 -0
  49. package/dist/tui/agent.d.ts +45 -4
  50. package/dist/tui/agent.d.ts.map +1 -1
  51. package/dist/tui/agent.js +165 -18
  52. package/dist/tui/agent.js.map +1 -1
  53. package/dist/tui/permission-review.d.ts.map +1 -1
  54. package/dist/tui/permission-review.js +151 -1
  55. package/dist/tui/permission-review.js.map +1 -1
  56. package/dist/tui/stream-blocks.d.ts +24 -0
  57. package/dist/tui/stream-blocks.d.ts.map +1 -1
  58. package/dist/tui/stream-blocks.js +74 -0
  59. package/dist/tui/stream-blocks.js.map +1 -1
  60. package/dist/tui/types.d.ts +3 -1
  61. package/dist/tui/types.d.ts.map +1 -1
  62. package/package.json +4 -4
package/dist/tui/App.js CHANGED
@@ -34,6 +34,7 @@ import { Composer, suggestionWindowSize, } from './Composer.js';
34
34
  import { CopyPicker } from './CopyPicker.js';
35
35
  import { EditPromptPicker } from './EditPromptPicker.js';
36
36
  import { LiveActivity, formatElapsed } from './LiveActivity.js';
37
+ import { TaskList } from './TaskList.js';
37
38
  import { PermissionOverlay } from './PermissionOverlay.js';
38
39
  import { Picker } from './Picker.js';
39
40
  import { ResumePicker } from './ResumePicker.js';
@@ -57,12 +58,36 @@ import { PERMISSION_REVIEW_PAGE_ROWS, buildPermissionReview, buildPermissionSumm
57
58
  import { describeRunInterruption } from './run-interruption.js';
58
59
  import { moveSelection } from './selection-window.js';
59
60
  import { baseBranchReviewPrompt, commitReviewPrompt, hostCommandNames, kernelCommandDescriptors, mergeHostCommands, renderOutcome, reviewPrompt, runSlash, } from './slashCommands.js';
60
- import { splitCompleteBlocks } from './stream-blocks.js';
61
+ import { splitCompleteBlocks, splitSafeCut } from './stream-blocks.js';
61
62
  import { terminalSupportsHyperlinks } from './terminal-hyperlinks.js';
62
63
  import { theme } from './theme.js';
63
64
  import { useSelectionIndex } from './use-selection-index.js';
64
65
  import { renderWorkspaceDiff, workspaceDiff } from './workspace-diff.js';
65
66
  import { listReviewBranches, listReviewCommits, reviewMergeBase, } from './workspace-review.js';
67
+ /** The value a question row carries: an option id, or the free-text escape. */
68
+ const FREE_TEXT_ANSWER = '__free_text__';
69
+ /**
70
+ * The streaming assistant bubble, carried across events within one turn.
71
+ *
72
+ * `text` is everything the model has said and is what gets persisted;
73
+ * `pending` is the part not yet on screen, held until it forms a whole block.
74
+ * The two are separate because they answer different questions — what was
75
+ * said, and what has been shown — and conflating them is how a reply gets
76
+ * saved with a paragraph the operator never saw, or shown twice.
77
+ */
78
+ /**
79
+ * How long held reply text may wait before a sentence of it is shown.
80
+ *
81
+ * Blocks are released as they complete (`splitCompleteBlocks`), which keeps a
82
+ * fast reply landing a paragraph at a time. A slow one — a long paragraph
83
+ * arriving over twenty seconds — completes no block until its last character
84
+ * and was invisible for its whole length. Past this interval the pending
85
+ * text is released to its last safe cut instead (`splitSafeCut`): a sentence
86
+ * or a line, never a half-word, never inside a fence. Long enough that a
87
+ * fast stream never reaches it; short enough that a slow one reads as
88
+ * writing rather than as silence.
89
+ */
90
+ const STREAM_RELEASE_MS = 250;
66
91
  /** Last non-empty assistant text in a durable conversation, newest first. */
67
92
  function latestAssistantOutput(messages) {
68
93
  for (let i = messages.length - 1; i >= 0; i -= 1) {
@@ -292,6 +317,22 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
292
317
  */
293
318
  const [pickerNotice, setPickerNotice] = useState(null);
294
319
  const [permission, setPermission] = useState(null);
320
+ /**
321
+ * The question currently on screen and the promise the tool is waiting
322
+ * on. Resolved exactly once: by a chosen row, by free text, by Esc (skip)
323
+ * or by Ctrl+C (abort). A second resolution is a no-op, not a second
324
+ * answer.
325
+ */
326
+ const questionRef = useRef(null);
327
+ const resolveQuestion = useCallback((answer) => {
328
+ const pending = questionRef.current;
329
+ if (!pending?.resolve)
330
+ return;
331
+ const resolve = pending.resolve;
332
+ pending.resolve = null;
333
+ questionRef.current = null;
334
+ resolve(answer);
335
+ }, []);
295
336
  const [permissionReviewOffset, setPermissionReviewOffsetState] = useState(0);
296
337
  const permissionReviewOffsetRef = useRef(0);
297
338
  const [permissionDetailsOpen, setPermissionDetailsOpenState] = useState(false);
@@ -329,6 +370,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
329
370
  // Tools currently executing — rendered live (spinner + elapsed) below the
330
371
  // transcript, then committed as static lines on completion.
331
372
  const [activeTools, setActiveTools] = useState([]);
373
+ /**
374
+ * The model's plan for the current request, in creation order, updated in
375
+ * place on every status change. Cleared when the next request begins —
376
+ * a plan is scoped to the request it was made for, and one that outlived
377
+ * it would read as work still pending on a turn that already ended.
378
+ */
379
+ const [tasks, setTasks] = useState([]);
380
+ /** Ids already seen this request, so an opening row is written once. */
381
+ const knownTaskIdsRef = useRef(new Set());
332
382
  // Bumped to reset the <Static> transcript log (on /clear, /clear-screen and /resume).
333
383
  const [resetKey, setResetKey] = useState(0);
334
384
  /**
@@ -1027,6 +1077,20 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1027
1077
  }
1028
1078
  return { sessions, sessionId };
1029
1079
  }, [hasUnsettledTurn, pushMessage, state]);
1080
+ /**
1081
+ * Shift+Tab: `prompt` → `accept-edits` → `plan` → `prompt`. Three stops
1082
+ * only — `auto` and `strict` are deliberate choices made by name in
1083
+ * `/permissions`, not stops on a key an operator presses on reflex. From
1084
+ * either of those the key returns to `prompt`, which is the direction a
1085
+ * reflex should fall. The change goes through the same gate `/permissions`
1086
+ * uses, so it is refused while a turn is active, and the refusal is
1087
+ * explained on screen.
1088
+ */
1089
+ const cyclePermissionMode = useCallback(() => {
1090
+ const current = permissionModeRef.current;
1091
+ const next = current === 'prompt' ? 'accept-edits' : current === 'accept-edits' ? 'plan' : 'prompt';
1092
+ applyPermissionMode(next);
1093
+ }, [applyPermissionMode]);
1030
1094
  const runConversationExport = useCallback((destination) => {
1031
1095
  const source = stableExportSource();
1032
1096
  if (!source)
@@ -1209,6 +1273,39 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1209
1273
  })();
1210
1274
  return;
1211
1275
  }
1276
+ if (picker.kind === 'user-question') {
1277
+ const pending = questionRef.current;
1278
+ setChoicePicker(null);
1279
+ if (!pending)
1280
+ return;
1281
+ if (value === FREE_TEXT_ANSWER) {
1282
+ // The scope on a text prompt is what its other kinds check
1283
+ // against a conversation switch; a question belongs to the turn
1284
+ // that asked it, and the turn holds the promise.
1285
+ const scope = scopeRef.current;
1286
+ if (!scope) {
1287
+ resolveQuestion({ kind: 'skip' });
1288
+ return;
1289
+ }
1290
+ textPromptTokenRef.current += 1;
1291
+ setTextPrompt({
1292
+ token: textPromptTokenRef.current,
1293
+ kind: 'user-question',
1294
+ title: pending.question.question,
1295
+ placeholder: 'Type your answer and press Enter · Esc skips',
1296
+ emptyNotice: 'An empty answer is a skip. Press Esc to skip.',
1297
+ initialValue: '',
1298
+ sessionId: scope.sessionId,
1299
+ });
1300
+ return;
1301
+ }
1302
+ // The tool's own result row carries the answer the model saw; this
1303
+ // row is the operator's, so a reader of the transcript sees the
1304
+ // question and the choice together where it happened.
1305
+ pushMessage('system', `Answered "${pending.question.question}": ${labelOfOption(pending.question, value)}`, false, '?');
1306
+ resolveQuestion({ kind: 'answer', selectedOptionIds: [value] });
1307
+ return;
1308
+ }
1212
1309
  if (picker.kind === 'review-commit') {
1213
1310
  advanceQueueContinuation();
1214
1311
  enqueueQueued({
@@ -1348,12 +1445,35 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1348
1445
  * if every exit releases it; this is the function every exit calls.
1349
1446
  */
1350
1447
  const flushStream = useCallback((st) => {
1448
+ if (st.releaseTimer) {
1449
+ clearTimeout(st.releaseTimer);
1450
+ st.releaseTimer = undefined;
1451
+ }
1351
1452
  if (!st.pending || st.pending.length === 0)
1352
1453
  return;
1353
1454
  const id = st.assistantId ?? pushMessage('assistant', '', true);
1354
1455
  st.assistantId = id;
1355
1456
  appendToMessage(id, st.pending);
1356
1457
  st.pending = '';
1458
+ st.releasedAt = Date.now();
1459
+ }, [appendToMessage, pushMessage]);
1460
+ /**
1461
+ * Release held text to its last safe cut — a sentence or a line — when it
1462
+ * has waited longer than `STREAM_RELEASE_MS`. Called from the delta path
1463
+ * with the clock, and from the timer the delta path arms so a stream that
1464
+ * goes quiet mid-paragraph still shows what it has.
1465
+ */
1466
+ const releaseStalled = useCallback((st) => {
1467
+ if (!st.pending || st.pending.length === 0)
1468
+ return;
1469
+ const { ready, rest } = splitSafeCut(st.pending);
1470
+ if (ready.length === 0)
1471
+ return;
1472
+ const id = st.assistantId ?? pushMessage('assistant', '', true);
1473
+ st.assistantId = id;
1474
+ st.pending = rest;
1475
+ appendToMessage(id, ready);
1476
+ st.releasedAt = Date.now();
1357
1477
  }, [appendToMessage, pushMessage]);
1358
1478
  const finalizeMessage = useCallback((id, finalContent) => {
1359
1479
  setMessages((prev) => prev.map((m) => m.id === id ? { ...m, content: finalContent ?? m.content, pending: false } : m));
@@ -1452,7 +1572,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1452
1572
  ...(sessionsRef.current ? { sessionGoals: sessionsRef.current.goals } : {}),
1453
1573
  ...(activeCtx.mcpServers ? { mcpServers: activeCtx.mcpServers } : {}),
1454
1574
  ...(activeCtx.plugins ? { plugins: activeCtx.plugins } : {}),
1575
+ ...(activeCtx.web ? { web: activeCtx.web } : {}),
1455
1576
  ...(activeCtx.sandbox ? { sandbox: activeCtx.sandbox } : {}),
1577
+ // Somebody is at this terminal, so the model may ask them one
1578
+ // question when a decision is genuinely theirs.
1579
+ askUser: true,
1456
1580
  });
1457
1581
  if (signal?.aborted) {
1458
1582
  void s.close();
@@ -2243,6 +2367,19 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2243
2367
  if (!prompt)
2244
2368
  return;
2245
2369
  setTextPrompt(null);
2370
+ if (prompt.kind === 'user-question') {
2371
+ const text = value.trim();
2372
+ const pending = questionRef.current;
2373
+ if (text.length === 0) {
2374
+ resolveQuestion({ kind: 'skip' });
2375
+ return;
2376
+ }
2377
+ if (pending) {
2378
+ pushMessage('system', `Answered "${pending.question.question}": ${text}`, false, '?');
2379
+ }
2380
+ resolveQuestion({ kind: 'answer', selectedOptionIds: [], freeText: text });
2381
+ return;
2382
+ }
2246
2383
  const scope = scopeRef.current;
2247
2384
  if (!scope || scope.sessionId !== prompt.sessionId) {
2248
2385
  pushMessage('system', prompt.kind === 'conversation-title'
@@ -2275,8 +2412,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2275
2412
  queueMicrotask(() => {
2276
2413
  if (textPromptRef.current === prompt)
2277
2414
  setTextPrompt(null);
2415
+ // A cancelled free-text answer is a skipped question, not a hung tool.
2416
+ if (prompt?.kind === 'user-question')
2417
+ resolveQuestion({ kind: 'skip' });
2278
2418
  });
2279
- }, [setTextPrompt]);
2419
+ }, [resolveQuestion, setTextPrompt]);
2280
2420
  /**
2281
2421
  * `/fork`: continue in a copy, leaving this conversation where it is.
2282
2422
  *
@@ -2461,6 +2601,39 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2461
2601
  sendTerminalNotification({ kind: 'approval-required' });
2462
2602
  });
2463
2603
  }, [sendTerminalNotification, setPermissionDetailsOpen, setPermissionReviewOffset]);
2604
+ /**
2605
+ * `ask_user_question`, on screen. The question becomes a finite chooser —
2606
+ * the model's options, then "Something else…" when it allowed free text —
2607
+ * and the tool's promise waits on the row the operator picks. One question
2608
+ * at a time: the tool is not concurrency-safe, so a second cannot arrive
2609
+ * while the first is up.
2610
+ */
2611
+ const askQuestion = useCallback((question) => new Promise((resolve) => {
2612
+ questionRef.current = { question, resolve };
2613
+ const values = [
2614
+ ...question.options.map((option) => option.id),
2615
+ ...(question.allowFreeText ? [FREE_TEXT_ANSWER] : []),
2616
+ ];
2617
+ setSelectedChoice(0);
2618
+ setChoicePicker({
2619
+ kind: 'user-question',
2620
+ title: question.question,
2621
+ notice: question.header
2622
+ ? `${question.header} · Enter answers · Esc skips`
2623
+ : 'Enter answers · Esc skips',
2624
+ values,
2625
+ options: [
2626
+ ...question.options.map((option) => ({
2627
+ label: option.label,
2628
+ description: option.description ?? '',
2629
+ })),
2630
+ ...(question.allowFreeText
2631
+ ? [{ label: 'Something else…', description: 'Answer in your own words' }]
2632
+ : []),
2633
+ ],
2634
+ });
2635
+ sendTerminalNotification({ kind: 'approval-required' });
2636
+ }), [sendTerminalNotification, setChoicePicker, setSelectedChoice]);
2464
2637
  // Render one agent event onto the transcript. Shared by the local turn loop
2465
2638
  // and the daemon-attach poller, so both paths produce identical output.
2466
2639
  // `st` carries the streaming-assistant bubble id + accumulated text across
@@ -2491,6 +2664,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2491
2664
  sessionId: st.sessionId,
2492
2665
  };
2493
2666
  }
2667
+ // The release clock starts with the first character, not with the
2668
+ // turn: the wait before the first token is the model's, and
2669
+ // counting it would release the opening sentence on arrival.
2670
+ st.releasedAt ??= Date.now();
2494
2671
  st.text += event.text;
2495
2672
  // Held, not appended. Appending each delta is what produced text
2496
2673
  // that types itself out — nothing animates it, but a few
@@ -2501,7 +2678,21 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2501
2678
  if (ready.length > 0) {
2502
2679
  st.pending = rest;
2503
2680
  appendToMessage(ensureAssistant(), ready);
2681
+ st.releasedAt = Date.now();
2504
2682
  }
2683
+ // A block is the preferred unit; a sentence is the fallback for
2684
+ // a paragraph that is taking longer than an operator will wait
2685
+ // for it. The timer covers the stream going quiet between two
2686
+ // deltas, which the clock check alone cannot see.
2687
+ if (st.releaseTimer)
2688
+ clearTimeout(st.releaseTimer);
2689
+ if (Date.now() - (st.releasedAt ?? 0) >= STREAM_RELEASE_MS) {
2690
+ releaseStalled(st);
2691
+ }
2692
+ st.releaseTimer = setTimeout(() => {
2693
+ st.releaseTimer = undefined;
2694
+ releaseStalled(st);
2695
+ }, STREAM_RELEASE_MS);
2505
2696
  break;
2506
2697
  }
2507
2698
  case 'tool-start': {
@@ -2582,9 +2773,34 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2582
2773
  case 'usage':
2583
2774
  setUsage({ totalTokens: event.totalTokens, cost: event.cost });
2584
2775
  break;
2585
- case 'task':
2586
- pushMessage('tool', event.subject, false, event.status === 'completed' ? '☑' : '☐');
2776
+ case 'task': {
2777
+ // The live list gets every change; the transcript records the
2778
+ // opening and the close, as it did before the list existed.
2779
+ // Decided from a ref, not inside the state updater: React runs
2780
+ // updaters lazily, so a flag set there is still unset when the
2781
+ // transcript row below is chosen.
2782
+ const isNew = !knownTaskIdsRef.current.has(event.taskId);
2783
+ knownTaskIdsRef.current.add(event.taskId);
2784
+ const item = {
2785
+ id: event.taskId,
2786
+ subject: event.subject,
2787
+ status: event.status,
2788
+ };
2789
+ setTasks((prev) => {
2790
+ const index = prev.findIndex((task) => task.id === item.id);
2791
+ return index < 0 ? [...prev, item] : prev.map((task, i) => (i === index ? item : task));
2792
+ });
2793
+ if (event.status === 'completed') {
2794
+ pushMessage('tool', event.subject, false, '☑');
2795
+ }
2796
+ else if (event.status === 'failed') {
2797
+ pushMessage('tool', event.subject, false, '☒');
2798
+ }
2799
+ else if (isNew) {
2800
+ pushMessage('tool', event.subject, false, '☐');
2801
+ }
2587
2802
  break;
2803
+ }
2588
2804
  case 'context':
2589
2805
  // Into the TRANSCRIPT, not a status line. A status indicator is
2590
2806
  // present while nothing is happening and gone afterwards, so
@@ -2721,6 +2937,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2721
2937
  else {
2722
2938
  pushMessage('user', text, false, undefined, undefined, undefined, humanPromptMeta(attached.length, attachments));
2723
2939
  }
2940
+ // A plan is scoped to the request it was made for. Cleared here, at
2941
+ // the start of the next one, rather than when the previous turn
2942
+ // ended: a finished list stays on screen until the operator moves on,
2943
+ // which is the moment it has told them everything it can.
2944
+ setTasks([]);
2945
+ knownTaskIdsRef.current = new Set();
2724
2946
  // The model interleaves text → tool → text across iterations; `applyEvent`
2725
2947
  // renders each one in order.
2726
2948
  const st = {
@@ -2870,6 +3092,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2870
3092
  ...(goalRound ? { goalRound } : {}),
2871
3093
  // The mode above decides whether this callback is consulted.
2872
3094
  onPermission: askPermission,
3095
+ onQuestion: askQuestion,
2873
3096
  inboundMessages: () => inbox.drain(),
2874
3097
  extraSystem: composeSkillsPrompt(activeSkills) ?? undefined,
2875
3098
  onConversationMessages: (messages) => {
@@ -3218,7 +3441,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3218
3441
  pushMessage('system', 'Permission choices are unavailable until the active turn, prompt, compaction, queued work, or embedded-session approval latch can be safely settled.');
3219
3442
  return;
3220
3443
  }
3221
- const values = ['prompt', 'auto', 'strict'];
3444
+ const values = ['prompt', 'accept-edits', 'auto', 'strict', 'plan'];
3222
3445
  setSelectedChoice(Math.max(0, values.indexOf(permissionModeRef.current)));
3223
3446
  setChoicePicker({
3224
3447
  kind: 'permission-mode',
@@ -4410,6 +4633,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4410
4633
  if (key.escape || (key.ctrl && input === 'c')) {
4411
4634
  reviewChoiceInFlightRef.current = null;
4412
4635
  setChoicePicker(null);
4636
+ // Esc leaves the question unanswered and the turn running; the
4637
+ // tool tells the model so. Ctrl+C is the operator saying stop.
4638
+ if (picker.kind === 'user-question') {
4639
+ resolveQuestion({ kind: key.escape ? 'skip' : 'abort' });
4640
+ }
4413
4641
  return;
4414
4642
  }
4415
4643
  if (key.home || key.end || key.pageUp || key.pageDown) {
@@ -4620,7 +4848,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4620
4848
  : goalStatus && phase === 'ready' && state === 'idle'
4621
4849
  ? undefined
4622
4850
  : hintForPhase(phase, state, session?.hasProvider === true, composerHasDraft);
4623
- return (_jsx(Box, { flexDirection: "column", display: externalEditorRequest ? 'none' : 'flex', children: _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [transcriptOwned || !lifecycleOwnsViewport ? (_jsx(TranscriptFrame, { children: _jsx(Transcript, { messages: finalized, pending: messages.find((m) => m.pending) ?? null, state: state, settled: renderedSettled, resetKey: resetKey, raw: rawOutput, hyperlinks: hyperlinks, showLive: !lifecycleOwnsViewport, header: transcriptOwned ? (_jsx(Banner, { version: ctx.version, session: session, permissionMode: permissionMode, cwd: ctx.cwd })) : undefined }) })) : null, phase === 'trust' ? (_jsx(TrustPrompt, { cwd: ctx.cwd })) : phase === 'resume' ? (_jsx(ResumePicker, { conversations: resumeList, selected: selectedResume })) : phase === 'edit' ? (_jsx(EditPromptPicker, { prompts: editList, selected: selectedEdit })) : phase === 'picker' ? (_jsx(Picker, { detected: pickerDetected ?? detected, currentProvider: currentProvider, selectionKind: pickerSelectionKind, currentModel: session?.modelSummary ?? null, initialView: pickerInitialView, onSubmit: handlePickerSubmit, onCancel: handlePickerCancel, onCredential: handleTypedCredential, onLogin: startLoginFromPicker, onLoginComplete: finishLoginFromPicker, keyEntryFor: keyEntryFor, notice: pickerNotice })) : (_jsxs(_Fragment, { children: [agentSurface === null ? (_jsx(LiveActivity, { activeTools: visibleActiveTools, working: state === 'thinking' || state === 'tool', interruptible: abortRef.current !== null, animate: stdout.isTTY === true })) : null, permission ? (_jsx(PermissionOverlay, { toolCalls: permission.toolCalls, review: permission.review, summary: permission.summary, detailsOpen: permissionDetailsOpen, reviewOffset: permissionReviewOffset, columns: terminal.columns })) : null, textPrompt ? (_jsx(TextPrompt, { title: textPrompt.title, placeholder: textPrompt.placeholder, initialValue: textPrompt.initialValue, emptyNotice: textPrompt.emptyNotice, hidden: permission !== null || agentSurface !== null, onSubmit: submitTextPrompt, onCancel: cancelTextPrompt }, textPrompt.token)) : permission === null && agentSurface === null && choicePicker ? (_jsx(ChoicePicker, { title: choicePicker.title, notice: choicePicker.notice, options: choicePicker.options, selected: selectedChoice, windowSize: choicePicker.kind === 'command' ? choicePicker.windowSize : undefined })) : permission === null && agentSurface === null && copyPicker ? (_jsx(CopyPicker, { targets: copyPicker.targets, selected: selectedCopy })) : null, _jsxs(ComposerFrame, { focus: state === 'idle' &&
4851
+ return (_jsx(Box, { flexDirection: "column", display: externalEditorRequest ? 'none' : 'flex', children: _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [transcriptOwned || !lifecycleOwnsViewport ? (_jsx(TranscriptFrame, { children: _jsx(Transcript, { messages: finalized, pending: messages.find((m) => m.pending) ?? null, state: state, settled: renderedSettled, resetKey: resetKey, raw: rawOutput, hyperlinks: hyperlinks, showLive: !lifecycleOwnsViewport, header: transcriptOwned ? (_jsx(Banner, { version: ctx.version, session: session, permissionMode: permissionMode, cwd: ctx.cwd })) : undefined }) })) : null, phase === 'trust' ? (_jsx(TrustPrompt, { cwd: ctx.cwd })) : phase === 'resume' ? (_jsx(ResumePicker, { conversations: resumeList, selected: selectedResume })) : phase === 'edit' ? (_jsx(EditPromptPicker, { prompts: editList, selected: selectedEdit })) : phase === 'picker' ? (_jsx(Picker, { detected: pickerDetected ?? detected, currentProvider: currentProvider, selectionKind: pickerSelectionKind, currentModel: session?.modelSummary ?? null, initialView: pickerInitialView, onSubmit: handlePickerSubmit, onCancel: handlePickerCancel, onCredential: handleTypedCredential, onLogin: startLoginFromPicker, onLoginComplete: finishLoginFromPicker, keyEntryFor: keyEntryFor, notice: pickerNotice })) : (_jsxs(_Fragment, { children: [agentSurface === null ? (_jsx(LiveActivity, { activeTools: visibleActiveTools, working: state === 'thinking' || state === 'tool', interruptible: abortRef.current !== null, animate: stdout.isTTY === true })) : null, agentSurface === null && permission === null ? _jsx(TaskList, { tasks: tasks }) : null, permission ? (_jsx(PermissionOverlay, { toolCalls: permission.toolCalls, review: permission.review, summary: permission.summary, detailsOpen: permissionDetailsOpen, reviewOffset: permissionReviewOffset, columns: terminal.columns })) : null, textPrompt ? (_jsx(TextPrompt, { title: textPrompt.title, placeholder: textPrompt.placeholder, initialValue: textPrompt.initialValue, emptyNotice: textPrompt.emptyNotice, hidden: permission !== null || agentSurface !== null, onSubmit: submitTextPrompt, onCancel: cancelTextPrompt }, textPrompt.token)) : permission === null && agentSurface === null && choicePicker ? (_jsx(ChoicePicker, { title: choicePicker.title, notice: choicePicker.notice, options: choicePicker.options, selected: selectedChoice, windowSize: choicePicker.kind === 'command' ? choicePicker.windowSize : undefined })) : permission === null && agentSurface === null && copyPicker ? (_jsx(CopyPicker, { targets: copyPicker.targets, selected: selectedCopy })) : null, _jsxs(ComposerFrame, { focus: state === 'idle' &&
4624
4852
  phase === 'ready' &&
4625
4853
  conversationMutation === null &&
4626
4854
  textPrompt === null &&
@@ -4640,7 +4868,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4640
4868
  ? queuePause.outcome === 'paused'
4641
4869
  ? 'held after a resumable run paused; wait for recovery, change model, or send a message to release it'
4642
4870
  : `paused after a ${queuePause.outcome} turn; send a message or change model to continue`
4643
- : 'sending when ready'] }) })) : null, _jsx(Composer, { disabled: phase !== 'ready' ||
4871
+ : 'sending when ready'] }) })) : null, _jsx(Composer, { permissionMode: permissionMode, onCycleMode: cyclePermissionMode, disabled: phase !== 'ready' ||
4644
4872
  state === 'awaiting-permission' ||
4645
4873
  compacting ||
4646
4874
  conversationMutation !== null ||
@@ -4731,10 +4959,18 @@ function goalStatusLabel(goal) {
4731
4959
  return 'Goal achieved';
4732
4960
  }
4733
4961
  }
4962
+ /** The label the operator saw for the option id they chose. */
4963
+ function labelOfOption(question, optionId) {
4964
+ return question.options.find((option) => option.id === optionId)?.label ?? optionId;
4965
+ }
4734
4966
  function permissionModeDescription(mode) {
4735
4967
  switch (mode) {
4736
4968
  case 'prompt':
4737
4969
  return 'Ask before an undecided tool call runs';
4970
+ case 'accept-edits':
4971
+ return 'Approve file edits and writes without asking; shell and everything else still ask';
4972
+ case 'plan':
4973
+ return 'Read-only: the agent explores and presents a plan; leave plan mode to carry it out';
4738
4974
  case 'auto':
4739
4975
  return 'Approve undecided calls unless a rule or safety gate refuses';
4740
4976
  case 'strict':