@namzu/cli 14.0.0 → 14.2.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 +18 -7
- package/dist/integrations/providers/discover.d.ts +18 -0
- package/dist/integrations/providers/discover.d.ts.map +1 -1
- package/dist/integrations/providers/discover.js +79 -37
- package/dist/integrations/providers/discover.js.map +1 -1
- package/dist/integrations/providers/harness-credentials.d.ts +44 -9
- package/dist/integrations/providers/harness-credentials.d.ts.map +1 -1
- package/dist/integrations/providers/harness-credentials.js +140 -11
- package/dist/integrations/providers/harness-credentials.js.map +1 -1
- package/dist/integrations/providers/identity.d.ts +15 -14
- package/dist/integrations/providers/identity.d.ts.map +1 -1
- package/dist/integrations/providers/identity.js +15 -14
- package/dist/integrations/providers/identity.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/providers/oauth.d.ts +8 -3
- package/dist/integrations/providers/oauth.d.ts.map +1 -1
- package/dist/integrations/providers/oauth.js +37 -11
- package/dist/integrations/providers/oauth.js.map +1 -1
- package/dist/integrations/providers/subscription-login.d.ts.map +1 -1
- package/dist/integrations/providers/subscription-login.js +12 -8
- package/dist/integrations/providers/subscription-login.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 +437 -63
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/ChoicePicker.d.ts +14 -0
- package/dist/tui/ChoicePicker.d.ts.map +1 -0
- package/dist/tui/ChoicePicker.js +15 -0
- package/dist/tui/ChoicePicker.js.map +1 -0
- 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 +8 -1
- package/dist/tui/Picker.d.ts.map +1 -1
- package/dist/tui/Picker.js +24 -11
- package/dist/tui/Picker.js.map +1 -1
- package/dist/tui/StatusBar.d.ts +23 -39
- package/dist/tui/StatusBar.d.ts.map +1 -1
- package/dist/tui/StatusBar.js +67 -105
- package/dist/tui/StatusBar.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 +15 -9
- 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/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 +9 -1
- package/dist/tui/slashCommands.d.ts.map +1 -1
- package/dist/tui/slashCommands.js +21 -16
- package/dist/tui/slashCommands.js.map +1 -1
- package/package.json +5 -5
package/dist/tui/App.js
CHANGED
|
@@ -5,11 +5,11 @@ 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
14
|
import { join, relative } from 'node:path';
|
|
15
15
|
import { DiskMessageFeedbackStore, GoalRoundLimitError, HostCommandRegistry, SessionGoalActivation, StaleGoalError, createAssistantMessage, createUserMessage, generateRunId, isCompactionMessage, kernelHostCommands, } from '@namzu/sdk';
|
|
@@ -19,7 +19,7 @@ import { pinTrustedProjectPath, resolveTrustedProjectContext, } from '../config/
|
|
|
19
19
|
import { visibleProjectInstructionPath } from '../context/project-path.js';
|
|
20
20
|
import { writeClipboardText } from '../integrations/clipboard/text.js';
|
|
21
21
|
import { terminalNotificationEnabled, writeTerminalNotification, } from '../integrations/notifications/terminal.js';
|
|
22
|
-
import { beginCodexDeviceLogin, beginSubscriptionLogin, clearAllStoredCredentials, credentialsPath, primaryProvider, readStoredCodexCredential, readStoredSubscriptionCredential, writePreferences, } from '../integrations/providers/index.js';
|
|
22
|
+
import { beginCodexDeviceLogin, beginSubscriptionLogin, clearAllStoredCredentials, credentialsPath, primaryProvider, readStoredCodexCredential, readStoredSubscriptionCredential, signedInSubscriptionProviders, writePreferences, } from '../integrations/providers/index.js';
|
|
23
23
|
import { appendMessages, forkConversation, forkConversationBeforeUser, listRecent, loadConversation, openSessions, replaceConversation, requireWritableConversation, setTitle, startConversation, titleOf, } from '../integrations/sessions/store.js';
|
|
24
24
|
import { conversationMarkdown, writeConversationExport, } from '../integrations/sessions/transcript-export.js';
|
|
25
25
|
import { isTrusted, trustDir } from '../integrations/trust/store.js';
|
|
@@ -27,7 +27,9 @@ import { checkUpdates } from '../integrations/updates.js';
|
|
|
27
27
|
import { appendMemory, composeMemoryPrompt, readMemory } from '../memory/store.js';
|
|
28
28
|
import { composeSkillsPrompt, discoverSkills, loadSkillBody } from '../skills/store.js';
|
|
29
29
|
import { discoverUserCommands } from '../user-commands/store.js';
|
|
30
|
+
import { ChoicePicker } from './ChoicePicker.js';
|
|
30
31
|
import { Composer } from './Composer.js';
|
|
32
|
+
import { CopyPicker } from './CopyPicker.js';
|
|
31
33
|
import { EditPromptPicker } from './EditPromptPicker.js';
|
|
32
34
|
import { LiveActivity, formatElapsed } from './LiveActivity.js';
|
|
33
35
|
import { PermissionOverlay } from './PermissionOverlay.js';
|
|
@@ -41,6 +43,7 @@ import { bottomSpacerRows } from './bottom-spacer.js';
|
|
|
41
43
|
import { keepRecentRows } from './compact-transcript.js';
|
|
42
44
|
import { approvalIsDeliberate } from './consent-timing.js';
|
|
43
45
|
import { planTurnPublication } from './conversation-history.js';
|
|
46
|
+
import { copyTargetsForResponse } from './copy-targets.js';
|
|
44
47
|
import { editablePrompts } from './edit-prompts.js';
|
|
45
48
|
import { liveWindow, transcriptLines } from './live-window.js';
|
|
46
49
|
import { describeCodexDeviceLoginStart, describeLoginOutcome, describeLoginStart, describeLogout, } from './login-prompt.js';
|
|
@@ -215,6 +218,10 @@ export function App({ ctx: initialCtx }) {
|
|
|
215
218
|
const [keyEntryFor, setKeyEntryFor] = useState(null);
|
|
216
219
|
/** Which decision owns the next picker mount. */
|
|
217
220
|
const [pickerInitialView, setPickerInitialView] = useState('providers');
|
|
221
|
+
/** A narrowed first-run roster; null keeps the complete discovered list. */
|
|
222
|
+
const [pickerDetected, setPickerDetected] = useState(null);
|
|
223
|
+
/** Whether Enter accepts the provider directly or continues to model choice. */
|
|
224
|
+
const [pickerSelectionKind, setPickerSelectionKind] = useState('provider-and-model');
|
|
218
225
|
/** The chain read from disk, held while the picker repairs its credential. */
|
|
219
226
|
const savedPrefsRef = useRef(null);
|
|
220
227
|
/**
|
|
@@ -314,6 +321,22 @@ export function App({ ctx: initialCtx }) {
|
|
|
314
321
|
const [editList, setEditList] = useState([]);
|
|
315
322
|
const [selectedEdit, setSelectedEdit] = useState(0);
|
|
316
323
|
const editCommittedRef = useRef(false);
|
|
324
|
+
/** `/copy` owns this exact response snapshot until selection or cancellation. */
|
|
325
|
+
const [copyPicker, setCopyPickerState] = useState(null);
|
|
326
|
+
const copyPickerRef = useRef(null);
|
|
327
|
+
const [selectedCopy, setSelectedCopy] = useState(0);
|
|
328
|
+
const setCopyPicker = useCallback((next) => {
|
|
329
|
+
copyPickerRef.current = next;
|
|
330
|
+
setCopyPickerState(next);
|
|
331
|
+
}, []);
|
|
332
|
+
/** Finite slash-command choice owned synchronously until apply or cancel. */
|
|
333
|
+
const [choicePicker, setChoicePickerState] = useState(null);
|
|
334
|
+
const choicePickerRef = useRef(null);
|
|
335
|
+
const [selectedChoice, setSelectedChoice] = useState(0);
|
|
336
|
+
const setChoicePicker = useCallback((next) => {
|
|
337
|
+
choicePickerRef.current = next;
|
|
338
|
+
setChoicePickerState(next);
|
|
339
|
+
}, []);
|
|
317
340
|
const [composerDraft, setComposerDraft] = useState(null);
|
|
318
341
|
const composerDraftTokenRef = useRef(0);
|
|
319
342
|
const exitArmedRef = useRef(false);
|
|
@@ -332,6 +355,9 @@ export function App({ ctx: initialCtx }) {
|
|
|
332
355
|
const loginRef = useRef(null);
|
|
333
356
|
const codexLoginRef = useRef(null);
|
|
334
357
|
const loginAbortCleanupRef = useRef(null);
|
|
358
|
+
/** Revokes automatic first-run construction when this App no longer exists. */
|
|
359
|
+
const [appLifetime] = useState(() => new AbortController());
|
|
360
|
+
useEffect(() => () => appLifetime.abort(new Error('The Namzu TUI was closed.')), [appLifetime]);
|
|
335
361
|
const cancelPendingLogin = useCallback(() => {
|
|
336
362
|
loginAbortCleanupRef.current?.();
|
|
337
363
|
loginAbortCleanupRef.current = null;
|
|
@@ -382,6 +408,7 @@ export function App({ ctx: initialCtx }) {
|
|
|
382
408
|
const [goalActivation] = useState(() => new SessionGoalActivation());
|
|
383
409
|
const goalDriveInFlightRef = useRef(false);
|
|
384
410
|
const [goalDriveVersion, setGoalDriveVersion] = useState(0);
|
|
411
|
+
const [goalStatus, setGoalStatus] = useState(null);
|
|
385
412
|
const wakeGoalDriver = useCallback(() => setGoalDriveVersion((version) => version + 1), []);
|
|
386
413
|
/**
|
|
387
414
|
* Conversation writes in the order the operator produced them.
|
|
@@ -500,6 +527,106 @@ export function App({ ctx: initialCtx }) {
|
|
|
500
527
|
]);
|
|
501
528
|
return id;
|
|
502
529
|
}, [nextId]);
|
|
530
|
+
const applyPermissionMode = useCallback((mode) => {
|
|
531
|
+
if (!session?.hasProvider) {
|
|
532
|
+
pushMessage('system', 'No active session — pick a provider before changing permissions.');
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
if (state !== 'idle' ||
|
|
536
|
+
abortRef.current !== null ||
|
|
537
|
+
hasUnsettledTurn() ||
|
|
538
|
+
queuedRef.current.length > 0 ||
|
|
539
|
+
permissionResolveRef.current !== null ||
|
|
540
|
+
compactingRef.current) {
|
|
541
|
+
pushMessage('system', 'Permission mode was not changed: wait for the active turn, prompt, compaction, and queued work to settle.');
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
if (!session.resetApprovalLatch) {
|
|
545
|
+
pushMessage('system', 'Permission mode was not changed: this embedded session cannot revoke an earlier "approve all" choice. Reconnect it before changing modes.');
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
session.resetApprovalLatch();
|
|
549
|
+
permissionModeRef.current = mode;
|
|
550
|
+
permissionModeSourceRef.current = 'session';
|
|
551
|
+
setPermissionModeState(mode);
|
|
552
|
+
pushMessage('system', `Permission mode changed to ${mode}. Any earlier "approve all" choice was revoked. Declarative deny rules and the built-in safety gate still take precedence.`);
|
|
553
|
+
}, [hasUnsettledTurn, pushMessage, session, state]);
|
|
554
|
+
const applyReasoningEffort = useCallback((effort) => {
|
|
555
|
+
if (!session?.hasProvider) {
|
|
556
|
+
pushMessage('system', 'No active session — pick a provider before changing reasoning effort.');
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
if (state !== 'idle' ||
|
|
560
|
+
abortRef.current !== null ||
|
|
561
|
+
hasUnsettledTurn() ||
|
|
562
|
+
queuedRef.current.length > 0 ||
|
|
563
|
+
permissionResolveRef.current !== null ||
|
|
564
|
+
compactingRef.current) {
|
|
565
|
+
pushMessage('system', 'Reasoning effort was not changed: wait for the active turn, prompt, compaction, and queued work to settle.');
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
if (effort === undefined) {
|
|
569
|
+
setReasoningEffort(undefined);
|
|
570
|
+
pushMessage('system', 'Reasoning effort reset to the provider default for future main-query turns.');
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (!session.reasoningEffortLevels?.includes(effort)) {
|
|
574
|
+
pushMessage('system', `Reasoning effort was not changed: ${effort} is not offered by every usable provider-chain member for this model.`);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
setReasoningEffort(effort);
|
|
578
|
+
pushMessage('system', `Reasoning effort changed to ${effort} for future main-query turns in this session.`);
|
|
579
|
+
}, [hasUnsettledTurn, pushMessage, session, setReasoningEffort, state]);
|
|
580
|
+
const applyChoiceSelection = useCallback((index) => {
|
|
581
|
+
const picker = choicePickerRef.current;
|
|
582
|
+
if (!picker)
|
|
583
|
+
return;
|
|
584
|
+
const value = picker.values[index];
|
|
585
|
+
if (index < 0 || index >= picker.values.length)
|
|
586
|
+
return;
|
|
587
|
+
setChoicePicker(null);
|
|
588
|
+
if (picker.kind === 'permission-mode') {
|
|
589
|
+
applyPermissionMode(value);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
applyReasoningEffort(value);
|
|
593
|
+
}, [applyPermissionMode, applyReasoningEffort, setChoicePicker]);
|
|
594
|
+
const sendCopyRequest = useCallback((index) => {
|
|
595
|
+
const picker = copyPickerRef.current;
|
|
596
|
+
const target = picker?.targets[index];
|
|
597
|
+
if (!picker || !target)
|
|
598
|
+
return;
|
|
599
|
+
// Release the overlay synchronously before its transcript notice lands.
|
|
600
|
+
// The queue pump still observes React state and can proceed only after
|
|
601
|
+
// this event has finished publishing the result.
|
|
602
|
+
setCopyPicker(null);
|
|
603
|
+
const result = writeClipboardText(target.text, {
|
|
604
|
+
isTTY: stdout.isTTY,
|
|
605
|
+
write: writeStdout,
|
|
606
|
+
});
|
|
607
|
+
const origin = picker.provenance === 'persisted'
|
|
608
|
+
? 'the latest persisted assistant output'
|
|
609
|
+
: 'the latest normally completed answer';
|
|
610
|
+
const selection = target.kind === 'whole' ? origin : `${target.label} from ${origin}`;
|
|
611
|
+
switch (result.kind) {
|
|
612
|
+
case 'request-sent':
|
|
613
|
+
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.`);
|
|
614
|
+
break;
|
|
615
|
+
case 'unavailable':
|
|
616
|
+
pushMessage('system', `Cannot send a copy request here — ${result.detail}. /copy needs an interactive terminal with OSC 52 clipboard support.`);
|
|
617
|
+
break;
|
|
618
|
+
case 'too-large':
|
|
619
|
+
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.`);
|
|
620
|
+
break;
|
|
621
|
+
case 'write-failed':
|
|
622
|
+
pushMessage('system', `Could not send the terminal copy request: ${result.detail}`);
|
|
623
|
+
break;
|
|
624
|
+
default: {
|
|
625
|
+
const exhaustive = result;
|
|
626
|
+
void exhaustive;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}, [pushMessage, setCopyPicker, stdout.isTTY, writeStdout]);
|
|
503
630
|
const sendTerminalNotification = useCallback((notification) => {
|
|
504
631
|
if (!terminalNotificationEnabled(ctx.tui?.notifications, notification))
|
|
505
632
|
return;
|
|
@@ -695,6 +822,8 @@ export function App({ ctx: initialCtx }) {
|
|
|
695
822
|
cancelPendingLogin();
|
|
696
823
|
setPickerNotice(null);
|
|
697
824
|
setKeyEntryFor(null);
|
|
825
|
+
setPickerDetected(null);
|
|
826
|
+
setPickerSelectionKind('provider-and-model');
|
|
698
827
|
setPickerInitialView('subscriptions');
|
|
699
828
|
setPhase('picker');
|
|
700
829
|
}, [cancelPendingLogin, pushMessage]);
|
|
@@ -816,6 +945,9 @@ export function App({ ctx: initialCtx }) {
|
|
|
816
945
|
return;
|
|
817
946
|
setDetected(probe.detected);
|
|
818
947
|
if (probe.needsRepickReason) {
|
|
948
|
+
setPickerDetected(null);
|
|
949
|
+
setPickerSelectionKind('provider-and-model');
|
|
950
|
+
setPickerInitialView('providers');
|
|
819
951
|
pushMessage('system', probe.needsRepickReason);
|
|
820
952
|
setPickerNotice(probe.needsRepickReason);
|
|
821
953
|
setPhase('picker');
|
|
@@ -832,6 +964,9 @@ export function App({ ctx: initialCtx }) {
|
|
|
832
964
|
// so the chain it declares — model pins and fallbacks included — is
|
|
833
965
|
// still the operator's answer once one is supplied.
|
|
834
966
|
savedPrefsRef.current = probe.preferences;
|
|
967
|
+
setPickerDetected(null);
|
|
968
|
+
setPickerSelectionKind('provider-and-model');
|
|
969
|
+
setPickerInitialView('providers');
|
|
835
970
|
setKeyEntryFor(probe.credentialGap.providerId);
|
|
836
971
|
pushMessage('system', probe.credentialGap.reason);
|
|
837
972
|
setPickerNotice(probe.credentialGap.reason);
|
|
@@ -839,9 +974,51 @@ export function App({ ctx: initialCtx }) {
|
|
|
839
974
|
return;
|
|
840
975
|
}
|
|
841
976
|
if (probe.preferences) {
|
|
977
|
+
setPickerDetected(null);
|
|
978
|
+
setPickerSelectionKind('provider-and-model');
|
|
842
979
|
await hydrateSession(probe.preferences, probe.detected, signal);
|
|
843
980
|
return;
|
|
844
981
|
}
|
|
982
|
+
const signedIn = signedInSubscriptionProviders(probe.detected);
|
|
983
|
+
if (signedIn.length === 1) {
|
|
984
|
+
const chosen = signedIn[0];
|
|
985
|
+
if (!chosen)
|
|
986
|
+
throw new Error('A signed-in provider disappeared during discovery.');
|
|
987
|
+
const automaticPreferences = {
|
|
988
|
+
version: 3,
|
|
989
|
+
providers: [{ id: chosen.entry.id }],
|
|
990
|
+
subagents: { active: [] },
|
|
991
|
+
};
|
|
992
|
+
// Use the picker-grade transactional admission even though no picker had
|
|
993
|
+
// to be drawn. A provider-less candidate must leave an actionable choice,
|
|
994
|
+
// not strand first run on the disabled unhealthy screen.
|
|
995
|
+
const admissionSignal = signal ?? appLifetime.signal;
|
|
996
|
+
try {
|
|
997
|
+
await hydrateSession(automaticPreferences, probe.detected, admissionSignal);
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
catch (error) {
|
|
1001
|
+
if (admissionSignal.aborted)
|
|
1002
|
+
return;
|
|
1003
|
+
setPickerDetected(null);
|
|
1004
|
+
setPickerSelectionKind('provider-and-model');
|
|
1005
|
+
setPickerInitialView('providers');
|
|
1006
|
+
setPickerNotice(`The signed-in ${chosen.entry.label} session could not start: ${error instanceof Error ? error.message : String(error)}`);
|
|
1007
|
+
setPhase('picker');
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
if (signedIn.length > 1) {
|
|
1012
|
+
setPickerDetected(signedIn);
|
|
1013
|
+
setPickerSelectionKind('signed-in-subscription');
|
|
1014
|
+
setPickerInitialView('providers');
|
|
1015
|
+
setPickerNotice('Claude and Codex subscriptions are already signed in on this device. Choose which one Namzu should use.');
|
|
1016
|
+
setPhase('picker');
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
setPickerDetected(null);
|
|
1020
|
+
setPickerSelectionKind('provider-and-model');
|
|
1021
|
+
setPickerInitialView('providers');
|
|
845
1022
|
setPhase('picker');
|
|
846
1023
|
}
|
|
847
1024
|
catch (err) {
|
|
@@ -850,7 +1027,7 @@ export function App({ ctx: initialCtx }) {
|
|
|
850
1027
|
setPhase('unhealthy');
|
|
851
1028
|
pushMessage('system', `Failed to probe agents: ${err instanceof Error ? err.message : String(err)}`);
|
|
852
1029
|
}
|
|
853
|
-
}, [hydrateSession, pushMessage]);
|
|
1030
|
+
}, [appLifetime.signal, hydrateSession, pushMessage]);
|
|
854
1031
|
// `startOrFinishLogin` is declared above `runProbe` and calls it, so it
|
|
855
1032
|
// reads the current one through a ref rather than closing over a stale
|
|
856
1033
|
// binding or forcing the two into a declaration order that reads backwards.
|
|
@@ -1959,10 +2136,16 @@ export function App({ ctx: initialCtx }) {
|
|
|
1959
2136
|
// been solved — the session behind this picker is running.
|
|
1960
2137
|
setPickerNotice(null);
|
|
1961
2138
|
setKeyEntryFor(null);
|
|
2139
|
+
setPickerDetected(null);
|
|
2140
|
+
setPickerSelectionKind('provider-and-model');
|
|
1962
2141
|
setPickerInitialView('providers');
|
|
1963
2142
|
setPhase('picker');
|
|
1964
2143
|
return;
|
|
1965
2144
|
case 'permission-mode': {
|
|
2145
|
+
applyPermissionMode(slash.mode);
|
|
2146
|
+
return;
|
|
2147
|
+
}
|
|
2148
|
+
case 'permission-mode-picker': {
|
|
1966
2149
|
if (!session?.hasProvider) {
|
|
1967
2150
|
pushMessage('system', 'No active session — pick a provider before changing permissions.');
|
|
1968
2151
|
return;
|
|
@@ -1972,24 +2155,34 @@ export function App({ ctx: initialCtx }) {
|
|
|
1972
2155
|
hasUnsettledTurn() ||
|
|
1973
2156
|
queuedRef.current.length > 0 ||
|
|
1974
2157
|
permissionResolveRef.current !== null ||
|
|
1975
|
-
compactingRef.current
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
}
|
|
1979
|
-
if (!session.resetApprovalLatch) {
|
|
1980
|
-
pushMessage('system', 'Permission mode was not changed: this embedded session cannot revoke an earlier "approve all" choice. Reconnect it before changing modes.');
|
|
2158
|
+
compactingRef.current ||
|
|
2159
|
+
!session.resetApprovalLatch) {
|
|
2160
|
+
pushMessage('system', 'Permission choices are unavailable until the active turn, prompt, compaction, queued work, or embedded-session approval latch can be safely settled.');
|
|
1981
2161
|
return;
|
|
1982
2162
|
}
|
|
1983
|
-
|
|
1984
|
-
permissionModeRef.current
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
2163
|
+
const values = ['prompt', 'auto', 'strict'];
|
|
2164
|
+
setSelectedChoice(Math.max(0, values.indexOf(permissionModeRef.current)));
|
|
2165
|
+
setChoicePicker({
|
|
2166
|
+
kind: 'permission-mode',
|
|
2167
|
+
title: 'Select Permission Mode',
|
|
2168
|
+
notice: permissionPickerNotice(session),
|
|
2169
|
+
values,
|
|
2170
|
+
options: values.map((mode) => ({
|
|
2171
|
+
label: mode,
|
|
2172
|
+
description: permissionModeDescription(mode),
|
|
2173
|
+
current: mode === permissionModeRef.current,
|
|
2174
|
+
})),
|
|
2175
|
+
});
|
|
1988
2176
|
return;
|
|
1989
2177
|
}
|
|
1990
2178
|
case 'reasoning-effort': {
|
|
1991
|
-
|
|
1992
|
-
|
|
2179
|
+
applyReasoningEffort(slash.effort ?? undefined);
|
|
2180
|
+
return;
|
|
2181
|
+
}
|
|
2182
|
+
case 'reasoning-effort-picker': {
|
|
2183
|
+
const levels = session?.reasoningEffortLevels;
|
|
2184
|
+
if (!session?.hasProvider || levels === undefined) {
|
|
2185
|
+
pushMessage('system', 'No exact reasoning-effort menu is available for the current session.');
|
|
1993
2186
|
return;
|
|
1994
2187
|
}
|
|
1995
2188
|
if (state !== 'idle' ||
|
|
@@ -1998,20 +2191,22 @@ export function App({ ctx: initialCtx }) {
|
|
|
1998
2191
|
queuedRef.current.length > 0 ||
|
|
1999
2192
|
permissionResolveRef.current !== null ||
|
|
2000
2193
|
compactingRef.current) {
|
|
2001
|
-
pushMessage('system', 'Reasoning
|
|
2002
|
-
return;
|
|
2003
|
-
}
|
|
2004
|
-
if (slash.effort === null) {
|
|
2005
|
-
setReasoningEffort(undefined);
|
|
2006
|
-
pushMessage('system', 'Reasoning effort reset to the provider default for future main-query turns.');
|
|
2007
|
-
return;
|
|
2008
|
-
}
|
|
2009
|
-
if (!session.reasoningEffortLevels?.includes(slash.effort)) {
|
|
2010
|
-
pushMessage('system', `Reasoning effort was not changed: ${slash.effort} is not offered by every usable provider-chain member for this model.`);
|
|
2194
|
+
pushMessage('system', 'Reasoning-effort choices are unavailable until the active turn, prompt, compaction, and queued work settle.');
|
|
2011
2195
|
return;
|
|
2012
2196
|
}
|
|
2013
|
-
|
|
2014
|
-
|
|
2197
|
+
const values = [undefined, ...levels];
|
|
2198
|
+
const current = reasoningEffortRef.current;
|
|
2199
|
+
setSelectedChoice(Math.max(0, values.findIndex((value) => value === current)));
|
|
2200
|
+
setChoicePicker({
|
|
2201
|
+
kind: 'reasoning-effort',
|
|
2202
|
+
title: `Select Reasoning Level for ${session.modelSummary ?? 'current model'}`,
|
|
2203
|
+
values,
|
|
2204
|
+
options: values.map((effort) => ({
|
|
2205
|
+
label: effort ?? 'default',
|
|
2206
|
+
description: reasoningEffortDescription(effort),
|
|
2207
|
+
current: effort === current,
|
|
2208
|
+
})),
|
|
2209
|
+
});
|
|
2015
2210
|
return;
|
|
2016
2211
|
}
|
|
2017
2212
|
case 'login':
|
|
@@ -2392,30 +2587,11 @@ export function App({ ctx: initialCtx }) {
|
|
|
2392
2587
|
pushMessage('system', 'Nothing to copy yet — /copy applies to the latest assistant answer that finished normally.');
|
|
2393
2588
|
return;
|
|
2394
2589
|
}
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2590
|
+
setSelectedCopy(0);
|
|
2591
|
+
setCopyPicker({
|
|
2592
|
+
targets: copyTargetsForResponse(target.text),
|
|
2593
|
+
provenance: target.provenance,
|
|
2398
2594
|
});
|
|
2399
|
-
switch (result.kind) {
|
|
2400
|
-
case 'request-sent':
|
|
2401
|
-
pushMessage('system', `Copy request sent for the ${target.provenance === 'persisted'
|
|
2402
|
-
? 'latest persisted assistant output'
|
|
2403
|
-
: '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.`);
|
|
2404
|
-
break;
|
|
2405
|
-
case 'unavailable':
|
|
2406
|
-
pushMessage('system', `Cannot send a copy request here — ${result.detail}. /copy needs an interactive terminal with OSC 52 clipboard support.`);
|
|
2407
|
-
break;
|
|
2408
|
-
case 'too-large':
|
|
2409
|
-
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.`);
|
|
2410
|
-
break;
|
|
2411
|
-
case 'write-failed':
|
|
2412
|
-
pushMessage('system', `Could not send the terminal copy request: ${result.detail}`);
|
|
2413
|
-
break;
|
|
2414
|
-
default: {
|
|
2415
|
-
const exhaustive = result;
|
|
2416
|
-
void exhaustive;
|
|
2417
|
-
}
|
|
2418
|
-
}
|
|
2419
2595
|
return;
|
|
2420
2596
|
}
|
|
2421
2597
|
case 'raw': {
|
|
@@ -2501,6 +2677,8 @@ export function App({ ctx: initialCtx }) {
|
|
|
2501
2677
|
}, [
|
|
2502
2678
|
activeSkills,
|
|
2503
2679
|
advanceQueueContinuation,
|
|
2680
|
+
applyPermissionMode,
|
|
2681
|
+
applyReasoningEffort,
|
|
2504
2682
|
ctx.cwd,
|
|
2505
2683
|
doResume,
|
|
2506
2684
|
enqueueQueued,
|
|
@@ -2510,11 +2688,45 @@ export function App({ ctx: initialCtx }) {
|
|
|
2510
2688
|
pushMessage,
|
|
2511
2689
|
rawOutput,
|
|
2512
2690
|
resetTranscript,
|
|
2691
|
+
setChoicePicker,
|
|
2692
|
+
setCopyPicker,
|
|
2513
2693
|
setReasoningEffort,
|
|
2514
2694
|
slashCtx,
|
|
2515
2695
|
startFreshConversation,
|
|
2516
2696
|
state,
|
|
2517
2697
|
]);
|
|
2698
|
+
// Keep the footer on the same durable goal record the driver mutates. A ref
|
|
2699
|
+
// alone cannot repaint React, so every goal mutation bumps goalDriveVersion;
|
|
2700
|
+
// generation/session checks stop a late disk read labelling a resumed chat.
|
|
2701
|
+
useEffect(() => {
|
|
2702
|
+
void goalDriveVersion;
|
|
2703
|
+
const sessions = sessionsRef.current;
|
|
2704
|
+
const scope = scopeRef.current;
|
|
2705
|
+
if (!sessions?.goals || !scope) {
|
|
2706
|
+
setGoalStatus(null);
|
|
2707
|
+
return;
|
|
2708
|
+
}
|
|
2709
|
+
const generation = conversationGenRef.current;
|
|
2710
|
+
const sessionId = scope.sessionId;
|
|
2711
|
+
let disposed = false;
|
|
2712
|
+
void sessions.goals
|
|
2713
|
+
.getGoal(sessionId, sessions.tenantId)
|
|
2714
|
+
.then((goal) => {
|
|
2715
|
+
if (disposed ||
|
|
2716
|
+
conversationGenRef.current !== generation ||
|
|
2717
|
+
scopeRef.current?.sessionId !== sessionId) {
|
|
2718
|
+
return;
|
|
2719
|
+
}
|
|
2720
|
+
setGoalStatus(goal);
|
|
2721
|
+
})
|
|
2722
|
+
.catch(() => {
|
|
2723
|
+
if (!disposed && conversationGenRef.current === generation)
|
|
2724
|
+
setGoalStatus(null);
|
|
2725
|
+
});
|
|
2726
|
+
return () => {
|
|
2727
|
+
disposed = true;
|
|
2728
|
+
};
|
|
2729
|
+
}, [goalDriveVersion, phase, session]);
|
|
2518
2730
|
// Admit automatic work only at a whole-App durable boundary. This effect
|
|
2519
2731
|
// reserves; it never starts a turn itself. The one queue pump below remains
|
|
2520
2732
|
// the sole starter, which is what keeps a human prompt that arrives during
|
|
@@ -2535,6 +2747,8 @@ export function App({ ctx: initialCtx }) {
|
|
|
2535
2747
|
conversationMutationRef.current ||
|
|
2536
2748
|
exportingRef.current ||
|
|
2537
2749
|
compactingRef.current ||
|
|
2750
|
+
choicePickerRef.current ||
|
|
2751
|
+
copyPickerRef.current ||
|
|
2538
2752
|
goalCommandInFlightRef.current) {
|
|
2539
2753
|
return;
|
|
2540
2754
|
}
|
|
@@ -2559,6 +2773,8 @@ export function App({ ctx: initialCtx }) {
|
|
|
2559
2773
|
conversationMutationRef.current ||
|
|
2560
2774
|
exportingRef.current ||
|
|
2561
2775
|
compactingRef.current ||
|
|
2776
|
+
choicePickerRef.current ||
|
|
2777
|
+
copyPickerRef.current ||
|
|
2562
2778
|
goalCommandInFlightRef.current ||
|
|
2563
2779
|
queuedRef.current.length > 0 ||
|
|
2564
2780
|
!goalActivation.isArmed(sessionId, armed)) {
|
|
@@ -2616,6 +2832,8 @@ export function App({ ctx: initialCtx }) {
|
|
|
2616
2832
|
conversationMutation,
|
|
2617
2833
|
session,
|
|
2618
2834
|
state,
|
|
2835
|
+
choicePicker,
|
|
2836
|
+
copyPicker,
|
|
2619
2837
|
wakeGoalDriver,
|
|
2620
2838
|
]);
|
|
2621
2839
|
// Drain the queue: when a turn settles (idle) and nothing is running,
|
|
@@ -2630,6 +2848,8 @@ export function App({ ctx: initialCtx }) {
|
|
|
2630
2848
|
conversationMutationRef.current ||
|
|
2631
2849
|
exportingRef.current ||
|
|
2632
2850
|
compactingRef.current ||
|
|
2851
|
+
choicePickerRef.current ||
|
|
2852
|
+
copyPickerRef.current ||
|
|
2633
2853
|
goalCommandInFlightRef.current)
|
|
2634
2854
|
return;
|
|
2635
2855
|
// Read and remove from the synchronous source of truth. `queued` is the
|
|
@@ -2639,7 +2859,17 @@ export function App({ ctx: initialCtx }) {
|
|
|
2639
2859
|
const next = dequeueQueued();
|
|
2640
2860
|
if (next !== undefined)
|
|
2641
2861
|
void runTurn(next);
|
|
2642
|
-
}, [
|
|
2862
|
+
}, [
|
|
2863
|
+
state,
|
|
2864
|
+
phase,
|
|
2865
|
+
queued,
|
|
2866
|
+
queuePause,
|
|
2867
|
+
choicePicker,
|
|
2868
|
+
copyPicker,
|
|
2869
|
+
dequeueQueued,
|
|
2870
|
+
hasUnsettledTurn,
|
|
2871
|
+
runTurn,
|
|
2872
|
+
]);
|
|
2643
2873
|
// One-shot update check on launch.
|
|
2644
2874
|
// Best-effort; surfaces a single notice when something newer is out.
|
|
2645
2875
|
const updateCheckedRef = useRef(false);
|
|
@@ -2654,7 +2884,7 @@ export function App({ ctx: initialCtx }) {
|
|
|
2654
2884
|
pushMessage('system', `⬆ Update available:\n${lines.join('\n')}`);
|
|
2655
2885
|
});
|
|
2656
2886
|
}, [phase, ctx.version, pushMessage]);
|
|
2657
|
-
const hydrateFromPicker = useCallback(async (prefs, detectedNow, signal) => {
|
|
2887
|
+
const hydrateFromPicker = useCallback(async (prefs, detectedNow, signal, revealAllOnFailure = false) => {
|
|
2658
2888
|
try {
|
|
2659
2889
|
await hydrateSession(prefs, detectedNow, signal);
|
|
2660
2890
|
}
|
|
@@ -2664,6 +2894,11 @@ export function App({ ctx: initialCtx }) {
|
|
|
2664
2894
|
// choice stays on the picker with the actionable construction error.
|
|
2665
2895
|
if (signal.aborted)
|
|
2666
2896
|
return;
|
|
2897
|
+
if (revealAllOnFailure) {
|
|
2898
|
+
setPickerDetected(null);
|
|
2899
|
+
setPickerSelectionKind('provider-and-model');
|
|
2900
|
+
setPickerInitialView('providers');
|
|
2901
|
+
}
|
|
2667
2902
|
setPickerNotice(`Could not start the selected provider: ${err instanceof Error ? err.message : String(err)}`);
|
|
2668
2903
|
}
|
|
2669
2904
|
}, [hydrateSession]);
|
|
@@ -2679,6 +2914,8 @@ export function App({ ctx: initialCtx }) {
|
|
|
2679
2914
|
const next = [credential, ...detected.filter((d) => d.entry.id !== credential.entry.id)];
|
|
2680
2915
|
setDetected(next);
|
|
2681
2916
|
setKeyEntryFor(null);
|
|
2917
|
+
setPickerDetected(null);
|
|
2918
|
+
setPickerSelectionKind('provider-and-model');
|
|
2682
2919
|
setPickerNotice(null);
|
|
2683
2920
|
// The disposition, not the key. `credential` never reaches a message.
|
|
2684
2921
|
pushMessage('system', disposition);
|
|
@@ -2720,7 +2957,9 @@ export function App({ ctx: initialCtx }) {
|
|
|
2720
2957
|
pushMessage('system', `Could not save preferences: ${err instanceof Error ? err.message : String(err)}`);
|
|
2721
2958
|
return;
|
|
2722
2959
|
}
|
|
2723
|
-
|
|
2960
|
+
// Keep a narrowed signed-in roster stable while construction is pending.
|
|
2961
|
+
// Only a real refusal broadens back to the general, sign-in-capable picker.
|
|
2962
|
+
void hydrateFromPicker(prefs, detected, signal, true);
|
|
2724
2963
|
}, [detected, hydrateFromPicker, pushMessage]);
|
|
2725
2964
|
/**
|
|
2726
2965
|
* Leaving the picker returns to whatever was on screen before it.
|
|
@@ -2742,6 +2981,8 @@ export function App({ ctx: initialCtx }) {
|
|
|
2742
2981
|
*/
|
|
2743
2982
|
const handlePickerCancel = useCallback(() => {
|
|
2744
2983
|
setPickerInitialView('providers');
|
|
2984
|
+
setPickerDetected(null);
|
|
2985
|
+
setPickerSelectionKind('provider-and-model');
|
|
2745
2986
|
if (session?.hasProvider) {
|
|
2746
2987
|
setPhase('ready');
|
|
2747
2988
|
return;
|
|
@@ -2867,6 +3108,62 @@ export function App({ ctx: initialCtx }) {
|
|
|
2867
3108
|
resolvePermission({ kind: 'approve-all' });
|
|
2868
3109
|
return;
|
|
2869
3110
|
}
|
|
3111
|
+
// Finite slash-command choices own the keyboard below a live permission
|
|
3112
|
+
// request and above the ordinary composer. Their values are a captured
|
|
3113
|
+
// menu, so a provider/session change cannot silently reinterpret Enter.
|
|
3114
|
+
if (choicePickerRef.current) {
|
|
3115
|
+
const options = choicePickerRef.current.options;
|
|
3116
|
+
if (key.escape || (key.ctrl && input === 'c')) {
|
|
3117
|
+
setChoicePicker(null);
|
|
3118
|
+
return;
|
|
3119
|
+
}
|
|
3120
|
+
if (key.upArrow) {
|
|
3121
|
+
setSelectedChoice((index) => Math.max(0, index - 1));
|
|
3122
|
+
return;
|
|
3123
|
+
}
|
|
3124
|
+
if (key.downArrow) {
|
|
3125
|
+
setSelectedChoice((index) => Math.min(options.length - 1, index + 1));
|
|
3126
|
+
return;
|
|
3127
|
+
}
|
|
3128
|
+
if (key.return) {
|
|
3129
|
+
applyChoiceSelection(selectedChoice);
|
|
3130
|
+
return;
|
|
3131
|
+
}
|
|
3132
|
+
if (/^[1-9]$/.test(input)) {
|
|
3133
|
+
const index = Number(input) - 1;
|
|
3134
|
+
if (options[index])
|
|
3135
|
+
applyChoiceSelection(index);
|
|
3136
|
+
}
|
|
3137
|
+
return;
|
|
3138
|
+
}
|
|
3139
|
+
// `/copy` holds a source snapshot, not a lifecycle phase. Permission
|
|
3140
|
+
// stays above it in this handler so an approval request that arrives
|
|
3141
|
+
// while the picker is open cannot have its keys stolen by the chooser.
|
|
3142
|
+
if (copyPickerRef.current) {
|
|
3143
|
+
const targets = copyPickerRef.current.targets;
|
|
3144
|
+
if (key.escape || (key.ctrl && input === 'c')) {
|
|
3145
|
+
setCopyPicker(null);
|
|
3146
|
+
return;
|
|
3147
|
+
}
|
|
3148
|
+
if (key.upArrow) {
|
|
3149
|
+
setSelectedCopy((index) => Math.max(0, index - 1));
|
|
3150
|
+
return;
|
|
3151
|
+
}
|
|
3152
|
+
if (key.downArrow) {
|
|
3153
|
+
setSelectedCopy((index) => Math.min(targets.length - 1, index + 1));
|
|
3154
|
+
return;
|
|
3155
|
+
}
|
|
3156
|
+
if (key.return) {
|
|
3157
|
+
sendCopyRequest(selectedCopy);
|
|
3158
|
+
return;
|
|
3159
|
+
}
|
|
3160
|
+
if (/^[1-9]$/.test(input)) {
|
|
3161
|
+
const index = Number(input) - 1;
|
|
3162
|
+
if (targets[index])
|
|
3163
|
+
sendCopyRequest(index);
|
|
3164
|
+
}
|
|
3165
|
+
return;
|
|
3166
|
+
}
|
|
2870
3167
|
// Esc interrupts a running turn (Ctrl+C stays reserved for exit). Mirrors
|
|
2871
3168
|
// the Ctrl+C interrupt path: abort, drop the queue, one "Interrupted." line.
|
|
2872
3169
|
if (key.escape && abortRef.current) {
|
|
@@ -2943,16 +3240,29 @@ export function App({ ctx: initialCtx }) {
|
|
|
2943
3240
|
// Background is left natural — we inherit the terminal's own background
|
|
2944
3241
|
// and only theme the foreground. Forcing
|
|
2945
3242
|
// a filled bg left mismatched patches around bordered areas, so we don't.
|
|
2946
|
-
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, 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 }) :
|
|
3243
|
+
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 })) : choicePicker ? (_jsx(ChoicePicker, { title: choicePicker.title, notice: choicePicker.notice, options: choicePicker.options, selected: selectedChoice })) : copyPicker ? (_jsx(CopyPicker, { targets: copyPicker.targets, selected: selectedCopy })) : null, _jsxs(ComposerFrame, { focus: state === 'idle' &&
|
|
3244
|
+
phase === 'ready' &&
|
|
3245
|
+
conversationMutation === null &&
|
|
3246
|
+
choicePicker === null &&
|
|
3247
|
+
copyPicker === null, hidden: permission !== null || choicePicker !== null || copyPicker !== null, children: [queued.length > 0 &&
|
|
3248
|
+
permission === null &&
|
|
3249
|
+
choicePicker === null &&
|
|
3250
|
+
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
|
|
2947
3251
|
? `paused after a ${queuePause.outcome} turn; send a message or change model to continue`
|
|
2948
3252
|
: 'sending when ready'] }) })) : null, _jsx(Composer, { disabled: phase !== 'ready' ||
|
|
2949
3253
|
state === 'awaiting-permission' ||
|
|
2950
3254
|
compacting ||
|
|
2951
|
-
conversationMutation !== null, hidden: permission !== null,
|
|
3255
|
+
conversationMutation !== null, hidden: permission !== null || choicePicker !== null || copyPicker !== null,
|
|
2952
3256
|
// A turn is running, so Esc is the interrupt and not
|
|
2953
3257
|
// the composer's clear.
|
|
2954
|
-
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
|
|
2955
|
-
|
|
3258
|
+
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 ?? null, effort: reasoningEffort ?? 'default', goal: phase === 'ready' &&
|
|
3259
|
+
state === 'idle' &&
|
|
3260
|
+
conversationMutation === null &&
|
|
3261
|
+
!compacting &&
|
|
3262
|
+
permission === null &&
|
|
3263
|
+
choicePicker === null &&
|
|
3264
|
+
copyPicker === null
|
|
3265
|
+
? goalStatusLabel(goalStatus)
|
|
2956
3266
|
: null, state: state, hint: conversationMutation === 'fork'
|
|
2957
3267
|
? 'forking conversation — input is paused'
|
|
2958
3268
|
: conversationMutation === 'edit'
|
|
@@ -2961,7 +3271,15 @@ export function App({ ctx: initialCtx }) {
|
|
|
2961
3271
|
? 'starting a fresh conversation — input is paused'
|
|
2962
3272
|
: compacting
|
|
2963
3273
|
? 'compacting conversation — input is paused'
|
|
2964
|
-
:
|
|
3274
|
+
: permission
|
|
3275
|
+
? hintForPhase(phase, state, session?.hasProvider === true)
|
|
3276
|
+
: choicePicker
|
|
3277
|
+
? 'choice open — ↑↓ / 1–9 select · enter apply · esc cancel'
|
|
3278
|
+
: copyPicker
|
|
3279
|
+
? 'copy target open — ↑↓ / 1–9 select · esc cancel'
|
|
3280
|
+
: goalStatus && phase === 'ready' && state === 'idle'
|
|
3281
|
+
? undefined
|
|
3282
|
+
: hintForPhase(phase, state, session?.hasProvider === true), usage: usage, context: context }) })] })] }));
|
|
2965
3283
|
}
|
|
2966
3284
|
function Banner({ version, session, permissionMode, cwd, }) {
|
|
2967
3285
|
const cols = process.stdout.columns ?? 80;
|
|
@@ -3019,6 +3337,62 @@ canReturn = false) {
|
|
|
3019
3337
|
return 'agent is working — esc to interrupt';
|
|
3020
3338
|
return '/help · Esc×2 edit previous · @file / Ctrl+V attach · Ctrl+C ×2 to exit';
|
|
3021
3339
|
}
|
|
3340
|
+
function goalStatusLabel(goal) {
|
|
3341
|
+
if (!goal)
|
|
3342
|
+
return null;
|
|
3343
|
+
switch (goal.phase) {
|
|
3344
|
+
case 'active':
|
|
3345
|
+
return 'Pursuing goal';
|
|
3346
|
+
case 'paused':
|
|
3347
|
+
return 'Goal paused (/goal resume)';
|
|
3348
|
+
case 'blocked':
|
|
3349
|
+
return 'Goal stalled (/goal resume)';
|
|
3350
|
+
case 'complete':
|
|
3351
|
+
return 'Goal achieved';
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
function permissionModeDescription(mode) {
|
|
3355
|
+
switch (mode) {
|
|
3356
|
+
case 'prompt':
|
|
3357
|
+
return 'Ask before an undecided tool call runs';
|
|
3358
|
+
case 'auto':
|
|
3359
|
+
return 'Approve undecided calls unless a rule or safety gate refuses';
|
|
3360
|
+
case 'strict':
|
|
3361
|
+
return 'Refuse undecided calls; explicit allow rules still work';
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
function permissionPickerNotice(session) {
|
|
3365
|
+
const notices = [];
|
|
3366
|
+
if (session.approvalLatched()) {
|
|
3367
|
+
notices.push('Approve all is active; applying any mode revokes it.');
|
|
3368
|
+
}
|
|
3369
|
+
const exempt = session.promptExemptTools();
|
|
3370
|
+
if (exempt.length > 0)
|
|
3371
|
+
notices.push(`Never prompted: ${exempt.join(', ')}.`);
|
|
3372
|
+
return notices.length > 0 ? notices.join(' ') : undefined;
|
|
3373
|
+
}
|
|
3374
|
+
function reasoningEffortDescription(effort) {
|
|
3375
|
+
switch (effort) {
|
|
3376
|
+
case undefined:
|
|
3377
|
+
return 'Use the provider and model default';
|
|
3378
|
+
case 'none':
|
|
3379
|
+
return 'No deliberate reasoning where the model supports it';
|
|
3380
|
+
case 'minimal':
|
|
3381
|
+
return 'Fastest responses with minimal reasoning';
|
|
3382
|
+
case 'low':
|
|
3383
|
+
return 'Fast responses with light reasoning';
|
|
3384
|
+
case 'medium':
|
|
3385
|
+
return 'Balance speed and reasoning depth';
|
|
3386
|
+
case 'high':
|
|
3387
|
+
return 'Deeper reasoning for complex problems';
|
|
3388
|
+
case 'xhigh':
|
|
3389
|
+
return 'Very deep reasoning for hard problems';
|
|
3390
|
+
case 'max':
|
|
3391
|
+
return 'Maximum reasoning depth offered by this model';
|
|
3392
|
+
case 'ultra':
|
|
3393
|
+
return 'Highest host-level reasoning setting when supported';
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3022
3396
|
/**
|
|
3023
3397
|
* The summary text, as collapsible detail under the compaction row.
|
|
3024
3398
|
*
|