@namzu/cli 32.0.0 → 33.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 (175) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/dist/browser/commands.js +11 -7
  3. package/dist/browser/commands.js.map +1 -1
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +21 -1
  6. package/dist/cli.js.map +1 -1
  7. package/dist/commands/archive.d.ts +4 -0
  8. package/dist/commands/archive.d.ts.map +1 -0
  9. package/dist/commands/archive.js +65 -0
  10. package/dist/commands/archive.js.map +1 -0
  11. package/dist/commands/drain.js +3 -3
  12. package/dist/commands/drain.js.map +1 -1
  13. package/dist/commands/exec.js +1 -1
  14. package/dist/commands/exec.js.map +1 -1
  15. package/dist/commands/login.d.ts +1 -1
  16. package/dist/commands/login.d.ts.map +1 -1
  17. package/dist/commands/login.js +33 -11
  18. package/dist/commands/login.js.map +1 -1
  19. package/dist/commands/mcp-oauth-login.d.ts +19 -0
  20. package/dist/commands/mcp-oauth-login.d.ts.map +1 -0
  21. package/dist/commands/mcp-oauth-login.js +213 -0
  22. package/dist/commands/mcp-oauth-login.js.map +1 -0
  23. package/dist/commands/mcp.d.ts +3 -0
  24. package/dist/commands/mcp.d.ts.map +1 -0
  25. package/dist/commands/mcp.js +288 -0
  26. package/dist/commands/mcp.js.map +1 -0
  27. package/dist/commands/schedule.d.ts.map +1 -1
  28. package/dist/commands/schedule.js +5 -2
  29. package/dist/commands/schedule.js.map +1 -1
  30. package/dist/commands/skills.d.ts.map +1 -1
  31. package/dist/commands/skills.js +60 -5
  32. package/dist/commands/skills.js.map +1 -1
  33. package/dist/commands/types.d.ts +2 -0
  34. package/dist/commands/types.d.ts.map +1 -1
  35. package/dist/commands/worktree.d.ts +6 -0
  36. package/dist/commands/worktree.d.ts.map +1 -0
  37. package/dist/commands/worktree.js +86 -0
  38. package/dist/commands/worktree.js.map +1 -0
  39. package/dist/config/user-config.d.ts +2 -0
  40. package/dist/config/user-config.d.ts.map +1 -1
  41. package/dist/config/user-config.js +76 -6
  42. package/dist/config/user-config.js.map +1 -1
  43. package/dist/doctor/checks/credentials.d.ts.map +1 -1
  44. package/dist/doctor/checks/credentials.js +2 -0
  45. package/dist/doctor/checks/credentials.js.map +1 -1
  46. package/dist/integrations/mcp/oauth-store.d.ts +40 -0
  47. package/dist/integrations/mcp/oauth-store.d.ts.map +1 -0
  48. package/dist/integrations/mcp/oauth-store.js +549 -0
  49. package/dist/integrations/mcp/oauth-store.js.map +1 -0
  50. package/dist/integrations/mcp/servers.d.ts.map +1 -1
  51. package/dist/integrations/mcp/servers.js +155 -2
  52. package/dist/integrations/mcp/servers.js.map +1 -1
  53. package/dist/integrations/mcp/user-config.d.ts +25 -0
  54. package/dist/integrations/mcp/user-config.d.ts.map +1 -0
  55. package/dist/integrations/mcp/user-config.js +92 -0
  56. package/dist/integrations/mcp/user-config.js.map +1 -0
  57. package/dist/integrations/providers/credential-store.d.ts +8 -0
  58. package/dist/integrations/providers/credential-store.d.ts.map +1 -1
  59. package/dist/integrations/providers/credential-store.js +111 -22
  60. package/dist/integrations/providers/credential-store.js.map +1 -1
  61. package/dist/integrations/providers/discover.d.ts +11 -5
  62. package/dist/integrations/providers/discover.d.ts.map +1 -1
  63. package/dist/integrations/providers/discover.js +26 -12
  64. package/dist/integrations/providers/discover.js.map +1 -1
  65. package/dist/integrations/providers/index.d.ts +1 -1
  66. package/dist/integrations/providers/index.d.ts.map +1 -1
  67. package/dist/integrations/providers/index.js +1 -1
  68. package/dist/integrations/providers/index.js.map +1 -1
  69. package/dist/integrations/sessions/display-title.d.ts +3 -0
  70. package/dist/integrations/sessions/display-title.d.ts.map +1 -0
  71. package/dist/integrations/sessions/display-title.js +5 -0
  72. package/dist/integrations/sessions/display-title.js.map +1 -0
  73. package/dist/integrations/sessions/store.d.ts +23 -5
  74. package/dist/integrations/sessions/store.d.ts.map +1 -1
  75. package/dist/integrations/sessions/store.js +164 -24
  76. package/dist/integrations/sessions/store.js.map +1 -1
  77. package/dist/integrations/subagents/runtime.d.ts +4 -2
  78. package/dist/integrations/subagents/runtime.d.ts.map +1 -1
  79. package/dist/integrations/subagents/runtime.js +62 -8
  80. package/dist/integrations/subagents/runtime.js.map +1 -1
  81. package/dist/integrations/subagents/worktree-driver.d.ts +17 -0
  82. package/dist/integrations/subagents/worktree-driver.d.ts.map +1 -0
  83. package/dist/integrations/subagents/worktree-driver.js +72 -0
  84. package/dist/integrations/subagents/worktree-driver.js.map +1 -0
  85. package/dist/integrations/worktrees/managed.d.ts +49 -0
  86. package/dist/integrations/worktrees/managed.d.ts.map +1 -0
  87. package/dist/integrations/worktrees/managed.js +231 -0
  88. package/dist/integrations/worktrees/managed.js.map +1 -0
  89. package/dist/schedule/build.d.ts +2 -0
  90. package/dist/schedule/build.d.ts.map +1 -1
  91. package/dist/schedule/build.js +11 -2
  92. package/dist/schedule/build.js.map +1 -1
  93. package/dist/schedule/commands/add.d.ts +1 -1
  94. package/dist/schedule/commands/add.d.ts.map +1 -1
  95. package/dist/schedule/commands/add.js +12 -2
  96. package/dist/schedule/commands/add.js.map +1 -1
  97. package/dist/schedule/commands/list.d.ts.map +1 -1
  98. package/dist/schedule/commands/list.js +10 -1
  99. package/dist/schedule/commands/list.js.map +1 -1
  100. package/dist/schedule/daemon/daemon.js +1 -1
  101. package/dist/schedule/daemon/daemon.js.map +1 -1
  102. package/dist/schedule/fire/fire.d.ts.map +1 -1
  103. package/dist/schedule/fire/fire.js +6 -1
  104. package/dist/schedule/fire/fire.js.map +1 -1
  105. package/dist/schedule/script-check.d.ts.map +1 -1
  106. package/dist/schedule/script-check.js +2 -1
  107. package/dist/schedule/script-check.js.map +1 -1
  108. package/dist/skills/audit.d.ts +31 -0
  109. package/dist/skills/audit.d.ts.map +1 -0
  110. package/dist/skills/audit.js +75 -0
  111. package/dist/skills/audit.js.map +1 -0
  112. package/dist/tui/App.d.ts.map +1 -1
  113. package/dist/tui/App.js +355 -97
  114. package/dist/tui/App.js.map +1 -1
  115. package/dist/tui/BrandHeader.js +1 -1
  116. package/dist/tui/BrandHeader.js.map +1 -1
  117. package/dist/tui/Composer.d.ts.map +1 -1
  118. package/dist/tui/Composer.js +6 -11
  119. package/dist/tui/Composer.js.map +1 -1
  120. package/dist/tui/Picker.d.ts.map +1 -1
  121. package/dist/tui/Picker.js +18 -3
  122. package/dist/tui/Picker.js.map +1 -1
  123. package/dist/tui/ResumePicker.d.ts +2 -1
  124. package/dist/tui/ResumePicker.d.ts.map +1 -1
  125. package/dist/tui/ResumePicker.js +5 -4
  126. package/dist/tui/ResumePicker.js.map +1 -1
  127. package/dist/tui/SaveSkillOverlay.js +1 -1
  128. package/dist/tui/SaveSkillOverlay.js.map +1 -1
  129. package/dist/tui/ScheduleReviewOverlay.d.ts +27 -0
  130. package/dist/tui/ScheduleReviewOverlay.d.ts.map +1 -0
  131. package/dist/tui/ScheduleReviewOverlay.js +166 -0
  132. package/dist/tui/ScheduleReviewOverlay.js.map +1 -0
  133. package/dist/tui/Transcript.js +2 -2
  134. package/dist/tui/Transcript.js.map +1 -1
  135. package/dist/tui/agent.d.ts +1 -1
  136. package/dist/tui/agent.d.ts.map +1 -1
  137. package/dist/tui/agent.js +15 -6
  138. package/dist/tui/agent.js.map +1 -1
  139. package/dist/tui/browser-notices.d.ts.map +1 -1
  140. package/dist/tui/browser-notices.js +6 -1
  141. package/dist/tui/browser-notices.js.map +1 -1
  142. package/dist/tui/credential-entry.d.ts +4 -29
  143. package/dist/tui/credential-entry.d.ts.map +1 -1
  144. package/dist/tui/credential-entry.js +7 -29
  145. package/dist/tui/credential-entry.js.map +1 -1
  146. package/dist/tui/login-prompt.d.ts +3 -1
  147. package/dist/tui/login-prompt.d.ts.map +1 -1
  148. package/dist/tui/login-prompt.js +11 -0
  149. package/dist/tui/login-prompt.js.map +1 -1
  150. package/dist/tui/provider-list.d.ts.map +1 -1
  151. package/dist/tui/provider-list.js +3 -1
  152. package/dist/tui/provider-list.js.map +1 -1
  153. package/dist/tui/schedule/host-commands.d.ts +3 -0
  154. package/dist/tui/schedule/host-commands.d.ts.map +1 -1
  155. package/dist/tui/schedule/host-commands.js +72 -15
  156. package/dist/tui/schedule/host-commands.js.map +1 -1
  157. package/dist/tui/schedule/integration.d.ts +2 -0
  158. package/dist/tui/schedule/integration.d.ts.map +1 -1
  159. package/dist/tui/schedule/integration.js +3 -0
  160. package/dist/tui/schedule/integration.js.map +1 -1
  161. package/dist/tui/schedule/resume.js +2 -2
  162. package/dist/tui/schedule/resume.js.map +1 -1
  163. package/dist/tui/schedule/tool-host.d.ts +3 -0
  164. package/dist/tui/schedule/tool-host.d.ts.map +1 -1
  165. package/dist/tui/schedule/tool-host.js +34 -9
  166. package/dist/tui/schedule/tool-host.js.map +1 -1
  167. package/dist/tui/slashCommands.d.ts +10 -1
  168. package/dist/tui/slashCommands.d.ts.map +1 -1
  169. package/dist/tui/slashCommands.js +17 -4
  170. package/dist/tui/slashCommands.js.map +1 -1
  171. package/dist/tui/turn-interruption.d.ts.map +1 -1
  172. package/dist/tui/turn-interruption.js +4 -0
  173. package/dist/tui/turn-interruption.js.map +1 -1
  174. package/package.json +6 -5
  175. package/skills/schedule-task/SKILL.md +41 -15
