@namzu/cli 32.0.0 → 33.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 (119) hide show
  1. package/CHANGELOG.md +44 -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 +62 -0
  10. package/dist/commands/archive.js.map +1 -0
  11. package/dist/commands/login.d.ts +1 -1
  12. package/dist/commands/login.d.ts.map +1 -1
  13. package/dist/commands/login.js +33 -11
  14. package/dist/commands/login.js.map +1 -1
  15. package/dist/commands/mcp-oauth-login.d.ts +19 -0
  16. package/dist/commands/mcp-oauth-login.d.ts.map +1 -0
  17. package/dist/commands/mcp-oauth-login.js +213 -0
  18. package/dist/commands/mcp-oauth-login.js.map +1 -0
  19. package/dist/commands/mcp.d.ts +3 -0
  20. package/dist/commands/mcp.d.ts.map +1 -0
  21. package/dist/commands/mcp.js +288 -0
  22. package/dist/commands/mcp.js.map +1 -0
  23. package/dist/commands/skills.d.ts.map +1 -1
  24. package/dist/commands/skills.js +60 -5
  25. package/dist/commands/skills.js.map +1 -1
  26. package/dist/commands/types.d.ts +2 -0
  27. package/dist/commands/types.d.ts.map +1 -1
  28. package/dist/commands/worktree.d.ts +6 -0
  29. package/dist/commands/worktree.d.ts.map +1 -0
  30. package/dist/commands/worktree.js +85 -0
  31. package/dist/commands/worktree.js.map +1 -0
  32. package/dist/config/user-config.d.ts +2 -0
  33. package/dist/config/user-config.d.ts.map +1 -1
  34. package/dist/config/user-config.js +76 -6
  35. package/dist/config/user-config.js.map +1 -1
  36. package/dist/doctor/checks/credentials.d.ts.map +1 -1
  37. package/dist/doctor/checks/credentials.js +2 -0
  38. package/dist/doctor/checks/credentials.js.map +1 -1
  39. package/dist/integrations/mcp/oauth-store.d.ts +40 -0
  40. package/dist/integrations/mcp/oauth-store.d.ts.map +1 -0
  41. package/dist/integrations/mcp/oauth-store.js +549 -0
  42. package/dist/integrations/mcp/oauth-store.js.map +1 -0
  43. package/dist/integrations/mcp/servers.d.ts.map +1 -1
  44. package/dist/integrations/mcp/servers.js +155 -2
  45. package/dist/integrations/mcp/servers.js.map +1 -1
  46. package/dist/integrations/mcp/user-config.d.ts +25 -0
  47. package/dist/integrations/mcp/user-config.d.ts.map +1 -0
  48. package/dist/integrations/mcp/user-config.js +92 -0
  49. package/dist/integrations/mcp/user-config.js.map +1 -0
  50. package/dist/integrations/providers/credential-store.d.ts +8 -0
  51. package/dist/integrations/providers/credential-store.d.ts.map +1 -1
  52. package/dist/integrations/providers/credential-store.js +111 -22
  53. package/dist/integrations/providers/credential-store.js.map +1 -1
  54. package/dist/integrations/providers/discover.d.ts +11 -5
  55. package/dist/integrations/providers/discover.d.ts.map +1 -1
  56. package/dist/integrations/providers/discover.js +26 -12
  57. package/dist/integrations/providers/discover.js.map +1 -1
  58. package/dist/integrations/providers/index.d.ts +1 -1
  59. package/dist/integrations/providers/index.d.ts.map +1 -1
  60. package/dist/integrations/providers/index.js +1 -1
  61. package/dist/integrations/providers/index.js.map +1 -1
  62. package/dist/integrations/sessions/store.d.ts +23 -5
  63. package/dist/integrations/sessions/store.d.ts.map +1 -1
  64. package/dist/integrations/sessions/store.js +164 -24
  65. package/dist/integrations/sessions/store.js.map +1 -1
  66. package/dist/integrations/subagents/runtime.d.ts +4 -2
  67. package/dist/integrations/subagents/runtime.d.ts.map +1 -1
  68. package/dist/integrations/subagents/runtime.js +62 -8
  69. package/dist/integrations/subagents/runtime.js.map +1 -1
  70. package/dist/integrations/subagents/worktree-driver.d.ts +17 -0
  71. package/dist/integrations/subagents/worktree-driver.d.ts.map +1 -0
  72. package/dist/integrations/subagents/worktree-driver.js +72 -0
  73. package/dist/integrations/subagents/worktree-driver.js.map +1 -0
  74. package/dist/integrations/worktrees/managed.d.ts +49 -0
  75. package/dist/integrations/worktrees/managed.d.ts.map +1 -0
  76. package/dist/integrations/worktrees/managed.js +231 -0
  77. package/dist/integrations/worktrees/managed.js.map +1 -0
  78. package/dist/schedule/fire/fire.d.ts.map +1 -1
  79. package/dist/schedule/fire/fire.js +5 -0
  80. package/dist/schedule/fire/fire.js.map +1 -1
  81. package/dist/skills/audit.d.ts +31 -0
  82. package/dist/skills/audit.d.ts.map +1 -0
  83. package/dist/skills/audit.js +75 -0
  84. package/dist/skills/audit.js.map +1 -0
  85. package/dist/tui/App.d.ts.map +1 -1
  86. package/dist/tui/App.js +245 -46
  87. package/dist/tui/App.js.map +1 -1
  88. package/dist/tui/Picker.d.ts.map +1 -1
  89. package/dist/tui/Picker.js +18 -3
  90. package/dist/tui/Picker.js.map +1 -1
  91. package/dist/tui/ResumePicker.d.ts +2 -1
  92. package/dist/tui/ResumePicker.d.ts.map +1 -1
  93. package/dist/tui/ResumePicker.js +3 -3
  94. package/dist/tui/ResumePicker.js.map +1 -1
  95. package/dist/tui/agent.d.ts.map +1 -1
  96. package/dist/tui/agent.js +14 -5
  97. package/dist/tui/agent.js.map +1 -1
  98. package/dist/tui/browser-notices.d.ts.map +1 -1
  99. package/dist/tui/browser-notices.js +6 -1
  100. package/dist/tui/browser-notices.js.map +1 -1
  101. package/dist/tui/credential-entry.d.ts +4 -29
  102. package/dist/tui/credential-entry.d.ts.map +1 -1
  103. package/dist/tui/credential-entry.js +7 -29
  104. package/dist/tui/credential-entry.js.map +1 -1
  105. package/dist/tui/login-prompt.d.ts +3 -1
  106. package/dist/tui/login-prompt.d.ts.map +1 -1
  107. package/dist/tui/login-prompt.js +11 -0
  108. package/dist/tui/login-prompt.js.map +1 -1
  109. package/dist/tui/provider-list.d.ts.map +1 -1
  110. package/dist/tui/provider-list.js +3 -1
  111. package/dist/tui/provider-list.js.map +1 -1
  112. package/dist/tui/slashCommands.d.ts +10 -1
  113. package/dist/tui/slashCommands.d.ts.map +1 -1
  114. package/dist/tui/slashCommands.js +16 -3
  115. package/dist/tui/slashCommands.js.map +1 -1
  116. package/dist/tui/turn-interruption.d.ts.map +1 -1
  117. package/dist/tui/turn-interruption.js +4 -0
  118. package/dist/tui/turn-interruption.js.map +1 -1
  119. package/package.json +6 -5
