@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.
- package/CHANGELOG.md +44 -0
- package/dist/browser/commands.js +11 -7
- package/dist/browser/commands.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +21 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/archive.d.ts +4 -0
- package/dist/commands/archive.d.ts.map +1 -0
- package/dist/commands/archive.js +62 -0
- package/dist/commands/archive.js.map +1 -0
- package/dist/commands/login.d.ts +1 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +33 -11
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/mcp-oauth-login.d.ts +19 -0
- package/dist/commands/mcp-oauth-login.d.ts.map +1 -0
- package/dist/commands/mcp-oauth-login.js +213 -0
- package/dist/commands/mcp-oauth-login.js.map +1 -0
- package/dist/commands/mcp.d.ts +3 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +288 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/skills.d.ts.map +1 -1
- package/dist/commands/skills.js +60 -5
- package/dist/commands/skills.js.map +1 -1
- package/dist/commands/types.d.ts +2 -0
- package/dist/commands/types.d.ts.map +1 -1
- package/dist/commands/worktree.d.ts +6 -0
- package/dist/commands/worktree.d.ts.map +1 -0
- package/dist/commands/worktree.js +85 -0
- package/dist/commands/worktree.js.map +1 -0
- package/dist/config/user-config.d.ts +2 -0
- package/dist/config/user-config.d.ts.map +1 -1
- package/dist/config/user-config.js +76 -6
- package/dist/config/user-config.js.map +1 -1
- package/dist/doctor/checks/credentials.d.ts.map +1 -1
- package/dist/doctor/checks/credentials.js +2 -0
- package/dist/doctor/checks/credentials.js.map +1 -1
- package/dist/integrations/mcp/oauth-store.d.ts +40 -0
- package/dist/integrations/mcp/oauth-store.d.ts.map +1 -0
- package/dist/integrations/mcp/oauth-store.js +549 -0
- package/dist/integrations/mcp/oauth-store.js.map +1 -0
- package/dist/integrations/mcp/servers.d.ts.map +1 -1
- package/dist/integrations/mcp/servers.js +155 -2
- package/dist/integrations/mcp/servers.js.map +1 -1
- package/dist/integrations/mcp/user-config.d.ts +25 -0
- package/dist/integrations/mcp/user-config.d.ts.map +1 -0
- package/dist/integrations/mcp/user-config.js +92 -0
- package/dist/integrations/mcp/user-config.js.map +1 -0
- package/dist/integrations/providers/credential-store.d.ts +8 -0
- package/dist/integrations/providers/credential-store.d.ts.map +1 -1
- package/dist/integrations/providers/credential-store.js +111 -22
- package/dist/integrations/providers/credential-store.js.map +1 -1
- package/dist/integrations/providers/discover.d.ts +11 -5
- package/dist/integrations/providers/discover.d.ts.map +1 -1
- package/dist/integrations/providers/discover.js +26 -12
- package/dist/integrations/providers/discover.js.map +1 -1
- package/dist/integrations/providers/index.d.ts +1 -1
- package/dist/integrations/providers/index.d.ts.map +1 -1
- package/dist/integrations/providers/index.js +1 -1
- package/dist/integrations/providers/index.js.map +1 -1
- package/dist/integrations/sessions/store.d.ts +23 -5
- package/dist/integrations/sessions/store.d.ts.map +1 -1
- package/dist/integrations/sessions/store.js +164 -24
- package/dist/integrations/sessions/store.js.map +1 -1
- package/dist/integrations/subagents/runtime.d.ts +4 -2
- package/dist/integrations/subagents/runtime.d.ts.map +1 -1
- package/dist/integrations/subagents/runtime.js +62 -8
- package/dist/integrations/subagents/runtime.js.map +1 -1
- package/dist/integrations/subagents/worktree-driver.d.ts +17 -0
- package/dist/integrations/subagents/worktree-driver.d.ts.map +1 -0
- package/dist/integrations/subagents/worktree-driver.js +72 -0
- package/dist/integrations/subagents/worktree-driver.js.map +1 -0
- package/dist/integrations/worktrees/managed.d.ts +49 -0
- package/dist/integrations/worktrees/managed.d.ts.map +1 -0
- package/dist/integrations/worktrees/managed.js +231 -0
- package/dist/integrations/worktrees/managed.js.map +1 -0
- package/dist/schedule/fire/fire.d.ts.map +1 -1
- package/dist/schedule/fire/fire.js +5 -0
- package/dist/schedule/fire/fire.js.map +1 -1
- package/dist/skills/audit.d.ts +31 -0
- package/dist/skills/audit.d.ts.map +1 -0
- package/dist/skills/audit.js +75 -0
- package/dist/skills/audit.js.map +1 -0
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +245 -46
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/Picker.d.ts.map +1 -1
- package/dist/tui/Picker.js +18 -3
- package/dist/tui/Picker.js.map +1 -1
- package/dist/tui/ResumePicker.d.ts +2 -1
- package/dist/tui/ResumePicker.d.ts.map +1 -1
- package/dist/tui/ResumePicker.js +3 -3
- package/dist/tui/ResumePicker.js.map +1 -1
- package/dist/tui/agent.d.ts.map +1 -1
- package/dist/tui/agent.js +14 -5
- package/dist/tui/agent.js.map +1 -1
- package/dist/tui/browser-notices.d.ts.map +1 -1
- package/dist/tui/browser-notices.js +6 -1
- package/dist/tui/browser-notices.js.map +1 -1
- package/dist/tui/credential-entry.d.ts +4 -29
- package/dist/tui/credential-entry.d.ts.map +1 -1
- package/dist/tui/credential-entry.js +7 -29
- package/dist/tui/credential-entry.js.map +1 -1
- package/dist/tui/login-prompt.d.ts +3 -1
- package/dist/tui/login-prompt.d.ts.map +1 -1
- package/dist/tui/login-prompt.js +11 -0
- package/dist/tui/login-prompt.js.map +1 -1
- package/dist/tui/provider-list.d.ts.map +1 -1
- package/dist/tui/provider-list.js +3 -1
- package/dist/tui/provider-list.js.map +1 -1
- package/dist/tui/slashCommands.d.ts +10 -1
- package/dist/tui/slashCommands.d.ts.map +1 -1
- package/dist/tui/slashCommands.js +16 -3
- package/dist/tui/slashCommands.js.map +1 -1
- package/dist/tui/turn-interruption.d.ts.map +1 -1
- package/dist/tui/turn-interruption.js +4 -0
- package/dist/tui/turn-interruption.js.map +1 -1
- 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,
|
|
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
|
|
1183
|
-
// A contraction can
|
|
1184
|
-
// receives resize
|
|
1185
|
-
//
|
|
1186
|
-
// messages, draft and selected child. Regular frames
|
|
1187
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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',
|
|
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
|
-
//
|
|
2652
|
-
//
|
|
2653
|
-
//
|
|
2654
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
:
|
|
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
|
|
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')
|