package/dist/tui/App.js CHANGED
@@ -19,15 +19,17 @@ import { ToolOutputViewer } from './ToolOutputViewer.js';
19
19
  import { join, relative } from 'node:path';
20
20
  import { DEFAULT_MAX_GOAL_ROUNDS, DiskMessageFeedbackStore, GoalRoundLimitError, HostCommandRegistry, SessionGoalActivation, StaleGoalError, TurnCancelled, asSessionId, createAssistantMessage, createUserMessage, generateSessionId, generateTurnId, isCompactionMessage, kernelHostCommands, SKILL_TOOL_NAME, } from '@namzu/sdk';
21
21
  import { Box, Text, useApp, useInput, useStdout, useWindowSize } from 'ink';
22
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
22
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
23
23
  import { pinTrustedProjectPath, resolveTrustedProjectContext, } from '../config/trusted-project-context.js';
24
24
  import { visibleProjectInstructionPath } from '../context/project-path.js';
25
25
  import { runtimeContextLabel } from '../context/runtime-message.js';
26
26
  import { writeClipboardText } from '../integrations/clipboard/text.js';
27
27
  import { terminalNotificationEnabled, writeTerminalNotification, } from '../integrations/notifications/terminal.js';
28
- import { beginCodexDeviceLogin, beginSubscriptionLogin, clearAllStoredCredentials, clearStoredCodexCredential, clearStoredSubscriptionCredential, credentialsPath, parsePastedInput, primaryProvider, readStoredCodexCredential, readStoredSubscriptionCredential, signedInSubscriptionProviders, writePreferences, } from '../integrations/providers/index.js';
29
- import { activeConversationTurn, archiveConversation, forkConversation, forkConversationBeforeUser, listRecent, loadConversation, loadResumableConversation, openSessions, requireWritableConversation, setTitle, startConversation, titleOf, } from '../integrations/sessions/store.js';
28
+ import { beginCodexDeviceLogin, beginSubscriptionLogin, clearAllStoredCredentials, clearStoredCodexCredential, clearStoredGeminiApiKey as clearStoredGoogleApiKey, clearStoredSubscriptionCredential, credentialsPath, googleApiKeyPath, parsePastedInput, primaryProvider, readStoredCodexCredential, readStoredGeminiApiKey as readStoredGoogleApiKey, readStoredSubscriptionCredential, signedInSubscriptionProviders, writePreferences, writeStoredGeminiApiKey as writeStoredGoogleApiKey, } from '../integrations/providers/index.js';
29
+ import { ConversationIndexRefreshError, activeConversationTurn, archiveConversation, forkConversation, forkConversationBeforeUser, listArchived, listRecent, loadConversation, loadResumableConversation, openSessions, requireWritableConversation, setTitle, startConversation, titleOf, unarchiveConversation, } from '../integrations/sessions/store.js';
30
30
  import { conversationMarkdown, writeConversationExport, } from '../integrations/sessions/transcript-export.js';
31
+ import { displayConversationTitle } from '../integrations/sessions/display-title.js';
32
+ import { openManagedWorktrees, worktreeOpenHint } from '../integrations/worktrees/managed.js';
31
33
  import { NARRATION_TOOL_NAME } from '../integrations/subagents/runtime.js';
32
34
  import { agentsSlashCommand } from '../integrations/subagents/slash.js';
33
35
  import { isTrusted, trustDir } from '../integrations/trust/store.js';
@@ -61,6 +63,7 @@ import { Picker } from './Picker.js';
61
63
  import { ResumePicker } from './ResumePicker.js';
62
64
  import { resolveNamzuHome } from '../integrations/state/home.js';
63
65
  import { createScheduleIntegration } from './schedule/integration.js';
66
+ import { ScheduleReviewOverlay, } from './ScheduleReviewOverlay.js';
64
67
  import { SaveSkillOverlay } from './SaveSkillOverlay.js';
65
68
  import { SKILL_CREATOR_SKILL, buildSaveSkillTool, learnSkillPrompt, newSkillPrompt, } from '../skills/save.js';
66
69
  import { SUGGESTIONS_STOPPED_NOTICE, createTurnActivity, judgeSkillSuggestion, observeTurnEvent, skillSuggestionNotice, } from './skills/learning.js';
@@ -88,7 +91,7 @@ import { checklistInView, liveWindow, settledBeforeStreaming } from './live-wind
88
91
  import { ViewportBound } from './ViewportBound.js';
89
92
  import { resolveModelSwitch, } from './model-switch.js';
90
93
  import { parseModelSelectionIntent, resolveModelSelectionIntent } from './model-selection-intent.js';
91
- import { describeCodexDeviceLoginStart, describeLoginOutcome, describeLoginStart, describeLogout, describeProviderLogout, } from './login-prompt.js';
94
+ import { describeCodexDeviceLoginStart, describeLoginOutcome, describeLoginStart, describeAllCredentialsLogout, describeProviderLogout, } from './login-prompt.js';
92
95
  import { selectPrimaryProvider } from './provider-selection.js';
93
96
  import { eraseLastChoiceGrapheme } from './terminal-choice-text.js';
94
97
  import { renderCheckpoints, renderRestore } from '../checkpoints/store.js';