package/dist/tui/App.js CHANGED
@@ -25,9 +25,10 @@ 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 { openManagedWorktrees, worktreeOpenHint } from '../integrations/worktrees/managed.js';
31
32
  import { NARRATION_TOOL_NAME } from '../integrations/subagents/runtime.js';
32
33
  import { agentsSlashCommand } from '../integrations/subagents/slash.js';
33
34
  import { isTrusted, trustDir } from '../integrations/trust/store.js';
@@ -88,7 +89,7 @@ import { checklistInView, liveWindow, settledBeforeStreaming } from './live-wind
88
89
  import { ViewportBound } from './ViewportBound.js';
89
90
  import { resolveModelSwitch, } from './model-switch.js';
90
91
  import { parseModelSelectionIntent, resolveModelSelectionIntent } from './model-selection-intent.js';
91
- import { describeCodexDeviceLoginStart, describeLoginOutcome, describeLoginStart, describeLogout, describeProviderLogout, } from './login-prompt.js';
92
+ import { describeCodexDeviceLoginStart, describeLoginOutcome, describeLoginStart, describeAllCredentialsLogout, describeProviderLogout, } from './login-prompt.js';
92
93
  import { selectPrimaryProvider } from './provider-selection.js';
93
94
  import { eraseLastChoiceGrapheme } from './terminal-choice-text.js';
