@namzu/cli 13.0.0 → 14.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.
- package/README.md +13 -2
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +79 -21
- package/dist/commands/login.js.map +1 -1
- package/dist/doctor/checks/credentials.d.ts.map +1 -1
- package/dist/doctor/checks/credentials.js +7 -1
- package/dist/doctor/checks/credentials.js.map +1 -1
- package/dist/integrations/providers/codex-device-login.d.ts +27 -0
- package/dist/integrations/providers/codex-device-login.d.ts.map +1 -0
- package/dist/integrations/providers/codex-device-login.js +195 -0
- package/dist/integrations/providers/codex-device-login.js.map +1 -0
- package/dist/integrations/providers/codex-oauth.d.ts +10 -0
- package/dist/integrations/providers/codex-oauth.d.ts.map +1 -0
- package/dist/integrations/providers/codex-oauth.js +95 -0
- package/dist/integrations/providers/codex-oauth.js.map +1 -0
- package/dist/integrations/providers/credential-store.d.ts +23 -4
- package/dist/integrations/providers/credential-store.d.ts.map +1 -1
- package/dist/integrations/providers/credential-store.js +125 -19
- package/dist/integrations/providers/credential-store.js.map +1 -1
- package/dist/integrations/providers/discover.d.ts +39 -13
- package/dist/integrations/providers/discover.d.ts.map +1 -1
- package/dist/integrations/providers/discover.js +122 -44
- package/dist/integrations/providers/discover.js.map +1 -1
- package/dist/integrations/providers/harness-credentials.d.ts +36 -0
- package/dist/integrations/providers/harness-credentials.d.ts.map +1 -0
- package/dist/integrations/providers/harness-credentials.js +146 -0
- package/dist/integrations/providers/harness-credentials.js.map +1 -0
- package/dist/integrations/providers/identity.d.ts +8 -15
- package/dist/integrations/providers/identity.d.ts.map +1 -1
- package/dist/integrations/providers/identity.js +8 -15
- package/dist/integrations/providers/identity.js.map +1 -1
- package/dist/integrations/providers/index.d.ts +6 -3
- package/dist/integrations/providers/index.d.ts.map +1 -1
- package/dist/integrations/providers/index.js +5 -2
- package/dist/integrations/providers/index.js.map +1 -1
- package/dist/integrations/providers/oauth.d.ts +2 -2
- package/dist/integrations/providers/oauth.d.ts.map +1 -1
- package/dist/integrations/providers/oauth.js +8 -5
- package/dist/integrations/providers/oauth.js.map +1 -1
- package/dist/integrations/providers/register.d.ts.map +1 -1
- package/dist/integrations/providers/register.js +9 -1
- package/dist/integrations/providers/register.js.map +1 -1
- package/dist/integrations/providers/registry.d.ts +8 -1
- package/dist/integrations/providers/registry.d.ts.map +1 -1
- package/dist/integrations/providers/registry.js +29 -0
- package/dist/integrations/providers/registry.js.map +1 -1
- package/dist/tui/App.d.ts +5 -5
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +257 -98
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/CopyPicker.d.ts +8 -0
- package/dist/tui/CopyPicker.d.ts.map +1 -0
- package/dist/tui/CopyPicker.js +20 -0
- package/dist/tui/CopyPicker.js.map +1 -0
- package/dist/tui/Picker.d.ts +12 -3
- package/dist/tui/Picker.d.ts.map +1 -1
- package/dist/tui/Picker.js +88 -27
- package/dist/tui/Picker.js.map +1 -1
- package/dist/tui/agent.d.ts +2 -5
- package/dist/tui/agent.d.ts.map +1 -1
- package/dist/tui/agent.js +112 -17
- package/dist/tui/agent.js.map +1 -1
- package/dist/tui/copy-targets.d.ts +23 -0
- package/dist/tui/copy-targets.d.ts.map +1 -0
- package/dist/tui/copy-targets.js +189 -0
- package/dist/tui/copy-targets.js.map +1 -0
- package/dist/tui/login-prompt.d.ts +5 -0
- package/dist/tui/login-prompt.d.ts.map +1 -1
- package/dist/tui/login-prompt.js +14 -0
- package/dist/tui/login-prompt.js.map +1 -1
- package/dist/tui/model-choices.d.ts.map +1 -1
- package/dist/tui/model-choices.js +6 -1
- package/dist/tui/model-choices.js.map +1 -1
- package/dist/tui/slashCommands.d.ts +1 -1
- package/dist/tui/slashCommands.d.ts.map +1 -1
- package/dist/tui/slashCommands.js +2 -2
- package/dist/tui/slashCommands.js.map +1 -1
- package/package.json +6 -6
package/dist/tui/App.js
CHANGED
|
@@ -5,51 +5,53 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
5
5
|
*
|
|
6
6
|
* Session lifecycle:
|
|
7
7
|
* 1. Mount → probeAgentSession() (readPreferences + discoverProviders).
|
|
8
|
-
* 2. If a
|
|
9
|
-
* 3.
|
|
10
|
-
*
|
|
11
|
-
* 4.
|
|
12
|
-
*
|
|
8
|
+
* 2. If a current preferences file exists → createAgentSession(prefs, detected) → ready.
|
|
9
|
+
* 3. With no preferences, reuse one signed-in installed-harness subscription
|
|
10
|
+
* directly, or ask between available subscriptions when there is a choice.
|
|
11
|
+
* 4. Otherwise show <Picker/>. An explicit picker choice is persisted;
|
|
12
|
+
* automatic reuse is not mistaken for a durable operator preference.
|
|
13
13
|
*/
|
|
14
|
-
import { DiskMessageFeedbackStore, GoalRoundLimitError, HostCommandRegistry, SessionGoalActivation, StaleGoalError, kernelHostCommands, createAssistantMessage, createUserMessage, generateRunId, isCompactionMessage, } from '@namzu/sdk';
|
|
15
|
-
import { Box, Text, useApp, useInput, useStdout } from 'ink';
|
|
16
14
|
import { join, relative } from 'node:path';
|
|
15
|
+
import { DiskMessageFeedbackStore, GoalRoundLimitError, HostCommandRegistry, SessionGoalActivation, StaleGoalError, createAssistantMessage, createUserMessage, generateRunId, isCompactionMessage, kernelHostCommands, } from '@namzu/sdk';
|
|
16
|
+
import { Box, Text, useApp, useInput, useStdout } from 'ink';
|
|
17
17
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
18
|
-
import { visibleProjectInstructionPath } from '../context/project-path.js';
|
|
19
18
|
import { pinTrustedProjectPath, resolveTrustedProjectContext, } from '../config/trusted-project-context.js';
|
|
20
|
-
import {
|
|
19
|
+
import { visibleProjectInstructionPath } from '../context/project-path.js';
|
|
21
20
|
import { writeClipboardText } from '../integrations/clipboard/text.js';
|
|
22
21
|
import { terminalNotificationEnabled, writeTerminalNotification, } from '../integrations/notifications/terminal.js';
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
22
|
+
import { beginCodexDeviceLogin, beginSubscriptionLogin, clearAllStoredCredentials, credentialsPath, primaryProvider, readStoredCodexCredential, readStoredSubscriptionCredential, signedInSubscriptionProviders, writePreferences, } from '../integrations/providers/index.js';
|
|
23
|
+
import { appendMessages, forkConversation, forkConversationBeforeUser, listRecent, loadConversation, openSessions, replaceConversation, requireWritableConversation, setTitle, startConversation, titleOf, } from '../integrations/sessions/store.js';
|
|
24
|
+
import { conversationMarkdown, writeConversationExport, } from '../integrations/sessions/transcript-export.js';
|
|
25
25
|
import { isTrusted, trustDir } from '../integrations/trust/store.js';
|
|
26
|
+
import { checkUpdates } from '../integrations/updates.js';
|
|
26
27
|
import { appendMemory, composeMemoryPrompt, readMemory } from '../memory/store.js';
|
|
27
28
|
import { composeSkillsPrompt, discoverSkills, loadSkillBody } from '../skills/store.js';
|
|
28
|
-
import {
|
|
29
|
-
import { LiveActivity, formatElapsed } from './LiveActivity.js';
|
|
30
|
-
import { bottomSpacerRows } from './bottom-spacer.js';
|
|
31
|
-
import { expandFileMentions } from './mentions.js';
|
|
29
|
+
import { discoverUserCommands } from '../user-commands/store.js';
|
|
32
30
|
import { Composer } from './Composer.js';
|
|
31
|
+
import { CopyPicker } from './CopyPicker.js';
|
|
33
32
|
import { EditPromptPicker } from './EditPromptPicker.js';
|
|
34
|
-
import {
|
|
35
|
-
import { NAMZU_MARK, NAMZU_MARK_COLOR, NAMZU_WORDMARK, NAMZU_WORDMARK_GRADIENT, NAMZU_WORDMARK_MIN_WIDTH, } from './logo.js';
|
|
33
|
+
import { LiveActivity, formatElapsed } from './LiveActivity.js';
|
|
36
34
|
import { PermissionOverlay } from './PermissionOverlay.js';
|
|
37
|
-
import { approvalIsDeliberate } from './consent-timing.js';
|
|
38
35
|
import { Picker } from './Picker.js';
|
|
36
|
+
import { ResumePicker } from './ResumePicker.js';
|
|
39
37
|
import { StatusBar } from './StatusBar.js';
|
|
40
38
|
import { Transcript, willCollapse } from './Transcript.js';
|
|
41
|
-
import {
|
|
42
|
-
import { appendMessages, forkConversation, forkConversationBeforeUser, listRecent, loadConversation, openSessions, replaceConversation, requireWritableConversation, setTitle, startConversation, titleOf, } from '../integrations/sessions/store.js';
|
|
43
|
-
import { conversationMarkdown, writeConversationExport, } from '../integrations/sessions/transcript-export.js';
|
|
39
|
+
import { TrustPrompt } from './TrustPrompt.js';
|
|
44
40
|
import { createAgentSession, probeAgentSession, } from './agent.js';
|
|
45
|
-
import {
|
|
46
|
-
import {
|
|
41
|
+
import { bottomSpacerRows } from './bottom-spacer.js';
|
|
42
|
+
import { keepRecentRows } from './compact-transcript.js';
|
|
43
|
+
import { approvalIsDeliberate } from './consent-timing.js';
|
|
47
44
|
import { planTurnPublication } from './conversation-history.js';
|
|
48
|
-
import {
|
|
45
|
+
import { copyTargetsForResponse, } from './copy-targets.js';
|
|
46
|
+
import { editablePrompts } from './edit-prompts.js';
|
|
47
|
+
import { liveWindow, transcriptLines } from './live-window.js';
|
|
48
|
+
import { describeCodexDeviceLoginStart, describeLoginOutcome, describeLoginStart, describeLogout, } from './login-prompt.js';
|
|
49
|
+
import { NAMZU_MARK, NAMZU_MARK_COLOR, NAMZU_WORDMARK, NAMZU_WORDMARK_GRADIENT, NAMZU_WORDMARK_MIN_WIDTH, } from './logo.js';
|
|
50
|
+
import { expandFileMentions } from './mentions.js';
|
|
51
|
+
import { openInBrowser } from './open-browser.js';
|
|
49
52
|
import { hostCommandNames, kernelCommandDescriptors, mergeHostCommands, renderOutcome, reviewPrompt, runSlash, } from './slashCommands.js';
|
|
50
53
|
import { splitCompleteBlocks } from './stream-blocks.js';
|
|
51
54
|
import { theme } from './theme.js';
|
|
52
|
-
import { keepRecentRows } from './compact-transcript.js';
|
|
53
55
|
import { renderWorkspaceDiff, workspaceDiff } from './workspace-diff.js';
|
|
54
56
|
/** Last non-empty assistant text in a durable conversation, newest first. */
|
|
55
57
|
function latestAssistantOutput(messages) {
|
|
@@ -213,6 +215,12 @@ export function App({ ctx: initialCtx }) {
|
|
|
213
215
|
* chosen yet would be answering a question that was not asked.
|
|
214
216
|
*/
|
|
215
217
|
const [keyEntryFor, setKeyEntryFor] = useState(null);
|
|
218
|
+
/** Which decision owns the next picker mount. */
|
|
219
|
+
const [pickerInitialView, setPickerInitialView] = useState('providers');
|
|
220
|
+
/** A narrowed first-run roster; null keeps the complete discovered list. */
|
|
221
|
+
const [pickerDetected, setPickerDetected] = useState(null);
|
|
222
|
+
/** Whether Enter accepts the provider directly or continues to model choice. */
|
|
223
|
+
const [pickerSelectionKind, setPickerSelectionKind] = useState('provider-and-model');
|
|
216
224
|
/** The chain read from disk, held while the picker repairs its credential. */
|
|
217
225
|
const savedPrefsRef = useRef(null);
|
|
218
226
|
/**
|
|
@@ -312,6 +320,14 @@ export function App({ ctx: initialCtx }) {
|
|
|
312
320
|
const [editList, setEditList] = useState([]);
|
|
313
321
|
const [selectedEdit, setSelectedEdit] = useState(0);
|
|
314
322
|
const editCommittedRef = useRef(false);
|
|
323
|
+
/** `/copy` owns this exact response snapshot until selection or cancellation. */
|
|
324
|
+
const [copyPicker, setCopyPickerState] = useState(null);
|
|
325
|
+
const copyPickerRef = useRef(null);
|
|
326
|
+
const [selectedCopy, setSelectedCopy] = useState(0);
|
|
327
|
+
const setCopyPicker = useCallback((next) => {
|
|
328
|
+
copyPickerRef.current = next;
|
|
329
|
+
setCopyPickerState(next);
|
|
330
|
+
}, []);
|
|
315
331
|
const [composerDraft, setComposerDraft] = useState(null);
|
|
316
332
|
const composerDraftTokenRef = useRef(0);
|
|
317
333
|
const exitArmedRef = useRef(false);
|
|
@@ -328,12 +344,18 @@ export function App({ ctx: initialCtx }) {
|
|
|
328
344
|
* which the code that comes back cannot be exchanged for anything.
|
|
329
345
|
*/
|
|
330
346
|
const loginRef = useRef(null);
|
|
347
|
+
const codexLoginRef = useRef(null);
|
|
331
348
|
const loginAbortCleanupRef = useRef(null);
|
|
349
|
+
/** Revokes automatic first-run construction when this App no longer exists. */
|
|
350
|
+
const [appLifetime] = useState(() => new AbortController());
|
|
351
|
+
useEffect(() => () => appLifetime.abort(new Error('The Namzu TUI was closed.')), [appLifetime]);
|
|
332
352
|
const cancelPendingLogin = useCallback(() => {
|
|
333
353
|
loginAbortCleanupRef.current?.();
|
|
334
354
|
loginAbortCleanupRef.current = null;
|
|
335
355
|
loginRef.current?.cancel();
|
|
336
356
|
loginRef.current = null;
|
|
357
|
+
codexLoginRef.current?.cancel();
|
|
358
|
+
codexLoginRef.current = null;
|
|
337
359
|
}, []);
|
|
338
360
|
const runProbeRef = useRef(null);
|
|
339
361
|
useEffect(() => cancelPendingLogin, [cancelPendingLogin]);
|
|
@@ -495,6 +517,42 @@ export function App({ ctx: initialCtx }) {
|
|
|
495
517
|
]);
|
|
496
518
|
return id;
|
|
497
519
|
}, [nextId]);
|
|
520
|
+
const sendCopyRequest = useCallback((index) => {
|
|
521
|
+
const picker = copyPickerRef.current;
|
|
522
|
+
const target = picker?.targets[index];
|
|
523
|
+
if (!picker || !target)
|
|
524
|
+
return;
|
|
525
|
+
// Release the overlay synchronously before its transcript notice lands.
|
|
526
|
+
// The queue pump still observes React state and can proceed only after
|
|
527
|
+
// this event has finished publishing the result.
|
|
528
|
+
setCopyPicker(null);
|
|
529
|
+
const result = writeClipboardText(target.text, {
|
|
530
|
+
isTTY: stdout.isTTY,
|
|
531
|
+
write: writeStdout,
|
|
532
|
+
});
|
|
533
|
+
const origin = picker.provenance === 'persisted'
|
|
534
|
+
? 'the latest persisted assistant output'
|
|
535
|
+
: 'the latest normally completed answer';
|
|
536
|
+
const selection = target.kind === 'whole' ? origin : `${target.label} from ${origin}`;
|
|
537
|
+
switch (result.kind) {
|
|
538
|
+
case 'request-sent':
|
|
539
|
+
pushMessage('system', `Copy request sent for ${selection} (${result.bytes.toLocaleString()} bytes). Terminal, multiplexer or remote-session policy may ignore OSC 52; if the clipboard did not change, enable terminal clipboard access.`);
|
|
540
|
+
break;
|
|
541
|
+
case 'unavailable':
|
|
542
|
+
pushMessage('system', `Cannot send a copy request here — ${result.detail}. /copy needs an interactive terminal with OSC 52 clipboard support.`);
|
|
543
|
+
break;
|
|
544
|
+
case 'too-large':
|
|
545
|
+
pushMessage('system', `Cannot send this selection to the terminal clipboard — it is ${result.bytes.toLocaleString()} bytes and the OSC 52 safety limit is ${result.limit.toLocaleString()}. Nothing was truncated.`);
|
|
546
|
+
break;
|
|
547
|
+
case 'write-failed':
|
|
548
|
+
pushMessage('system', `Could not send the terminal copy request: ${result.detail}`);
|
|
549
|
+
break;
|
|
550
|
+
default: {
|
|
551
|
+
const exhaustive = result;
|
|
552
|
+
void exhaustive;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}, [pushMessage, setCopyPicker, stdout.isTTY, writeStdout]);
|
|
498
556
|
const sendTerminalNotification = useCallback((notification) => {
|
|
499
557
|
if (!terminalNotificationEnabled(ctx.tui?.notifications, notification))
|
|
500
558
|
return;
|
|
@@ -651,9 +709,9 @@ export function App({ ctx: initialCtx }) {
|
|
|
651
709
|
/**
|
|
652
710
|
* Sign in to a subscription without leaving namzu.
|
|
653
711
|
*
|
|
654
|
-
*
|
|
655
|
-
*
|
|
656
|
-
*
|
|
712
|
+
* A bare `/login` no longer guesses a provider: it mounts the subscription
|
|
713
|
+
* picker first. The argument form remains the paste-completion half of the
|
|
714
|
+
* browser-callback attempt already in flight.
|
|
657
715
|
*
|
|
658
716
|
* ## Why it re-probes instead of installing the credential directly
|
|
659
717
|
*
|
|
@@ -681,46 +739,19 @@ export function App({ ctx: initialCtx }) {
|
|
|
681
739
|
loginAbortCleanupRef.current?.();
|
|
682
740
|
loginAbortCleanupRef.current = null;
|
|
683
741
|
pushMessage('system', describeLoginOutcome(outcome));
|
|
684
|
-
if (outcome.ok)
|
|
742
|
+
if (outcome.ok) {
|
|
743
|
+
setPickerInitialView('providers');
|
|
685
744
|
await runProbeRef.current?.();
|
|
745
|
+
}
|
|
686
746
|
return;
|
|
687
747
|
}
|
|
688
748
|
cancelPendingLogin();
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
return;
|
|
696
|
-
}
|
|
697
|
-
loginRef.current = start;
|
|
698
|
-
pushMessage('system', describeLoginStart({
|
|
699
|
-
url: start.url,
|
|
700
|
-
loopback: start.loopback,
|
|
701
|
-
browserOpened: openInBrowser(start.url),
|
|
702
|
-
}));
|
|
703
|
-
// The automatic half, when there is one. Not awaited by the caller:
|
|
704
|
-
// the composer stays live throughout, so the operator can paste
|
|
705
|
-
// instead — or type anything else — while this waits.
|
|
706
|
-
const waiting = start.waitForCallback();
|
|
707
|
-
if (!waiting)
|
|
708
|
-
return;
|
|
709
|
-
const outcome = await waiting;
|
|
710
|
-
if (loginRef.current !== start)
|
|
711
|
-
return; // superseded, or finished by paste
|
|
712
|
-
loginRef.current = null;
|
|
713
|
-
loginAbortCleanupRef.current?.();
|
|
714
|
-
loginAbortCleanupRef.current = null;
|
|
715
|
-
start.cancel();
|
|
716
|
-
// A cancelled listener resolves to a refusal, and saying "sign-in
|
|
717
|
-
// failed" when the operator finished it another way would be a lie
|
|
718
|
-
// about their own successful login.
|
|
719
|
-
if (!outcome.ok && outcome.reason.includes('cancelled'))
|
|
720
|
-
return;
|
|
721
|
-
pushMessage('system', describeLoginOutcome(outcome));
|
|
722
|
-
if (outcome.ok)
|
|
723
|
-
await runProbeRef.current?.();
|
|
749
|
+
setPickerNotice(null);
|
|
750
|
+
setKeyEntryFor(null);
|
|
751
|
+
setPickerDetected(null);
|
|
752
|
+
setPickerSelectionKind('provider-and-model');
|
|
753
|
+
setPickerInitialView('subscriptions');
|
|
754
|
+
setPhase('picker');
|
|
724
755
|
}, [cancelPendingLogin, pushMessage]);
|
|
725
756
|
/**
|
|
726
757
|
* Sign in from the PICKER, where the transcript does not exist.
|
|
@@ -737,10 +768,43 @@ export function App({ ctx: initialCtx }) {
|
|
|
737
768
|
* no-browser case is handed to `namzu login`, which reads the pasted
|
|
738
769
|
* address from standard input and exists for exactly that machine.
|
|
739
770
|
*/
|
|
740
|
-
const startLoginFromPicker = useCallback(async (signal) => {
|
|
771
|
+
const startLoginFromPicker = useCallback(async (provider, signal) => {
|
|
741
772
|
if (signal.aborted)
|
|
742
773
|
return;
|
|
743
774
|
cancelPendingLogin();
|
|
775
|
+
if (provider === 'codex') {
|
|
776
|
+
let start;
|
|
777
|
+
try {
|
|
778
|
+
start = await beginCodexDeviceLogin({ signal });
|
|
779
|
+
}
|
|
780
|
+
catch (error) {
|
|
781
|
+
if (!signal.aborted) {
|
|
782
|
+
setPickerNotice(`Could not start Codex sign-in: ${error instanceof Error ? error.message : String(error)}`);
|
|
783
|
+
}
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
if (signal.aborted) {
|
|
787
|
+
start.cancel();
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
codexLoginRef.current = start;
|
|
791
|
+
setPickerNotice(describeCodexDeviceLoginStart({
|
|
792
|
+
url: start.url,
|
|
793
|
+
userCode: start.userCode,
|
|
794
|
+
browserOpened: openInBrowser(start.url),
|
|
795
|
+
}));
|
|
796
|
+
const outcome = await start.waitForCompletion();
|
|
797
|
+
if (codexLoginRef.current !== start || signal.aborted)
|
|
798
|
+
return;
|
|
799
|
+
codexLoginRef.current = null;
|
|
800
|
+
start.cancel();
|
|
801
|
+
setPickerNotice(describeLoginOutcome(outcome));
|
|
802
|
+
if (outcome.ok) {
|
|
803
|
+
setPickerInitialView('providers');
|
|
804
|
+
await runProbeRef.current?.(signal);
|
|
805
|
+
}
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
744
808
|
let start;
|
|
745
809
|
try {
|
|
746
810
|
start = await beginSubscriptionLogin({ signal });
|
|
@@ -793,8 +857,10 @@ export function App({ ctx: initialCtx }) {
|
|
|
793
857
|
if (!outcome.ok && outcome.reason.includes('cancelled'))
|
|
794
858
|
return;
|
|
795
859
|
setPickerNotice(describeLoginOutcome(outcome));
|
|
796
|
-
if (outcome.ok)
|
|
860
|
+
if (outcome.ok) {
|
|
861
|
+
setPickerInitialView('providers');
|
|
797
862
|
await runProbeRef.current?.(signal);
|
|
863
|
+
}
|
|
798
864
|
}, [cancelPendingLogin]);
|
|
799
865
|
const runProbe = useCallback(async (signal) => {
|
|
800
866
|
try {
|
|
@@ -805,6 +871,9 @@ export function App({ ctx: initialCtx }) {
|
|
|
805
871
|
return;
|
|
806
872
|
setDetected(probe.detected);
|
|
807
873
|
if (probe.needsRepickReason) {
|
|
874
|
+
setPickerDetected(null);
|
|
875
|
+
setPickerSelectionKind('provider-and-model');
|
|
876
|
+
setPickerInitialView('providers');
|
|
808
877
|
pushMessage('system', probe.needsRepickReason);
|
|
809
878
|
setPickerNotice(probe.needsRepickReason);
|
|
810
879
|
setPhase('picker');
|
|
@@ -821,6 +890,9 @@ export function App({ ctx: initialCtx }) {
|
|
|
821
890
|
// so the chain it declares — model pins and fallbacks included — is
|
|
822
891
|
// still the operator's answer once one is supplied.
|
|
823
892
|
savedPrefsRef.current = probe.preferences;
|
|
893
|
+
setPickerDetected(null);
|
|
894
|
+
setPickerSelectionKind('provider-and-model');
|
|
895
|
+
setPickerInitialView('providers');
|
|
824
896
|
setKeyEntryFor(probe.credentialGap.providerId);
|
|
825
897
|
pushMessage('system', probe.credentialGap.reason);
|
|
826
898
|
setPickerNotice(probe.credentialGap.reason);
|
|
@@ -828,9 +900,51 @@ export function App({ ctx: initialCtx }) {
|
|
|
828
900
|
return;
|
|
829
901
|
}
|
|
830
902
|
if (probe.preferences) {
|
|
903
|
+
setPickerDetected(null);
|
|
904
|
+
setPickerSelectionKind('provider-and-model');
|
|
831
905
|
await hydrateSession(probe.preferences, probe.detected, signal);
|
|
832
906
|
return;
|
|
833
907
|
}
|
|
908
|
+
const signedIn = signedInSubscriptionProviders(probe.detected);
|
|
909
|
+
if (signedIn.length === 1) {
|
|
910
|
+
const chosen = signedIn[0];
|
|
911
|
+
if (!chosen)
|
|
912
|
+
throw new Error('A signed-in provider disappeared during discovery.');
|
|
913
|
+
const automaticPreferences = {
|
|
914
|
+
version: 3,
|
|
915
|
+
providers: [{ id: chosen.entry.id }],
|
|
916
|
+
subagents: { active: [] },
|
|
917
|
+
};
|
|
918
|
+
// Use the picker-grade transactional admission even though no picker had
|
|
919
|
+
// to be drawn. A provider-less candidate must leave an actionable choice,
|
|
920
|
+
// not strand first run on the disabled unhealthy screen.
|
|
921
|
+
const admissionSignal = signal ?? appLifetime.signal;
|
|
922
|
+
try {
|
|
923
|
+
await hydrateSession(automaticPreferences, probe.detected, admissionSignal);
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
catch (error) {
|
|
927
|
+
if (admissionSignal.aborted)
|
|
928
|
+
return;
|
|
929
|
+
setPickerDetected(null);
|
|
930
|
+
setPickerSelectionKind('provider-and-model');
|
|
931
|
+
setPickerInitialView('providers');
|
|
932
|
+
setPickerNotice(`The signed-in ${chosen.entry.label} session could not start: ${error instanceof Error ? error.message : String(error)}`);
|
|
933
|
+
setPhase('picker');
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
if (signedIn.length > 1) {
|
|
938
|
+
setPickerDetected(signedIn);
|
|
939
|
+
setPickerSelectionKind('signed-in-subscription');
|
|
940
|
+
setPickerInitialView('providers');
|
|
941
|
+
setPickerNotice('Claude and Codex subscriptions are already signed in on this device. Choose which one Namzu should use.');
|
|
942
|
+
setPhase('picker');
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
setPickerDetected(null);
|
|
946
|
+
setPickerSelectionKind('provider-and-model');
|
|
947
|
+
setPickerInitialView('providers');
|
|
834
948
|
setPhase('picker');
|
|
835
949
|
}
|
|
836
950
|
catch (err) {
|
|
@@ -839,7 +953,7 @@ export function App({ ctx: initialCtx }) {
|
|
|
839
953
|
setPhase('unhealthy');
|
|
840
954
|
pushMessage('system', `Failed to probe agents: ${err instanceof Error ? err.message : String(err)}`);
|
|
841
955
|
}
|
|
842
|
-
}, [hydrateSession, pushMessage]);
|
|
956
|
+
}, [appLifetime.signal, hydrateSession, pushMessage]);
|
|
843
957
|
// `startOrFinishLogin` is declared above `runProbe` and calls it, so it
|
|
844
958
|
// reads the current one through a ref rather than closing over a stale
|
|
845
959
|
// binding or forcing the two into a declaration order that reads backwards.
|
|
@@ -1948,6 +2062,9 @@ export function App({ ctx: initialCtx }) {
|
|
|
1948
2062
|
// been solved — the session behind this picker is running.
|
|
1949
2063
|
setPickerNotice(null);
|
|
1950
2064
|
setKeyEntryFor(null);
|
|
2065
|
+
setPickerDetected(null);
|
|
2066
|
+
setPickerSelectionKind('provider-and-model');
|
|
2067
|
+
setPickerInitialView('providers');
|
|
1951
2068
|
setPhase('picker');
|
|
1952
2069
|
return;
|
|
1953
2070
|
case 'permission-mode': {
|
|
@@ -2007,9 +2124,9 @@ export function App({ ctx: initialCtx }) {
|
|
|
2007
2124
|
return;
|
|
2008
2125
|
case 'logout': {
|
|
2009
2126
|
const path = credentialsPath();
|
|
2010
|
-
const had = readStoredSubscriptionCredential() !== null;
|
|
2127
|
+
const had = readStoredSubscriptionCredential() !== null || readStoredCodexCredential() !== null;
|
|
2011
2128
|
try {
|
|
2012
|
-
|
|
2129
|
+
clearAllStoredCredentials();
|
|
2013
2130
|
}
|
|
2014
2131
|
catch (err) {
|
|
2015
2132
|
pushMessage('system', `Could not remove ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -2380,30 +2497,11 @@ export function App({ ctx: initialCtx }) {
|
|
|
2380
2497
|
pushMessage('system', 'Nothing to copy yet — /copy applies to the latest assistant answer that finished normally.');
|
|
2381
2498
|
return;
|
|
2382
2499
|
}
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2500
|
+
setSelectedCopy(0);
|
|
2501
|
+
setCopyPicker({
|
|
2502
|
+
targets: copyTargetsForResponse(target.text),
|
|
2503
|
+
provenance: target.provenance,
|
|
2386
2504
|
});
|
|
2387
|
-
switch (result.kind) {
|
|
2388
|
-
case 'request-sent':
|
|
2389
|
-
pushMessage('system', `Copy request sent for the ${target.provenance === 'persisted'
|
|
2390
|
-
? 'latest persisted assistant output'
|
|
2391
|
-
: 'latest normally completed answer'} (${result.bytes.toLocaleString()} bytes). Terminal, multiplexer or remote-session policy may ignore OSC 52; if the clipboard did not change, enable terminal clipboard access.`);
|
|
2392
|
-
break;
|
|
2393
|
-
case 'unavailable':
|
|
2394
|
-
pushMessage('system', `Cannot send a copy request here — ${result.detail}. /copy needs an interactive terminal with OSC 52 clipboard support.`);
|
|
2395
|
-
break;
|
|
2396
|
-
case 'too-large':
|
|
2397
|
-
pushMessage('system', `Cannot send this answer to the terminal clipboard — it is ${result.bytes.toLocaleString()} bytes and the OSC 52 safety limit is ${result.limit.toLocaleString()}. Nothing was truncated.`);
|
|
2398
|
-
break;
|
|
2399
|
-
case 'write-failed':
|
|
2400
|
-
pushMessage('system', `Could not send the terminal copy request: ${result.detail}`);
|
|
2401
|
-
break;
|
|
2402
|
-
default: {
|
|
2403
|
-
const exhaustive = result;
|
|
2404
|
-
void exhaustive;
|
|
2405
|
-
}
|
|
2406
|
-
}
|
|
2407
2505
|
return;
|
|
2408
2506
|
}
|
|
2409
2507
|
case 'raw': {
|
|
@@ -2498,6 +2596,7 @@ export function App({ ctx: initialCtx }) {
|
|
|
2498
2596
|
pushMessage,
|
|
2499
2597
|
rawOutput,
|
|
2500
2598
|
resetTranscript,
|
|
2599
|
+
setCopyPicker,
|
|
2501
2600
|
setReasoningEffort,
|
|
2502
2601
|
slashCtx,
|
|
2503
2602
|
startFreshConversation,
|
|
@@ -2523,6 +2622,7 @@ export function App({ ctx: initialCtx }) {
|
|
|
2523
2622
|
conversationMutationRef.current ||
|
|
2524
2623
|
exportingRef.current ||
|
|
2525
2624
|
compactingRef.current ||
|
|
2625
|
+
copyPickerRef.current ||
|
|
2526
2626
|
goalCommandInFlightRef.current) {
|
|
2527
2627
|
return;
|
|
2528
2628
|
}
|
|
@@ -2547,6 +2647,7 @@ export function App({ ctx: initialCtx }) {
|
|
|
2547
2647
|
conversationMutationRef.current ||
|
|
2548
2648
|
exportingRef.current ||
|
|
2549
2649
|
compactingRef.current ||
|
|
2650
|
+
copyPickerRef.current ||
|
|
2550
2651
|
goalCommandInFlightRef.current ||
|
|
2551
2652
|
queuedRef.current.length > 0 ||
|
|
2552
2653
|
!goalActivation.isArmed(sessionId, armed)) {
|
|
@@ -2604,6 +2705,7 @@ export function App({ ctx: initialCtx }) {
|
|
|
2604
2705
|
conversationMutation,
|
|
2605
2706
|
session,
|
|
2606
2707
|
state,
|
|
2708
|
+
copyPicker,
|
|
2607
2709
|
wakeGoalDriver,
|
|
2608
2710
|
]);
|
|
2609
2711
|
// Drain the queue: when a turn settles (idle) and nothing is running,
|
|
@@ -2618,6 +2720,7 @@ export function App({ ctx: initialCtx }) {
|
|
|
2618
2720
|
conversationMutationRef.current ||
|
|
2619
2721
|
exportingRef.current ||
|
|
2620
2722
|
compactingRef.current ||
|
|
2723
|
+
copyPickerRef.current ||
|
|
2621
2724
|
goalCommandInFlightRef.current)
|
|
2622
2725
|
return;
|
|
2623
2726
|
// Read and remove from the synchronous source of truth. `queued` is the
|
|
@@ -2627,7 +2730,16 @@ export function App({ ctx: initialCtx }) {
|
|
|
2627
2730
|
const next = dequeueQueued();
|
|
2628
2731
|
if (next !== undefined)
|
|
2629
2732
|
void runTurn(next);
|
|
2630
|
-
}, [
|
|
2733
|
+
}, [
|
|
2734
|
+
state,
|
|
2735
|
+
phase,
|
|
2736
|
+
queued,
|
|
2737
|
+
queuePause,
|
|
2738
|
+
copyPicker,
|
|
2739
|
+
dequeueQueued,
|
|
2740
|
+
hasUnsettledTurn,
|
|
2741
|
+
runTurn,
|
|
2742
|
+
]);
|
|
2631
2743
|
// One-shot update check on launch.
|
|
2632
2744
|
// Best-effort; surfaces a single notice when something newer is out.
|
|
2633
2745
|
const updateCheckedRef = useRef(false);
|
|
@@ -2642,7 +2754,7 @@ export function App({ ctx: initialCtx }) {
|
|
|
2642
2754
|
pushMessage('system', `⬆ Update available:\n${lines.join('\n')}`);
|
|
2643
2755
|
});
|
|
2644
2756
|
}, [phase, ctx.version, pushMessage]);
|
|
2645
|
-
const hydrateFromPicker = useCallback(async (prefs, detectedNow, signal) => {
|
|
2757
|
+
const hydrateFromPicker = useCallback(async (prefs, detectedNow, signal, revealAllOnFailure = false) => {
|
|
2646
2758
|
try {
|
|
2647
2759
|
await hydrateSession(prefs, detectedNow, signal);
|
|
2648
2760
|
}
|
|
@@ -2652,6 +2764,11 @@ export function App({ ctx: initialCtx }) {
|
|
|
2652
2764
|
// choice stays on the picker with the actionable construction error.
|
|
2653
2765
|
if (signal.aborted)
|
|
2654
2766
|
return;
|
|
2767
|
+
if (revealAllOnFailure) {
|
|
2768
|
+
setPickerDetected(null);
|
|
2769
|
+
setPickerSelectionKind('provider-and-model');
|
|
2770
|
+
setPickerInitialView('providers');
|
|
2771
|
+
}
|
|
2655
2772
|
setPickerNotice(`Could not start the selected provider: ${err instanceof Error ? err.message : String(err)}`);
|
|
2656
2773
|
}
|
|
2657
2774
|
}, [hydrateSession]);
|
|
@@ -2667,6 +2784,8 @@ export function App({ ctx: initialCtx }) {
|
|
|
2667
2784
|
const next = [credential, ...detected.filter((d) => d.entry.id !== credential.entry.id)];
|
|
2668
2785
|
setDetected(next);
|
|
2669
2786
|
setKeyEntryFor(null);
|
|
2787
|
+
setPickerDetected(null);
|
|
2788
|
+
setPickerSelectionKind('provider-and-model');
|
|
2670
2789
|
setPickerNotice(null);
|
|
2671
2790
|
// The disposition, not the key. `credential` never reaches a message.
|
|
2672
2791
|
pushMessage('system', disposition);
|
|
@@ -2708,7 +2827,9 @@ export function App({ ctx: initialCtx }) {
|
|
|
2708
2827
|
pushMessage('system', `Could not save preferences: ${err instanceof Error ? err.message : String(err)}`);
|
|
2709
2828
|
return;
|
|
2710
2829
|
}
|
|
2711
|
-
|
|
2830
|
+
// Keep a narrowed signed-in roster stable while construction is pending.
|
|
2831
|
+
// Only a real refusal broadens back to the general, sign-in-capable picker.
|
|
2832
|
+
void hydrateFromPicker(prefs, detected, signal, true);
|
|
2712
2833
|
}, [detected, hydrateFromPicker, pushMessage]);
|
|
2713
2834
|
/**
|
|
2714
2835
|
* Leaving the picker returns to whatever was on screen before it.
|
|
@@ -2729,6 +2850,9 @@ export function App({ ctx: initialCtx }) {
|
|
|
2729
2850
|
* what the empty picker's own footer has always claimed `esc` does.
|
|
2730
2851
|
*/
|
|
2731
2852
|
const handlePickerCancel = useCallback(() => {
|
|
2853
|
+
setPickerInitialView('providers');
|
|
2854
|
+
setPickerDetected(null);
|
|
2855
|
+
setPickerSelectionKind('provider-and-model');
|
|
2732
2856
|
if (session?.hasProvider) {
|
|
2733
2857
|
setPhase('ready');
|
|
2734
2858
|
return;
|
|
@@ -2854,6 +2978,34 @@ export function App({ ctx: initialCtx }) {
|
|
|
2854
2978
|
resolvePermission({ kind: 'approve-all' });
|
|
2855
2979
|
return;
|
|
2856
2980
|
}
|
|
2981
|
+
// `/copy` holds a source snapshot, not a lifecycle phase. Permission
|
|
2982
|
+
// stays above it in this handler so an approval request that arrives
|
|
2983
|
+
// while the picker is open cannot have its keys stolen by the chooser.
|
|
2984
|
+
if (copyPickerRef.current) {
|
|
2985
|
+
const targets = copyPickerRef.current.targets;
|
|
2986
|
+
if (key.escape || (key.ctrl && input === 'c')) {
|
|
2987
|
+
setCopyPicker(null);
|
|
2988
|
+
return;
|
|
2989
|
+
}
|
|
2990
|
+
if (key.upArrow) {
|
|
2991
|
+
setSelectedCopy((index) => Math.max(0, index - 1));
|
|
2992
|
+
return;
|
|
2993
|
+
}
|
|
2994
|
+
if (key.downArrow) {
|
|
2995
|
+
setSelectedCopy((index) => Math.min(targets.length - 1, index + 1));
|
|
2996
|
+
return;
|
|
2997
|
+
}
|
|
2998
|
+
if (key.return) {
|
|
2999
|
+
sendCopyRequest(selectedCopy);
|
|
3000
|
+
return;
|
|
3001
|
+
}
|
|
3002
|
+
if (/^[1-9]$/.test(input)) {
|
|
3003
|
+
const index = Number(input) - 1;
|
|
3004
|
+
if (targets[index])
|
|
3005
|
+
sendCopyRequest(index);
|
|
3006
|
+
}
|
|
3007
|
+
return;
|
|
3008
|
+
}
|
|
2857
3009
|
// Esc interrupts a running turn (Ctrl+C stays reserved for exit). Mirrors
|
|
2858
3010
|
// the Ctrl+C interrupt path: abort, drop the queue, one "Interrupted." line.
|
|
2859
3011
|
if (key.escape && abortRef.current) {
|
|
@@ -2930,12 +3082,15 @@ export function App({ ctx: initialCtx }) {
|
|
|
2930
3082
|
// Background is left natural — we inherit the terminal's own background
|
|
2931
3083
|
// and only theme the foreground. Forcing
|
|
2932
3084
|
// a filled bg left mismatched patches around bordered areas, so we don't.
|
|
2933
|
-
return (_jsxs(Box, { flexDirection: "column", children: [phase !== 'ready' ? (_jsx(Banner, { version: ctx.version, session: session, permissionMode: permissionMode, cwd: ctx.cwd })) : null, _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [phase === 'trust' ? (_jsx(TrustPrompt, { cwd: ctx.cwd })) : phase === 'resume' ? (_jsx(ResumePicker, { conversations: resumeList, selected: selectedResume })) : phase === 'edit' ? (_jsx(EditPromptPicker, { prompts: editList, selected: selectedEdit })) : phase === 'picker' ? (_jsx(Picker, { detected: detected, currentProvider: currentProvider, currentModel: session?.modelSummary ?? null, onSubmit: handlePickerSubmit, onCancel: handlePickerCancel, onCredential: handleTypedCredential, onLogin: (signal) => void startLoginFromPicker(signal), keyEntryFor: keyEntryFor, notice: pickerNotice })) : (_jsxs(_Fragment, { children: [_jsx(TranscriptFrame, { children: _jsx(Transcript, { messages: finalized, pending: messages.find((m) => m.pending) ?? null, state: state, settled: window.settled, resetKey: resetKey, raw: rawOutput, header: phase === 'ready' ? (_jsx(Banner, { version: ctx.version, session: session, permissionMode: permissionMode, cwd: ctx.cwd })) : undefined }) }), spacerRows > 0 ? _jsx(Box, { height: spacerRows }) : null, _jsx(LiveActivity, { activeTools: activeTools, thinking: state === 'thinking' && !messages.some((m) => m.pending) }), permission ? _jsx(PermissionOverlay, { toolCalls: permission.toolCalls }) :
|
|
3085
|
+
return (_jsxs(Box, { flexDirection: "column", children: [phase !== 'ready' ? (_jsx(Banner, { version: ctx.version, session: session, permissionMode: permissionMode, cwd: ctx.cwd })) : null, _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [phase === 'trust' ? (_jsx(TrustPrompt, { cwd: ctx.cwd })) : phase === 'resume' ? (_jsx(ResumePicker, { conversations: resumeList, selected: selectedResume })) : phase === 'edit' ? (_jsx(EditPromptPicker, { prompts: editList, selected: selectedEdit })) : phase === 'picker' ? (_jsx(Picker, { detected: pickerDetected ?? detected, currentProvider: currentProvider, selectionKind: pickerSelectionKind, currentModel: session?.modelSummary ?? null, initialView: pickerInitialView, onSubmit: handlePickerSubmit, onCancel: handlePickerCancel, onCredential: handleTypedCredential, onLogin: (provider, signal) => void startLoginFromPicker(provider, signal), keyEntryFor: keyEntryFor, notice: pickerNotice })) : (_jsxs(_Fragment, { children: [_jsx(TranscriptFrame, { children: _jsx(Transcript, { messages: finalized, pending: messages.find((m) => m.pending) ?? null, state: state, settled: window.settled, resetKey: resetKey, raw: rawOutput, header: phase === 'ready' ? (_jsx(Banner, { version: ctx.version, session: session, permissionMode: permissionMode, cwd: ctx.cwd })) : undefined }) }), spacerRows > 0 ? _jsx(Box, { height: spacerRows }) : null, _jsx(LiveActivity, { activeTools: activeTools, thinking: state === 'thinking' && !messages.some((m) => m.pending) }), permission ? (_jsx(PermissionOverlay, { toolCalls: permission.toolCalls })) : copyPicker ? (_jsx(CopyPicker, { targets: copyPicker.targets, selected: selectedCopy })) : null, _jsxs(ComposerFrame, { focus: state === 'idle' &&
|
|
3086
|
+
phase === 'ready' &&
|
|
3087
|
+
conversationMutation === null &&
|
|
3088
|
+
copyPicker === null, hidden: permission !== null || copyPicker !== null, children: [queued.length > 0 && permission === null && copyPicker === null ? (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: theme.text.muted, children: [queuePause ? '⏸' : '⏎', " ", queued.length, " message", queued.length > 1 ? 's' : '', " queued \u2014", ' ', queuePause
|
|
2934
3089
|
? `paused after a ${queuePause.outcome} turn; send a message or change model to continue`
|
|
2935
3090
|
: 'sending when ready'] }) })) : null, _jsx(Composer, { disabled: phase !== 'ready' ||
|
|
2936
3091
|
state === 'awaiting-permission' ||
|
|
2937
3092
|
compacting ||
|
|
2938
|
-
conversationMutation !== null, hidden: permission !== null,
|
|
3093
|
+
conversationMutation !== null, hidden: permission !== null || copyPicker !== null,
|
|
2939
3094
|
// A turn is running, so Esc is the interrupt and not
|
|
2940
3095
|
// the composer's clear.
|
|
2941
3096
|
escapeInterrupts: !compacting && (state === 'thinking' || state === 'tool'), onSubmit: handleSubmit, onNotice: (text) => pushMessage('system', text), onEditPrevious: openPromptEditor, draftToRestore: composerDraft, onDraftRestored: (token) => setComposerDraft((draft) => (draft?.token === token ? null : draft)), userCommands: userCommands, history: history })] })] })), _jsx(Box, { paddingTop: 1, children: _jsx(StatusBar, { cwd: ctx.cwd, provider: session?.providerSummary ?? null, model: session?.modelSummary
|
|
@@ -2948,7 +3103,11 @@ export function App({ ctx: initialCtx }) {
|
|
|
2948
3103
|
? 'starting a fresh conversation — input is paused'
|
|
2949
3104
|
: compacting
|
|
2950
3105
|
? 'compacting conversation — input is paused'
|
|
2951
|
-
:
|
|
3106
|
+
: permission
|
|
3107
|
+
? hintForPhase(phase, state, session?.hasProvider === true)
|
|
3108
|
+
: copyPicker
|
|
3109
|
+
? 'copy target open — ↑↓ / 1–9 select · esc cancel'
|
|
3110
|
+
: hintForPhase(phase, state, session?.hasProvider === true), usage: usage, context: context }) })] })] }));
|
|
2952
3111
|
}
|
|
2953
3112
|
function Banner({ version, session, permissionMode, cwd, }) {
|
|
2954
3113
|
const cols = process.stdout.columns ?? 80;
|