@@ -101,6 +104,7 @@ import { moveSelection } from './selection-window.js';
101
104
  import { describeShellEscape, describeShellEscapeForModel, runShellEscape, shellEscapeCommand, } from './shell-escape.js';
102
105
  import { baseBranchReviewPrompt, commitReviewPrompt, hostCommandNames, kernelCommandDescriptors, mergeHostCommands, renderAgents, renderOutcome, reviewPrompt, runSlash, } from './slashCommands.js';
103
106
  import { splitCompleteBlocks, splitSafeCut } from './stream-blocks.js';
107
+ import { terminalDisplayText } from './terminal-display.js';
104
108
  import { terminalSupportsHyperlinks } from './terminal-hyperlinks.js';
105
109
  import { theme } from './theme.js';
106
110
  import { useSelectionIndex } from './use-selection-index.js';
@@ -488,7 +492,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
488
492
  ...(job.signal ? { signal: job.signal } : {}),
489
493
  });
490
494
  idleJobNoticesRef.current.push(text);
491
- pushMessage('system', text, false, '⚙');
495
+ pushMessage('system', text, false, 'J');
492
496
  });
493
497
  }, [session]);
494
498
  const [usage, setUsage] = useState(null);
@@ -622,7 +626,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
622
626
  choicePickerCommittedRef.current = null;
623
627
  setChoicePickerState(next);
624
628
  }, []);
625
- useEffect(() => {
629
+ // A painted menu must already accept its first key. A passive effect can
630
+ // run after Ink flushes that frame, leaving the ownership fence closed.
631
+ useLayoutEffect(() => {
626
632
  choicePickerCommittedRef.current = choicePicker;
627
633
  }, [choicePicker]);
628
634
  /** Host-owned text decision; its value never enters model prompt history. */
@@ -988,6 +994,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
988
994
  isIdle: () => stateRef.current === 'idle' && abortRef.current === null,
989
995
  say: (text) => scheduleLiveRef.current?.say(text),
990
996
  ask: (question, signal) => scheduleLiveRef.current?.ask(question, signal) ?? Promise.resolve({ kind: 'skip' }),
997
+ review: (request, signal) => scheduleLiveRef.current?.review(request, signal) ?? Promise.resolve('cancel'),
991
998
  askPermission: (request) => scheduleLiveRef.current?.askPermission(request) ??
992
999
  Promise.resolve({ kind: 'reject', feedback: 'Nobody can answer yet.' }),
993
1000
  submit: (text, createdBy) => scheduleLiveRef.current?.submit(text, createdBy),
@@ -1004,6 +1011,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1004
1011
  saveSkillPromptRef.current = next;
1005
1012
  setSaveSkillPromptState(next);
1006
1013
  }, []);
1014
+ const [scheduleReviewPrompt, setScheduleReviewPromptState] = useState(null);
1015
+ const scheduleReviewTokenRef = useRef(0);
1016
+ const scheduleReviewPromptRef = useRef(null);
1017
+ const setScheduleReviewPrompt = useCallback((next) => {
1018
+ scheduleReviewPromptRef.current = next;
1019
+ setScheduleReviewPromptState(next);
1020
+ }, []);
1007
1021
  const saveSkillNotifyRef = useRef(null);
1008
1022
  const saveSkillToolRef = useRef(null);
1009
1023
  if (saveSkillToolRef.current === null) {
@@ -1037,7 +1051,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1037
1051
  setSaveSkillPrompt(entry);
1038
1052
  saveSkillNotifyRef.current?.();
1039
1053
  }),
1040
- saved: ({ name, path }) => scheduleLiveRef.current?.say(`✎ Saved skill ${name} to ${path}. The model is offered it from the next turn; /skills ${name} activates it now.`),
1054
+ saved: ({ name, path }) => scheduleLiveRef.current?.say(`Saved skill ${name} to ${path}. The model is offered it from the next turn; /skills ${name} activates it now.`),
1041
1055
  });
1042
1056
  }
1043
1057
  /**
@@ -1165,9 +1179,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1165
1179
  // outlived the row it named would resolve `/expand 3` to output the operator
1166
1180
  // can no longer see anywhere, which is the failure this surface is being
1167
1181
  // cleaned of, one layer up.
1168
- const resetTranscript = useCallback(() => {
1182
+ const resetTranscript = useCallback((options = {}) => {
1169
1183
  if (stdout.isTTY)
1170
1184
  writeStdout('\x1b[2J\x1b[3J\x1b[H');
1185
+ if (!options.preserveOutputViewer)
1186
+ setOutputViewer(null);
1171
1187
  // The scrollback floor goes with the log it counted. <Static> is remounted
1172
1188
  // by the key below and has emitted nothing again; a floor that survived
1173
1189
  // would keep the next conversation's rows out of the live window.
@@ -1179,12 +1195,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1179
1195
  const previous = previousTerminalRef.current;
1180
1196
  previousTerminalRef.current = terminal;
1181
1197
  if (externalEditorRequestRef.current === null &&
1182
- (terminal.columns < previous.columns || terminal.rows < previous.rows)) {
1183
- // A contraction can reflow live rows into native scrollback before Ink
1184
- // receives resize. Its old row count cannot erase those rows. Rebuild
1185
- // Namzu's normal-buffer view at the committed size, retaining the same
1186
- // messages, draft and selected child. Regular frames remain incremental.
1187
- resetTranscript();
1198
+ (terminal.columns !== previous.columns || terminal.rows < previous.rows)) {
1199
+ // A contraction can move live rows into native scrollback before Ink
1200
+ // receives resize; expansion leaves already-printed Static rows wrapped
1201
+ // at their old width. Rebuild the normal-buffer transcript at the new
1202
+ // width, retaining messages, draft and selected child. Regular frames
1203
+ // remain incremental.
1204
+ resetTranscript({ preserveOutputViewer: true });
1188
1205
  }
1189
1206
  }, [terminal, resetTranscript]);
1190
1207
  const requestExternalEditor = useCallback((seed) => {
@@ -1802,26 +1819,44 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1802
1819
  }
1803
1820
  }, [conversationLearning, ctx.cwd, ctx.skills, pushMessage]);
1804
1821
  const removeStoredCredential = useCallback((target) => {
1805
- const path = credentialsPath();
1822
+ const path = target === 'google' ? googleApiKeyPath() : credentialsPath();
1806
1823
  const hadClaude = readStoredSubscriptionCredential() !== null;
1807
1824
  const hadCodex = readStoredCodexCredential() !== null;
1825
+ const hadGoogleKey = readStoredGoogleApiKey() !== null;
1826
+ if (target === 'all') {
1827
+ const failures = [];
1828
+ try {
1829
+ clearAllStoredCredentials();
1830
+ }
1831
+ catch (error) {
1832
+ failures.push(`${credentialsPath()}: ${error instanceof Error ? error.message : String(error)}`);
1833
+ }
1834
+ try {
1835
+ clearStoredGoogleApiKey();
1836
+ }
1837
+ catch (error) {
1838
+ failures.push(`${googleApiKeyPath()}: ${error instanceof Error ? error.message : String(error)}`);
1839
+ }
1840
+ if (failures.length > 0) {
1841
+ pushMessage('system', `Some stored credentials could not be removed: ${failures.join('; ')}. Check both stores before retrying.`);
1842
+ return;
1843
+ }
1844
+ pushMessage('system', describeAllCredentialsLogout(path, googleApiKeyPath(), hadClaude || hadCodex || hadGoogleKey));
1845
+ return;
1846
+ }
1808
1847
  try {
1809
1848
  if (target === 'anthropic')
1810
1849
  clearStoredSubscriptionCredential();
1811
1850
  else if (target === 'codex')
1812
1851
  clearStoredCodexCredential();
1813
1852
  else
1814
- clearAllStoredCredentials();
1853
+ clearStoredGoogleApiKey();
1815
1854
  }
1816
1855
  catch (error) {
1817
1856
  pushMessage('system', `Could not remove ${path}: ${error instanceof Error ? error.message : String(error)}`);
1818
1857
  return;
1819
1858
  }
1820
- if (target === 'all') {
1821
- pushMessage('system', describeLogout(path, hadClaude || hadCodex));
1822
- return;
1823
- }
1824
- pushMessage('system', describeProviderLogout(path, target, target === 'anthropic' ? hadClaude : hadCodex));
1859
+ pushMessage('system', describeProviderLogout(path, target, target === 'anthropic' ? hadClaude : target === 'codex' ? hadCodex : hadGoogleKey));
1825
1860
  }, [pushMessage]);
1826
1861
  /** Publish one recoverable, read-only tombstone before leaving the TUI. */