94
95
  import { renderCheckpoints, renderRestore } from '../checkpoints/store.js';
@@ -101,6 +102,7 @@ import { moveSelection } from './selection-window.js';
101
102
  import { describeShellEscape, describeShellEscapeForModel, runShellEscape, shellEscapeCommand, } from './shell-escape.js';
102
103
  import { baseBranchReviewPrompt, commitReviewPrompt, hostCommandNames, kernelCommandDescriptors, mergeHostCommands, renderAgents, renderOutcome, reviewPrompt, runSlash, } from './slashCommands.js';
103
104
  import { splitCompleteBlocks, splitSafeCut } from './stream-blocks.js';
105
+ import { terminalDisplayText } from './terminal-display.js';
104
106
  import { terminalSupportsHyperlinks } from './terminal-hyperlinks.js';
105
107
  import { theme } from './theme.js';
106
108
  import { useSelectionIndex } from './use-selection-index.js';
@@ -1165,9 +1167,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1165
1167
  // outlived the row it named would resolve `/expand 3` to output the operator
1166
1168
  // can no longer see anywhere, which is the failure this surface is being
1167
1169
  // cleaned of, one layer up.
1168
- const resetTranscript = useCallback(() => {
1170
+ const resetTranscript = useCallback((options = {}) => {
1169
1171
  if (stdout.isTTY)
1170
1172
  writeStdout('\x1b[2J\x1b[3J\x1b[H');
1173
+ if (!options.preserveOutputViewer)
1174
+ setOutputViewer(null);
1171
1175
  // The scrollback floor goes with the log it counted. <Static> is remounted
1172
1176
  // by the key below and has emitted nothing again; a floor that survived
1173
1177
  // would keep the next conversation's rows out of the live window.
@@ -1179,12 +1183,13 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1179
1183
  const previous = previousTerminalRef.current;
1180
1184
  previousTerminalRef.current = terminal;
1181
1185
  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();
1186
+ (terminal.columns !== previous.columns || terminal.rows < previous.rows)) {
1187
+ // A contraction can move live rows into native scrollback before Ink
1188
+ // receives resize; expansion leaves already-printed Static rows wrapped
1189
+ // at their old width. Rebuild the normal-buffer transcript at the new
1190
+ // width, retaining messages, draft and selected child. Regular frames
1191
+ // remain incremental.
1192
+ resetTranscript({ preserveOutputViewer: true });
1188
1193
  }
1189
1194
  }, [terminal, resetTranscript]);
1190
1195
  const requestExternalEditor = useCallback((seed) => {
@@ -1802,26 +1807,44 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1802
1807
  }
1803
1808
  }, [conversationLearning, ctx.cwd, ctx.skills, pushMessage]);
1804
1809
  const removeStoredCredential = useCallback((target) => {
1805
- const path = credentialsPath();
1810
+ const path = target === 'google' ? googleApiKeyPath() : credentialsPath();
1806
1811
  const hadClaude = readStoredSubscriptionCredential() !== null;
1807
1812
  const hadCodex = readStoredCodexCredential() !== null;
1813
+ const hadGoogleKey = readStoredGoogleApiKey() !== null;
1814
+ if (target === 'all') {
1815
+ const failures = [];
1816
+ try {
1817
+ clearAllStoredCredentials();
1818
+ }
1819
+ catch (error) {
1820
+ failures.push(`${credentialsPath()}: ${error instanceof Error ? error.message : String(error)}`);
1821
+ }
1822
+ try {
1823
+ clearStoredGoogleApiKey();
1824
+ }
1825
+ catch (error) {
1826
+ failures.push(`${googleApiKeyPath()}: ${error instanceof Error ? error.message : String(error)}`);
1827
+ }
1828
+ if (failures.length > 0) {
1829
+ pushMessage('system', `Some stored credentials could not be removed: ${failures.join('; ')}. Check both stores before retrying.`);
1830
+ return;
1831
+ }
1832
+ pushMessage('system', describeAllCredentialsLogout(path, googleApiKeyPath(), hadClaude || hadCodex || hadGoogleKey));
1833
+ return;
1834
+ }
1808
1835
  try {
1809
1836
  if (target === 'anthropic')
1810
1837
  clearStoredSubscriptionCredential();
1811
1838
  else if (target === 'codex')
1812
1839
  clearStoredCodexCredential();
1813
1840
  else
1814
- clearAllStoredCredentials();
1841
+ clearStoredGoogleApiKey();
1815
1842
  }
1816
1843
  catch (error) {
1817
1844
  pushMessage('system', `Could not remove ${path}: ${error instanceof Error ? error.message : String(error)}`);
1818
1845
  return;
1819
1846
  }
1820
- if (target === 'all') {
1821
- pushMessage('system', describeLogout(path, hadClaude || hadCodex));
1822
- return;
1823
- }
1824
- pushMessage('system', describeProviderLogout(path, target, target === 'anthropic' ? hadClaude : hadCodex));
1847
+ pushMessage('system', describeProviderLogout(path, target, target === 'anthropic' ? hadClaude : target === 'codex' ? hadCodex : hadGoogleKey));
1825
1848
  }, [pushMessage]);
