@namzu/cli 14.1.0 → 14.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +20 -9
  2. package/dist/commands/login.d.ts +21 -3
  3. package/dist/commands/login.d.ts.map +1 -1
  4. package/dist/commands/login.js +33 -21
  5. package/dist/commands/login.js.map +1 -1
  6. package/dist/integrations/providers/discover.d.ts +4 -0
  7. package/dist/integrations/providers/discover.d.ts.map +1 -1
  8. package/dist/integrations/providers/discover.js +55 -37
  9. package/dist/integrations/providers/discover.js.map +1 -1
  10. package/dist/integrations/providers/harness-credentials.d.ts +44 -9
  11. package/dist/integrations/providers/harness-credentials.d.ts.map +1 -1
  12. package/dist/integrations/providers/harness-credentials.js +140 -11
  13. package/dist/integrations/providers/harness-credentials.js.map +1 -1
  14. package/dist/integrations/providers/identity.d.ts +20 -24
  15. package/dist/integrations/providers/identity.d.ts.map +1 -1
  16. package/dist/integrations/providers/identity.js +20 -24
  17. package/dist/integrations/providers/identity.js.map +1 -1
  18. package/dist/integrations/providers/index.d.ts +1 -1
  19. package/dist/integrations/providers/index.d.ts.map +1 -1
  20. package/dist/integrations/providers/index.js +1 -1
  21. package/dist/integrations/providers/index.js.map +1 -1
  22. package/dist/integrations/providers/oauth.d.ts +8 -3
  23. package/dist/integrations/providers/oauth.d.ts.map +1 -1
  24. package/dist/integrations/providers/oauth.js +37 -11
  25. package/dist/integrations/providers/oauth.js.map +1 -1
  26. package/dist/integrations/providers/subscription-login.d.ts +7 -41
  27. package/dist/integrations/providers/subscription-login.d.ts.map +1 -1
  28. package/dist/integrations/providers/subscription-login.js +26 -149
  29. package/dist/integrations/providers/subscription-login.js.map +1 -1
  30. package/dist/tui/App.d.ts.map +1 -1
  31. package/dist/tui/App.js +316 -83
  32. package/dist/tui/App.js.map +1 -1
  33. package/dist/tui/ChoicePicker.d.ts +14 -0
  34. package/dist/tui/ChoicePicker.d.ts.map +1 -0
  35. package/dist/tui/ChoicePicker.js +15 -0
  36. package/dist/tui/ChoicePicker.js.map +1 -0
  37. package/dist/tui/Picker.d.ts +4 -2
  38. package/dist/tui/Picker.d.ts.map +1 -1
  39. package/dist/tui/Picker.js +99 -10
  40. package/dist/tui/Picker.js.map +1 -1
  41. package/dist/tui/StatusBar.d.ts +24 -40
  42. package/dist/tui/StatusBar.d.ts.map +1 -1
  43. package/dist/tui/StatusBar.js +71 -106
  44. package/dist/tui/StatusBar.js.map +1 -1
  45. package/dist/tui/agent.d.ts.map +1 -1
  46. package/dist/tui/agent.js +10 -8
  47. package/dist/tui/agent.js.map +1 -1
  48. package/dist/tui/login-prompt.d.ts +0 -1
  49. package/dist/tui/login-prompt.d.ts.map +1 -1
  50. package/dist/tui/login-prompt.js +1 -6
  51. package/dist/tui/login-prompt.js.map +1 -1
  52. package/dist/tui/slashCommands.d.ts +8 -0
  53. package/dist/tui/slashCommands.d.ts.map +1 -1
  54. package/dist/tui/slashCommands.js +20 -15
  55. package/dist/tui/slashCommands.js.map +1 -1
  56. package/package.json +4 -4
