@namzu/cli 21.0.0 → 22.0.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 (134) hide show
  1. package/CHANGELOG.md +116 -0
  2. package/README.md +31 -10
  3. package/dist/commands/run.js +1 -1
  4. package/dist/commands/run.js.map +1 -1
  5. package/dist/config/load.d.ts.map +1 -1
  6. package/dist/config/load.js +23 -2
  7. package/dist/config/load.js.map +1 -1
  8. package/dist/config/schema.d.ts +13 -9
  9. package/dist/config/schema.d.ts.map +1 -1
  10. package/dist/config/schema.js.map +1 -1
  11. package/dist/doctor/checks/credentials.d.ts.map +1 -1
  12. package/dist/doctor/checks/credentials.js +2 -0
  13. package/dist/doctor/checks/credentials.js.map +1 -1
  14. package/dist/installation.d.ts +5 -0
  15. package/dist/installation.d.ts.map +1 -0
  16. package/dist/installation.js +48 -0
  17. package/dist/installation.js.map +1 -0
  18. package/dist/integrations/providers/current-credential.d.ts +4 -0
  19. package/dist/integrations/providers/current-credential.d.ts.map +1 -0
  20. package/dist/integrations/providers/current-credential.js +45 -0
  21. package/dist/integrations/providers/current-credential.js.map +1 -0
  22. package/dist/integrations/providers/discover.d.ts +12 -1
  23. package/dist/integrations/providers/discover.d.ts.map +1 -1
  24. package/dist/integrations/providers/discover.js +17 -1
  25. package/dist/integrations/providers/discover.js.map +1 -1
  26. package/dist/integrations/providers/gemini-credentials.d.ts +13 -0
  27. package/dist/integrations/providers/gemini-credentials.d.ts.map +1 -0
  28. package/dist/integrations/providers/gemini-credentials.js +148 -0
  29. package/dist/integrations/providers/gemini-credentials.js.map +1 -0
  30. package/dist/integrations/providers/register.d.ts.map +1 -1
  31. package/dist/integrations/providers/register.js +5 -0
  32. package/dist/integrations/providers/register.js.map +1 -1
  33. package/dist/integrations/providers/registry.d.ts +1 -1
  34. package/dist/integrations/providers/registry.d.ts.map +1 -1
  35. package/dist/integrations/providers/registry.js +10 -0
  36. package/dist/integrations/providers/registry.js.map +1 -1
  37. package/dist/integrations/providers/setup.d.ts +49 -0
  38. package/dist/integrations/providers/setup.d.ts.map +1 -0
  39. package/dist/integrations/providers/setup.js +105 -0
  40. package/dist/integrations/providers/setup.js.map +1 -0
  41. package/dist/integrations/sessions/store.d.ts +1 -0
  42. package/dist/integrations/sessions/store.d.ts.map +1 -1
  43. package/dist/integrations/sessions/store.js +17 -4
  44. package/dist/integrations/sessions/store.js.map +1 -1
  45. package/dist/integrations/subagents/activity.d.ts.map +1 -1
  46. package/dist/integrations/subagents/activity.js +12 -3
  47. package/dist/integrations/subagents/activity.js.map +1 -1
  48. package/dist/integrations/subagents/history.d.ts +26 -0
  49. package/dist/integrations/subagents/history.d.ts.map +1 -0
  50. package/dist/integrations/subagents/history.js +76 -0
  51. package/dist/integrations/subagents/history.js.map +1 -0
  52. package/dist/integrations/subagents/model-effort.d.ts +4 -0
  53. package/dist/integrations/subagents/model-effort.d.ts.map +1 -0
  54. package/dist/integrations/subagents/model-effort.js +12 -0
  55. package/dist/integrations/subagents/model-effort.js.map +1 -0
  56. package/dist/integrations/subagents/runtime.d.ts +21 -4
  57. package/dist/integrations/subagents/runtime.d.ts.map +1 -1
  58. package/dist/integrations/subagents/runtime.js +205 -23
  59. package/dist/integrations/subagents/runtime.js.map +1 -1
  60. package/dist/integrations/web/search.d.ts +11 -0
  61. package/dist/integrations/web/search.d.ts.map +1 -0
  62. package/dist/integrations/web/search.js +73 -0
  63. package/dist/integrations/web/search.js.map +1 -0
  64. package/dist/tui/AgentExplorer.d.ts.map +1 -1
  65. package/dist/tui/AgentExplorer.js +5 -5
  66. package/dist/tui/AgentExplorer.js.map +1 -1
  67. package/dist/tui/App.d.ts.map +1 -1
  68. package/dist/tui/App.js +242 -88
  69. package/dist/tui/App.js.map +1 -1
  70. package/dist/tui/BrandHeader.js +3 -3
  71. package/dist/tui/BrandHeader.js.map +1 -1
  72. package/dist/tui/Composer.d.ts.map +1 -1
  73. package/dist/tui/Composer.js +18 -2
  74. package/dist/tui/Composer.js.map +1 -1
  75. package/dist/tui/PermissionOverlay.d.ts +4 -1
  76. package/dist/tui/PermissionOverlay.d.ts.map +1 -1
  77. package/dist/tui/PermissionOverlay.js +6 -5
  78. package/dist/tui/PermissionOverlay.js.map +1 -1
  79. package/dist/tui/Picker.d.ts +2 -1
  80. package/dist/tui/Picker.d.ts.map +1 -1
  81. package/dist/tui/Picker.js +10 -3
  82. package/dist/tui/Picker.js.map +1 -1
  83. package/dist/tui/ProviderSetup.d.ts +6 -0
  84. package/dist/tui/ProviderSetup.d.ts.map +1 -0
  85. package/dist/tui/ProviderSetup.js +125 -0
  86. package/dist/tui/ProviderSetup.js.map +1 -0
  87. package/dist/tui/ResumePicker.d.ts.map +1 -1
  88. package/dist/tui/ResumePicker.js +5 -3
  89. package/dist/tui/ResumePicker.js.map +1 -1
  90. package/dist/tui/StatusPanel.d.ts +5 -0
  91. package/dist/tui/StatusPanel.d.ts.map +1 -0
  92. package/dist/tui/StatusPanel.js +11 -0
  93. package/dist/tui/StatusPanel.js.map +1 -0
  94. package/dist/tui/ToolOutputViewer.d.ts +11 -0
  95. package/dist/tui/ToolOutputViewer.d.ts.map +1 -0
  96. package/dist/tui/ToolOutputViewer.js +39 -0
  97. package/dist/tui/ToolOutputViewer.js.map +1 -0
  98. package/dist/tui/Transcript.d.ts.map +1 -1
  99. package/dist/tui/Transcript.js +9 -3
  100. package/dist/tui/Transcript.js.map +1 -1
  101. package/dist/tui/agent.d.ts +7 -1
  102. package/dist/tui/agent.d.ts.map +1 -1
  103. package/dist/tui/agent.js +195 -46
  104. package/dist/tui/agent.js.map +1 -1
  105. package/dist/tui/live-window.d.ts.map +1 -1
  106. package/dist/tui/live-window.js +5 -1
  107. package/dist/tui/live-window.js.map +1 -1
  108. package/dist/tui/logo.d.ts +4 -4
  109. package/dist/tui/logo.d.ts.map +1 -1
  110. package/dist/tui/logo.js +4 -4
  111. package/dist/tui/logo.js.map +1 -1
  112. package/dist/tui/model-catalogue-view.d.ts +3 -0
  113. package/dist/tui/model-catalogue-view.d.ts.map +1 -0
  114. package/dist/tui/model-catalogue-view.js +50 -0
  115. package/dist/tui/model-catalogue-view.js.map +1 -0
  116. package/dist/tui/permission-review.d.ts +2 -0
  117. package/dist/tui/permission-review.d.ts.map +1 -1
  118. package/dist/tui/permission-review.js +57 -3
  119. package/dist/tui/permission-review.js.map +1 -1
  120. package/dist/tui/slashCommands.d.ts +7 -0
  121. package/dist/tui/slashCommands.d.ts.map +1 -1
  122. package/dist/tui/slashCommands.js +75 -1
  123. package/dist/tui/slashCommands.js.map +1 -1
  124. package/dist/tui/status-card.d.ts +3 -0
  125. package/dist/tui/status-card.d.ts.map +1 -0
  126. package/dist/tui/status-card.js +30 -0
  127. package/dist/tui/status-card.js.map +1 -0
  128. package/dist/tui/status-panel-layout.d.ts +11 -0
  129. package/dist/tui/status-panel-layout.d.ts.map +1 -0
  130. package/dist/tui/status-panel-layout.js +42 -0
  131. package/dist/tui/status-panel-layout.js.map +1 -0
  132. package/dist/tui/types.d.ts +4 -0
  133. package/dist/tui/types.d.ts.map +1 -1
  134. package/package.json +4 -3