1826
1849
  /** Publish one recoverable, read-only tombstone before leaving the TUI. */
1827
1850
  const archiveCurrentConversation = useCallback(() => {
@@ -1864,7 +1887,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
1864
1887
  closeAndExit();
1865
1888
  }
1866
1889
  catch (error) {
1867
- pushMessage('system', `Could not archive this conversation: ${error instanceof Error ? error.message : String(error)}`);
1890
+ pushMessage('system', error instanceof ConversationIndexRefreshError
1891
+ ? error.message
1892
+ : `Could not archive this conversation: ${error instanceof Error ? error.message : String(error)}`);
1868
1893
  }
1869
1894
  finally {
1870
1895
  if (!archived && conversationMutationRef.current === 'archive') {
@@ -2648,10 +2673,11 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
2648
2673
  setMentionCandidates(files);
2649
2674
  });
2650
2675
  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)
2676
+ // Failed and superseded provider choices returned above. A usable
2677
+ // provider change can recover a failed turn's dependent queue, but a
2678
+ // paused turn still owns the conversation's durable active slot. Keep
2679
+ // that queue held until its checkpoint is resumed and settles.
2680
+ if (signal !== undefined && queuePauseRef.current?.outcome !== 'paused')
2655
2681
  advanceQueueContinuation();
2656
2682
  if (s.hasProvider) {
2657
2683
  setTranscriptOwned(true);
@@ -3044,9 +3070,6 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3044
3070
  // terminal scrollback; the live tail stays deliberately small so an activity
3045
3071
  // tick never repaints the whole conversation.
3046
3072
  //
3047
- useEffect(() => {
3048
- setOutputViewer(null);
3049
- }, [resetKey]);
3050
3073
  useEffect(() => {
3051
3074
  if (permission !== null)
3052
3075
  setOutputViewer(null);
@@ -3185,7 +3208,8 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3185
3208
  const scope = scopeRef.current;
3186
3209
  if (!sessions || !scope || !conversationMaterializedRef.current || !session?.hasProvider)
3187
3210
  return false;
3188
- const active = await activeConversationTurn(sessions, scope.sessionId).catch(() => undefined);
3211
+ const resumedSessionId = scope.sessionId;
3212
+ const active = await activeConversationTurn(sessions, resumedSessionId).catch(() => undefined);
3189
3213
  if (!active)
3190
3214
  return false;
3191
3215
  if (abortRef.current || state !== 'idle') {
@@ -3266,7 +3290,19 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3266
3290
  finalizeMessage(st.assistantId);
3267
3291
  // The resumed segment was appended to the log; the fold is the context
3268
3292
  // the next prompt continues from.
3269
- modelHistoryRef.current = await loadConversation(sessions, scope.sessionId);
3293
+ modelHistoryRef.current = await loadConversation(sessions, resumedSessionId);
3294
+ // A successfully completed checkpoint frees the conversation's active
3295
+ // slot. Only then may prompts queued behind the paused turn start. A
3296
+ // repeated pause, failed resume, unreadable log, or conversation switch
3297
+ // keeps dependent work held for an explicit operator decision.
3298
+ if (st.completed &&
3299
+ queuePauseRef.current?.outcome === 'paused' &&
3300
+ scopeRef.current?.sessionId === resumedSessionId &&
3301
+ (await activeConversationTurn(sessions, resumedSessionId)
3302
+ .then((next) => next === undefined)
3303
+ .catch(() => false))) {
3304
+ advanceQueueContinuation();
3305
+ }
3270
3306
  }
3271
3307
  catch (err) {
3272
3308
  pushMessage('system', `Could not resume the turn: ${err instanceof Error ? err.message : String(err)}`);
@@ -3283,7 +3319,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3283
3319
  await restoreBrowser?.().catch(() => undefined);
3284
3320
  }
3285
3321
  return true;
3286
- }, [detected, finalizeMessage, flushStream, pushMessage, session, setHandoffPark, state]);
3322
+ }, [advanceQueueContinuation, detected, finalizeMessage, flushStream, pushMessage, session, setHandoffPark, state]);
3287
3323
  // `namzu resume <id>` of a scheduled run parked on a decision: what the