1827
1862
  const archiveCurrentConversation = useCallback(() => {
@@ -1864,7 +1899,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1864
1899
  closeAndExit();
1865
1900
  }
1866
1901
  catch (error) {
1867
- pushMessage('system', `Could not archive this conversation: ${error instanceof Error ? error.message : String(error)}`);
1902
+ pushMessage('system', error instanceof ConversationIndexRefreshError
1903
+ ? error.message
1904
+ : `Could not archive this conversation: ${error instanceof Error ? error.message : String(error)}`);
1868
1905
  }
1869
1906
  finally {
1870
1907
  if (!archived && conversationMutationRef.current === 'archive') {
@@ -2648,10 +2685,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2648
2685
  setMentionCandidates(files);
2649
2686
  });
2650
2687
  setCurrentProvider(primaryProvider(prefs).id);
2651
- // A picker-owned, usable provider switch is an explicit recovery from a
2652
- // paused failed turn. Failed and superseded candidates return above and
2653
- // therefore cannot release its dependent queue.
2654
- if (signal !== undefined)
2688
+ // Failed and superseded provider choices returned above. A usable
2689
+ // provider change can recover a failed turn's dependent queue, but a
2690
+ // paused turn still owns the conversation's durable active slot. Keep
2691
+ // that queue held until its checkpoint is resumed and settles.
2692
+ if (signal !== undefined && queuePauseRef.current?.outcome !== 'paused')
2655
2693
  advanceQueueContinuation();
2656
2694
  if (s.hasProvider) {
2657
2695
  setTranscriptOwned(true);
@@ -3044,9 +3082,6 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3044
3082
  // terminal scrollback; the live tail stays deliberately small so an activity
3045
3083
  // tick never repaints the whole conversation.
3046
3084
  //
3047
- useEffect(() => {
3048
- setOutputViewer(null);
3049
- }, [resetKey]);
3050
3085
  useEffect(() => {
3051
3086
  if (permission !== null)
3052
3087
  setOutputViewer(null);
@@ -3067,9 +3102,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3067
3102
  // settling in the background prints through the child screen. Returning
3068
3103
  // advances the floor once and emits those finalized rows exactly once.
3069
3104
  const nextSettled = settledBeforeStreaming(messages, window.settled, settledRef.current);
3070
- if (agentSurface === null && outputViewer === null)
3105
+ if (agentSurface === null && outputViewer === null && scheduleReviewPrompt === null)
3071
3106
  settledRef.current = nextSettled;
3072
- const renderedSettled = agentSurface === null && outputViewer === null ? nextSettled : settledRef.current;
3107
+ const renderedSettled = agentSurface === null && outputViewer === null && scheduleReviewPrompt === null
3108
+ ? nextSettled
3109
+ : settledRef.current;
3073
3110
  // One merged vocabulary for the session: this host's own commands plus
3074
3111
  // whatever the kernel's registry reports. Built here so `/help`, the
3075
3112
  // autocomplete and the dispatcher all answer from the same list — three
@@ -3185,7 +3222,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3185
3222
  const scope = scopeRef.current;
3186
3223
  if (!sessions || !scope || !conversationMaterializedRef.current || !session?.hasProvider)
3187
3224
  return false;
3188
- const active = await activeConversationTurn(sessions, scope.sessionId).catch(() => undefined);
3225
+ const resumedSessionId = scope.sessionId;
3226
+ const active = await activeConversationTurn(sessions, resumedSessionId).catch(() => undefined);
3189
3227
  if (!active)
3190
3228
  return false;
3191
3229
  if (abortRef.current || state !== 'idle') {
@@ -3245,7 +3283,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3245
3283
  }
3246
3284
  // Said once the turn really continues: a scheduled park can still
3247
3285
  // be left waiting or abandoned above.
3248
- pushMessage('system', PAUSED_TURN_LINES.resuming, false, '▶');
3286
+ pushMessage('system', PAUSED_TURN_LINES.resuming);
3249
3287
  for await (const event of session.resumePaused({
3250
3288
  turnId: active.turnId,
3251
3289
  signal: ac.signal,
@@ -3266,7 +3304,19 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3266
3304
  finalizeMessage(st.assistantId);
3267
3305
  // The resumed segment was appended to the log; the fold is the context
3268
3306
  // the next prompt continues from.
3269
- modelHistoryRef.current = await loadConversation(sessions, scope.sessionId);
3307
+ modelHistoryRef.current = await loadConversation(sessions, resumedSessionId);
3308
+ // A successfully completed checkpoint frees the conversation's active
3309
+ // slot. Only then may prompts queued behind the paused turn start. A
3310
+ // repeated pause, failed resume, unreadable log, or conversation switch
3311
+ // keeps dependent work held for an explicit operator decision.
3312
+ if (st.completed &&
3313
+ queuePauseRef.current?.outcome === 'paused' &&
3314
+ scopeRef.current?.sessionId === resumedSessionId &&
3315
+ (await activeConversationTurn(sessions, resumedSessionId)
3316
+ .then((next) => next === undefined)
3317
+ .catch(() => false))) {
3318
+ advanceQueueContinuation();
3319
+ }
3270
3320
  }
3271
3321
  catch (err) {
3272
3322
  pushMessage('system', `Could not resume the turn: ${err instanceof Error ? err.message : String(err)}`);
@@ -3283,7 +3333,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3283
3333
  await restoreBrowser?.().catch(() => undefined);
3284
3334
  }
3285
3335
  return true;
3286
- }, [detected, finalizeMessage, flushStream, pushMessage, session, setHandoffPark, state]);
3336
+ }, [advanceQueueContinuation, detected, finalizeMessage, flushStream, pushMessage, session, setHandoffPark, state]);
3287
3337
  // `namzu resume <id>` of a scheduled run parked on a decision: what the
3288
3338
  // notification and `/schedule` tell the operator to run. Continue it once
3289
3339
  // the screen is ready, exactly as `/resume` would, so the parked call is on
@@ -3331,6 +3381,35 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3331
3381
  pushMessage('system', `Could not list conversations: ${err instanceof Error ? err.message : String(err)}`);
3332
3382
  }
3333
3383
  }, [ensureSessions, pushMessage, resumeActiveTurn]);
3384
+ const doUnarchive = useCallback(async () => {
3385
+ if (state !== 'idle' ||
3386
+ abortRef.current !== null ||
3387
+ hasUnsettledTurn() ||
3388
+ queuedRef.current.length > 0 ||
3389
+ compactingRef.current ||
3390
+ exportingRef.current) {
3391
+ pushMessage('system', 'Finish the current turn and queued work before restoring a conversation.');
3392
+ return;
3393
+ }
3394
+ const sessions = sessionsRef.current ?? (await ensureSessions(), sessionsRef.current);
3395
+ if (!sessions) {
3396
+ pushMessage('system', 'Conversation history is unavailable in this folder.');
3397
+ return;
3398
+ }
3399
+ try {
3400
+ const archived = await listArchived(sessions, 100);
3401
+ if (archived.length === 0) {
3402
+ pushMessage('system', 'No archived conversations in this project.');
3403
+ return;
3404
+ }
3405
+ setResumeList(archived);
3406
+ setSelectedResume(0);
3407
+ setPhase('unarchive');
3408
+ }
3409
+ catch (err) {
3410
+ pushMessage('system', `Could not list archived conversations: ${err instanceof Error ? err.message : String(err)}`);
3411
+ }
3412
+ }, [ensureSessions, hasUnsettledTurn, pushMessage, state]);
3334
3413
  // Resolve a pending permission prompt with the user's decision and tear
3335
3414
  // down the overlay. No-op if nothing is pending.