package/dist/tui/App.js CHANGED
@@ -11,6 +11,9 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
11
11
  * 4. Otherwise show <Picker/>. An explicit picker choice is persisted;
12
12
  * automatic reuse is not mistaken for a durable operator preference.
13
13
  */
14
+ import { discoverProviders } from '../integrations/providers/discover.js';
15
+ import { ProviderSetup } from './ProviderSetup.js';
16
+ import { ToolOutputViewer } from './ToolOutputViewer.js';
14
17
  import { join, relative } from 'node:path';
15
18
  import { DEFAULT_MAX_GOAL_ROUNDS, DefaultPathBuilder, DiskMessageFeedbackStore, GoalRoundLimitError, HostCommandRegistry, RunCancelled, SessionGoalActivation, StaleGoalError, asSessionId, createAssistantMessage, createUserMessage, generateRunId, generateSessionId, isCompactionMessage, kernelHostCommands, } from '@namzu/sdk';
16
19
  import { Box, Text, useApp, useInput, useStdout, useWindowSize } from 'ink';
@@ -44,6 +47,7 @@ import { ResumePicker } from './ResumePicker.js';
44
47
  import { StatusBar } from './StatusBar.js';
45
48
  import { TaskList } from './TaskList.js';
46
49
  import { TextPrompt } from './TextPrompt.js';
50
+ import { modelCatalogueView } from './model-catalogue-view.js';
47
51
  import { Transcript, willCollapse } from './Transcript.js';
48
52
  import { TrustPrompt } from './TrustPrompt.js';
49
53
  import { createAgentSession, describeProviderModels, probeAgentSession, } from './agent.js';
@@ -277,6 +281,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
277
281
  const [messages, setMessages] = useState([]);
278
282
  /** Plain, copy-friendly rendering of the same retained transcript rows. */
279
283
  const [rawOutput, setRawOutput] = useState(false);
284
+ const [outputViewer, setOutputViewer] = useState(null);
280
285
  /**
281
286
  * The conversation sent to the model, in the SDK's own lossless shape.
282
287
  *
@@ -533,6 +538,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
533
538
  copyPickerCommittedRef.current = copyPicker;
534
539
  }, [copyPicker]);
535
540
  /** Finite slash-command choice owned synchronously until apply or cancel. */
541
+ const [providerSetup, setProviderSetup] = useState(false);
536
542
  const [choicePicker, setChoicePickerState] = useState(null);
537
543
  const choicePickerRef = useRef(null);
538
544
  const [choiceQuery, setChoiceQuery] = useState('');
@@ -708,6 +714,18 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
708
714
  setActiveTools([]);
709
715
  }, []);
710
716
  const permissionResolveRef = useRef(null);