package/dist/tui/App.js CHANGED
@@ -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, signedInSubscriptionProviders, writePreferences, } from '../integrations/providers/index.js';
22
+ import { beginCodexDeviceLogin, beginSubscriptionLogin, clearAllStoredCredentials, credentialsPath, parsePastedInput, 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,6 +27,7 @@ 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';
31
32
  import { CopyPicker } from './CopyPicker.js';
32
33
  import { EditPromptPicker } from './EditPromptPicker.js';
@@ -42,7 +43,7 @@ import { bottomSpacerRows } from './bottom-spacer.js';
42
43
  import { keepRecentRows } from './compact-transcript.js';
43
44
  import { approvalIsDeliberate } from './consent-timing.js';
44
45
  import { planTurnPublication } from './conversation-history.js';
45
- import { copyTargetsForResponse, } from './copy-targets.js';
46
+ import { copyTargetsForResponse } from './copy-targets.js';
46
47
  import { editablePrompts } from './edit-prompts.js';
47
48
  import { liveWindow, transcriptLines } from './live-window.js';
48
49
  import { describeCodexDeviceLoginStart, describeLoginOutcome, describeLoginStart, describeLogout, } from './login-prompt.js';
@@ -328,6 +329,14 @@ export function App({ ctx: initialCtx }) {
328
329
  copyPickerRef.current = next;
329
330
  setCopyPickerState(next);
330
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
+ }, []);
331
340
  const [composerDraft, setComposerDraft] = useState(null);
332
341
  const composerDraftTokenRef = useRef(0);
333
342
  const exitArmedRef = useRef(false);