3336
3415
  const resolvePermission = useCallback((decision) => {
@@ -3511,6 +3590,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3511
3590
  resumeCommittedRef.current = true;
3512
3591
  let msgs;
3513
3592
  try {
3593
+ await requireWritableConversation(sessions, conv.id, 'resume conversation');
3514
3594
  msgs = await loadConversation(sessions, conv.id);
3515
3595
  }
3516
3596
  catch (err) {
@@ -3555,7 +3635,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3555
3635
  // would have nothing to find.
3556
3636
  void hydrateSavedChildren();
3557
3637
  conversationMaterializedRef.current = true;
3558
- pushMessage('system', `Resumed: ${conv.title}`);
3638
+ pushMessage('system', `Resumed: ${displayConversationTitle(conv.title)}`);
3559
3639
  if (discardedQueued > 0) {
3560
3640
  pushMessage('system', `Discarded ${discardedQueued} queued prompt${discardedQueued === 1 ? '' : 's'} from the conversation you left.`);
3561
3641
  }
@@ -3580,6 +3660,42 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3580
3660
  resetTranscript,
3581
3661
  wakeGoalDriver,
3582
3662
  ]);
3663
+ const restoreArchivedConversation = useCallback(async (conv) => {
3664
+ const sessions = sessionsRef.current;
3665
+ if (!sessions) {
3666
+ setPhase('ready');
3667
+ return;
3668
+ }
3669
+ if (state !== 'idle' ||
3670
+ abortRef.current !== null ||
3671
+ hasUnsettledTurn() ||
3672
+ queuedRef.current.length > 0 ||
3673
+ compactingRef.current ||
3674
+ exportingRef.current) {
3675
+ setPhase('ready');
3676
+ pushMessage('system', 'Finish the current turn and queued work before restoring a conversation.');
3677
+ return;
3678
+ }
3679
+ resumeCommittedRef.current = true;
3680
+ try {
3681
+ await unarchiveConversation(sessions, conv.id);
3682
+ }
3683
+ catch (err) {
3684
+ resumeCommittedRef.current = false;
3685
+ setPhase('ready');
3686
+ pushMessage('system', err instanceof ConversationIndexRefreshError
3687
+ ? err.message
3688
+ : `Could not restore: ${err instanceof Error ? err.message : String(err)}`);
3689
+ return;
3690
+ }
3691
+ pushMessage('system', `Restored: ${displayConversationTitle(conv.title)}`);
3692
+ try {
3693
+ await resumeConversation(conv);
3694
+ }
3695
+ finally {
3696
+ resumeCommittedRef.current = false;
3697
+ }
3698
+ }, [hasUnsettledTurn, pushMessage, resumeConversation, state]);
3583
3699
  /**
3584
3700
  * Start a new conversation without deleting the one being left.
3585
3701
  *
@@ -3814,7 +3930,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3814
3930
  session?.resetTaskStore?.();
3815
3931
  goalActivation.clear();
3816
3932
  wakeGoalDriver();
3817
- pushMessage('system', `Forked into "${forked.title}" — ${forked.copied} message(s) copied. This screen continues in the copy; ${original} is unchanged and still in /resume.`);
3933
+ pushMessage('system', `Forked into "${displayConversationTitle(forked.title)}" — ${forked.copied} message(s) copied. This screen continues in the copy; ${original} is unchanged and still in /resume.`);
3818
3934
  }
3819
3935
  catch (err) {
3820
3936
  pushMessage('system', `Could not fork: ${err instanceof Error ? err.message : String(err)}`);
@@ -3941,7 +4057,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3941
4057
  });
3942
4058
  setEditList([]);
3943
4059
  setPhase('ready');
3944
- pushMessage('system', `Forked into "${forked.title}" before the selected prompt. Edit it below; ${source} is unchanged and remains in /resume.`);
4060
+ pushMessage('system', `Forked into "${displayConversationTitle(forked.title)}" before the selected prompt. Edit it below; ${source} is unchanged and remains in /resume.`);
3945
4061
  }
3946
4062
  catch (err) {
3947
4063
  setPhase('ready');
@@ -4053,6 +4169,39 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4053
4169
  setChoicePicker(picker);
4054
4170
  sendTerminalNotification({ kind: 'approval-required' });
4055
4171
  }), [closeLiveReply, sendTerminalNotification, setChoicePicker, setSelectedChoice, resolveQuestion]);
4172
+ const reviewSchedule = useCallback((request, signal) => new Promise((resolve) => {
4173
+ if (signal?.aborted) {
4174
+ resolve('cancel');
4175
+ return;
4176
+ }
4177
+ closeLiveReply();
4178
+ // A second tool call must not replace an open review and leave its
4179
+ // Promise waiting forever. Each decision still belongs to its own
4180
+ // captured proposal.
4181
+ scheduleReviewPromptRef.current?.resolve('cancel');
4182
+ let settled = false;
4183
+ const entry = {
4184
+ token: ++scheduleReviewTokenRef.current,
4185
+ request,
4186
+ resolve: (answer) => {
4187
+ if (settled)
4188
+ return;
4189
+ settled = true;
4190
+ signal?.removeEventListener('abort', onAbort);
4191
+ // Let the overlay's key dispatch finish before App regains its
4192
+ // keyboard handler, so Esc cannot interrupt this same turn.
4193
+ queueMicrotask(() => {
4194
+ if (scheduleReviewPromptRef.current === entry)
4195
+ setScheduleReviewPrompt(null);
4196
+ });
4197
+ resolve(answer);
4198
+ },
4199
+ };
4200
+ const onAbort = () => entry.resolve('cancel');
4201
+ signal?.addEventListener('abort', onAbort, { once: true });
4202
+ setScheduleReviewPrompt(entry);
4203
+ sendTerminalNotification({ kind: 'approval-required' });
4204
+ }), [closeLiveReply, sendTerminalNotification, setScheduleReviewPrompt]);
4056
4205
  /**
4057
4206
  * Grow this turn's open task block, or open one. The whole plan rides along
4058
4207
  * so the block shows the checklist as it stood after the operation; a
@@ -4444,7 +4593,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4444
4593
  case 'job':
4445
4594
  // The kernel saw it end during a turn; the model reads the notice
4446
4595
  // on its next tool result, the operator reads this row.
4447
- pushMessage('system', describeJobExit(event), false, '⚙');
4596
+ pushMessage('system', describeJobExit(event), false, 'J');
4448
4597
  break;
4449
4598
  case 'context':
4450
4599
  // Into the TRANSCRIPT, not a status line. A status indicator is
@@ -4539,8 +4688,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4539
4688
  `${describeTurnInterruption(event)}\nWhen that is done, press Enter to continue · Esc to stop.`, false, '‖');
4540
4689
  setHandoffPark({ turnId: event.turnId, reason: event.handoff.reason });
4541
4690
  }
4542
- else
4543
- pushMessage('system', describeTurnInterruption(event), false, '‖');
4691
+ else {
4692
+ pushMessage('system', `${describeTurnInterruption(event)}\nUse /resume to continue this checkpoint on the selected provider, or /abandon to start a new turn.`, false, '‖');
4693
+ }
4544
4694
  break;
4545
4695
  case 'error':
4546
4696
  closeAssistant();
@@ -5816,6 +5966,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5816
5966
  const choices = [
5817
5967
  ...(readStoredSubscriptionCredential() ? ['anthropic'] : []),
5818
5968
  ...(readStoredCodexCredential() ? ['codex'] : []),
5969
+ ...(readStoredGoogleApiKey() ? ['google'] : []),
5819
5970
  ];
5820
5971
  if (choices.length === 0) {
5821
5972
  removeStoredCredential('all');
@@ -5828,14 +5979,16 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5828
5979
  setSelectedChoice(0);
5829
5980
  setChoicePicker({
5830
5981
  kind: 'credential-logout',
5831
- title: 'Choose a stored subscription to remove',
5982
+ title: 'Choose a stored credential to remove',
5832
5983
  notice: 'Only credentials created by Namzu are listed. Device sessions owned by other tools are left alone.',
5833
5984
  values: choices,
5834
5985
  options: choices.map((choice) => ({
5835
- label: choice === 'anthropic' ? 'Claude' : 'Codex',
5986
+ label: choice === 'anthropic' ? 'Claude' : choice === 'codex' ? 'Codex' : 'Gemini API key',
5836
5987
  description: choice === 'anthropic'
5837
5988
  ? 'Remove only Namzu’s Claude subscription.'
5838
- : 'Remove only Namzu’s Codex subscription.',
5989
+ : choice === 'codex'
5990
+ ? 'Remove only Namzu’s Codex subscription.'
5991
+ : 'Remove only Namzu’s saved Gemini API key.',
5839
5992
  })),
5840
5993
  });
5841
5994
  return;
@@ -5992,12 +6145,80 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5992
6145
  case 'resume':
5993
6146
  void doResume();
5994
6147
  return;
6148
+ case 'unarchive':
6149
+ void doUnarchive();
6150
+ return;
5995
6151
  case 'title':
5996
6152
  void doTitle(slash.title, slash.clear);
5997
6153
  return;
5998
6154
  case 'fork':
5999
6155
  void doFork();
6000
6156
  return;
6157
+ case 'worktree': {
6158
+ const [verb = 'list', label, extra] = slash.args;
6159
+ const notice = (content) => pushMessage('system', terminalDisplayText(content));
6160
+ if (!((verb === 'list' && label === undefined) ||
6161
+ (verb === 'create' && extra === undefined) ||
6162
+ (verb === 'fork' && extra === undefined) ||
6163
+ (verb === 'resume' &&
6164
+ label !== undefined &&
6165
+ extra === undefined))) {
6166
+ pushMessage('system', 'Usage: /worktree [list|create [name]|fork [name]|resume <name>]');
6167
+ return;
6168
+ }
6169
+ if (verb === 'fork' &&
6170
+ (abortRef.current ||
6171
+ hasUnsettledTurn() ||
6172
+ queuedRef.current.length > 0)) {
6173
+ pushMessage('system', 'Wait for the current turn and queued prompts to finish before forking to a worktree.');
6174
+ return;
6175
+ }
6176
+ if (conversationMutationRef.current)
6177
+ return;
6178
+ if (verb === 'fork') {
6179
+ conversationMutationRef.current = 'fork';
6180
+ setConversationMutation('fork');
6181
+ }
6182
+ void (async () => {
6183
+ try {
6184
+ const manager = await openManagedWorktrees(ctx.cwd);
6185
+ if (verb === 'list') {
6186
+ const items = await manager.list();
6187
+ notice(items.length === 0
6188
+ ? 'No managed worktrees in this repository.'
6189
+ : items
6190
+ .map((item) => `${item.label}: ${item.path}${item.dirty ? ' (uncommitted files)' : ''}`)
6191
+ .join('\n'));
6192
+ }
6193
+ else if (verb === 'create') {
6194
+ const created = await manager.create(label);
6195
+ notice(`Created ${created.branch}. Open it in a new terminal: ${worktreeOpenHint(created.path)}${created.sourceDirty ? '\nUncommitted files stayed in this checkout.' : ''}`);
6196
+ }
6197
+ else if (verb === 'fork') {
6198
+ const scope = await materializeConversation();
6199
+ if (!scope)
6200
+ throw new Error('Conversation history is unavailable in this folder.');
6201
+ await persistenceTailRef.current;
6202
+ const forked = await manager.fork(scope.sessionId, label);
6203
+ notice(`Forked ${forked.copied} messages into ${displayConversationTitle(forked.title)}. Open the new checkout in a new terminal: ${worktreeOpenHint(forked.path, forked.conversationId)}${forked.sourceDirty ? '\nUncommitted files stayed in this checkout.' : ''}`);
6204
+ }
6205
+ else if (label) {
6206
+ const target = await manager.resume(label);
6207
+ notice(`Open ${label} in a new terminal: ${worktreeOpenHint(target.worktree.path, target.conversationId)}`);
6208
+ }
6209
+ }
6210
+ catch (error) {
6211
+ notice(`Worktree: ${error instanceof Error ? error.message : String(error)}`);
6212
+ }
6213
+ finally {
6214
+ if (verb === 'fork') {
6215
+ conversationMutationRef.current = null;
6216
+ setConversationMutation(null);
6217
+ }
6218
+ }
6219
+ })();
6220
+ return;
6221
+ }
6001
6222
  case 'restore':
6002
6223
  void doRestore(slash.turn);
6003
6224
  return;
@@ -6448,12 +6669,14 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6448
6669
  appLifetime,
6449
6670
  ctx.cwd,
6450
6671
  doResume,
6672
+ doUnarchive,
6451
6673
  enqueueQueued,
6452
6674
  exitWithSummary,
6453
6675
  hasUnsettledTurn,
6454
6676
  goalActivation,
6455
6677
  goalStatus,
6456
6678
  hostCommands,
6679
+ materializeConversation,
6457
6680
  nextId,
6458
6681
  openAgentCockpit,
6459
6682
  runAgentsCommand,
@@ -6529,6 +6752,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6529
6752
  pushMessage('system', text);
6530
6753
  },
6531
6754
  ask: askQuestion,
6755
+ review: reviewSchedule,
6532
6756
  askPermission: onPermission,
6533
6757
  submit: (text, createdBy) => handleSubmit(text, undefined, 'submit', {
6534
6758
  source: createdBy === 'operator' ? 'operator-loop' : 'model-loop',
@@ -6767,7 +6991,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6767
6991
  if (ups.length === 0)
6768
6992
  return;
6769
6993
  const lines = ups.map((u) => ` • ${u.name} ${u.current} → ${u.latest} (${u.how})`);
6770
- pushMessage('system', `⬆ Update available:\n${lines.join('\n')}`);
6994
+ pushMessage('system', `Update available:\n${lines.join('\n')}`);
6771
6995
  });
6772
6996
  }, [phase, ctx.version, pushMessage]);
6773
6997
  const hydrateFromPicker = useCallback(async (prefs, detectedNow, signal, options = {}) => {
@@ -6778,29 +7002,24 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6778
7002
  announce: true,
6779
7003
  chooseReasoningEffort: options.chooseReasoningEffort,
6780
7004
  });
7005
+ return !signal.aborted;
6781
7006
  }
6782
7007
  catch (err) {
6783
7008
  // A superseded choice no longer owns even its failure message. Its
6784
7009
  // eventual session object is disposed inside `hydrateSession`; a live
6785
7010
  // choice stays on the picker with the actionable construction error.
6786
7011
  if (signal.aborted)
6787
- return;
7012
+ return false;
6788
7013
  if (options.revealAllOnFailure) {
6789
7014
  setPickerDetected(null);
6790
7015
  setPickerSelectionKind('provider-and-model');
6791
7016
  setPickerInitialView('providers');
6792
7017
  }
6793
7018
  setPickerNotice(`Could not start the selected provider: ${err instanceof Error ? err.message : String(err)}`);
7019
+ return false;
6794
7020
  }
6795
7021
  }, [hydrateSession]);
6796
- /**
6797
- * A credential the operator typed. Held in memory for this process only.
6798
- *
6799
- * Deliberately does NOT call `writePreferences`: preferences are a file, and
6800
- * the whole contract of this entry point is that nothing lands on disk. The
6801
- * provider choice is not persisted either, because persisting it would leave
6802
- * a preference pointing at a credential that will not exist next launch.
6803
- */
7022
+ /** A typed Google API key is saved only after its provider session opens. */
6804
7023
  const handleTypedCredential = useCallback((credential, disposition, signal) => {
6805
7024
  const next = [credential, ...detected.filter((d) => d.entry.id !== credential.entry.id)];
6806
7025
  setDetected(next);
@@ -6816,7 +7035,32 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6816
7035
  const prefs = saved && primaryProvider(saved).id === credential.entry.id
6817
7036
  ? saved
6818
7037
  : selectPrimaryProvider(saved, { id: credential.entry.id });
6819
- void hydrateFromPicker(prefs, next, signal);
7038
+ void (async () => {
7039
+ const started = await hydrateFromPicker(prefs, next, signal);
7040
+ if (!started || credential.entry.id !== 'google')
7041
+ return;
7042
+ let path;
7043
+ try {
7044
+ path = writeStoredGoogleApiKey(credential.apiKey ?? '');
7045
+ }
7046
+ catch (error) {
7047
+ pushMessage('system', `Gemini is connected, but its API key could not be saved privately: ${error instanceof Error ? error.message : String(error)}. It is usable only in this session.`);
7048
+ return;
7049
+ }
7050
+ setDetected((current) => current.map((provider) => provider.entry.id === 'google' &&
7051
+ provider.source.kind === 'session' &&
7052
+ provider.apiKey === credential.apiKey
7053
+ ? { ...provider, source: { kind: 'stored-gemini-key', path } }
7054
+ : provider));
7055
+ try {
7056
+ writePreferences(prefs);
7057
+ }
7058
+ catch (error) {
7059
+ pushMessage('system', `Gemini API key was saved privately at ${path}, but the provider preference could not be saved: ${error instanceof Error ? error.message : String(error)}. Run /logout gemini to remove the key.`);
7060
+ return;
7061
+ }
7062
+ pushMessage('system', `Gemini API key saved privately at ${path} for future launches. Run /logout gemini to remove it.`);
7063
+ })();
6820
7064
  }, [detected, hydrateFromPicker, pushMessage]);
6821
7065
  const handlePickerSubmit = useCallback((selection, signal) => {
6822
7066
  const prefs = selectPrimaryProvider(savedPrefsRef.current, {
@@ -6868,7 +7112,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6868
7112
  // The save-skill screen owns its keys, Esc and Ctrl+C included: neither
6869
7113
  // may interrupt the turn that is waiting on its answer. A permission
6870
7114
  // request (an agent's, say) takes the screen over it and gets the keys.
6871
- if (saveSkillPromptRef.current && permission === null)
7115
+ if ((saveSkillPromptRef.current || scheduleReviewPromptRef.current) && permission === null)
6872
7116
  return;
6873
7117
  // Startup has refused admission, so there is no draft or active turn to
6874
7118
  // protect with the ready screen's two-press exit ladder.
@@ -6923,7 +7167,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6923
7167
  return;
6924
7168
  }
6925
7169
  // Resume picker owns the keyboard while open.
6926
- if (phase === 'resume') {
7170
+ if (phase === 'resume' || phase === 'unarchive') {
6927
7171
  // Once a conversation is being read, the keyboard does nothing here.
6928
7172
  // The choice is already being acted on; a second Enter would start a
6929
7173
  // second read and an Esc would hand back a screen that is about to be
@@ -6940,7 +7184,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6940
7184
  else if (key.return) {
6941
7185
  const conv = resumeList[selectedResumeRef.current];
6942
7186
  if (conv)
6943
- void resumeConversation(conv);
7187
+ void (phase === 'unarchive' ? restoreArchivedConversation(conv) : resumeConversation(conv));
6944
7188
  }
6945
7189
  else if (key.escape || (key.ctrl && input === 'c'))
6946
7190
  setPhase('ready');
@@ -7459,8 +7703,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
7459
7703
  ? subagents.find((agent) => agent.viewId === agentSurface.selectedId)
7460
7704
  : undefined;
7461
7705
  const lifecycleOwnsViewport = outputViewer !== null ||
7706
+ scheduleReviewPrompt !== null ||
7462
7707
  phase === 'trust' ||
7463
7708
  phase === 'resume' ||
7709
+ phase === 'unarchive' ||
7464
7710
  phase === 'edit' ||
7465
7711
  phase === 'picker' ||
7466
7712
  agentSurface !== null;
@@ -7472,6 +7718,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
7472
7718
  textPrompt === null &&
7473
7719
  choicePicker === null &&
7474
7720
  copyPicker === null &&
7721
+ scheduleReviewPrompt === null &&
7475
7722
  agentSurface === null
7476
7723
  ? goalStatusLabel(goalStatus, goalStatus ? goalActivation.isArmed(goalStatus.sessionId, goalStatus) : false)
7477
7724
  : null;
@@ -7490,33 +7737,35 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
7490
7737
  ? 'starting a fresh conversation — input is paused'
7491
7738
  : compacting
7492
7739
  ? 'compacting conversation — input is paused'
7493
- : saveSkillPrompt
7494
- ? 'save skill — ←/→ choose · enter apply · esc cancel'
7495
- : permission
7496
- ? hintForPhase(phase, state, session?.hasProvider === true)
7497
- : agentSurface?.kind === 'cockpit'
7498
- ? agentSurface.focus === 'workflows'
7499
- ? 'workflows — enter select · esc return'
7500
- : agentSurface.focus === 'phases'
7501
- ? 'agent phases — enter agents · esc return'
7502
- : 'agents — enter inspect · left phases · esc return'
7503
- : agentSurface?.kind === 'transcript'
7504
- ? 'observing agent — esc agents · q parent'
7505
- : textPrompt
7506
- ? 'editor open · enter apply · esc cancel'
7507
- : choicePicker
7508
- ? choicePickerSearchable(choicePicker)
7509
- ? 'type to filter · ↑↓ select · enter apply · esc back'
7510
- : effortSlider
7511
- ? '←/→ adjust · 1–9 select · enter apply · esc back'
7512
- : '↑↓ / 1–9 select · enter apply · esc back'
7513
- : copyPicker
7514
- ? 'copy target open — ↑↓ / 1–9 select · esc cancel'
7515
- : handoffPark && phase === 'ready' && state === 'idle' && !composerHasDraft
7516
- ? 'enter continue · esc stop'
7517
- : goalStatus && phase === 'ready' && state === 'idle'
7518
- ? undefined
7519
- : hintForPhase(phase, state, session?.hasProvider === true, composerHasDraft);
7740
+ : permission
7741
+ ? hintForPhase(phase, state, session?.hasProvider === true)
7742
+ : scheduleReviewPrompt
7743
+ ? 'review schedule — ↑/↓ scroll · ←/→ choose · enter apply · esc cancel'
7744
+ : saveSkillPrompt
7745
+ ? 'save skill — ←/→ choose · enter apply · esc cancel'
7746
+ : agentSurface?.kind === 'cockpit'
7747
+ ? agentSurface.focus === 'workflows'
7748
+ ? 'workflows — enter select · esc return'
7749
+ : agentSurface.focus === 'phases'
7750
+ ? 'agent phases — enter agents · esc return'
7751
+ : 'agents — enter inspect · left phases · esc return'
7752
+ : agentSurface?.kind === 'transcript'
7753
+ ? 'observing agent — esc agents · q parent'
7754
+ : textPrompt
7755
+ ? 'editor open · enter apply · esc cancel'
7756
+ : choicePicker
7757
+ ? choicePickerSearchable(choicePicker)
7758
+ ? 'type to filter · ↑↓ select · enter apply · esc back'
7759
+ : effortSlider
7760
+ ? '←/→ adjust · 1–9 select · enter apply · esc back'
7761
+ : '↑↓ / 1–9 select · enter apply · esc back'
7762
+ : copyPicker
7763
+ ? 'copy target open — ↑↓ / 1–9 select · esc cancel'
7764
+ : handoffPark && phase === 'ready' && state === 'idle' && !composerHasDraft
7765
+ ? 'enter continue · esc stop'
7766
+ : goalStatus && phase === 'ready' && state === 'idle'
7767
+ ? undefined
7768
+ : hintForPhase(phase, state, session?.hasProvider === true, composerHasDraft);
7520
7769
  const permissionSource = reviewSource(permission, scopeRef.current?.sessionId);
7521
7770
  const permissionOwner = permissionSource
7522
7771
  ? subagents.find((agent) => agent.sessionId === permissionSource)
@@ -7568,24 +7817,26 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
7568
7817
  setPickerSelectionKind('provider-and-model');
7569
7818
  setPhase('picker');
7570
7819
  void discoverProviders({ skipProbes: true }).then(setPickerDetected).catch((error) => setPickerNotice(String(error)));
7571
- } })) : 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,
7820
+ } })) : 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' || phase === 'unarchive' ? (_jsx(ResumePicker, { conversations: resumeList, selected: selectedResume, mode: phase === 'unarchive' ? 'restore' : 'resume' })) : 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,
7572
7821
  // The same condition `handlePickerCancel` branches on, read