717
+ const permissionQueueRef = useRef([]);
718
+ const [queuedPermissionCount, setQueuedPermissionCount] = useState(0);
719
+ useEffect(() => () => {
720
+ const decision = {
721
+ kind: 'reject',
722
+ feedback: 'The approval screen closed.',
723
+ };
724
+ permissionResolveRef.current?.(decision);
725
+ permissionResolveRef.current = null;
726
+ for (const pending of permissionQueueRef.current.splice(0))
727
+ pending.resolve(decision);
728
+ }, []);
711
729
  /**
712
730
  * When the pending prompt took the screen, or `null` with none open.
713
731
  *
@@ -767,6 +785,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
767
785
  const goalDriveInFlightRef = useRef(false);
768
786
  const [goalDriveVersion, setGoalDriveVersion] = useState(0);
769
787
  const [goalStatus, setGoalStatus] = useState(null);
788
+ const pendingGoalResumeRef = useRef(null);
770
789
  const wakeGoalDriver = useCallback(() => setGoalDriveVersion((version) => version + 1), []);
771
790
  /**
772
791
  * Conversation writes in the order the operator produced them.
@@ -940,7 +959,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
940
959
  externalEditorRequestRef.current = null;
941
960
  request.controller.abort(new Error('the terminal session closed'));
942
961
  }, []);
943
- const pushMessage = useCallback((role, content, pending = false, glyph, detail, glyphColor, meta) => {
962
+ const pushMessage = useCallback((role, content, pending = false, glyph, detail, glyphColor, meta, activity) => {
944
963
  const id = nextId();
945
964
  setMessages((prev) => [
946
965
  ...prev,
@@ -953,6 +972,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
953
972
  detail,
954
973
  glyphColor,
955
974
  meta,
975
+ activity,
956
976
  // Numbered only if this body will actually be COLLAPSED — the
957
977
  // number exists to be read off a hint, and a body that fits
958
978
  // prints no hint. Numbering every body instead would leave gaps
@@ -963,7 +983,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
963
983
  //
964
984
  // Derived from `prev` rather than a counter, so the number is a
965
985
  // fact about the transcript rather than a second record of it.
966
- ...(willCollapse(detail)
986
+ ...((activity && detail?.length) || willCollapse(detail)
967
987
  ? {
968
988
  detailRef: prev.filter((m) => m.detailRef !== undefined).length + 1,
969
989
  }
@@ -972,6 +992,29 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
972
992
  ]);
973
993
  return id;
974
994
  }, [nextId]);
995
+ const reportedAgentsRef = useRef(new Set());
996
+ useEffect(() => {
997
+ reportedAgentsRef.current.clear();
998
+ }, [session]);
999
+ useEffect(() => {
1000
+ const current = session?.subagents?.getSnapshot() ?? [];
1001
+ for (const agent of subagents) {
1002
+ if (!current.some((item) => item.viewId === agent.viewId &&
1003
+ item.workflowId === agent.workflowId &&
1004
+ item.status === agent.status))
1005
+ continue;
1006
+ if (!['completed', 'failed', 'cancelled'].includes(agent.status))
1007
+ continue;
1008
+ if (reportedAgentsRef.current.has(agent.viewId))
1009
+ continue;
1010
+ reportedAgentsRef.current.add(agent.viewId);
1011
+ const completed = agent.status === 'completed';
1012
+ const status = completed
1013
+ ? 'Completed'
1014
+ : (agent.latestActivity ?? (agent.status === 'cancelled' ? 'Cancelled' : 'Failed'));
1015
+ pushMessage('tool', `${agent.description} · ${status}`, false, completed ? '✓' : '✗', undefined, completed ? theme.status.ok : theme.status.error, 'ctrl+t · agent details');
1016
+ }
1017
+ }, [subagents, pushMessage, session]);
975
1018
  const applyPermissionMode = useCallback((mode) => {
976
1019
  if (!session?.hasProvider) {
977
1020
  pushMessage('system', 'Choose a model before changing permissions.');
@@ -1449,6 +1492,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1449
1492
  return;
1450
1493
  }
1451
1494
  setChoicePicker(null);
1495
+ if (picker.kind === 'resume-goal') {
1496
+ if (value === 'resume' && scopeRef.current?.sessionId === picker.sessionId)
1497
+ commandPickerSubmitRef.current('/goal resume');
1498
+ return;
1499
+ }
1452
1500
  if (picker.kind === 'command') {
1453
1501
  const command = value;
1454
1502
  if (command.problem) {
@@ -1646,6 +1694,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1646
1694
  lastCompletedOutputRef.current = persistedOutput
1647
1695
  ? { text: persistedOutput, provenance: 'persisted' }
1648
1696
  : null;
1697
+ pendingGoalResumeRef.current = requestedConversationId;
1649
1698
  initialConversationIdRef.current = undefined;
1650
1699
  conversationMaterializedRef.current = true;
1651
1700
  }
@@ -1785,6 +1834,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1785
1834
  // Re-hydration (a provider switch via /model) builds a second session;
1786
1835
  // without this the first one's tool-server child processes stay alive
1787
1836
  // for the rest of the TUI's life.
1837
+ const previousInstructionFiles = previousSessionRef.current?.instructionFiles;
1788
1838
  void previousSessionRef.current?.close();
1789
1839
  previousSessionRef.current = s;
1790
1840
  setSession(s);
@@ -1826,7 +1876,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1826
1876
  // they did not expect — an instructions file in a parent directory
1827
1877
  // they forgot about is exactly the thing that makes the agent
1828
1878
  // behave oddly for no visible reason.
1829
- if (s.instructionFiles.length > 0) {
1879
+ if (s.instructionFiles.length > 0 &&
1880
+ JSON.stringify([...s.instructionFiles].sort()) !==
1881
+ JSON.stringify(previousInstructionFiles ? [...previousInstructionFiles].sort() : undefined)) {
1830
1882
  pushMessage('system', `Project instructions: ${s.instructionFiles.map((p) => relative(activeCtx.cwd, p) || p).join(', ')}`);
1831
1883
  }
1832
1884
  for (const skip of s.skippedInstructionFiles) {
@@ -2107,7 +2159,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2107
2159
  setPickerDetected(signedIn);
2108
2160
  setPickerSelectionKind('signed-in-subscription');
2109
2161
  setPickerInitialView('providers');
2110
- setPickerNotice('Claude and Codex subscriptions are already signed in on this device. Choose which one Namzu should use.');
2162
+ setPickerNotice('Multiple model sessions are already signed in on this device. Choose which one Namzu should use.');
2111
2163
  setPhase('picker');
2112
2164
  return;
2113
2165
  }
@@ -2177,6 +2229,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2177
2229
  // terminal scrollback; the live tail stays deliberately small so an activity
2178
2230
  // tick never repaints the whole conversation.
2179
2231
  //
2232
+ useEffect(() => {
2233
+ setOutputViewer(null);
2234
+ }, [resetKey]);
2235
+ useEffect(() => {
2236
+ if (permission !== null)
2237
+ setOutputViewer(null);
2238
+ }, [permission]);
2180
2239
  // A ref, and mutated during render, because the split has to be MONOTONIC:
2181
2240
  // a row that has been printed to scrollback can never come back, and `max`
2182
2241
  // is idempotent, so a repeated render reaches the same answer.
@@ -2192,9 +2251,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2192
2251
  // Freeze the parent's Static floor while it is open; otherwise a parent turn
2193
2252
  // settling in the background prints through the child screen. Returning
2194
2253
  // advances the floor once and emits those finalized rows exactly once.
2195
- if (agentSurface === null)
2254
+ if (agentSurface === null && outputViewer === null)
2196
2255
  settledRef.current = window.settled;
2197
- const renderedSettled = agentSurface === null ? window.settled : settledRef.current;
2256
+ const renderedSettled = agentSurface === null && outputViewer === null ? window.settled : settledRef.current;
2198
2257
  // One merged vocabulary for the session: this host's own commands plus
2199
2258
  // whatever the kernel's registry reports. Built here so `/help`, the
2200
2259
  // autocomplete and the dispatcher all answer from the same list — three
@@ -2260,6 +2319,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2260
2319
  instructionFiles: session?.instructionFiles ?? [],
2261
2320
  userCommands,
2262
2321
  configDebug: ctx.configDebug ?? null,
2322
+ webSearchSummary: session?.webSearchSummary,
2323
+ columns: terminal.columns,
2324
+ sessionId: scopeRef.current?.sessionId,
2263
2325
  };
2264
2326
  // `/resume`: open the picker with this folder's recent conversations.
2265
2327
  const doResume = useCallback(async () => {
@@ -2304,6 +2366,19 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2304
2366
  setState('thinking');
2305
2367
  resolve(decision);
2306
2368
  }
2369
+ if (decision.kind === 'approve-all') {
2370
+ for (const pending of permissionQueueRef.current.splice(0))
2371
+ pending.resolve(decision);
2372
+ }
2373
+ const next = permissionQueueRef.current.shift();
2374
+ setQueuedPermissionCount(permissionQueueRef.current.length);
2375
+ if (next) {
2376
+ permissionResolveRef.current = next.resolve;
2377
+ permissionOpenedAtRef.current = Date.now();
2378
+ setPermissionDetailsOpen(!next.permission.summary.complete);
2379
+ setPermission(next.permission);
2380
+ setState('awaiting-permission');
2381
+ }
2307
2382
  }, [setPermissionDetailsOpen, setPermissionReviewOffset]);
2308
2383
  /**
2309
2384
  * Stop the running turn, and hand the screen back in a usable state.
@@ -2325,6 +2400,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2325
2400
  */