@@ -399,6 +408,7 @@ export function App({ ctx: initialCtx }) {
399
408
  const [goalActivation] = useState(() => new SessionGoalActivation());
400
409
  const goalDriveInFlightRef = useRef(false);
401
410
  const [goalDriveVersion, setGoalDriveVersion] = useState(0);
411
+ const [goalStatus, setGoalStatus] = useState(null);
402
412
  const wakeGoalDriver = useCallback(() => setGoalDriveVersion((version) => version + 1), []);
403
413
  /**
404
414
  * Conversation writes in the order the operator produced them.
@@ -517,6 +527,70 @@ export function App({ ctx: initialCtx }) {
517
527
  ]);
518
528
  return id;
519
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]);
520
594
  const sendCopyRequest = useCallback((index) => {
521
595
  const picker = copyPickerRef.current;
522
596
  const target = picker?.targets[index];
@@ -762,15 +836,14 @@ export function App({ ctx: initialCtx }) {
762
836
  * nobody can see. Everything this path says goes into `pickerNotice`, which
763
837
  * the picker renders.
764
838
  *
765
- * It uses the browser route only. There is no paste input on this screen,
766
- * and rather than invent one — a second text field beside the credential
767
- * one, on the screen an operator reaches when they are already stuck — the
768
- * no-browser case is handed to `namzu login`, which reads the pasted
769
- * address from standard input and exists for exactly that machine.
839
+ * The registered browser callback shows a code rather than returning to a
840
+ * local listener. The picker therefore owns the matching paste field; sending a
841
+ * first-run operator to another terminal would make the sign-in technically
842
+ * started here but impossible to finish on the screen that started it.
770
843
  */
771
844
  const startLoginFromPicker = useCallback(async (provider, signal) => {
772
845
  if (signal.aborted)
773
- return;
846
+ return 'failed';
774
847
  cancelPendingLogin();
775
848
  if (provider === 'codex') {
776
849
  let start;
@@ -781,11 +854,11 @@ export function App({ ctx: initialCtx }) {
781
854
  if (!signal.aborted) {
782
855
  setPickerNotice(`Could not start Codex sign-in: ${error instanceof Error ? error.message : String(error)}`);
783
856
  }
784
- return;
857
+ return 'failed';
785
858
  }
786
859
  if (signal.aborted) {
787
860
  start.cancel();
788
- return;
861
+ return 'failed';
789
862
  }
790
863
  codexLoginRef.current = start;
791
864
  setPickerNotice(describeCodexDeviceLoginStart({
@@ -795,7 +868,7 @@ export function App({ ctx: initialCtx }) {
795
868
  }));
796
869
  const outcome = await start.waitForCompletion();
797
870
  if (codexLoginRef.current !== start || signal.aborted)
798
- return;
871
+ return 'failed';
799
872
  codexLoginRef.current = null;
800
873
  start.cancel();
801
874
  setPickerNotice(describeLoginOutcome(outcome));
@@ -803,7 +876,7 @@ export function App({ ctx: initialCtx }) {
803
876
  setPickerInitialView('providers');
804
877
  await runProbeRef.current?.(signal);
805
878
  }
806
- return;
879
+ return 'finished';
807
880
  }
808
881
  let start;
809
882
  try {
@@ -811,13 +884,13 @@ export function App({ ctx: initialCtx }) {
811
884
  }
812
885
  catch (err) {
813
886
  if (signal.aborted)
814
- return;
887
+ return 'failed';
815
888
  setPickerNotice(`Could not start a sign-in: ${err instanceof Error ? err.message : String(err)}`);
816
- return;
889
+ return 'failed';
817
890
  }
818
891
  if (signal.aborted) {
819
892
  start.cancel();
820
- return;
893
+ return 'failed';
821
894
  }
822
895
  loginRef.current = start;
823
896
  const cancelForPicker = () => {
@@ -832,36 +905,39 @@ export function App({ ctx: initialCtx }) {
832
905
  signal.addEventListener('abort', cancelForPicker, { once: true });
833
906
  if (signal.aborted) {
834
907
  cancelForPicker();
835
- return;
908
+ return 'failed';
836
909
  }
837
910
  setPickerNotice(describeLoginStart({
838
911
  url: start.url,
839
- loopback: start.loopback,
840
912
  browserOpened: openInBrowser(start.url),
841
- // Named for THIS screen. Telling someone at the picker to type a
842
- // slash command is what made the feature unreachable; telling them
843
- // to run a command in another terminal is something they can do
844
- // from where they are sitting.
845
- completionHint: 'run "namzu login" in a terminal and paste it there',
913
+ completionHint: 'paste it below and press enter',
846
914
  }));
847
- const waiting = start.waitForCallback();
848
- if (!waiting)
849
- return;
850
- const outcome = await waiting;
851
- if (loginRef.current !== start || signal.aborted)
852
- return;
915
+ return 'awaiting-input';
916
+ }, [cancelPendingLogin]);
917
+ const finishLoginFromPicker = useCallback(async (pasted, signal) => {
918
+ const pending = loginRef.current;
919
+ if (!pending) {
920
+ setPickerNotice('There is no Claude sign-in waiting for this code. Start it again.');
921
+ return 'finished';
922
+ }
923
+ if (!parsePastedInput(pasted).code) {
924
+ setPickerNotice('That does not contain an authorization code. Paste the whole finished address, or the code Claude showed.');
925
+ return 'retry';
926
+ }
927
+ const outcome = await pending.completeWithPastedCode(pasted);
928
+ if (loginRef.current !== pending || signal.aborted)
929
+ return 'finished';
853
930
  loginRef.current = null;
854
- cleanup();
931
+ loginAbortCleanupRef.current?.();
855
932
  loginAbortCleanupRef.current = null;
856
- start.cancel();
857
- if (!outcome.ok && outcome.reason.includes('cancelled'))
858
- return;
933
+ pending.cancel();
859
934
  setPickerNotice(describeLoginOutcome(outcome));
860
935
  if (outcome.ok) {
861
936
  setPickerInitialView('providers');
862
937
  await runProbeRef.current?.(signal);
863
938
  }
864
- }, [cancelPendingLogin]);
939
+ return 'finished';
940
+ }, []);
865
941
  const runProbe = useCallback(async (signal) => {
866
942
  try {
867
943
  if (signal?.aborted)
@@ -2068,6 +2144,10 @@ export function App({ ctx: initialCtx }) {
2068
2144
  setPhase('picker');
2069
2145
  return;
2070
2146
  case 'permission-mode': {
2147
+ applyPermissionMode(slash.mode);
2148
+ return;
2149
+ }
2150
+ case 'permission-mode-picker': {
2071
2151
  if (!session?.hasProvider) {
2072
2152
  pushMessage('system', 'No active session — pick a provider before changing permissions.');
2073
2153
  return;
@@ -2077,24 +2157,34 @@ export function App({ ctx: initialCtx }) {
2077
2157
  hasUnsettledTurn() ||
2078
2158
  queuedRef.current.length > 0 ||
2079
2159
  permissionResolveRef.current !== null ||
2080
- compactingRef.current) {
2081
- pushMessage('system', 'Permission mode was not changed: wait for the active turn, prompt, compaction, and queued work to settle.');
2082
- return;
2083
- }
2084
- if (!session.resetApprovalLatch) {
2085
- pushMessage('system', 'Permission mode was not changed: this embedded session cannot revoke an earlier "approve all" choice. Reconnect it before changing modes.');
2160
+ compactingRef.current ||
2161
+ !session.resetApprovalLatch) {
2162
+ pushMessage('system', 'Permission choices are unavailable until the active turn, prompt, compaction, queued work, or embedded-session approval latch can be safely settled.');
2086
2163
  return;
2087
2164
  }
2088
- session.resetApprovalLatch();
2089
- permissionModeRef.current = slash.mode;
2090
- permissionModeSourceRef.current = 'session';
2091
- setPermissionModeState(slash.mode);
2092
- pushMessage('system', `Permission mode changed to ${slash.mode}. Any earlier "approve all" choice was revoked. Declarative deny rules and the built-in safety gate still take precedence.`);
2165
+ const values = ['prompt', 'auto', 'strict'];
2166
+ setSelectedChoice(Math.max(0, values.indexOf(permissionModeRef.current)));
2167
+ setChoicePicker({
2168
+ kind: 'permission-mode',
2169
+ title: 'Select Permission Mode',
2170
+ notice: permissionPickerNotice(session),
2171
+ values,
2172
+ options: values.map((mode) => ({
2173
+ label: mode,
2174
+ description: permissionModeDescription(mode),
2175
+ current: mode === permissionModeRef.current,
2176
+ })),
2177
+ });
2093
2178
  return;
2094
2179
  }
2095
2180
  case 'reasoning-effort': {
2096
- if (!session?.hasProvider) {
2097
- pushMessage('system', 'No active session — pick a provider before changing reasoning effort.');
2181
+ applyReasoningEffort(slash.effort ?? undefined);
2182
+ return;
2183
+ }
2184
+ case 'reasoning-effort-picker': {
2185
+ const levels = session?.reasoningEffortLevels;
2186
+ if (!session?.hasProvider || levels === undefined) {
2187
+ pushMessage('system', 'No exact reasoning-effort menu is available for the current session.');
2098
2188
  return;
2099
2189
  }
2100
2190
  if (state !== 'idle' ||
@@ -2103,20 +2193,22 @@ export function App({ ctx: initialCtx }) {
2103
2193
  queuedRef.current.length > 0 ||
2104
2194
  permissionResolveRef.current !== null ||
2105
2195
  compactingRef.current) {
2106
- pushMessage('system', 'Reasoning effort was not changed: wait for the active turn, prompt, compaction, and queued work to settle.');
2107
- return;
2108
- }
2109
- if (slash.effort === null) {
2110
- setReasoningEffort(undefined);
2111
- pushMessage('system', 'Reasoning effort reset to the provider default for future main-query turns.');
2196
+ pushMessage('system', 'Reasoning-effort choices are unavailable until the active turn, prompt, compaction, and queued work settle.');
2112
2197
  return;
2113
2198
  }
2114
- if (!session.reasoningEffortLevels?.includes(slash.effort)) {
2115
- pushMessage('system', `Reasoning effort was not changed: ${slash.effort} is not offered by every usable provider-chain member for this model.`);
2116
- return;
2117
- }
2118
- setReasoningEffort(slash.effort);
2119
- pushMessage('system', `Reasoning effort changed to ${slash.effort} for future main-query turns in this session.`);
2199
+ const values = [undefined, ...levels];
2200
+ const current = reasoningEffortRef.current;
2201
+ setSelectedChoice(Math.max(0, values.findIndex((value) => value === current)));
2202
+ setChoicePicker({
2203
+ kind: 'reasoning-effort',
2204
+ title: `Select Reasoning Level for ${session.modelSummary ?? 'current model'}`,
2205
+ values,
2206
+ options: values.map((effort) => ({
2207
+ label: effort ?? 'default',
2208
+ description: reasoningEffortDescription(effort),
2209
+ current: effort === current,
2210
+ })),
2211
+ });
2120
2212
  return;
2121
2213
  }
2122
2214
  case 'login':
@@ -2587,6 +2679,8 @@ export function App({ ctx: initialCtx }) {
2587
2679
  }, [
2588
2680
  activeSkills,
2589
2681
  advanceQueueContinuation,
2682
+ applyPermissionMode,
2683
+ applyReasoningEffort,
2590
2684
  ctx.cwd,
2591
2685
  doResume,
2592
2686
  enqueueQueued,
@@ -2596,12 +2690,45 @@ export function App({ ctx: initialCtx }) {
2596
2690
  pushMessage,
2597
2691
  rawOutput,
2598
2692
  resetTranscript,
2693
+ setChoicePicker,
2599
2694
  setCopyPicker,
2600
2695
  setReasoningEffort,
2601
2696
  slashCtx,
2602
2697
  startFreshConversation,
2603
2698
  state,
2604
2699
  ]);
2700
+ // Keep the footer on the same durable goal record the driver mutates. A ref
2701
+ // alone cannot repaint React, so every goal mutation bumps goalDriveVersion;
2702
+ // generation/session checks stop a late disk read labelling a resumed chat.
2703
+ useEffect(() => {
2704
+ void goalDriveVersion;
2705
+ const sessions = sessionsRef.current;
2706
+ const scope = scopeRef.current;
2707
+ if (!sessions?.goals || !scope) {
2708
+ setGoalStatus(null);
2709
+ return;
2710
+ }
2711
+ const generation = conversationGenRef.current;
2712
+ const sessionId = scope.sessionId;
2713
+ let disposed = false;
2714
+ void sessions.goals
2715
+ .getGoal(sessionId, sessions.tenantId)
2716
+ .then((goal) => {
2717
+ if (disposed ||
2718
+ conversationGenRef.current !== generation ||
2719
+ scopeRef.current?.sessionId !== sessionId) {
2720
+ return;
2721
+ }
2722
+ setGoalStatus(goal);
2723
+ })
2724
+ .catch(() => {
2725
+ if (!disposed && conversationGenRef.current === generation)
2726
+ setGoalStatus(null);
2727
+ });
2728
+ return () => {
2729
+ disposed = true;
2730
+ };
2731
+ }, [goalDriveVersion, phase, session]);
2605
2732
  // Admit automatic work only at a whole-App durable boundary. This effect
2606
2733
  // reserves; it never starts a turn itself. The one queue pump below remains
2607
2734
  // the sole starter, which is what keeps a human prompt that arrives during
@@ -2622,6 +2749,7 @@ export function App({ ctx: initialCtx }) {
2622
2749
  conversationMutationRef.current ||
2623
2750
  exportingRef.current ||
2624
2751
  compactingRef.current ||
2752
+ choicePickerRef.current ||
2625
2753
  copyPickerRef.current ||
2626
2754
  goalCommandInFlightRef.current) {
2627
2755
  return;
@@ -2647,6 +2775,7 @@ export function App({ ctx: initialCtx }) {
2647
2775
  conversationMutationRef.current ||
2648
2776
  exportingRef.current ||
2649
2777
  compactingRef.current ||
2778
+ choicePickerRef.current ||
2650
2779
  copyPickerRef.current ||
2651
2780
  goalCommandInFlightRef.current ||
2652
2781
  queuedRef.current.length > 0 ||
@@ -2705,6 +2834,7 @@ export function App({ ctx: initialCtx }) {
2705
2834
  conversationMutation,
2706
2835
  session,
2707
2836
  state,
2837
+ choicePicker,
2708
2838
  copyPicker,
2709
2839
  wakeGoalDriver,
2710
2840
  ]);
@@ -2720,6 +2850,7 @@ export function App({ ctx: initialCtx }) {
2720
2850
  conversationMutationRef.current ||
2721
2851
  exportingRef.current ||
2722
2852
  compactingRef.current ||
2853
+ choicePickerRef.current ||
2723
2854
  copyPickerRef.current ||
2724
2855
  goalCommandInFlightRef.current)
2725
2856
  return;
@@ -2735,6 +2866,7 @@ export function App({ ctx: initialCtx }) {
2735
2866
  phase,
2736
2867
  queued,
2737
2868
  queuePause,
2869
+ choicePicker,
2738
2870
  copyPicker,
2739
2871
  dequeueQueued,
2740
2872
  hasUnsettledTurn,
@@ -2978,6 +3110,34 @@ export function App({ ctx: initialCtx }) {
2978
3110
  resolvePermission({ kind: 'approve-all' });
2979
3111
  return;
2980
3112
  }
3113
+ // Finite slash-command choices own the keyboard below a live permission
3114
+ // request and above the ordinary composer. Their values are a captured
3115
+ // menu, so a provider/session change cannot silently reinterpret Enter.
3116
+ if (choicePickerRef.current) {
3117
+ const options = choicePickerRef.current.options;
3118
+ if (key.escape || (key.ctrl && input === 'c')) {
3119
+ setChoicePicker(null);
3120
+ return;
3121
+ }
3122
+ if (key.upArrow) {
3123
+ setSelectedChoice((index) => Math.max(0, index - 1));
3124
+ return;
3125
+ }
3126
+ if (key.downArrow) {
3127
+ setSelectedChoice((index) => Math.min(options.length - 1, index + 1));
3128
+ return;
3129
+ }
3130
+ if (key.return) {
3131
+ applyChoiceSelection(selectedChoice);
3132
+ return;
3133
+ }
3134
+ if (/^[1-9]$/.test(input)) {
3135
+ const index = Number(input) - 1;
3136
+ if (options[index])
3137
+ applyChoiceSelection(index);
3138
+ }
3139
+ return;
3140
+ }
2981
3141
  // `/copy` holds a source snapshot, not a lifecycle phase. Permission
2982
3142
  // stays above it in this handler so an approval request that arrives
2983
3143
  // while the picker is open cannot have its keys stolen by the chooser.
@@ -3082,32 +3242,46 @@ export function App({ ctx: initialCtx }) {
3082
3242
  // Background is left natural — we inherit the terminal's own background
3083
3243
  // and only theme the foreground. Forcing
3084
3244
  // a filled bg left mismatched patches around bordered areas, so we don't.
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
3089
- ? `paused after a ${queuePause.outcome} turn; send a message or change model to continue`
3090
- : 'sending when ready'] }) })) : null, _jsx(Composer, { disabled: phase !== 'ready' ||
3091
- state === 'awaiting-permission' ||
3092
- compacting ||
3093
- conversationMutation !== null, hidden: permission !== null || copyPicker !== null,
3094
- // A turn is running, so Esc is the interrupt and not
3095
- // the composer's clear.
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
3097
- ? `${session.modelSummary}${reasoningEffort ? ` · effort ${reasoningEffort}` : ''}`
3098
- : null, state: state, hint: conversationMutation === 'fork'
3099
- ? 'forking conversation — input is paused'
3100
- : conversationMutation === 'edit'
3101
- ? 'branching before prompt — input is paused'
3102
- : conversationMutation === 'new'
3103
- ? 'starting a fresh conversation — input is paused'
3104
- : compacting
3105
- ? 'compacting conversation — input is paused'
3106
- : permission
3107
- ? hintForPhase(phase, state, session?.hasProvider === true)
3245
+ return (_jsx(Box, { flexDirection: "column", children: _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: startLoginFromPicker, onLoginComplete: finishLoginFromPicker, 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' &&
3246
+ phase === 'ready' &&
3247
+ conversationMutation === null &&
3248
+ choicePicker === null &&
3249
+ copyPicker === null, hidden: permission !== null || choicePicker !== null || copyPicker !== null, children: [queued.length > 0 &&
3250
+ permission === null &&
3251
+ choicePicker === null &&
3252
+ 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
3253
+ ? `paused after a ${queuePause.outcome} turn; send a message or change model to continue`
3254
+ : 'sending when ready'] }) })) : null, _jsx(Composer, { disabled: phase !== 'ready' ||
3255
+ state === 'awaiting-permission' ||
3256
+ compacting ||
3257
+ conversationMutation !== null, hidden: permission !== null || choicePicker !== null || copyPicker !== null,
3258
+ // A turn is running, so Esc is the interrupt and not
3259
+ // the composer's clear.
3260
+ 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' &&
3261
+ state === 'idle' &&
3262
+ conversationMutation === null &&
3263
+ !compacting &&
3264
+ permission === null &&
3265
+ choicePicker === null &&
3266
+ copyPicker === null
3267
+ ? goalStatusLabel(goalStatus)
3268
+ : null, state: state, hint: conversationMutation === 'fork'
3269
+ ? 'forking conversation — input is paused'
3270
+ : conversationMutation === 'edit'
3271
+ ? 'branching before prompt — input is paused'
3272
+ : conversationMutation === 'new'
3273
+ ? 'starting a fresh conversation — input is paused'
3274
+ : compacting
3275
+ ? 'compacting conversation — input is paused'
3276
+ : permission
3277
+ ? hintForPhase(phase, state, session?.hasProvider === true)
3278
+ : choicePicker
3279
+ ? 'choice open — ↑↓ / 1–9 select · enter apply · esc cancel'
3108
3280
  : copyPicker
3109
3281
  ? 'copy target open — ↑↓ / 1–9 select · esc cancel'
3110
- : hintForPhase(phase, state, session?.hasProvider === true), usage: usage, context: context }) })] })] }));
3282
+ : goalStatus && phase === 'ready' && state === 'idle'
3283
+ ? undefined
3284
+ : hintForPhase(phase, state, session?.hasProvider === true), usage: usage, context: context }) })] }) }));
3111
3285
  }