3288
3324
  // notification and `/schedule` tell the operator to run. Continue it once
3289
3325
  // the screen is ready, exactly as `/resume` would, so the parked call is on
@@ -3331,6 +3367,35 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3331
3367
  pushMessage('system', `Could not list conversations: ${err instanceof Error ? err.message : String(err)}`);
3332
3368
  }
3333
3369
  }, [ensureSessions, pushMessage, resumeActiveTurn]);
3370
+ const doUnarchive = useCallback(async () => {
3371
+ if (state !== 'idle' ||
3372
+ abortRef.current !== null ||
3373
+ hasUnsettledTurn() ||
3374
+ queuedRef.current.length > 0 ||
3375
+ compactingRef.current ||
3376
+ exportingRef.current) {
3377
+ pushMessage('system', 'Finish the current turn and queued work before restoring a conversation.');
3378
+ return;
3379
+ }
3380
+ const sessions = sessionsRef.current ?? (await ensureSessions(), sessionsRef.current);
3381
+ if (!sessions) {
3382
+ pushMessage('system', 'Conversation history is unavailable in this folder.');
3383
+ return;
3384
+ }
3385
+ try {
3386
+ const archived = await listArchived(sessions, 100);
3387
+ if (archived.length === 0) {
3388
+ pushMessage('system', 'No archived conversations in this project.');
3389
+ return;
3390
+ }
3391
+ setResumeList(archived);
3392
+ setSelectedResume(0);
3393
+ setPhase('unarchive');
3394
+ }
3395
+ catch (err) {
3396
+ pushMessage('system', `Could not list archived conversations: ${err instanceof Error ? err.message : String(err)}`);
3397
+ }
3398
+ }, [ensureSessions, hasUnsettledTurn, pushMessage, state]);
3334
3399
  // Resolve a pending permission prompt with the user's decision and tear
3335
3400
  // down the overlay. No-op if nothing is pending.
