@namzu/cli 28.1.1 → 29.0.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.
Files changed (99) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/dist/integrations/subagents/runtime.d.ts +16 -0
  3. package/dist/integrations/subagents/runtime.d.ts.map +1 -1
  4. package/dist/integrations/subagents/runtime.js +34 -4
  5. package/dist/integrations/subagents/runtime.js.map +1 -1
  6. package/dist/permissions/checks.d.ts.map +1 -1
  7. package/dist/permissions/checks.js +7 -1
  8. package/dist/permissions/checks.js.map +1 -1
  9. package/dist/permissions/rules.d.ts.map +1 -1
  10. package/dist/permissions/rules.js +4 -8
  11. package/dist/permissions/rules.js.map +1 -1
  12. package/dist/skills/store.d.ts.map +1 -1
  13. package/dist/skills/store.js +5 -1
  14. package/dist/skills/store.js.map +1 -1
  15. package/dist/tui/AgentExplorer.d.ts +28 -4
  16. package/dist/tui/AgentExplorer.d.ts.map +1 -1
  17. package/dist/tui/AgentExplorer.js +177 -35
  18. package/dist/tui/AgentExplorer.js.map +1 -1
  19. package/dist/tui/App.d.ts.map +1 -1
  20. package/dist/tui/App.js +231 -21
  21. package/dist/tui/App.js.map +1 -1
  22. package/dist/tui/Composer.d.ts.map +1 -1
  23. package/dist/tui/Composer.js +10 -4
  24. package/dist/tui/Composer.js.map +1 -1
  25. package/dist/tui/ComposerFrame.d.ts +10 -1
  26. package/dist/tui/ComposerFrame.d.ts.map +1 -1
  27. package/dist/tui/ComposerFrame.js +27 -4
  28. package/dist/tui/ComposerFrame.js.map +1 -1
  29. package/dist/tui/EffortSlider.d.ts +54 -0
  30. package/dist/tui/EffortSlider.d.ts.map +1 -0
  31. package/dist/tui/EffortSlider.js +131 -0
  32. package/dist/tui/EffortSlider.js.map +1 -0
  33. package/dist/tui/LiveActivity.d.ts +23 -3
  34. package/dist/tui/LiveActivity.d.ts.map +1 -1
  35. package/dist/tui/LiveActivity.js +23 -17
  36. package/dist/tui/LiveActivity.js.map +1 -1
  37. package/dist/tui/Markdown.d.ts +29 -2
  38. package/dist/tui/Markdown.d.ts.map +1 -1
  39. package/dist/tui/Markdown.js +74 -36
  40. package/dist/tui/Markdown.js.map +1 -1
  41. package/dist/tui/StatusBar.js +1 -1
  42. package/dist/tui/StatusBar.js.map +1 -1
  43. package/dist/tui/Transcript.d.ts +24 -1
  44. package/dist/tui/Transcript.d.ts.map +1 -1
  45. package/dist/tui/Transcript.js +35 -8
  46. package/dist/tui/Transcript.js.map +1 -1
  47. package/dist/tui/ViewportBound.d.ts +48 -0
  48. package/dist/tui/ViewportBound.d.ts.map +1 -0
  49. package/dist/tui/ViewportBound.js +55 -0
  50. package/dist/tui/ViewportBound.js.map +1 -0
  51. package/dist/tui/agent-transcript-rows.d.ts +59 -0
  52. package/dist/tui/agent-transcript-rows.d.ts.map +1 -0
  53. package/dist/tui/agent-transcript-rows.js +115 -0
  54. package/dist/tui/agent-transcript-rows.js.map +1 -0
  55. package/dist/tui/agent.d.ts +25 -0
  56. package/dist/tui/agent.d.ts.map +1 -1
  57. package/dist/tui/agent.js +49 -3
  58. package/dist/tui/agent.js.map +1 -1
  59. package/dist/tui/live-window.d.ts +16 -0
  60. package/dist/tui/live-window.d.ts.map +1 -1
  61. package/dist/tui/live-window.js +50 -3
  62. package/dist/tui/live-window.js.map +1 -1
  63. package/dist/tui/markdown-block-cache.d.ts +9 -7
  64. package/dist/tui/markdown-block-cache.d.ts.map +1 -1
  65. package/dist/tui/markdown-block-cache.js +9 -7
  66. package/dist/tui/markdown-block-cache.js.map +1 -1
  67. package/dist/tui/markdown-table.d.ts +55 -0
  68. package/dist/tui/markdown-table.d.ts.map +1 -0
  69. package/dist/tui/markdown-table.js +175 -0
  70. package/dist/tui/markdown-table.js.map +1 -0
  71. package/dist/tui/markdown-wrap.d.ts +53 -0
  72. package/dist/tui/markdown-wrap.d.ts.map +1 -0
  73. package/dist/tui/markdown-wrap.js +189 -0
  74. package/dist/tui/markdown-wrap.js.map +1 -0
  75. package/dist/tui/markdownParser.d.ts +6 -0
  76. package/dist/tui/markdownParser.d.ts.map +1 -1
  77. package/dist/tui/markdownParser.js +49 -10
  78. package/dist/tui/markdownParser.js.map +1 -1
  79. package/dist/tui/slashCommands.d.ts.map +1 -1
  80. package/dist/tui/slashCommands.js +3 -0
  81. package/dist/tui/slashCommands.js.map +1 -1
  82. package/dist/tui/stream-blocks.d.ts.map +1 -1
  83. package/dist/tui/stream-blocks.js +32 -3
  84. package/dist/tui/stream-blocks.js.map +1 -1
  85. package/dist/tui/theme.d.ts +13 -0
  86. package/dist/tui/theme.d.ts.map +1 -1
  87. package/dist/tui/theme.js +16 -0
  88. package/dist/tui/theme.js.map +1 -1
  89. package/dist/tui/types.d.ts +7 -2
  90. package/dist/tui/types.d.ts.map +1 -1
  91. package/dist/tui/units.d.ts +6 -0
  92. package/dist/tui/units.d.ts.map +1 -0
  93. package/dist/tui/units.js +20 -0
  94. package/dist/tui/units.js.map +1 -0
  95. package/dist/tui/web-activity.d.ts +58 -0
  96. package/dist/tui/web-activity.d.ts.map +1 -0
  97. package/dist/tui/web-activity.js +105 -0
  98. package/dist/tui/web-activity.js.map +1 -0
  99. package/package.json +6 -6