7573
7822
  // where it is decided rather than guessed on the screen: with no
7574
7823
  // session behind the picker, esc exits namzu and the footer says
7575
7824
  // so instead of offering a cancel nobody will see.
7576
- 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 &&
7825
+ cancelExits: !session?.hasProvider })) : (_jsxs(_Fragment, { children: [agentSurface === null && outputViewer === null && scheduleReviewPrompt === null ? (_jsx(LiveActivity, { compact: compactWork, activeTools: visibleActiveTools, working: state === 'thinking' || state === 'tool', interruptible: abortRef.current !== null, animate: stdout.isTTY === true &&
7577
7826
  permission === null &&
7578
7827
  textPrompt === null &&
7579
- saveSkillPrompt === null, thinking: thinking, tokens: turnTokens, waitingForYou: choicePicker !== null ||
7828
+ saveSkillPrompt === null &&
7829
+ scheduleReviewPrompt === null, thinking: thinking, tokens: turnTokens, waitingForYou: choicePicker !== null ||
7580
7830
  permission !== null ||
7581
7831
  textPrompt !== null ||
7582
- saveSkillPrompt !== null })) : 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, batchOnly: permission.batchOnly === true, siteNotes: permission.siteNotes, ...(permission.screenConsent === true
7832
+ saveSkillPrompt !== null ||
7833
+ scheduleReviewPrompt !== null })) : null, agentSurface === null && outputViewer === null && permission === null && scheduleReviewPrompt === 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, batchOnly: permission.batchOnly === true, siteNotes: permission.siteNotes, ...(permission.screenConsent === true
7583
7834
  ? { screenConsent: { provider: session?.providerSummary ?? null } }
7584
- : {}) })) : null, saveSkillPrompt && permission === null ? (_jsx(SaveSkillOverlay, { request: saveSkillPrompt.request, cwd: ctx.cwd, columns: Math.max(1, (terminal.columns ?? 80) - 2), rows: terminal.rows, onAnswer: saveSkillPrompt.resolve }, saveSkillPrompt.request.markdown)) : 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 &&
7835
+ : {}) })) : null, saveSkillPrompt && permission === null && scheduleReviewPrompt === null ? (_jsx(SaveSkillOverlay, { request: saveSkillPrompt.request, cwd: ctx.cwd, columns: Math.max(1, (terminal.columns ?? 80) - 2), rows: terminal.rows, onAnswer: saveSkillPrompt.resolve }, saveSkillPrompt.request.markdown)) : null, scheduleReviewPrompt && permission === null ? (_jsx(ScheduleReviewOverlay, { request: scheduleReviewPrompt.request, columns: Math.max(1, (terminal.columns ?? 80) - 2), rows: Math.max(1, (terminal.rows ?? 24) - 2), onAnswer: scheduleReviewPrompt.resolve }, scheduleReviewPrompt.token)) : 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 || scheduleReviewPrompt !== null || (agentSurface !== null || outputViewer !== null), onSubmit: submitTextPrompt, onCancel: cancelTextPrompt }, textPrompt.token)) : permission === null && scheduleReviewPrompt === null &&
7585
7836
  agentSurface === null &&