2326
2401
  const interruptTurn = useCallback(() => {
2327
2402
  const cancelledSwitch = cancelPendingModelSwitch();
2403
+ for (const pending of permissionQueueRef.current.splice(0)) {
2404
+ pending.resolve({ kind: 'reject', feedback: 'User interrupted.' });
2405
+ }
2406
+ setQueuedPermissionCount(0);
2328
2407
  if (permissionResolveRef.current)
2329
2408
  resolvePermission({ kind: 'reject', feedback: 'User interrupted.' });
2330
2409
  const ac = abortRef.current;
@@ -2445,6 +2524,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2445
2524
  lastCompletedOutputRef.current = persistedOutput
2446
2525
  ? { text: persistedOutput, provenance: 'persisted' }
2447
2526
  : null;
2527
+ pendingGoalResumeRef.current = conv.id;
2448
2528
  scope.sessionId = conv.id; // new turns now attribute to the resumed session
2449
2529
  conversationMaterializedRef.current = true;
2450
2530
  pushMessage('system', `Resumed: ${conv.title}`);
@@ -2859,6 +2939,14 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2859
2939
  }
2860
2940
  const summary = buildPermissionSummary(review.text);
2861
2941
  return new Promise((resolve) => {
2942
+ if (permissionResolveRef.current) {
2943
+ permissionQueueRef.current.push({
2944
+ permission: { ...req, review: review.text, summary },
2945
+ resolve,
2946
+ });
2947
+ setQueuedPermissionCount(permissionQueueRef.current.length);
2948
+ return;
2949
+ }
2862
2950
  permissionResolveRef.current = resolve;
2863
2951
  permissionOpenedAtRef.current = Date.now();
2864
2952
  setPermissionReviewOffset(0);
@@ -2937,6 +3025,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2937
3025
  }
2938
3026
  switch (event.kind) {
2939
3027
  case 'delta': {
3028
+ if (event.messageId && st.sourceMessageId !== event.messageId) {
3029
+ if (st.sourceMessageId !== undefined)
3030
+ closeAssistant();
3031
+ st.sourceMessageId = event.messageId;
3032
+ }
2940
3033
  setState('thinking');
2941
3034
  if (event.messageId && event.runId && st.sessionId) {
2942
3035
  lastAssistantMessage.current = {
@@ -2981,11 +3074,19 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2981
3074
  closeAssistant();
2982
3075
  setThinking(null);
2983
3076
  setState('tool');
3077
+ const waitingAgent = event.toolName === 'wait_for_task' && event.taskId
3078
+ ? subagentsRef.current.find((agent) => agent.taskId === event.taskId &&
3079
+ (event.runId === undefined || agent.workflowId === event.runId))
3080
+ : undefined;
2984
3081
  const tool = {
3082
+ ...(waitingAgent ? { taskId: waitingAgent.taskId } : {}),
2985
3083
  id: event.toolUseId,
2986
3084
  ...(event.runId ? { runId: event.runId } : {}),
2987
3085
  toolName: event.toolName,
2988
- label: formatToolCall(event.toolName, event.summary, event.standalone),
3086
+ activity: event.activity,
3087
+ label: waitingAgent
3088
+ ? `Waiting · ${waitingAgent.description}`
3089
+ : formatToolCall(event.toolName, event.summary, event.standalone),
2989
3090
  startedAt: Date.now(),
2990
3091
  detail: event.detail,
2991
3092
  };
@@ -3029,6 +3130,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3029
3130
  activeToolsRef.current = [...running.slice(0, i), ...running.slice(i + 1)];
3030
3131
  setActiveTools(activeToolsRef.current);
3031
3132
  }
3133
+ // Lifecycle updates own completion rows, even when the model never waits.
3134
+ // Keep unknown waits and errors visible; never hide an unrepresented task.
3135
+ const inspectableWait = done?.taskId && subagentsRef.current.some((agent) => agent.taskId === done.taskId &&
3136
+ (event.runId === undefined || agent.workflowId === event.runId) &&
3137
+ agent.transcript.some((row) => row.kind === 'assistant' && row.text.trim().length > 0));
3138
+ if (!event.isError && inspectableWait && event.toolName === 'wait_for_task') {
3139
+ setState(activeToolsRef.current.length > 0 ? 'tool' : 'thinking');
3140
+ break;
3141
+ }
3032
3142
  const representedSuccessfulAgent = !event.isError &&
3033
3143
  event.toolName.toLowerCase() === 'agent' &&
3034
3144
  subagentsRef.current.some((agent) => agent.toolUseId === event.toolUseId &&
@@ -3041,6 +3151,22 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3041
3151
  setState(activeToolsRef.current.length > 0 ? 'tool' : 'thinking');
3042
3152
  break;
3043
3153
  }
3154
+ if (!event.isError && done?.activity === 'exploration') {
3155
+ // Preserve the result, including the summary removed from detail by the adapter.
3156
+ const output = event.output !== undefined
3157
+ ? event.output.split('\n')
3158
+ : [...(event.summary ? [event.summary] : []), ...(event.detail ?? [])];
3159
+ pushMessage('tool', done.label, false, '└', output, theme.text.muted, output.length > 0 ? 'ctrl+o output' : undefined, 'exploration');
3160
+ setState(activeToolsRef.current.length > 0 ? 'tool' : 'thinking');
3161
+ break;
3162
+ }
3163
+ const catalogue = !event.isError && event.toolName === 'agent_models' && event.output !== undefined
3164
+ ? modelCatalogueView(event.output) : undefined;
3165
+ if (catalogue !== undefined) {
3166
+ pushMessage('tool', catalogue, false, '✓', event.output?.split('\n'), theme.status.ok, 'ctrl+o details', 'catalogue');
3167
+ setState(activeToolsRef.current.length > 0 ? 'tool' : 'thinking');
3168
+ break;
3169
+ }
3044
3170
  pushMessage('tool', done?.label ?? formatToolCall(event.toolName, event.summary), false, event.isError ? '✗' : '✓', done?.detail, event.isError ? theme.status.error : theme.status.ok, event.durationMs !== undefined
3045
3171
  ? formatElapsed(event.durationMs)
3046
3172
  : done
@@ -3636,6 +3762,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3636
3762
  abortRef.current = null;
3637
3763
  if (ownsTurn)
3638
3764
  activeTurnTokenRef.current = null;
3765
+ permissionResolveRef.current?.({ kind: 'reject', feedback: 'The owning turn ended.' });
3766
+ for (const pending of permissionQueueRef.current.splice(0))
3767
+ pending.resolve({ kind: 'reject', feedback: 'The owning turn ended.' });
3768
+ setQueuedPermissionCount(0);
3639
3769
  permissionResolveRef.current = null;
3640
3770
  permissionOpenedAtRef.current = null;
3641
3771
  setPermission(null);
@@ -3959,7 +4089,16 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3959
4089
  if (slash) {
3960
4090
  switch (slash.kind) {
3961
4091
  case 'message':
3962
- pushMessage(slash.role, slash.content);
4092
+ if (slash.statusRows) {
4093
+ setMessages((previous) => [...previous, {
4094
+ id: nextId(),
4095
+ role: slash.role,
4096
+ content: slash.content,
4097
+ statusRows: slash.statusRows,
4098
+ }]);
4099
+ }
4100
+ else
4101
+ pushMessage(slash.role, slash.content);
3963
4102
  return;
3964
4103
  case 'command-picker': {
3965
4104
  if (slash.commands.length === 0) {
@@ -3989,6 +4128,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3989
4128
  }
3990
4129
  return;
3991
4130
  }
4131
+ case 'provider-setup':
4132
+ if (state !== 'idle' || permission || choicePickerRef.current) {
4133
+ pushMessage('system', 'Provider setup is available once the active turn and prompts finish.');
4134
+ return;
4135
+ }
4136
+ setProviderSetup(true);
4137
+ return;
3992
4138
  case 'settings-picker': {
3993
4139
  const commands = [
3994
4140
  {
@@ -4010,16 +4156,22 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4010
4156
  description: `${permissionModeLabel(effectivePermissionMode(permissionModeRef.current, session?.approvalLatched() ?? false))} · this session.`,
4011
4157
  },
4012
4158
  {
4013
- name: 'status config',
4014
- label: 'Configuration',
4159
+ name: 'status',
4160
+ label: 'Web & session',
4161
+ description: `${session?.webSearchSummary ?? 'Not resolved'} · view tools, workspace and usage.`,
4162
+ },
4163
+ { name: 'setup', label: 'Provider setup', description: 'Check installed CLIs and access; connect or install without leaving Namzu.' },
4164
+ {
4165
+ name: 'config sources',
4166
+ label: 'Setting sources',
4015
4167
  description: 'Show which configuration files and overrides are in use.',
4016
4168
  },
4017
4169
  ];
4018
4170
  setSelectedChoice(0);
4019
4171
  setChoicePicker({
4020
4172
  kind: 'command',
4021
- title: 'Settings',
4022
- notice: 'Select a setting to view or change it.',
4173
+ title: 'Configuration',
4174
+ notice: 'Model, reasoning and permission changes apply to this session.',
4023
4175
  values: commands,
4024
4176
  options: commands.map((command) => ({
4025
4177
  label: command.label,
@@ -4779,6 +4931,19 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4779
4931
  return;
4780
4932
  }
4781
4933
  setGoalStatus(goal);
4934
+ if (pendingGoalResumeRef.current === sessionId && phase === 'ready' && !choicePickerRef.current) {
4935
+ pendingGoalResumeRef.current = null;
4936
+ if (goal && (goal.phase === 'active' || goal.phase === 'paused')) {
4937
+ setSelectedChoice(0);
4938
+ setChoicePicker({ kind: 'resume-goal', title: 'Continue the saved goal?',
4939
+ notice: goal.objective, sessionId, values: ['resume', 'later'],
4940
+ options: [
4941
+ { label: 'Resume goal', description: 'Continue automatic work toward this objective.' },
4942
+ { label: 'Not now', description: 'Keep automatic work stopped; resume later with /goal resume.' },
4943
+ ],
4944
+ });
4945
+ }
4946
+ }
4782
4947
  })
4783
4948
  .catch(() => {
4784
4949
  if (!disposed && conversationGenRef.current === generation)
@@ -4787,7 +4952,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4787
4952
  return () => {
4788
4953
  disposed = true;
4789
4954
  };
4790
- }, [goalDriveVersion, phase, session]);
4955
+ }, [goalDriveVersion, phase, session, setChoicePicker]);
4791
4956
  // Admit automatic work only at a whole-App durable boundary. This effect
4792
4957
  // reserves; it never starts a turn itself. The one queue pump below remains
4793
4958
  // the sole starter, which is what keeps a human prompt that arrives during
@@ -5049,6 +5214,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5049
5214
  exitWithSummary();
5050
5215
  }, [session, exitWithSummary]);
5051
5216
  useInput((input, key) => {
5217
+ if (outputViewer !== null)
5218
+ return;
5052
5219
  // Startup has refused admission, so there is no draft or active turn to
5053
5220
  // protect with the ready screen's two-press exit ladder.
5054
5221
  if (phase === 'unhealthy') {
@@ -5169,9 +5336,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5169
5336
  if (permission && (key.pageUp || key.pageDown || key.home || key.end)) {
5170
5337
  const source = permissionDetailsOpenRef.current
5171
5338
  ? permission.review
5172
- : permission.summary.text;
5339
+ : (permission.summary.compactText ?? permission.summary.text);
5173
5340
  const count = permissionReviewRows(source, terminal.columns).length;
5174
- const pageRows = permissionReviewPageRows(terminal.rows);
5341
+ const pageRows = Math.max(1, permissionReviewPageRows(terminal.rows) - (permission.runId ? 1 : 0));
5175
5342
  const maxOffset = Math.max(0, count - pageRows);
5176
5343
  const current = Math.min(permissionReviewOffsetRef.current, maxOffset);
5177
5344
  const next = key.home
@@ -5187,8 +5354,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5187
5354
  // Refusing is never deferred or gated — it is the direction a
5188
5355
  // mistake is recoverable in, so it answers on the first press.
5189
5356
  if (key.ctrl && input === 'c') {
5190
- resolvePermission({ kind: 'reject', feedback: 'User interrupted.' });
5191
- abortRef.current?.abort(new RunCancelled('user'));
5357
+ interruptTurn();
5192
5358
  return;
5193
5359
  }
5194
5360
  if (ch === 'n' || key.escape) {
@@ -5508,64 +5674,27 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5508
5674
  pushMessage('system', 'Interrupted.');
5509
5675
  return;
5510
5676
  }
5511
- // Ctrl+O expands the collapsed bodies that are still redrawable.
5512
- //
5513
- // It was advertised — on every collapsed body — as toggling full
5514
- // expansion for everything, and in that use it did nothing: finalized
5515
- // rows went through `<Static>`, which renders `items.slice(index)` and
5516
- // calls the render function only for items it has not emitted yet, so
5517
- // output already on screen was beyond its reach. Measured, with a
5518
- // twelve-line body up: pressing it produced one further frame whose
5519
- // transcript region was byte-identical to the one before.
5520
- //
5521
- // The rows at the end of the transcript are now drawn live rather than
5522
- // printed once, so for those the key does what it always claimed —
5523
- // flipping the flag re-renders the row where it already is. It reaches
5524
- // exactly as far back as the window does, and says so when that is
5525
- // nowhere. It does not quietly fall back to appending a copy: `/expand`
5526
- // is that, deliberately and visibly, and a key that sometimes redraws
5527
- // in place and sometimes prints a second copy further down would be two
5528
- // commands wearing one name.
5677
+ // Small expansions stay in place. Older or oversized bodies open a viewer.
5529
5678
  if (key.ctrl && input === 'o') {
5530
- // Taken outside the updater so both branches use the same one.
5531
- const id = nextId();
5532
- setMessages((prev) => {
5533
- const live = prev.filter((m) => !m.pending).slice(settledRef.current);
5534
- const collapsible = live.filter((m) => willCollapse(m.detail));
5535
- if (collapsible.length === 0) {
5536
- // Nothing redrawable is collapsed, so this reprints the most recent
5537
- // collapsed body as a new row — the rows above have been printed
5538
- // to the terminal's own scrollback, which cannot be rewritten.
5539
- const blocks = prev.filter((m) => m.detailRef !== undefined);
5540
- const block = blocks[blocks.length - 1];
5541
- if (!block) {
5542
- return [
5543
- ...prev,
5544
- {
5545
- id,
5546
- role: 'system',
5547
- content: 'Nothing to expand yet. Tool output longer than six lines collapses with a "… +N lines · ctrl+o" hint; Ctrl+O opens it in place while it is still on screen, and reprints the most recent one in full once it has scrolled away.',
5548
- },
5549
- ];
5550
- }
5551
- return [
5552
- ...prev,
5553
- {
5554
- id,
5555
- role: 'system',
5556
- content: `${block.content} — in full (${block.detail?.length ?? 0} lines)`,
5557
- glyph: '⤢',
5558
- detail: block.detail,
5559
- detailExpanded: true,
5560
- },
5561
- ];
5562
- }
5563
- // Expanded unless they are all open already, in which case this
5564
- // closes them again — the toggle it was always described as.
5565
- const expanding = collapsible.some((m) => m.detailExpanded !== true);
5566
- const ids = new Set(collapsible.map((m) => m.id));
5567
- return prev.map((m) => (ids.has(m.id) ? { ...m, detailExpanded: expanding } : m));
5568
- });
5679
+ const live = messages.filter((m) => !m.pending).slice(settledRef.current);
5680
+ const collapsible = live.filter((m) => (m.activity && (m.detail?.length ?? 0) > 0) || willCollapse(m.detail));
5681
+ const blocks = messages.filter((m) => m.detailRef !== undefined && (m.detail?.length ?? 0) > 0);
5682
+ const block = collapsible.at(-1) ?? blocks.at(-1);
5683
+ if (!block) {
5684
+ pushMessage('system', 'Nothing to expand yet. Ctrl+O opens retained tool output when available.');
5685
+ return;
5686
+ }
5687
+ const expanding = collapsible.some((m) => m.detailExpanded !== true);
5688
+ const ids = new Set(collapsible.map((m) => m.id));
5689
+ const proposed = messages.map((m) => ids.has(m.id) ? { ...m, detailExpanded: expanding } : m);
5690
+ const projected = liveWindow({ messages: proposed.filter((m) => !m.pending), rows: terminal.rows,
5691
+ columns: terminal.columns, furnitureRows: LIVE_FURNITURE_ROWS + taskFurniture + toolFurniture,
5692
+ settled: settledRef.current, raw: rawOutput });
5693
+ if (collapsible.length === 0 || (expanding && projected.settled > settledRef.current)) {
5694
+ setOutputViewer(block);
5695
+ }
5696
+ else
5697
+ setMessages(proposed);
5569
5698
  return;
5570
5699
  }
5571
5700
  if (key.ctrl && input === 'c') {
@@ -5590,7 +5719,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5590
5719
  // Active on every phase. It was gated off during the picker, which is what
5591
5720
  // left that screen without a usable exit; the picker branch above returns
5592
5721
  // immediately, so the Picker still owns everything except Ctrl+C.
5593
- { isActive: externalEditorRequest === null });
5722
+ { isActive: externalEditorRequest === null && !providerSetup });
5594
5723
  // Background is left natural — we inherit the terminal's own background
5595
5724
  // and only theme the foreground. Forcing
5596
5725
  // a filled bg left mismatched patches around bordered areas, so we don't.
@@ -5607,10 +5736,15 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5607
5736
  !(tool.runId
5608
5737
  ? representedSubagentTools.has(JSON.stringify([tool.runId, tool.id]))
5609
5738
  : representedUnscopedSubagentToolUseIds.has(tool.id)));
5739
+ const outputBlocks = outputViewer
5740
+ ? messages.filter((m) => m.detailRef !== undefined && (m.detail?.length ?? 0) > 0)
5741
+ : [];
5742
+ const outputIndex = outputBlocks.findIndex((m) => m.id === outputViewer?.id);
5610
5743
  const selectedSubagent = agentSurface
5611
5744
  ? subagents.find((agent) => agent.viewId === agentSurface.selectedId)
5612
5745
  : undefined;
5613
- const lifecycleOwnsViewport = phase === 'trust' ||
5746
+ const lifecycleOwnsViewport = outputViewer !== null ||
5747
+ phase === 'trust' ||
5614
5748
  phase === 'resume' ||
5615
5749
  phase === 'edit' ||
5616
5750
  phase === 'picker' ||
@@ -5624,7 +5758,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5624
5758
  choicePicker === null &&
5625
5759
  copyPicker === null &&
5626
5760
  agentSurface === null
5627
- ? goalStatusLabel(goalStatus)
5761
+ ? goalStatusLabel(goalStatus, goalStatus ? goalActivation.isArmed(goalStatus.sessionId, goalStatus) : false)
5628
5762
  : null;
5629
5763
  const statusHint = conversationMutation === 'fork'
5630
5764
  ? 'forking conversation — input is paused'
@@ -5657,8 +5791,21 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5657
5791
  : goalStatus && phase === 'ready' && state === 'idle'
5658
5792
  ? undefined
5659
5793
  : hintForPhase(phase, state, session?.hasProvider === true, composerHasDraft);
5794
+ const permissionOwner = permission?.runId
5795
+ ? subagents.find((agent) => agent.runId === permission.runId)
5796
+ : undefined;
5797
+ const permissionSourceLabel = permission?.runId
5798
+ ? permissionOwner ? `Agent: ${permissionOwner.description}` : `Run: ${permission.runId}`
5799
+ : undefined;
5660
5800
  const displayedPermissionMode = effectivePermissionMode(permissionMode, session?.approvalLatched() ?? false);
5661
- 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 }) }), phase === 'trust' ? (_jsx(TrustPrompt, { cwd: ctx.cwd })) : phase === 'unhealthy' ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: theme.status.error, bold: true, children: "Startup stopped" }), _jsx(Text, { color: theme.text.secondary, children: "Resolve the error above, then restart Namzu." })] })) : 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, { compact: compactWork, activeTools: visibleActiveTools, working: state === 'thinking' || state === 'tool', interruptible: abortRef.current !== null, animate: stdout.isTTY === true, thinking: thinking })) : null, agentSurface === null && permission === null ? (_jsx(TaskList, { tasks: tasks, compact: compactWork })) : null, permission ? (_jsx(PermissionOverlay, { toolCalls: permission.toolCalls, review: permission.review, summary: permission.summary, detailsOpen: permissionDetailsOpen, reviewOffset: permissionReviewOffset, choice: permissionChoice, 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, onSubmit: submitTextPrompt, onCancel: cancelTextPrompt }, textPrompt.token)) : permission === null && agentSurface === null && choicePicker ? (_jsx(ChoicePicker, { 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 && copyPicker ? (_jsx(CopyPicker, { targets: copyPicker.targets, selected: selectedCopy })) : null, _jsxs(ComposerFrame, { working: state === 'thinking' || state === 'tool' || visibleActiveTools.length > 0, focus: phase === 'ready' &&
5801
+ 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: () => {
5802
+ setProviderSetup(false);
5803
+ setPickerInitialView('providers');
5804
+ setPickerDetected(null);
5805
+ setPickerSelectionKind('provider-and-model');
5806
+ setPhase('picker');
5807
+ void discoverProviders({ skipProbes: true }).then(setPickerDetected).catch((error) => setPickerNotice(String(error)));
5808
+ } })) : phase === 'trust' ? (_jsx(TrustPrompt, { cwd: ctx.cwd })) : phase === 'unhealthy' ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: theme.status.error, bold: true, children: "Startup stopped" }), _jsx(Text, { color: theme.text.secondary, children: "Resolve the error above, then restart Namzu." })] })) : 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, onSetup: () => setProviderSetup(true), onCredential: handleTypedCredential, onLogin: startLoginFromPicker, onLoginComplete: finishLoginFromPicker, keyEntryFor: keyEntryFor, notice: pickerNotice })) : (_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, thinking: thinking })) : null, agentSurface === null && outputViewer === null && permission === null ? (_jsx(TaskList, { tasks: tasks, compact: compactWork })) : 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, { 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' &&
5662
5809
  state !== 'awaiting-permission' &&
5663
5810
  !compacting &&
5664
5811
  externalEditorRequest === null &&
@@ -5666,41 +5813,48 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5666
5813
  textPrompt === null &&
5667
5814
  choicePicker === null &&
5668
5815
  copyPicker === null &&
5669
- agentSurface === null, hidden: permission !== null ||
5816
+ agentSurface === null && outputViewer === null, hidden: permission !== null ||
5670
5817
  textPrompt !== null ||
5671
5818
  choicePicker !== null ||
5672
5819
  copyPicker !== null ||
5673
- agentSurface?.kind === 'transcript', children: [pendingSteers.length > 0 &&
5820
+ (agentSurface !== null || outputViewer !== null), children: [pendingSteers.length > 0 &&
5674
5821
  permission === null &&
5675
5822
  textPrompt === null &&
5676
5823
  choicePicker === null &&
5677
5824
  copyPicker === null &&
5678
- agentSurface === null ? (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: theme.accent.user, children: ["\u21B3 ", pendingSteers.length, " message", pendingSteers.length > 1 ? 's' : '', " steering the active turn \u2014 waiting for its next response boundary"] }) })) : null, queued.length > 0 &&
5825
+ agentSurface === null && outputViewer === null ? (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: theme.accent.user, children: ["\u21B3 ", pendingSteers.length, " message", pendingSteers.length > 1 ? 's' : '', " steering the active turn \u2014 waiting for its next response boundary"] }) })) : null, queued.length > 0 &&
5679
5826
  permission === null &&