3336
3401
  const resolvePermission = useCallback((decision) => {
@@ -3511,6 +3576,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3511
3576
  resumeCommittedRef.current = true;
3512
3577
  let msgs;
3513
3578
  try {
3579
+ await requireWritableConversation(sessions, conv.id, 'resume conversation');
3514
3580
  msgs = await loadConversation(sessions, conv.id);
3515
3581
  }
3516
3582
  catch (err) {
@@ -3580,6 +3646,42 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
3580
3646
  resetTranscript,
3581
3647
  wakeGoalDriver,
3582
3648
  ]);
3649
+ const restoreArchivedConversation = useCallback(async (conv) => {
3650
+ const sessions = sessionsRef.current;
3651
+ if (!sessions) {
3652
+ setPhase('ready');
3653
+ return;
3654
+ }
3655
+ if (state !== 'idle' ||
3656
+ abortRef.current !== null ||
3657
+ hasUnsettledTurn() ||
3658
+ queuedRef.current.length > 0 ||
3659
+ compactingRef.current ||
3660
+ exportingRef.current) {
3661
+ setPhase('ready');
3662
+ pushMessage('system', 'Finish the current turn and queued work before restoring a conversation.');
3663
+ return;
3664
+ }
3665
+ resumeCommittedRef.current = true;
3666
+ try {
3667
+ await unarchiveConversation(sessions, conv.id);
3668
+ }
3669
+ catch (err) {
3670
+ resumeCommittedRef.current = false;
3671
+ setPhase('ready');
3672
+ pushMessage('system', err instanceof ConversationIndexRefreshError
3673
+ ? err.message
3674
+ : `Could not restore: ${err instanceof Error ? err.message : String(err)}`);
3675
+ return;
3676
+ }
3677
+ pushMessage('system', `Restored: ${conv.title}`);
3678
+ try {
3679
+ await resumeConversation(conv);
3680
+ }
3681
+ finally {
3682
+ resumeCommittedRef.current = false;
3683
+ }
3684
+ }, [hasUnsettledTurn, pushMessage, resumeConversation, state]);
3583
3685
  /**
3584
3686
  * Start a new conversation without deleting the one being left.
3585
3687
  *
@@ -4539,8 +4641,9 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
4539
4641
  `${describeTurnInterruption(event)}\nWhen that is done, press Enter to continue · Esc to stop.`, false, '‖');
4540
4642
  setHandoffPark({ turnId: event.turnId, reason: event.handoff.reason });
4541
4643
  }
4542
- else
4543
- pushMessage('system', describeTurnInterruption(event), false, '‖');
4644
+ else {
4645
+ pushMessage('system', `${describeTurnInterruption(event)}\nUse /resume to continue this checkpoint on the selected provider, or /abandon to start a new turn.`, false, '‖');
4646
+ }
4544
4647
  break;
4545
4648
  case 'error':
4546
4649
  closeAssistant();
@@ -5816,6 +5919,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5816
5919
  const choices = [
5817
5920
  ...(readStoredSubscriptionCredential() ? ['anthropic'] : []),
5818
5921
  ...(readStoredCodexCredential() ? ['codex'] : []),
5922
+ ...(readStoredGoogleApiKey() ? ['google'] : []),
5819
5923
  ];
5820
5924
  if (choices.length === 0) {
5821
5925
  removeStoredCredential('all');
@@ -5828,14 +5932,16 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5828
5932
  setSelectedChoice(0);
5829
5933
  setChoicePicker({
5830
5934
  kind: 'credential-logout',
5831
- title: 'Choose a stored subscription to remove',
5935
+ title: 'Choose a stored credential to remove',
5832
5936
  notice: 'Only credentials created by Namzu are listed. Device sessions owned by other tools are left alone.',
5833
5937
  values: choices,
5834
5938
  options: choices.map((choice) => ({
5835
- label: choice === 'anthropic' ? 'Claude' : 'Codex',
5939
+ label: choice === 'anthropic' ? 'Claude' : choice === 'codex' ? 'Codex' : 'Gemini API key',
5836
5940
  description: choice === 'anthropic'
5837
5941
  ? 'Remove only Namzu’s Claude subscription.'
5838
- : 'Remove only Namzu’s Codex subscription.',
5942
+ : choice === 'codex'
5943
+ ? 'Remove only Namzu’s Codex subscription.'
5944
+ : 'Remove only Namzu’s saved Gemini API key.',
5839
5945
  })),
5840
5946
  });
5841
5947
  return;
@@ -5992,12 +6098,80 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
5992
6098
  case 'resume':
5993
6099
  void doResume();
5994
6100
  return;
6101
+ case 'unarchive':
6102
+ void doUnarchive();
6103
+ return;
5995
6104
  case 'title':
5996
6105
  void doTitle(slash.title, slash.clear);
5997
6106
  return;
5998
6107
  case 'fork':
5999
6108
  void doFork();
6000
6109
  return;
6110
+ case 'worktree': {
6111
+ const [verb = 'list', label, extra] = slash.args;
6112
+ const notice = (content) => pushMessage('system', terminalDisplayText(content));
6113
+ if (!((verb === 'list' && label === undefined) ||
6114
+ (verb === 'create' && extra === undefined) ||
6115
+ (verb === 'fork' && extra === undefined) ||
6116
+ (verb === 'resume' &&
6117
+ label !== undefined &&
6118
+ extra === undefined))) {
6119
+ pushMessage('system', 'Usage: /worktree [list|create [name]|fork [name]|resume <name>]');
6120
+ return;
6121
+ }
6122
+ if (verb === 'fork' &&
6123
+ (abortRef.current ||
6124
+ hasUnsettledTurn() ||
6125
+ queuedRef.current.length > 0)) {
6126
+ pushMessage('system', 'Wait for the current turn and queued prompts to finish before forking to a worktree.');
6127
+ return;
6128
+ }
6129
+ if (conversationMutationRef.current)
6130
+ return;
6131
+ if (verb === 'fork') {
6132
+ conversationMutationRef.current = 'fork';
6133
+ setConversationMutation('fork');
6134
+ }
6135
+ void (async () => {
6136
+ try {
6137
+ const manager = await openManagedWorktrees(ctx.cwd);
6138
+ if (verb === 'list') {
6139
+ const items = await manager.list();
6140
+ notice(items.length === 0
6141
+ ? 'No managed worktrees in this repository.'
6142
+ : items
6143
+ .map((item) => `${item.label}: ${item.path}${item.dirty ? ' (uncommitted files)' : ''}`)
6144
+ .join('\n'));
6145
+ }
6146
+ else if (verb === 'create') {
6147
+ const created = await manager.create(label);
6148
+ notice(`Created ${created.branch}. Open it in a new terminal: ${worktreeOpenHint(created.path)}${created.sourceDirty ? '\nUncommitted files stayed in this checkout.' : ''}`);
6149
+ }
6150
+ else if (verb === 'fork') {
6151
+ const scope = await materializeConversation();
6152
+ if (!scope)
6153
+ throw new Error('Conversation history is unavailable in this folder.');
6154
+ await persistenceTailRef.current;
6155
+ const forked = await manager.fork(scope.sessionId, label);
6156
+ notice(`Forked ${forked.copied} messages into ${forked.title}. Open the new checkout in a new terminal: ${worktreeOpenHint(forked.path, forked.conversationId)}${forked.sourceDirty ? '\nUncommitted files stayed in this checkout.' : ''}`);
6157
+ }
6158
+ else if (label) {
6159
+ const target = await manager.resume(label);
6160
+ notice(`Open ${label} in a new terminal: ${worktreeOpenHint(target.worktree.path, target.conversationId)}`);
6161
+ }
6162
+ }
6163
+ catch (error) {
6164
+ notice(`Worktree: ${error instanceof Error ? error.message : String(error)}`);
6165
+ }
6166
+ finally {
6167
+ if (verb === 'fork') {
6168
+ conversationMutationRef.current = null;
6169
+ setConversationMutation(null);
6170
+ }
6171
+ }
6172
+ })();
6173
+ return;
6174
+ }
6001
6175
  case 'restore':
6002
6176
  void doRestore(slash.turn);
6003
6177
  return;
@@ -6448,12 +6622,14 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6448
6622
  appLifetime,
6449
6623
  ctx.cwd,
6450
6624
  doResume,
6625
+ doUnarchive,
6451
6626
  enqueueQueued,
6452
6627
  exitWithSummary,
6453
6628
  hasUnsettledTurn,
6454
6629
  goalActivation,
6455
6630
  goalStatus,
6456
6631
  hostCommands,
6632
+ materializeConversation,
6457
6633
  nextId,
6458
6634
  openAgentCockpit,
6459
6635
  runAgentsCommand,
@@ -6778,29 +6954,24 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6778
6954
  announce: true,
6779
6955
  chooseReasoningEffort: options.chooseReasoningEffort,
6780
6956
  });
6957
+ return !signal.aborted;
6781
6958
  }
6782
6959
  catch (err) {
6783
6960
  // A superseded choice no longer owns even its failure message. Its
6784
6961
  // eventual session object is disposed inside `hydrateSession`; a live
6785
6962
  // choice stays on the picker with the actionable construction error.
6786
6963
  if (signal.aborted)
6787
- return;
6964
+ return false;
6788
6965
  if (options.revealAllOnFailure) {
6789
6966
  setPickerDetected(null);
6790
6967
  setPickerSelectionKind('provider-and-model');
6791
6968
  setPickerInitialView('providers');
6792
6969
  }
6793
6970
  setPickerNotice(`Could not start the selected provider: ${err instanceof Error ? err.message : String(err)}`);
6971
+ return false;
6794
6972
  }
6795
6973
  }, [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
- */
6974
+ /** A typed Google API key is saved only after its provider session opens. */
6804
6975
  const handleTypedCredential = useCallback((credential, disposition, signal) => {
6805
6976
  const next = [credential, ...detected.filter((d) => d.entry.id !== credential.entry.id)];
6806
6977
  setDetected(next);
@@ -6816,7 +6987,32 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6816
6987
  const prefs = saved && primaryProvider(saved).id === credential.entry.id
6817
6988
  ? saved
6818
6989
  : selectPrimaryProvider(saved, { id: credential.entry.id });
6819
- void hydrateFromPicker(prefs, next, signal);
6990
+ void (async () => {
6991
+ const started = await hydrateFromPicker(prefs, next, signal);
6992
+ if (!started || credential.entry.id !== 'google')
6993
+ return;
6994
+ let path;
6995
+ try {
6996
+ path = writeStoredGoogleApiKey(credential.apiKey ?? '');
6997
+ }
6998
+ catch (error) {
6999
+ 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.`);
7000
+ return;
7001
+ }
7002
+ setDetected((current) => current.map((provider) => provider.entry.id === 'google' &&
7003
+ provider.source.kind === 'session' &&
7004
+ provider.apiKey === credential.apiKey
7005
+ ? { ...provider, source: { kind: 'stored-gemini-key', path } }
7006
+ : provider));
7007
+ try {
7008
+ writePreferences(prefs);
7009
+ }
7010
+ catch (error) {
7011
+ 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.`);
7012
+ return;
7013
+ }
7014
+ pushMessage('system', `Gemini API key saved privately at ${path} for future launches. Run /logout gemini to remove it.`);
7015
+ })();
6820
7016
  }, [detected, hydrateFromPicker, pushMessage]);