7586
7837
  outputViewer === null &&
7587
7838
  choicePicker?.kind === 'reasoning-effort' &&
7588
- effortSlider ? (_jsx(EffortSlider, { title: choicePicker.title, notice: choicePicker.notice, options: choicePicker.options, selected: selectedChoice, layout: effortSlider, columns: Math.max(1, (terminal.columns ?? 80) - 2) })) : 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, ...(hypermode ? { mode: HYPERMODE } : {}), focus: phase === 'ready' &&
7839
+ effortSlider ? (_jsx(EffortSlider, { title: choicePicker.title, notice: choicePicker.notice, options: choicePicker.options, selected: selectedChoice, layout: effortSlider, columns: Math.max(1, (terminal.columns ?? 80) - 2) })) : permission === null && scheduleReviewPrompt === 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 && scheduleReviewPrompt === null && agentSurface === null && outputViewer === null && copyPicker ? (_jsx(CopyPicker, { targets: copyPicker.targets, selected: selectedCopy })) : null, _jsxs(ComposerFrame, { working: state === 'thinking' || state === 'tool' || visibleActiveTools.length > 0, ...(hypermode ? { mode: HYPERMODE } : {}), focus: phase === 'ready' &&
7589
7840
  state !== 'awaiting-permission' &&
7590
7841
  !compacting &&
7591
7842
  externalEditorRequest === null &&
@@ -7594,8 +7845,10 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
7594
7845
  choicePicker === null &&
7595
7846
  copyPicker === null &&
7596
7847
  saveSkillPrompt === null &&
7848
+ scheduleReviewPrompt === null &&
7597
7849
  agentSurface === null && outputViewer === null, hidden: permission !== null ||
7598
7850
  saveSkillPrompt !== null ||
7851
+ scheduleReviewPrompt !== null ||
7599
7852
  textPrompt !== null ||
7600
7853
  choicePicker !== null ||
7601
7854
  copyPicker !== null ||
@@ -7621,6 +7874,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
7621
7874
  externalEditorRequest !== null ||
7622
7875
  (agentSurface !== null || outputViewer !== null), hidden: permission !== null ||
7623
7876
  saveSkillPrompt !== null ||
7877
+ scheduleReviewPrompt !== null ||
7624
7878
  textPrompt !== null ||
7625
7879
  choicePicker !== null ||
7626
7880
  copyPicker !== null ||
@@ -7637,17 +7891,19 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
7637
7891
  })), mentionCandidates: mentionCandidates, history: history, ...(composerTriggers ? { triggers: composerTriggers } : {}), turnActive: !compacting && (state === 'thinking' || state === 'tool') })] })] })), _jsx(StatusBar, { cwd: ctx.cwd, provider: session?.providerSummary ?? null, model: session?.modelSummary ?? null, effort: reasoningEffort, hypermode: hypermode, goal: statusGoal, state: state, hint: statusHint, permissionMode: displayedPermissionMode, canCycleMode: true }), showComposerSurface &&