5680
5827
  textPrompt === null &&
5681
5828
  choicePicker === null &&
5682
5829
  copyPicker === null &&
5683
- agentSurface === null ? (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: theme.text.muted, children: [queuePause ? '⏸' : '⏎', " ", queued.length, " message", queued.length > 1 ? 's' : '', " queued \u2014", ' ', queuePause
5830
+ agentSurface === null && outputViewer === null ? (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: theme.text.muted, children: [queuePause ? '⏸' : '⏎', " ", queued.length, " message", queued.length > 1 ? 's' : '', " queued \u2014", ' ', queuePause
5684
5831
  ? queuePause.outcome === 'paused'
5685
5832
  ? 'held after a resumable run paused; wait for recovery, change model, or send a message to release it'
5686
5833
  : `paused after a ${queuePause.outcome} turn; send a message or change model to continue`
5687
- : 'sending when ready'] }) })) : null, _jsx(Composer, { reasoningEffortLevels: session?.reasoningEffortLevels, permissionMode: displayedPermissionMode, onCycleMode: cyclePermissionMode, disabled: phase !== 'ready' ||
5834
+ : 'sending when ready'] }) })) : null, _jsx(Composer, { reasoningEffortLevels: session?.reasoningEffortLevels, permissionMode: displayedPermissionMode, onCycleMode: cyclePermissionMode, disabled: outputViewer !== null ||
5835
+ phase !== 'ready' ||
5688
5836
  state === 'awaiting-permission' ||
5689
5837
  compacting ||
5690
5838
  conversationMutation !== null ||
5691
5839
  externalEditorRequest !== null ||
5692
- agentSurface !== null, hidden: permission !== null ||
5840
+ (agentSurface !== null || outputViewer !== null), hidden: permission !== null ||
5693
5841
  textPrompt !== null ||
5694
5842
  choicePicker !== null ||
5695
5843
  copyPicker !== null ||
5696
- agentSurface?.kind === 'transcript',
5844
+ (agentSurface !== null || outputViewer !== null),
5697
5845
  // A turn is running, so Esc is the interrupt and not
5698
5846
  // the composer's clear.
5699
5847
  escapeInterrupts: !compacting &&
5700
5848
  (state === 'thinking' || state === 'tool' || pendingModelSwitchRef.current !== null), onSubmit: handleSubmit, onNotice: (text) => pushMessage('system', text), onStepReasoningEffort: stepReasoningEffort, onExternalEdit: requestExternalEditor, onEditPrevious: openPromptEditor, draftToRestore: composerDraft, onDraftRestored: (token) => setComposerDraft((draft) => (draft?.token === token ? null : draft)), onDraftPresenceChange: setComposerHasDraft, onOpenAgentPanel: openAgentCockpit, userCommands: userCommands, builtins: hostCommands.map((command) => ({
5701
5849
  ...command,
5702
5850
  description: command.unavailable?.(slashCtx) ?? command.description,
5703
- })), mentionCandidates: mentionCandidates, history: history })] }), permission === null && agentSurface === null && liveSubagents.length > 0 ? (_jsx(AgentTaskPanel, { agents: liveSubagents, terminalRows: terminal.rows, terminalColumns: terminal.columns })) : permission === null && agentSurface?.kind === 'cockpit' ? (_jsx(AgentCockpit, { agents: subagents, selectedPhaseId: agentSurface.selectedPhaseId, selectedId: agentSurface.selectedId, focus: agentSurface.focus, terminalRows: terminal.rows, terminalColumns: terminal.columns })) : permission === null && agentSurface?.kind === 'transcript' && selectedSubagent ? (_jsx(AgentTranscript, { agent: selectedSubagent, tailOffset: agentSurface.tailOffset, terminalRows: terminal.rows, terminalColumns: terminal.columns })) : null] })), _jsx(Box, { paddingTop: 1, children: _jsx(StatusBar, { cwd: ctx.cwd, provider: session?.providerSummary ?? null, model: session?.modelSummary ?? null, effort: reasoningEffort ?? 'default', goal: statusGoal, state: state, hint: statusHint }) })] }) }));
5851
+ })), mentionCandidates: mentionCandidates, history: history })] }), permission === null && agentSurface === null && outputViewer === null && liveSubagents.length > 0 ? (_jsx(AgentTaskPanel, { agents: liveSubagents, terminalRows: terminal.rows, terminalColumns: terminal.columns })) : permission === null && agentSurface?.kind === 'cockpit' ? (_jsx(AgentCockpit, { agents: subagents, selectedPhaseId: agentSurface.selectedPhaseId, selectedId: agentSurface.selectedId, focus: agentSurface.focus, terminalRows: terminal.rows, terminalColumns: terminal.columns })) : permission === null && agentSurface?.kind === 'transcript' && selectedSubagent ? (_jsx(AgentTranscript, { agent: selectedSubagent, tailOffset: agentSurface.tailOffset, terminalRows: terminal.rows, terminalColumns: terminal.columns })) : null] })), outputViewer && permission === null ? (_jsx(ToolOutputViewer, { onPrevious: () => {
5852
+ if (outputIndex > 0)
5853
+ setOutputViewer(outputBlocks[outputIndex - 1]);
5854
+ }, onNext: () => {
5855
+ if (outputIndex >= 0 && outputIndex + 1 < outputBlocks.length)
5856
+ setOutputViewer(outputBlocks[outputIndex + 1]);
5857
+ }, title: outputViewer.content, lines: outputViewer.detail ?? [], rows: terminal.rows, columns: terminal.columns, onClose: () => setOutputViewer(null) }, outputViewer.id)) : null, _jsx(Box, { paddingTop: 1, children: _jsx(StatusBar, { cwd: ctx.cwd, provider: session?.providerSummary ?? null, model: session?.modelSummary ?? null, effort: reasoningEffort ?? 'default', goal: statusGoal, state: state, hint: statusHint }) })] }) }));
5704
5858
  }
5705
5859
  /** One line for a background job that ended, for the transcript and the next turn. */
5706
5860
  function describeJobExit(job) {
@@ -5759,12 +5913,12 @@ hasDraft = false) {
5759
5913
  // screen; only state-specific actions belong in the footer.
5760
5914
  return '';
5761
5915
  }
5762
- function goalStatusLabel(goal) {
5916
+ function goalStatusLabel(goal, armed) {
5763
5917
  if (!goal)
5764
5918
  return null;
5765
5919
  switch (goal.phase) {
5766
5920
  case 'active':
5767
- return 'Pursuing goal';
5921
+ return armed ? 'Pursuing goal' : 'Goal paused (/goal resume)';
5768
5922
  case 'paused':
5769
5923
  return 'Goal paused (/goal resume)';
5770
5924
  case 'blocked':