package/dist/tui/App.js CHANGED
@@ -38,14 +38,17 @@ import { effectivePermissionMode, permissionModeDescription, permissionModeLabel
38
38
  import { composeSkillsPrompt, discoverSkills, loadSkillBody } from '../skills/store.js';
39
39
  import { discoverUserCommands } from '../user-commands/store.js';
40
40
  import { AgentCockpit, AgentNarrationBand, AgentTaskPanel, AgentTranscript, activeSubagentCohorts, agentCockpitIsWide, agentPhasePageSize, agentPhases, agentPickerPageSize, agentTranscriptPageSize, agentWorkflowPageSize, agentWorkflows, maxAgentTranscriptTailOffset, } from './AgentExplorer.js';
41
+ import { completionRow, launchReceipt, settleLine } from './agent-transcript-rows.js';
41
42
  import { BrandHeader } from './BrandHeader.js';
42
43
  import { ChoicePicker } from './ChoicePicker.js';
43
44
  import { emptyPluginReport, pluginDetails, pluginOption, pluginStartupState } from './plugin-view.js';
44
45
  import { Composer, suggestionWindowSize, } from './Composer.js';
45
46
  import { ComposerFrame } from './ComposerFrame.js';
47
+ import { EffortSlider, effortSliderLayout } from './EffortSlider.js';
46
48
  import { CopyPicker } from './CopyPicker.js';
47
49
  import { EditPromptPicker } from './EditPromptPicker.js';
48
50
  import { LiveActivity, formatElapsed } from './LiveActivity.js';
51
+ import { webCallTitle, webLiveStatus, webSettledLine } from './web-activity.js';
49
52
  import { PermissionOverlay } from './PermissionOverlay.js';
50
53
  import { Picker } from './Picker.js';
51
54
  import { ResumePicker } from './ResumePicker.js';
@@ -67,7 +70,8 @@ import { assistantTranscriptTexts } from './conversation-history.js';
67
70
  import { copyTargetsForResponse } from './copy-targets.js';
68
71
  import { editablePrompts } from './edit-prompts.js';
69
72
  import { editDraftInExternalEditor } from './external-editor.js';
70
- import { checklistInView, liveWindow } from './live-window.js';
73
+ import { checklistInView, liveWindow, settledBeforeStreaming } from './live-window.js';
74
+ import { ViewportBound } from './ViewportBound.js';
71
75
  import { resolveModelSwitch, } from './model-switch.js';
72
76
  import { parseModelSelectionIntent, resolveModelSelectionIntent } from './model-selection-intent.js';
73
77
  import { describeCodexDeviceLoginStart, describeLoginOutcome, describeLoginStart, describeLogout, describeProviderLogout, } from './login-prompt.js';
@@ -113,6 +117,12 @@ const FREE_TEXT_ANSWER = '__free_text__';
113
117
  * writing rather than as silence.
114
118
  */
115
119
  const STREAM_RELEASE_MS = 250;
120
+ /**
121
+ * How long a batch of delegated agents stays still before its launch receipt
122
+ * is written. Agents of one response are begun within a few milliseconds of
123
+ * one another; this is the margin that keeps them on one receipt.
124
+ */
125
+ const RECEIPT_SETTLE_MS = 250;
116
126
  /** Last non-empty assistant text in a durable conversation, newest first. */
117
127
  function latestAssistantOutput(messages) {
118
128
  for (let i = messages.length - 1; i >= 0; i -= 1) {
@@ -471,6 +481,14 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
471
481
  * than as silence. Cleared by the first text or tool call that follows.
472
482
  */
473
483
  const [thinking, setThinking] = useState(null);
484
+ /** Output tokens of the running turn, for the Working row. See `StreamState`. */
485
+ const [turnTokens, setTurnTokens] = useState(0);
486
+ const turnTokensShownAt = useRef(0);
487
+ const turnTokensTimer = useRef(undefined);
488
+ useEffect(() => () => {
489
+ if (turnTokensTimer.current)
490
+ clearTimeout(turnTokensTimer.current);
491
+ }, []);
474
492
  // Tools currently executing — rendered live (spinner + elapsed) below the
475
493
  // transcript, then committed as static lines on completion.
476
494
  const [activeTools, setActiveTools] = useState([]);
@@ -719,8 +737,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
719
737
  const agents = subagentsRef.current;
720
738
  const workflows = agentWorkflows(agents);
721
739
  const currentWorkflow = [...workflows].reverse().find((workflow) => workflow.agents.some((agent) => agent.status === 'starting' || agent.status === 'queued' || agent.status === 'working')) ?? workflows.at(-1);
722
- const firstPhase = currentWorkflow?.phases[0];
723
- const firstAgent = firstPhase?.agents[0];
740
+ // Opened on the work that is moving: the first phase with a live agent,
741
+ // and its first live agent, rather than on a phase that finished
742
+ // minutes ago. With nothing live, the first phase, as before.
743
+ const live = (agent) => agent.status === 'starting' || agent.status === 'queued' || agent.status === 'working';
744
+ const firstPhase = currentWorkflow?.phases.find((phase) => phase.agents.some(live)) ?? currentWorkflow?.phases[0];
745
+ const firstAgent = firstPhase?.agents.find(live) ?? firstPhase?.agents[0];
724
746
  if (!firstPhase || !firstAgent)
725
747
  return false;
726
748
  setAgentSurface({
@@ -1230,12 +1252,65 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1230
1252
  tailOffset: 0,
1231
1253
  });
1232
1254
  }, [hydrateSavedChildren, pushMessage, setAgentSurface]);
1255
+ /**
1256
+ * Delegated work writes three kinds of row into settled history (see
1257
+ * `agent-transcript-rows.ts`): a launch receipt per batch, then one
1258
+ * completion row per agent. Each is exactly-once, gated by its own set,
1259
+ * and a batch's receipt is always written before any of its completions.
1260
+ *
1261
+ * A receipt waits until the batch is whole — no `Agent` call of the turn is
1262
+ * still starting without a row of its own, and the newest member arrived
1263
+ * at least {@link RECEIPT_SETTLE_MS} ago — so two agents launched in one
1264
+ * response read as one launch rather than two. A member that already
1265
+ * finished forces the receipt out, so it never trails its completion.
1266
+ */
1233
1267
  const reportedAgentsRef = useRef(new Set());
1268
+ const reportedBatchesRef = useRef(new Set());
1269
+ const batchSeenAtRef = useRef(new Map());
1270
+ const [receiptTick, setReceiptTick] = useState(0);
1234
1271
  useEffect(() => {
1235
1272
  reportedAgentsRef.current.clear();
1273
+ reportedBatchesRef.current.clear();
1274
+ batchSeenAtRef.current.clear();
1236
1275
  }, [session]);
1237
1276
  useEffect(() => {
1277
+ void receiptTick;
1238
1278
  const current = session?.subagents?.getSnapshot() ?? [];
1279
+ const now = Date.now();
1280
+ const batches = new Map();
1281
+ for (const agent of subagents) {
1282
+ if (agent.replayed)
1283
+ continue;
1284
+ if (!current.some((item) => item.viewId === agent.viewId))
1285
+ continue;
1286
+ const key = JSON.stringify([agent.workflowId, agent.batchId]);
1287
+ const members = batches.get(key) ?? [];
1288
+ members.push(agent);
1289
+ batches.set(key, members);
1290
+ }
1291
+ const launching = activeTools.some((tool) => tool.toolName.toLowerCase() === 'agent' &&
1292
+ !subagents.some((agent) => agent.toolUseId === tool.id &&
1293
+ (tool.turnId === undefined || agent.workflowId === tool.turnId)));
1294
+ let retryIn;
1295
+ for (const [key, members] of batches) {
1296
+ if (reportedBatchesRef.current.has(key))
1297
+ continue;
1298
+ // Re-stamped when the batch grows: the settle window runs from its newest member.
1299
+ const previous = batchSeenAtRef.current.get(key);
1300
+ const stamp = previous !== undefined && previous.count === members.length
1301
+ ? previous.at
1302
+ : now;
1303
+ if (stamp === now)
1304
+ batchSeenAtRef.current.set(key, { at: now, count: members.length });
1305
+ const settledMember = members.some((agent) => ['completed', 'failed', 'cancelled'].includes(agent.status));
1306
+ const waited = now - stamp;
1307
+ if (!settledMember && (launching || waited < RECEIPT_SETTLE_MS)) {
1308
+ retryIn = Math.min(retryIn ?? RECEIPT_SETTLE_MS, Math.max(50, RECEIPT_SETTLE_MS - waited));
1309
+ continue;
1310
+ }
1311
+ reportedBatchesRef.current.add(key);
1312
+ pushMessage('tool', launchReceipt(members).content, false, '●', undefined, theme.accent.assistant);
1313
+ }
1239
1314
  for (const agent of subagents) {
1240
1315
  if (!current.some((item) => item.viewId === agent.viewId &&
1241
1316
  item.workflowId === agent.workflowId &&
@@ -1245,14 +1320,21 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1245
1320
  continue;
1246
1321
  if (reportedAgentsRef.current.has(agent.viewId))
1247
1322
  continue;
1323
+ // Its receipt first, even if that means writing it alone.
1324
+ const key = JSON.stringify([agent.workflowId, agent.batchId]);
1325
+ if (!agent.replayed && !reportedBatchesRef.current.has(key)) {
1326
+ reportedBatchesRef.current.add(key);
1327
+ pushMessage('tool', launchReceipt(batches.get(key) ?? [agent]).content, false, '●', undefined, theme.accent.assistant);
1328
+ }
1248
1329
  reportedAgentsRef.current.add(agent.viewId);
1249
- const completed = agent.status === 'completed';
1250
- const status = completed
1251
- ? 'Completed'
1252
- : (agent.latestActivity ?? (agent.status === 'cancelled' ? 'Cancelled' : 'Failed'));
1253
- pushMessage('tool', `${agent.description} · ${status}`, false, completed ? '✓' : '✗', undefined, completed ? theme.status.ok : theme.status.error, 'ctrl+t · agent details');
1330
+ const row = completionRow(agent);
1331
+ pushMessage('tool', row.content, false, row.ok ? '✓' : '✗', row.detail.length > 0 ? row.detail : undefined, row.ok ? theme.status.ok : theme.status.error, row.hint, row.detail.length > 0 ? 'agent-result' : undefined);
1254
1332
  }
1255
- }, [subagents, pushMessage, session]);
1333
+ if (retryIn === undefined)
1334
+ return;
1335
+ const timer = setTimeout(() => setReceiptTick((tick) => tick + 1), retryIn);
1336
+ return () => clearTimeout(timer);
1337
+ }, [subagents, activeTools, pushMessage, session, receiptTick]);
1256
1338
  /**
1257
1339
  * A delivered `send_message` correction already lands its own row inside
1258
1340
  * the child's transcript (`SubagentActivityMonitor.recordMessage`); this
@@ -2627,6 +2709,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2627
2709
  }
2628
2710
  }, [activateTrustedProject, pushMessage, runProbe]);
2629
2711
  const finalized = messages.filter((m) => !m.pending);
2712
+ // Where the streaming reply stands among the finalized rows: a row written
2713
+ // while it streams is drawn below it, as it will be once it has finished.
2714
+ const streamingAt = messages.findIndex((m) => m.pending);
2715
+ const pendingAt = streamingAt < 0
2716
+ ? finalized.length
2717
+ : messages.slice(0, streamingAt).filter((m) => !m.pending).length;
2630
2718
  // Activity and the plan share the terminal with the draft. Collapse the two
2631
2719
  // lists together only when their full previews would crowd the input area.
2632
2720
  const fullToolFurniture = activeTools.length === 0 ? 0 : Math.min(activeTools.length, 3) * 2 + 2;
@@ -2673,9 +2761,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2673
2761
  // Freeze the parent's Static floor while it is open; otherwise a parent turn
2674
2762
  // settling in the background prints through the child screen. Returning
2675
2763
  // advances the floor once and emits those finalized rows exactly once.
2764
+ const nextSettled = settledBeforeStreaming(messages, window.settled, settledRef.current);
2676
2765
  if (agentSurface === null && outputViewer === null)
2677
- settledRef.current = window.settled;
2678
- const renderedSettled = agentSurface === null && outputViewer === null ? window.settled : settledRef.current;
2766
+ settledRef.current = nextSettled;
2767
+ const renderedSettled = agentSurface === null && outputViewer === null ? nextSettled : settledRef.current;
2679
2768
  // One merged vocabulary for the session: this host's own commands plus
2680
2769
  // whatever the kernel's registry reports. Built here so `/help`, the
2681
2770
  // autocomplete and the dispatcher all answer from the same list — three
@@ -3596,6 +3685,39 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3596
3685
  st.assistantId = null;
3597
3686
  }
3598
3687
  };
3688
+ // Redrawn at most five times a second: a count that changes on every
3689
+ // delta would re-render the live region per token for a figure no
3690
+ // one reads at that rate. A provider report always lands.
3691
+ // A count held back is drawn when the interval ends, so the last one
3692
+ // of a burst is never left unshown while the stream goes quiet.
3693
+ const noteTokens = (force = false) => {
3694
+ const show = () => {
3695
+ turnTokensShownAt.current = Date.now();
3696
+ setTurnTokens((st.reportedOutputTokens ?? 0) + Math.ceil((st.streamedChars ?? 0) / 4));
3697
+ };
3698
+ const wait = 200 - (Date.now() - turnTokensShownAt.current);
3699
+ if (force || wait <= 0) {
3700
+ if (turnTokensTimer.current)
3701
+ clearTimeout(turnTokensTimer.current);
3702
+ turnTokensTimer.current = undefined;
3703
+ show();
3704
+ }
3705
+ else if (!turnTokensTimer.current) {
3706
+ turnTokensTimer.current = setTimeout(() => {
3707
+ turnTokensTimer.current = undefined;
3708
+ show();
3709
+ }, wait);
3710
+ }
3711
+ };
3712
+ if (event.kind === 'delta' || event.kind === 'reasoning') {
3713
+ st.streamedChars = (st.streamedChars ?? 0) + event.text.length;
3714
+ noteTokens();
3715
+ }
3716
+ else if (event.kind === 'usage' && event.outputTokens !== undefined) {
3717
+ st.reportedOutputTokens = event.outputTokens;
3718
+ st.streamedChars = 0;
3719
+ noteTokens(true);
3720
+ }
3599
3721
  if (event.kind !== 'usage' && 'budget' in event && event.budget) {
3600
3722
  const budget = event.budget;
3601
3723
  const own = st.lastUsage;
@@ -3668,16 +3790,21 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3668
3790
  (event.turnId === undefined || agent.workflowId === event.turnId))
3669
3791
  : undefined;
3670
3792
  const tool = {
3671
- ...(waitingAgent ? { taskId: waitingAgent.taskId } : {}),
3793
+ ...(waitingAgent
3794
+ ? { taskId: waitingAgent.taskId, waitingOn: waitingAgent.description }
3795
+ : {}),
3672
3796
  id: event.toolUseId,
3673
3797
  ...(event.turnId ? { turnId: event.turnId } : {}),
3674
3798
  toolName: event.toolName,
3675
3799
  activity: event.activity,
3676
3800
  label: waitingAgent
3677
3801
  ? `Waiting · ${waitingAgent.description}`
3678
- : formatToolCall(event.toolName, event.summary, event.standalone),
3802
+ : event.web
3803
+ ? webCallTitle(event.web)
3804
+ : formatToolCall(event.toolName, event.summary, event.standalone),
3679
3805
  startedAt: Date.now(),
3680
3806
  detail: event.detail,
3807
+ ...(event.web ? { web: event.web, progress: webLiveStatus(event.web) } : {}),
3681
3808
  };
3682
3809
  activeToolsRef.current = [...activeToolsRef.current, tool];
3683
3810
  setActiveTools(activeToolsRef.current);
@@ -3760,6 +3887,32 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3760
3887
  setState(activeToolsRef.current.length > 0 ? 'tool' : 'thinking');
3761
3888
  break;
3762
3889
  }
3890
+ const web = event.web ?? done?.web;
3891
+ if (web) {
3892
+ // Two rows, the call and what it came to, drawn as `web` activity:
3893
+ // consecutive ones sit together and the result list stays behind
3894
+ // Ctrl+O, as the reference terminal draws them.
3895
+ const activity = {
3896
+ ...web,
3897
+ ...(event.web?.target ?? done?.web?.target
3898
+ ? { target: event.web?.target ?? done?.web?.target }
3899
+ : {}),
3900
+ };
3901
+ const durationMs = event.durationMs ?? (done ? Date.now() - done.startedAt : undefined);
3902
+ const output = web.hosted ? undefined : event.output;
3903
+ pushMessage('tool', webCallTitle(activity), false, event.isError ? '✗' : '✓', undefined, event.isError ? theme.status.error : theme.status.ok, undefined, 'web');
3904
+ const outputLines = output !== undefined ? output.split('\n') : undefined;
3905
+ pushMessage('tool', event.isError
3906
+ ? `failed: ${event.summary}`
3907
+ : webSettledLine(activity, {
3908
+ ...(durationMs !== undefined ? { durationMs } : {}),
3909
+ ...(activity.kind === 'fetch' && output !== undefined
3910
+ ? { bytes: Buffer.byteLength(output, 'utf8') }
3911
+ : {}),
3912
+ }), false, '⎿', outputLines, undefined, outputLines && outputLines.length > 0 ? 'ctrl+o output' : undefined, 'web');
3913
+ setState(activeToolsRef.current.length > 0 ? 'tool' : 'thinking');
3914
+ break;
3915
+ }
3763
3916
  if (isTaskTool(event.toolName)) {
3764
3917
  // The task event already wrote the block. A listing refreshes it
3765
3918
  // while there is a plan to show; with none it keeps its own row.
@@ -3926,6 +4079,18 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3926
4079
  const stopNotice = describeTurnStop(event.stopReason, event.budget);
3927
4080
  if (stopNotice)
3928
4081
  pushMessage('system', stopNotice, false, '■');
4082
+ if (st.startedAt !== undefined) {
4083
+ const since = st.startedAt;
4084
+ const delegated = subagentsRef.current.filter((agent) => agent.replayed !== true && agent.startedAt >= since);
4085
+ // A turn that delegated always says what its agents came to; one
4086
+ // that did not closes with its time only when it finished, since
4087
+ // a stop or a cancellation has already said how it ended.
4088
+ const closing = delegated.length > 0 || st.completed
4089
+ ? settleLine(Date.now() - since, delegated)
4090
+ : undefined;
4091
+ if (closing)
4092
+ pushMessage('system', closing, false, '✻', undefined, theme.text.muted);
4093
+ }
3929
4094
  break;
3930
4095
  }
3931
4096
  case 'paused':
@@ -4063,8 +4228,14 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4063
4228
  tasksRef.current = [];
4064
4229
  // The model interleaves text → tool → text across iterations; `applyEvent`
4065
4230
  // renders each one in order.
4231
+ if (turnTokensTimer.current)
4232
+ clearTimeout(turnTokensTimer.current);
4233
+ turnTokensTimer.current = undefined;
4234
+ setTurnTokens(0);
4235
+ turnTokensShownAt.current = 0;
4066
4236
  const st = {
4067
4237
  assistantId: null,
4238
+ startedAt: Date.now(),
4068
4239
  text: '',
4069
4240
  conversationMessages: undefined,
4070
4241
  pending: '',
@@ -6362,10 +6533,14 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6362
6533
  setSelectedChoice((index) => moveChoiceSelection(options, index, key.home ? 'first' : key.end ? 'last' : key.pageUp ? 'previous-page' : 'next-page', pageSize));
6363
6534
  return;
6364
6535
  }
6365
- if (key.upArrow) {
6536
+ if (key.upArrow || (key.leftArrow && picker.kind === 'reasoning-effort')) {
6366
6537
  setSelectedChoice((index) => moveChoiceSelection(options, index, 'previous'));
6367
6538
  return;
6368
6539
  }
6540
+ if (key.rightArrow && picker.kind === 'reasoning-effort') {
6541
+ setSelectedChoice((index) => moveChoiceSelection(options, index, 'next'));
6542
+ return;
6543
+ }
6369
6544
  if (key.downArrow) {
6370
6545
  setSelectedChoice((index) => moveChoiceSelection(options, index, 'next'));
6371
6546
  return;
@@ -6447,10 +6622,21 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6447
6622
  return;
6448
6623
  }
6449
6624
  // Small expansions stay in place. Older or oversized bodies open a viewer.
6625
+ //
6626
+ // A settled row cannot be repainted, so its hint keeps saying Ctrl+O
6627
+ // for as long as it is on screen; this press must keep reaching it. The
6628
+ // first press expands the live bodies in place; the next one, while a
6629
+ // settled body sits above them, opens the viewer on the newest settled
6630
+ // body (←/→ reach the rest) and folds the live ones back behind it, so
6631
+ // the press after that expands them again. Only with nothing settled to
6632
+ // open does a second press simply collapse.
6450
6633
  if (key.ctrl && input === 'o') {
6451
- const live = messages.filter((m) => !m.pending).slice(settledRef.current);
6634
+ const shown = messages.filter((m) => !m.pending);
6635
+ const live = shown.slice(settledRef.current);
6452
6636
  const collapsible = live.filter((m) => (m.activity && (m.detail?.length ?? 0) > 0) || willCollapse(m.detail));
6453
- const blocks = messages.filter((m) => m.detailRef !== undefined && (m.detail?.length ?? 0) > 0);
6637
+ const hasBody = (m) => m.detailRef !== undefined && (m.detail?.length ?? 0) > 0;
6638
+ const blocks = messages.filter(hasBody);
6639
+ const olderBlock = shown.slice(0, settledRef.current).filter(hasBody).at(-1);
6454
6640
  const block = collapsible.at(-1) ?? blocks.at(-1);
6455
6641
  if (!block) {
6456
6642
  pushMessage('system', 'Nothing to expand yet. Ctrl+O opens retained tool output when available.');
@@ -6459,6 +6645,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6459
6645
  const expanding = collapsible.some((m) => m.detailExpanded !== true);
6460
6646
  const ids = new Set(collapsible.map((m) => m.id));
6461
6647
  const proposed = messages.map((m) => ids.has(m.id) ? { ...m, detailExpanded: expanding } : m);
6648
+ if (!expanding && olderBlock) {
6649
+ setMessages(proposed);
6650
+ setOutputViewer(olderBlock);
6651
+ return;
6652
+ }
6462
6653
  const projected = liveWindow({ messages: proposed.filter((m) => !m.pending), rows: terminal.rows,
6463
6654
  columns: terminal.columns, furnitureRows: LIVE_FURNITURE_ROWS + taskFurniture + toolFurniture,
6464
6655
  settled: settledRef.current, raw: rawOutput });
@@ -6532,6 +6723,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6532
6723
  agentSurface === null
6533
6724
  ? goalStatusLabel(goalStatus, goalStatus ? goalActivation.isArmed(goalStatus.sessionId, goalStatus) : false)
6534
6725
  : null;
6726
+ // The effort chooser is a left-to-right slider wherever its stops fit on
6727
+ // one row, and the vertical list everywhere else (see EffortSlider.tsx).
6728
+ const effortSlider = choicePicker?.kind === 'reasoning-effort'
6729
+ ? effortSliderLayout(choicePicker.options.map((option) => option.label), Math.max(1, (terminal.columns ?? 80) - 2))
6730
+ : undefined;
6535
6731
  const statusHint = conversationMutation === 'fork'
6536
6732
  ? 'forking conversation — input is paused'
6537
6733
  : conversationMutation === 'edit'
@@ -6557,7 +6753,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6557
6753
  : choicePicker
6558
6754
  ? choicePickerSearchable(choicePicker)
6559
6755
  ? 'type to filter · ↑↓ select · enter apply · esc back'
6560
- : '↑↓ / 1–9 select · enter apply · esc back'
6756
+ : effortSlider
6757
+ ? '←/→ adjust · 1–9 select · enter apply · esc back'
6758
+ : '↑↓ / 1–9 select · enter apply · esc back'
6561
6759
  : copyPicker
6562
6760
  ? 'copy target open — ↑↓ / 1–9 select · esc cancel'
6563
6761
  : goalStatus && phase === 'ready' && state === 'idle'
@@ -6584,7 +6782,12 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6584
6782
  phase !== 'resume' &&
6585
6783
  phase !== 'edit' &&
6586
6784
  phase !== 'picker';
6587
- return (_jsx(Box, { flexDirection: "column", display: externalEditorRequest ? 'none' : 'flex', children: _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(TranscriptFrame, { children: _jsx(Transcript, { messages: finalized, pending: messages.find((m) => m.pending) ?? null, state: state, settled: transcriptOwned ? renderedSettled : 0, resetKey: resetKey, raw: rawOutput, hyperlinks: hyperlinks, showLive: !lifecycleOwnsViewport, header: transcriptOwned ? (_jsx(BrandHeader, { version: ctx.version, permissionMode: displayedPermissionMode })) : undefined }) }), providerSetup ? (_jsx(ProviderSetup, { cwd: ctx.cwd, onClose: () => setProviderSetup(false), onConnect: () => {
6785
+ return (
6786
+ // One row short of the terminal, always: see ViewportBound.tsx for the
6787
+ // renderer path a taller frame takes and the scrollback row it costs.
6788
+ _jsx(ViewportBound, { rows: terminal.rows, display: externalEditorRequest ? 'none' : 'flex', children: _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(TranscriptFrame, { children: _jsx(Transcript, { messages: finalized, pending: messages.find((m) => m.pending) ?? null, pendingAt: pendingAt, state: state, settled: transcriptOwned ? renderedSettled : 0, resetKey: resetKey, raw: rawOutput, hyperlinks: hyperlinks, showLive: !lifecycleOwnsViewport,
6789
+ // The one column of padding the enclosing box gives every row.
6790
+ staticIndent: 1, header: transcriptOwned ? (_jsx(BrandHeader, { version: ctx.version, permissionMode: displayedPermissionMode })) : undefined }) }), providerSetup ? (_jsx(ProviderSetup, { cwd: ctx.cwd, onClose: () => setProviderSetup(false), onConnect: () => {
6588
6791
  setProviderSetup(false);
6589
6792
  setPickerInitialView('providers');
6590
6793
  setPickerDetected(null);
@@ -6596,7 +6799,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6596
6799
  // where it is decided rather than guessed on the screen: with no
6597
6800
  // session behind the picker, esc exits namzu and the footer says
6598
6801
  // so instead of offering a cancel nobody will see.
6599
- cancelExits: !session?.hasProvider })) : (_jsxs(_Fragment, { children: [agentSurface === null && outputViewer === null ? (_jsx(LiveActivity, { compact: compactWork, activeTools: visibleActiveTools, working: state === 'thinking' || state === 'tool', interruptible: abortRef.current !== null, animate: stdout.isTTY === true && permission === null && textPrompt === null, thinking: thinking })) : null, agentSurface === null && outputViewer === null && permission === null ? (_jsx(TaskList, { tasks: liveTasks })) : null, permission ? (_jsx(PermissionOverlay, { toolCalls: permission.toolCalls, review: permission.review, summary: permission.summary, detailsOpen: permissionDetailsOpen, reviewOffset: permissionReviewOffset, choice: permissionChoice, queuedCount: queuedPermissionCount, sourceLabel: permissionSourceLabel, columns: terminal.columns, rows: terminal.rows })) : null, textPrompt ? (_jsx(TextPrompt, { columns: Math.max(1, (terminal.columns ?? 80) - 2), title: textPrompt.title, placeholder: textPrompt.placeholder, initialValue: textPrompt.initialValue, emptyNotice: textPrompt.emptyNotice, hidden: permission !== null || (agentSurface !== null || outputViewer !== null), onSubmit: submitTextPrompt, onCancel: cancelTextPrompt }, textPrompt.token)) : permission === null && agentSurface === null && outputViewer === null && choicePicker ? (_jsx(ChoicePicker, { busy: 'busy' in choicePicker && choicePicker.busy === true, columns: Math.max(1, (terminal.columns ?? 80) - 2), title: choicePicker.title, notice: choicePicker.notice, options: filterChoiceOptions(choicePicker.options, choiceQuery), query: choicePickerSearchable(choicePicker) ? choiceQuery : undefined, selected: selectedChoice, windowSize: choicePicker.kind === 'command' ? choicePicker.windowSize : undefined })) : permission === null && agentSurface === null && outputViewer === null && copyPicker ? (_jsx(CopyPicker, { targets: copyPicker.targets, selected: selectedCopy })) : null, _jsxs(ComposerFrame, { working: state === 'thinking' || state === 'tool' || visibleActiveTools.length > 0, focus: phase === 'ready' &&
6802
+ cancelExits: !session?.hasProvider })) : (_jsxs(_Fragment, { children: [agentSurface === null && outputViewer === null ? (_jsx(LiveActivity, { compact: compactWork, activeTools: visibleActiveTools, working: state === 'thinking' || state === 'tool', interruptible: abortRef.current !== null, animate: stdout.isTTY === true && permission === null && textPrompt === null, thinking: thinking, tokens: turnTokens })) : null, agentSurface === null && outputViewer === null && permission === null ? (_jsx(TaskList, { tasks: liveTasks })) : null, permission ? (_jsx(PermissionOverlay, { toolCalls: permission.toolCalls, review: permission.review, summary: permission.summary, detailsOpen: permissionDetailsOpen, reviewOffset: permissionReviewOffset, choice: permissionChoice, queuedCount: queuedPermissionCount, sourceLabel: permissionSourceLabel, columns: terminal.columns, rows: terminal.rows })) : null, textPrompt ? (_jsx(TextPrompt, { columns: Math.max(1, (terminal.columns ?? 80) - 2), title: textPrompt.title, placeholder: textPrompt.placeholder, initialValue: textPrompt.initialValue, emptyNotice: textPrompt.emptyNotice, hidden: permission !== null || (agentSurface !== null || outputViewer !== null), onSubmit: submitTextPrompt, onCancel: cancelTextPrompt }, textPrompt.token)) : permission === null &&
6803
+ agentSurface === null &&
6804
+ outputViewer === null &&
6805
+ choicePicker?.kind === 'reasoning-effort' &&
6806
+ effortSlider ? (_jsx(EffortSlider, { title: choicePicker.title, notice: choicePicker.notice, options: choicePicker.options, selected: selectedChoice, layout: effortSlider, columns: Math.max(1, (terminal.columns ?? 80) - 2), highest: choicePicker.options.at(-2)?.label === 'default' ? undefined : choicePicker.options.at(-2)?.label })) : permission === null && agentSurface === null && outputViewer === null && choicePicker ? (_jsx(ChoicePicker, { busy: 'busy' in choicePicker && choicePicker.busy === true, columns: Math.max(1, (terminal.columns ?? 80) - 2), title: choicePicker.title, notice: choicePicker.notice, options: filterChoiceOptions(choicePicker.options, choiceQuery), query: choicePickerSearchable(choicePicker) ? choiceQuery : undefined, selected: selectedChoice, windowSize: choicePicker.kind === 'command' ? choicePicker.windowSize : undefined })) : permission === null && agentSurface === null && outputViewer === null && copyPicker ? (_jsx(CopyPicker, { targets: copyPicker.targets, selected: selectedCopy })) : null, _jsxs(ComposerFrame, { working: state === 'thinking' || state === 'tool' || visibleActiveTools.length > 0, ...(orchestrateMode ? { mode: 'orchestrate' } : {}), focus: phase === 'ready' &&
6600
6807
  state !== 'awaiting-permission' &&
6601
6808
  !compacting &&
6602
6809
  externalEditorRequest === null &&
@@ -6643,10 +6850,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6643
6850
  permission === null &&
6644
6851
  agentSurface === null &&
6645
6852
  outputViewer === null ? (_jsx(AgentNarrationBand, { lines: narration })) : null, showComposerSurface &&
6646
- permission === null &&
6647
6853
  agentSurface === null &&
6648
6854
  outputViewer === null &&
6649
- liveSubagents.length > 0 ? (_jsx(AgentTaskPanel, { agents: liveSubagents, terminalRows: terminal.rows, terminalColumns: terminal.columns })) : showComposerSurface && permission === null && agentSurface?.kind === 'cockpit' ? (_jsx(AgentCockpit, { agents: subagents, selectedPhaseId: agentSurface.selectedPhaseId, selectedId: agentSurface.selectedId, focus: agentSurface.focus, terminalRows: terminal.rows, terminalColumns: terminal.columns })) : showComposerSurface &&
6855
+ liveSubagents.length > 0 ? (
6856
+ // Still drawn while a review is open, reduced to its header line:
6857
+ // the work already approved keeps moving, and the operator
6858
+ // deciding the next launch should be able to see that it does.
6859
+ _jsx(AgentTaskPanel, { agents: liveSubagents, terminalRows: terminal.rows, terminalColumns: terminal.columns, compact: permission !== null })) : showComposerSurface && permission === null && agentSurface?.kind === 'cockpit' ? (_jsx(AgentCockpit, { agents: subagents, selectedPhaseId: agentSurface.selectedPhaseId, selectedId: agentSurface.selectedId, focus: agentSurface.focus, terminalRows: terminal.rows, terminalColumns: terminal.columns })) : showComposerSurface &&
6650
6860
  permission === null &&
6651
6861
  agentSurface?.kind === 'transcript' &&
6652
6862
  selectedSubagent ? (_jsx(AgentTranscript, { agent: selectedSubagent, tailOffset: agentSurface.tailOffset, terminalRows: terminal.rows, terminalColumns: terminal.columns })) : null, outputViewer && permission === null ? (_jsx(ToolOutputViewer, { onPrevious: () => {