3112
3286
  function Banner({ version, session, permissionMode, cwd, }) {
3113
3287
  const cols = process.stdout.columns ?? 80;
@@ -3163,7 +3337,66 @@ canReturn = false) {
3163
3337
  return 'y approve · n / esc reject · a approve all';
3164
3338
  if (state !== 'idle')
3165
3339
  return 'agent is working — esc to interrupt';
3166
- return '/help · Esc×2 edit previous · @file / Ctrl+V attach · Ctrl+C ×2 to exit';
3340
+ // The composer already points to /help. Keeping the complete key legend in
3341
+ // every idle frame made ordinary conversation read like a permanent setup
3342
+ // screen; only state-specific actions belong in the footer.
3343
+ return '';
3344
+ }
3345
+ function goalStatusLabel(goal) {
3346
+ if (!goal)
3347
+ return null;
3348
+ switch (goal.phase) {
3349
+ case 'active':
3350
+ return 'Pursuing goal';
3351
+ case 'paused':
3352
+ return 'Goal paused (/goal resume)';
3353
+ case 'blocked':
3354
+ return 'Goal stalled (/goal resume)';
3355
+ case 'complete':
3356
+ return 'Goal achieved';
3357
+ }
3358
+ }
3359
+ function permissionModeDescription(mode) {
3360
+ switch (mode) {
3361
+ case 'prompt':
3362
+ return 'Ask before an undecided tool call runs';
3363
+ case 'auto':
3364
+ return 'Approve undecided calls unless a rule or safety gate refuses';
3365
+ case 'strict':
3366
+ return 'Refuse undecided calls; explicit allow rules still work';
3367
+ }
3368
+ }
3369
+ function permissionPickerNotice(session) {
3370
+ const notices = [];
3371
+ if (session.approvalLatched()) {
3372
+ notices.push('Approve all is active; applying any mode revokes it.');
3373
+ }
3374
+ const exempt = session.promptExemptTools();
3375
+ if (exempt.length > 0)
3376
+ notices.push(`Never prompted: ${exempt.join(', ')}.`);
3377
+ return notices.length > 0 ? notices.join(' ') : undefined;
3378
+ }
3379
+ function reasoningEffortDescription(effort) {
3380
+ switch (effort) {
3381
+ case undefined:
3382
+ return 'Use the provider and model default';
3383
+ case 'none':
3384
+ return 'No deliberate reasoning where the model supports it';
3385
+ case 'minimal':
3386
+ return 'Fastest responses with minimal reasoning';
3387
+ case 'low':
3388
+ return 'Fast responses with light reasoning';
3389
+ case 'medium':
3390
+ return 'Balance speed and reasoning depth';
3391
+ case 'high':
3392
+ return 'Deeper reasoning for complex problems';
3393
+ case 'xhigh':
3394
+ return 'Very deep reasoning for hard problems';
3395
+ case 'max':
3396
+ return 'Maximum reasoning depth offered by this model';
3397
+ case 'ultra':
3398
+ return 'Highest host-level reasoning setting when supported';
3399
+ }
3167
3400
  }
3168
3401
  /**
3169
3402
  * The summary text, as collapsible detail under the compaction row.