7638
7892
  permission === null &&
7639
7893
  agentSurface === null &&
7640
- outputViewer === null ? (_jsx(AgentNarrationBand, { lines: narration })) : null, showComposerSurface &&
7894
+ outputViewer === null &&
7895
+ scheduleReviewPrompt === null ? (_jsx(AgentNarrationBand, { lines: narration })) : null, showComposerSurface &&
7641
7896
  agentSurface === null &&
7642
7897
  outputViewer === null &&
7898
+ scheduleReviewPrompt === null &&
7643
7899
  liveSubagents.length > 0 ? (
7644
- // Still drawn while a review is open, reduced to its header line:
7645
- // the work already approved keeps moving, and the operator
7646
- // deciding the next launch should be able to see that it does.
7647
- _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 &&
7900
+ // The active agents remain in memory while the schedule review
7901
+ // owns the available rows; the panel returns after the decision.
7902
+ _jsx(AgentTaskPanel, { agents: liveSubagents, terminalRows: terminal.rows, terminalColumns: terminal.columns, compact: permission !== null })) : showComposerSurface && permission === null && scheduleReviewPrompt === null && agentSurface?.kind === 'cockpit' ? (_jsx(AgentCockpit, { agents: subagents, selectedPhaseId: agentSurface.selectedPhaseId, selectedId: agentSurface.selectedId, focus: agentSurface.focus, terminalRows: terminal.rows, terminalColumns: terminal.columns })) : showComposerSurface &&
7648
7903
  permission === null &&
7904
+ scheduleReviewPrompt === null &&
7649
7905
  agentSurface?.kind === 'transcript' &&
7650
- selectedSubagent ? (_jsx(AgentTranscript, { agent: selectedSubagent, tailOffset: agentSurface.tailOffset, terminalRows: terminal.rows, terminalColumns: terminal.columns })) : null, outputViewer && permission === null ? (_jsx(ToolOutputViewer, { onPrevious: () => {
7906
+ selectedSubagent ? (_jsx(AgentTranscript, { agent: selectedSubagent, tailOffset: agentSurface.tailOffset, terminalRows: terminal.rows, terminalColumns: terminal.columns })) : null, outputViewer && permission === null && scheduleReviewPrompt === null ? (_jsx(ToolOutputViewer, { onPrevious: () => {
7651
7907
  if (outputIndex > 0)
7652
7908
  setOutputViewer(outputBlocks[outputIndex - 1]);
7653
7909
  }, onNext: () => {
@@ -7687,6 +7943,8 @@ hasDraft = false) {
7687
7943
  return 'y trust this folder · n / esc exit';
7688
7944
  if (phase === 'resume')
7689
7945
  return '↑↓ navigate · enter resume · esc cancel';
7946
+ if (phase === 'unarchive')
7947
+ return '↑↓ navigate · enter restore · esc cancel';
7690
7948
  if (phase === 'edit')
7691
7949
  return '← older · → newer · enter fork and edit · esc cancel';
7692
7950
  if (phase === 'probing')