6821
7017
  const handlePickerSubmit = useCallback((selection, signal) => {
6822
7018
  const prefs = selectPrimaryProvider(savedPrefsRef.current, {
@@ -6923,7 +7119,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6923
7119
  return;
6924
7120
  }
6925
7121
  // Resume picker owns the keyboard while open.
6926
- if (phase === 'resume') {
7122
+ if (phase === 'resume' || phase === 'unarchive') {
6927
7123
  // Once a conversation is being read, the keyboard does nothing here.
6928
7124
  // The choice is already being acted on; a second Enter would start a
6929
7125
  // second read and an Esc would hand back a screen that is about to be
@@ -6940,7 +7136,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
6940
7136
  else if (key.return) {
6941
7137
  const conv = resumeList[selectedResumeRef.current];
6942
7138
  if (conv)
6943
- void resumeConversation(conv);
7139
+ void (phase === 'unarchive' ? restoreArchivedConversation(conv) : resumeConversation(conv));
6944
7140
  }
6945
7141
  else if (key.escape || (key.ctrl && input === 'c'))
6946
7142
  setPhase('ready');
@@ -7461,6 +7657,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
7461
7657
  const lifecycleOwnsViewport = outputViewer !== null ||
7462
7658
  phase === 'trust' ||
7463
7659
  phase === 'resume' ||
7660
+ phase === 'unarchive' ||
7464
7661
  phase === 'edit' ||
7465
7662
  phase === 'picker' ||
7466
7663
  agentSurface !== null;
@@ -7568,7 +7765,7 @@ export function App({ ctx: initialCtx, onExitSummary, externalEditor = defaultEx
7568
7765
  setPickerSelectionKind('provider-and-model');
7569
7766
  setPhase('picker');
7570
7767
  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,
7768
+ } })) : 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
7769
  // The same condition `handlePickerCancel` branches on, read
7573
7770
  // where it is decided rather than guessed on the screen: with no
7574
7771
  // session behind the picker, esc exits namzu and the footer says
@@ -7687,6 +7884,8 @@ hasDraft = false) {
7687
7884
  return 'y trust this folder · n / esc exit';
7688
7885
  if (phase === 'resume')
7689
7886
  return '↑↓ navigate · enter resume · esc cancel';
7887
+ if (phase === 'unarchive')
7888
+ return '↑↓ navigate · enter restore · esc cancel';
7690
7889
  if (phase === 'edit')
7691
7890
  return '← older · → newer · enter fork and edit · esc cancel';
7692
7891
  if (phase === 'probing')