@aragon-agent/cli 0.6.1 → 0.6.2

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/dist/ui/App.js CHANGED
@@ -14,11 +14,11 @@ import { computeCost } from '../agent/usage.js';
14
14
  import { parseMaxTokensInput, DEFAULT_TRANSCRIPT_RETAIN, DEFAULT_TRANSCRIPT_WINDOW, } from '../config/schema.js';
15
15
  import { updatePersistedConfig } from '../config/store.js';
16
16
  import { appendPrompt, loadPromptHistory } from '../config/prompt-history.js';
17
- import { bumpSubmitCount, setMouseNoticeSeen, setVtInputNoticeSeen } from '../config/ui-state.js';
17
+ import { bumpSubmitCount, setMouseNoticeSeen, setVtInputNoticeVersion, } from '../config/ui-state.js';
18
18
  import { getLogger } from '../logging/logger.js';
19
19
  import { registerSecret } from '../logging/secret-registry.js';
20
20
  import { detectCapabilities } from './capabilities.js';
21
- import { useStartupNotices } from './use-startup-notices.js';
21
+ import { useStartupNotices, VT_INPUT_NOTICE_VERSION } from './use-startup-notices.js';
22
22
  import { getTheme } from './theme.js';
23
23
  import { pickGlyphs } from './glyphs.js';
24
24
  import { Header } from './Header.js';
@@ -67,7 +67,7 @@ import { PlanReviewOverlay } from './overlays/PlanReviewOverlay.js';
67
67
  import { CommandRegistry, runSlashInput } from '../commands/registry.js';
68
68
  import { registerBuiltinCommands } from '../commands/builtins.js';
69
69
  import { makeSkillsCommand, registerSkillCommands } from '../commands/skills.js';
70
- import { MODE_LABEL, nextMode } from '../agent/agent-mode.js';
70
+ import { MODE_LABEL, MODE_TOGGLE_KEYS, nextMode } from '../agent/agent-mode.js';
71
71
  /**
72
72
  * Decide what the `Max tokens` field means, WITHOUT letting a bad value in it
73
73
  * discard the rest of the form.
@@ -707,7 +707,10 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
707
707
  ? { requested: cfg.transcriptRetainRequested, resolved: cfg.transcriptRetain }
708
708
  : undefined,
709
709
  // Its own one-shot key, NOT `mouseNoticeSeen`: everyone this notice is for
710
- // has already run aragon at least once and already has that flag set.
710
+ // has already run aragon at least once and already has that flag set. That
711
+ // key is now a VERSION for the same reason, one turn later — the audience
712
+ // for the text that finally names the fallback key is, again, exactly the
713
+ // set of users who already have the old flag on disk.
711
714
  //
712
715
  // `fullscreen` goes with it because only half of the damage is mode-
713
716
  // independent: `Shift+Tab` is broken either way, but inline never asked for
@@ -717,7 +720,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
717
720
  ? {
718
721
  nodeVersion: vtInputWarning.nodeVersion,
719
722
  fullscreen,
720
- onSeen: () => setVtInputNoticeSeen(true),
723
+ onSeen: () => setVtInputNoticeVersion(VT_INPUT_NOTICE_VERSION),
721
724
  }
722
725
  : undefined);
723
726
  // --- Exit snapshot: the only way state reaches cli.tsx (§4.4). ----------
@@ -1196,7 +1199,13 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
1196
1199
  // AFTER the overlay block, and with an EXPLICIT guard rather than relying on
1197
1200
  // ordering: that block only `return`s for the keys it recognizes, and
1198
1201
  // everything else falls through (AC-P2).
1199
- if (key.tab && key.shift) {
1202
+ //
1203
+ // TWO KEYS, ONE BRANCH (shift-tab-mode-toggle-still-dead-on-windows, C2).
1204
+ // `Ctrl+P` is not a Windows-only alias bolted on beside the real binding: it
1205
+ // is the rung that survives when `Shift+Tab` cannot be delivered at all, and
1206
+ // it shares this branch precisely so the two can never drift into different
1207
+ // behaviour. See `MODE_TOGGLE_KEYS` for why this key and not another.
1208
+ if ((key.tab && key.shift) || (key.ctrl && (input === 'p' || input === 'P'))) {
1200
1209
  if (stateRef.current.overlay)
1201
1210
  return;
1202
1211
  applyMode(nextMode(controller.getAgentMode()));
@@ -1496,7 +1505,17 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
1496
1505
  // rows; unbounded, it pushes the status bar past the frame height where
1497
1506
  // `overflow: hidden` clips it away (R-14 / M-14). Cap it against the viewport.
1498
1507
  const popupMaxRows = fullscreen ? Math.max(1, viewportBudget - 4) : undefined;
1499
- const composer = fullscreen ? (_jsx(Composer, { isActive: overlay === null, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), showHint: rows >= HINT_MIN_ROWS, submitCount: cfg.submitCount, hintsEnabled: cfg.hints, agentMode: state.agentMode, popupMaxRows: popupMaxRows, theme: theme, caps: caps, onSubmit: (text) => void handleSubmit(text), onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) })) : (_jsx(Box, { marginTop: 1, children: _jsx(PromptInput, { isActive: overlay === null, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), agentMode: state.agentMode, theme: theme, caps: caps, onSubmit: (text) => void handleSubmit(text), onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) }) }));
1508
+ // WHICH KEY THE HINT ROW NAMES (shift-tab-mode-toggle-still-dead-on-windows,
1509
+ // C3-1). Derived from `vtInputWarning` rather than from a probe of its own,
1510
+ // because that prop is already the single answer to "can this console deliver
1511
+ // `CSI Z`?" — `cli.tsx` computes it AFTER trying to force the bit on, so a
1512
+ // machine we just repaired is told to press `shift+tab` and a machine we could
1513
+ // not repair is told to press the key that provably survives.
1514
+ //
1515
+ // Both keys always WORK (they share one branch in the input handler); this
1516
+ // only decides which one the user is taught.
1517
+ const modeToggleKey = vtInputWarning ? MODE_TOGGLE_KEYS.fallback : MODE_TOGGLE_KEYS.primary;
1518
+ const composer = fullscreen ? (_jsx(Composer, { isActive: overlay === null, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), showHint: rows >= HINT_MIN_ROWS, submitCount: cfg.submitCount, hintsEnabled: cfg.hints, agentMode: state.agentMode, modeToggleKey: modeToggleKey, popupMaxRows: popupMaxRows, theme: theme, caps: caps, onSubmit: (text) => void handleSubmit(text), onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) })) : (_jsx(Box, { marginTop: 1, children: _jsx(PromptInput, { isActive: overlay === null, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), agentMode: state.agentMode, theme: theme, caps: caps, onSubmit: (text) => void handleSubmit(text), onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) }) }));
1500
1519
  return (_jsx(AppShell, { mode: mode, rows: rows, cols: cols, header: header, viewport: viewport, team: state.team ? (_jsx(TeamPanel, { snapshot: state.team, rows: rows, cols: cols, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null, rail: showRail && state.todos ? (_jsx(TodoPanel, { snapshot: state.todos, width: railWidth, rows: railRows, running: running, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null, strip: showStrip && state.todos ? (_jsx(TodoStrip, { snapshot: state.todos, cols: cols, theme: theme, caps: caps })) : null, toast: _jsx(BottomStatusRow, { mode: mode, toasts: state.toasts,
1501
1520
  // `!overlayNode` suppresses the line while an overlay owns the screen:
1502
1521
  // the full-screen branch replaces the whole viewport with the overlay,
@@ -34,10 +34,23 @@ export interface ComposerProps {
34
34
  hintsEnabled: boolean;
35
35
  /** Mirror of the controller's effective mode; drives the chip and the border. */
36
36
  agentMode: AgentMode;
37
+ /**
38
+ * Which key this hint should NAME for the mode toggle
39
+ * (shift-tab-mode-toggle-still-dead-on-windows, C3-1).
40
+ *
41
+ * A PROP RATHER THAN A CONSTANT because on some consoles the constant is a
42
+ * lie. This row used to render `shift+tab` unconditionally, with no link to
43
+ * the capability check three files away, so the machines where that key is
44
+ * swallowed were the ones being told to press it - permanently, on every
45
+ * frame, while the one warning they got scrolled out of the transcript
46
+ * minutes earlier (analysis section 3.7). Defaults to the primary key, which
47
+ * is what every unaffected machine renders.
48
+ */
49
+ modeToggleKey?: string;
37
50
  popupMaxRows?: number;
38
51
  theme: Theme;
39
52
  caps: TermCapabilities;
40
53
  onSubmit: (text: string) => void;
41
54
  onHelp?: () => void;
42
55
  }
43
- export declare function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, popupMaxRows, theme, caps, onSubmit, onHelp, }: ComposerProps): React.ReactElement;
56
+ export declare function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, modeToggleKey, popupMaxRows, theme, caps, onSubmit, onHelp, }: ComposerProps): React.ReactElement;
@@ -21,7 +21,7 @@ import { pickGlyphs } from './glyphs.js';
21
21
  import { HINT_FADE_AFTER } from '../config/schema.js';
22
22
  import { PromptInput } from './PromptInput.js';
23
23
  import { ModeChip } from './ModeChip.js';
24
- import { MODE_LABEL, nextMode } from '../agent/agent-mode.js';
24
+ import { MODE_LABEL, MODE_TOGGLE_KEYS, nextMode } from '../agent/agent-mode.js';
25
25
  /**
26
26
  * Build the hint row.
27
27
  *
@@ -32,7 +32,7 @@ import { MODE_LABEL, nextMode } from '../agent/agent-mode.js';
32
32
  * Progressive disclosure is for teaching discoverable things, not for retiring
33
33
  * an emergency exit (§4.6 / A-14 / M-10b).
34
34
  */
35
- function hintText(running, submitCount, glyphs, mode) {
35
+ function hintText(running, submitCount, glyphs, mode, toggleKey) {
36
36
  const dot = ` ${glyphs.midDot} `;
37
37
  if (running) {
38
38
  return [`${glyphs.enterKey} steer`, 'esc abort', `ctrl+c${glyphs.times}2 exit`].join(dot);
@@ -40,7 +40,7 @@ function hintText(running, submitCount, glyphs, mode) {
40
40
  // The hint names the DESTINATION, not the current state — the correct label
41
41
  // for a toggle affordance, and the reason the mode word here is the OPPOSITE
42
42
  // of the one on the chip beside it.
43
- const toggle = `shift+tab ${MODE_LABEL[nextMode(mode)].toLowerCase()}`;
43
+ const toggle = `${toggleKey} ${MODE_LABEL[nextMode(mode)].toLowerCase()}`;
44
44
  // The faded form KEEPS the mode toggle. Progressive disclosure exists to
45
45
  // retire things the user has demonstrably learned, and `submitCount` counts
46
46
  // submissions — a user can send fifty messages without ever discovering
@@ -56,7 +56,7 @@ function hintText(running, submitCount, glyphs, mode) {
56
56
  '? help',
57
57
  ].join(dot);
58
58
  }
59
- export function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, popupMaxRows, theme, caps, onSubmit, onHelp, }) {
59
+ export function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, modeToggleKey = MODE_TOGGLE_KEYS.primary, popupMaxRows, theme, caps, onSubmit, onHelp, }) {
60
60
  const [hasDraft, setHasDraft] = useState(false);
61
61
  const onDraftChange = useCallback((next) => setHasDraft(next), []);
62
62
  // Plan mode inserts ONE branch, above `draft` and below `running`. The
@@ -76,5 +76,5 @@ export function Composer({ isActive, running, history, commands, cwd, showHint,
76
76
  return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(PromptInput, { isActive: isActive, running: running, history: history, commands: commands, cwd: cwd, theme: theme, caps: caps, popupMaxRows: popupMaxRows, onSubmit: onSubmit, onHelp: onHelp, agentMode: agentMode, bordered: { color: borderColor }, onDraftChange: onDraftChange }), visible && (
77
77
  // The chip rides the EXISTING hint row rather than adding one, so it
78
78
  // costs zero rows on a 24-row terminal.
79
- _jsxs(Box, { flexDirection: "row", flexShrink: 0, children: [_jsx(Text, { children: ' ' }), _jsx(ModeChip, { mode: agentMode, theme: theme, caps: caps }), _jsx(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: hintText(running, submitCount, glyphs, agentMode) })] }))] }));
79
+ _jsxs(Box, { flexDirection: "row", flexShrink: 0, children: [_jsx(Text, { children: ' ' }), _jsx(ModeChip, { mode: agentMode, theme: theme, caps: caps }), _jsx(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: hintText(running, submitCount, glyphs, agentMode, modeToggleKey) })] }))] }));
80
80
  }
@@ -7,7 +7,13 @@ function keyRows(times) {
7
7
  ['Enter', 'Submit (idle) / queue steering (running); re-pins to the newest output'],
8
8
  ['Alt+Enter / Shift+Enter', 'Insert newline'],
9
9
  ['Esc', 'Abort run / close overlay'],
10
- ['Shift+Tab', 'Toggle plan mode (same as /plan)'],
10
+ // BOTH KEYS ARE LISTED HERE, and this row is why (shift-tab-mode-toggle-
11
+ // still-dead-on-windows, review R-7). The hint row above the composer is the
12
+ // other place the fallback is named, and it has two off-switches - a short
13
+ // terminal (`showHint`) and `hints: false`. A key that only exists behind
14
+ // either of them is a key the affected user never learns about, which is the
15
+ // one-shot notice's failure repeated with a different switch.
16
+ ['Shift+Tab / Ctrl+P', 'Toggle plan mode (same as /plan)'],
11
17
  [`Ctrl+C ${times}2`, 'Exit'],
12
18
  ['Ctrl+L', 'Redraw the frame'],
13
19
  ['Ctrl+T', 'Show/hide thinking (off by default)'],
@@ -54,6 +60,11 @@ const SKILL_ROWS = [
54
60
  ];
55
61
  const PLAN_ROWS = [
56
62
  ['Shift+Tab / /plan', 'Switch between BUILD (do it now) and PLAN (research first)'],
63
+ // A ROW OF ITS OWN, not a third name on the line above: the reader who needs
64
+ // this one is the reader whose `Shift+Tab` does nothing, and what they need is
65
+ // the reason, not another synonym. Listing the key in both places instead just
66
+ // says it twice and explains it nowhere.
67
+ ['Ctrl+P', 'The same toggle, for Windows consoles that deliver Shift+Tab as a plain Tab'],
57
68
  ['In PLAN mode', 'write_file, edit_file, bash, skill_install and skill_create are refused'],
58
69
  ['bash', 'Refused in full, including git status - use read_file / glob / grep'],
59
70
  ['ask_user', 'The agent asks 1-5 multiple-choice questions; Enter takes the recommendation'],
@@ -12,11 +12,27 @@
12
12
  * not otherwise see — rather than belonging to the view.
13
13
  */
14
14
  import type { ViewAction } from '../agent/reducer.js';
15
+ /**
16
+ * The revision of the text below that a user must have seen for this notice to
17
+ * stay quiet (`state.json::vtInputNoticeVersion`, `0` when absent).
18
+ *
19
+ * BUMP IT WHENEVER THE ADVICE CHANGES, and never for a rewording that leaves
20
+ * the recommended actions alone. The two failure modes sit on either side of
21
+ * this constant: leaving it alone after changing what the user is told to press
22
+ * makes the change inert for everyone who already ran the old build — which is
23
+ * the entire affected population, since the notice only appears on machines
24
+ * where the key has been broken all along — while dropping the gate altogether
25
+ * turns a one-shot into a banner on every launch.
26
+ *
27
+ * `1` = the first text naming `MODE_TOGGLE_KEYS.fallback`. `0` = the 0.6.1 text
28
+ * that offered only `/plan`, recorded back when this was a boolean.
29
+ */
30
+ export declare const VT_INPUT_NOTICE_VERSION = 1;
15
31
  /**
16
32
  * The `shift-tab-and-mouse-wheel-dead-on-some-terminals` notice (F1'). Shown at
17
- * most once, ever, and only on a Windows console that cannot deliver either
18
- * input — see `ui/win-vt-input.ts` for why that is a property of the Node
19
- * version rather than of the terminal.
33
+ * most once per `VT_INPUT_NOTICE_VERSION`, and only on a Windows console that
34
+ * cannot deliver either input — see `ui/win-vt-input.ts` for why that is a
35
+ * property of the Node version rather than of the terminal.
20
36
  *
21
37
  * IT NAMES WHAT THE KEY DOES NOW, not merely that it is unavailable, and that
22
38
  * is the load-bearing half of the text. `Shift+Tab` on these machines is not
@@ -25,6 +41,16 @@ import type { ViewAction } from '../agent/reducer.js';
25
41
  * draft. A user told only "this key does not work" keeps pressing it and keeps
26
42
  * losing text without ever connecting the two.
27
43
  *
44
+ * THE FALLBACK KEY COMES BEFORE `/plan` IN BOTH BRANCHES, and it is named from
45
+ * `MODE_TOGGLE_KEYS` rather than spelled out, so the four consumers of that
46
+ * constant cannot drift apart. This notice is the last one of them to be wired
47
+ * up, and it is also the only channel that reaches the affected user without
48
+ * being asked to: the composer hint does not exist at all in inline mode and is
49
+ * switched off by a short terminal or `hints: false` in full-screen, and
50
+ * `/help` has to be typed by someone whose current belief is "the key does
51
+ * nothing" rather than "I should read the help". Ordering follows cost — one
52
+ * keystroke before four and a return.
53
+ *
28
54
  * `Shift+Up` / `Shift+Down` is the scroll workaround named here because it is
29
55
  * the only one PROVEN to survive: both Node versions emit `\x1b[1;2A` for it,
30
56
  * byte for byte, in the probe data. `PgUp` / `PgDn` is expected to work too but
@@ -90,7 +116,12 @@ export interface VtInputNoticeOptions {
90
116
  * the renderer disagreeing about which mode is running.
91
117
  */
92
118
  fullscreen: boolean;
93
- /** Record that it was shown (`state.json`). Called once, after dispatch. */
119
+ /**
120
+ * Record that `VT_INPUT_NOTICE_VERSION` was shown (`state.json`). Called once,
121
+ * after dispatch — and it must write that constant, not `true`: the gate
122
+ * below compares against it, so a caller storing anything else either replays
123
+ * the notice forever or silences a future revision of the text.
124
+ */
94
125
  onSeen: () => void;
95
126
  }
96
127
  export declare function useStartupNotices(dispatch: (action: ViewAction) => void, mouseNotice?: MouseNoticeOptions, retainNotice?: RetainNoticeOptions, vtInputNotice?: VtInputNoticeOptions): void;
@@ -12,8 +12,9 @@
12
12
  * not otherwise see — rather than belonging to the view.
13
13
  */
14
14
  import { useEffect, useRef } from 'react';
15
+ import { MODE_TOGGLE_KEYS } from '../agent/agent-mode.js';
15
16
  import { readConfigFile } from '../config/store.js';
16
- import { getMouseNoticeSeen, getVtInputNoticeSeen } from '../config/ui-state.js';
17
+ import { getMouseNoticeSeen, getVtInputNoticeVersion } from '../config/ui-state.js';
17
18
  import { getLogger } from '../logging/logger.js';
18
19
  /**
19
20
  * The §6.1 notice (mouse-wheel-region-routing). Shown at most once, ever.
@@ -39,11 +40,27 @@ function retainRaisedNotice(requested, resolved) {
39
40
  'so the whole scroll horizon stays reachable. Lower transcriptWindow too if you meant ' +
40
41
  'to cap memory.');
41
42
  }
43
+ /**
44
+ * The revision of the text below that a user must have seen for this notice to
45
+ * stay quiet (`state.json::vtInputNoticeVersion`, `0` when absent).
46
+ *
47
+ * BUMP IT WHENEVER THE ADVICE CHANGES, and never for a rewording that leaves
48
+ * the recommended actions alone. The two failure modes sit on either side of
49
+ * this constant: leaving it alone after changing what the user is told to press
50
+ * makes the change inert for everyone who already ran the old build — which is
51
+ * the entire affected population, since the notice only appears on machines
52
+ * where the key has been broken all along — while dropping the gate altogether
53
+ * turns a one-shot into a banner on every launch.
54
+ *
55
+ * `1` = the first text naming `MODE_TOGGLE_KEYS.fallback`. `0` = the 0.6.1 text
56
+ * that offered only `/plan`, recorded back when this was a boolean.
57
+ */
58
+ export const VT_INPUT_NOTICE_VERSION = 1;
42
59
  /**
43
60
  * The `shift-tab-and-mouse-wheel-dead-on-some-terminals` notice (F1'). Shown at
44
- * most once, ever, and only on a Windows console that cannot deliver either
45
- * input — see `ui/win-vt-input.ts` for why that is a property of the Node
46
- * version rather than of the terminal.
61
+ * most once per `VT_INPUT_NOTICE_VERSION`, and only on a Windows console that
62
+ * cannot deliver either input — see `ui/win-vt-input.ts` for why that is a
63
+ * property of the Node version rather than of the terminal.
47
64
  *
48
65
  * IT NAMES WHAT THE KEY DOES NOW, not merely that it is unavailable, and that
49
66
  * is the load-bearing half of the text. `Shift+Tab` on these machines is not
@@ -52,6 +69,16 @@ function retainRaisedNotice(requested, resolved) {
52
69
  * draft. A user told only "this key does not work" keeps pressing it and keeps
53
70
  * losing text without ever connecting the two.
54
71
  *
72
+ * THE FALLBACK KEY COMES BEFORE `/plan` IN BOTH BRANCHES, and it is named from
73
+ * `MODE_TOGGLE_KEYS` rather than spelled out, so the four consumers of that
74
+ * constant cannot drift apart. This notice is the last one of them to be wired
75
+ * up, and it is also the only channel that reaches the affected user without
76
+ * being asked to: the composer hint does not exist at all in inline mode and is
77
+ * switched off by a short terminal or `hints: false` in full-screen, and
78
+ * `/help` has to be typed by someone whose current belief is "the key does
79
+ * nothing" rather than "I should read the help". Ordering follows cost — one
80
+ * keystroke before four and a return.
81
+ *
55
82
  * `Shift+Up` / `Shift+Down` is the scroll workaround named here because it is
56
83
  * the only one PROVEN to survive: both Node versions emit `\x1b[1;2A` for it,
57
84
  * byte for byte, in the probe data. `PgUp` / `PgDn` is expected to work too but
@@ -79,12 +106,13 @@ export function vtInputDeadNotice(nodeVersion, fullscreen) {
79
106
  const cause = `Node ${nodeVersion} on Windows does not turn on the console's VT input mode, so `;
80
107
  const cost = 'it will not switch mode, and with the / palette or an @ popup open it completes that ' +
81
108
  'instead, overwriting your draft. ';
109
+ const toggle = `Press ${MODE_TOGGLE_KEYS.fallback} to switch mode instead (or /plan)`;
82
110
  if (!fullscreen) {
83
111
  return (`${cause}Shift+Tab never arrives: it comes through as a PLAIN TAB - ${cost}` +
84
- 'Use /plan to switch mode instead. Node 22.17.0+ (or 24.2.0+) restores it.');
112
+ `${toggle}. Node 22.17.0+ (or 24.2.0+) restores it.`);
85
113
  }
86
114
  return (`${cause}two keys never arrive: the wheel sends nothing at all, and Shift+Tab arrives ` +
87
- `as a PLAIN TAB - ${cost}Use /plan to switch mode and Shift+Up / Shift+Down to scroll. ` +
115
+ `as a PLAIN TAB - ${cost}${toggle}, and Shift+Up / Shift+Down to scroll. ` +
88
116
  'Node 22.17.0+ (or 24.2.0+) restores both.');
89
117
  }
90
118
  export function useStartupNotices(dispatch, mouseNotice, retainNotice, vtInputNotice) {
@@ -132,6 +160,10 @@ export function useStartupNotices(dispatch, mouseNotice, retainNotice, vtInputNo
132
160
  // out on the retain effect below — `App` builds that literal during render.
133
161
  // Its one-shot key is checked only AFTER the presence test, so a machine that
134
162
  // is not affected never touches `state.json` at all.
163
+ //
164
+ // `>=`, not `!==`: a state file written by a NEWER build (a downgrade, or a
165
+ // shared home directory) has already told this user everything the text below
166
+ // says, so re-showing it would be noise rather than news.
135
167
  const vtNoticeRef = useRef(vtInputNotice);
136
168
  vtNoticeRef.current = vtInputNotice;
137
169
  const vtNodeVersion = vtInputNotice?.nodeVersion;
@@ -140,7 +172,7 @@ export function useStartupNotices(dispatch, mouseNotice, retainNotice, vtInputNo
140
172
  useEffect(() => {
141
173
  if (vtNodeVersion === undefined || vtNoticeShown.current)
142
174
  return;
143
- if (getVtInputNoticeSeen())
175
+ if (getVtInputNoticeVersion() >= VT_INPUT_NOTICE_VERSION)
144
176
  return;
145
177
  vtNoticeShown.current = true;
146
178
  dispatch({
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Turn `ENABLE_VIRTUAL_TERMINAL_INPUT` on for THIS console, from outside Node
3
+ * (shift-tab-mode-toggle-still-dead-on-windows, C1-M1).
4
+ *
5
+ * ASCII ONLY - this tree is inside the glyph scanner's scope.
6
+ *
7
+ * `win-vt-input.ts` answers "will this console ever deliver `CSI Z`?". This
8
+ * module answers "can we MAKE it deliver one?", and the measured answer is yes.
9
+ * The bit is a property of the console input buffer, not of Node, so any process
10
+ * attached to the same console can set it - and libuv's pre-22.17 translator
11
+ * forwards whatever characters the console generates, `ESC` `[` `Z` included.
12
+ * On Node 20.19.0, in one run, one injected `Shift+Tab` went from `09` to
13
+ * `1b 5b 5a` the instant an outside process OR-ed `0x0200` in (analysis section
14
+ * 3.2; raw probe data under `docs/diagnoses/.../repro/`).
15
+ *
16
+ * Node ships no FFI, so the outside process is `powershell.exe` calling
17
+ * `SetConsoleMode` through P/Invoke. Measured cost end to end: 722 ms, roughly
18
+ * 280 ms of process start plus 300 ms of `Add-Type` compiling C# at run time
19
+ * (analysis section 3.4). That is the whole price of not adding a native
20
+ * dependency, and it is paid ONCE, only on an affected Windows console.
21
+ *
22
+ * THREE TRAPS ARE ENCODED HERE, all of them measured rather than imagined:
23
+ *
24
+ * 1. `windowsHide: true` maps to `CREATE_NO_WINDOW`, which hands the child its
25
+ * OWN console. The helper then reads, sets and reports success against a
26
+ * console nobody is typing into: exit code 0, `SetConsoleMode` true, log
27
+ * clean, and no effect whatsoever - indistinguishable from "the fix was
28
+ * never written" (analysis section 3.5). Hence `windowsHide: false`.
29
+ * 2. The helper must open `CONIN$` with `CreateFileW`, never
30
+ * `GetStdHandle(STD_INPUT_HANDLE)`: it is spawned with stdin `ignore`, so
31
+ * its standard input is `NUL`. Sharing the real stdin instead is not an
32
+ * option - that handle is being reconfigured underneath it.
33
+ * 3. The `before` mode is the ONLY signal that distinguishes trap 1 from
34
+ * success, so the helper reports it and `decideVtForceOutcome` checks it.
35
+ * The check is a POSITIVE bitmask ("is this console already in raw mode"),
36
+ * not `before !== 0x01F7`: that number is one machine's default and varies
37
+ * with QuickEdit, with the host, and with a user who already turned VT input
38
+ * on. A magic-number comparison passes silently on the first machine whose
39
+ * default happens to differ, which puts trap 1 straight back (review R-3).
40
+ *
41
+ * WHAT UNDOES IT: `setRawMode(false)`. libuv rewrites the whole console mode on
42
+ * every raw-mode transition and its pre-22.17 path does not know about this bit,
43
+ * so one transition clears it (analysis section 3.3). That is why the caller
44
+ * must raise raw mode BEFORE calling this, and it is also the entire restore
45
+ * path - the bit outlives the process otherwise, and neither Windows nor the
46
+ * console clears it (analysis section 3.5b).
47
+ *
48
+ * FAILURE IS ALWAYS GRACEFUL. Constrained Language Mode disables `Add-Type`
49
+ * outright, and enterprise fleets ship it by policy - exactly the "another PC"
50
+ * population this bug is about. Every failure returns `ok: false` with a reason
51
+ * for the log and nothing else happens; the alternate mode-toggle key
52
+ * (`MODE_TOGGLE_KEYS.fallback`) is the rung below that needs none of this.
53
+ */
54
+ /** Console input mode bits (`consoleapi.h`). */
55
+ export declare const ENABLE_PROCESSED_INPUT = 1;
56
+ export declare const ENABLE_LINE_INPUT = 2;
57
+ export declare const ENABLE_ECHO_INPUT = 4;
58
+ export declare const ENABLE_WINDOW_INPUT = 8;
59
+ export declare const ENABLE_VIRTUAL_TERMINAL_INPUT = 512;
60
+ /**
61
+ * The three bits a cooked console has and a raw one does not.
62
+ *
63
+ * This is the fingerprint of "libuv has already put this console in raw mode",
64
+ * and it is what tells the two measured samples apart: `0x0008` (the CLI's own
65
+ * console, raw) versus `0x01F7` (a fresh `CREATE_NO_WINDOW` console, cooked).
66
+ */
67
+ export declare const COOKED_INPUT_BITS: number;
68
+ export declare function isRawConsoleMode(mode: number): boolean;
69
+ export type VtForceFailure = 'not_windows' | 'spawn_failed' | 'helper_failed' | 'no_before' | 'wrong_console' | 'bit_not_set';
70
+ export interface VtForceResult {
71
+ ok: boolean;
72
+ /** Absent on success; a stable machine token on failure, for the log. */
73
+ reason?: VtForceFailure;
74
+ before?: number;
75
+ after?: number;
76
+ /** Free-form context for the log line. Never shown to the user. */
77
+ detail?: string;
78
+ }
79
+ /**
80
+ * The PowerShell side, as a string.
81
+ *
82
+ * A LITERAL HERE-STRING (`@'` ... `'@`) around the C# so PowerShell expands
83
+ * nothing inside it, and `'CONIN$'` in single quotes for the same reason - the
84
+ * `$` is part of the device name, not a variable.
85
+ *
86
+ * Shipped inline rather than as a `.ps1` next to the build output, and that is a
87
+ * capability difference rather than tidiness: ExecutionPolicy blocks script
88
+ * FILES and does not apply to `-EncodedCommand`, so a locked-down machine - the
89
+ * population this bug keeps landing on - can still run it. It also removes the
90
+ * "where did my file go after packaging" class of failure entirely.
91
+ */
92
+ export declare function buildHelperScript(): string;
93
+ /** `-EncodedCommand` takes base64 of UTF-16LE, which needs no shell quoting. */
94
+ export declare function encodePowerShellCommand(script: string): string;
95
+ /**
96
+ * Prefer the absolute path so a sanitized or hijacked `PATH` cannot decide what
97
+ * runs; fall back to the bare name, which is still better than not trying.
98
+ */
99
+ export declare function resolvePowerShellExe(env?: NodeJS.ProcessEnv): string;
100
+ /** Pull `before=` / `after=` back out of the helper's stdout. */
101
+ export declare function parseConsoleModes(stdout: string): {
102
+ before?: number;
103
+ after?: number;
104
+ };
105
+ /**
106
+ * PURE, and the reason this feature is testable without a Windows box: every
107
+ * interesting outcome - including the `0x01F7` wrong-console trap that returns
108
+ * exit code 0 - is decided here from two strings and a number.
109
+ */
110
+ export declare function decideVtForceOutcome(run: {
111
+ status: number | null;
112
+ stdout: string;
113
+ stderr?: string;
114
+ }): VtForceResult;
115
+ export interface VtForceOptions {
116
+ platform?: string;
117
+ timeoutMs?: number;
118
+ /** Injected by the tests; defaults to a real `spawnSync`. */
119
+ run?: (exe: string, args: string[]) => {
120
+ status: number | null;
121
+ stdout: string;
122
+ stderr: string;
123
+ };
124
+ }
125
+ /**
126
+ * Set the bit on the console this process is attached to.
127
+ *
128
+ * SYNCHRONOUS on purpose. The caller has to know the answer before it decides
129
+ * whether the mouse can report and whether to warn the user, and at the one
130
+ * moment this runs - after raw mode is up, before Ink renders - there is nothing
131
+ * else for the event loop to do. An async version would buy nothing and would
132
+ * spread the ordering across two files.
133
+ *
134
+ * NEVER THROWS.
135
+ */
136
+ export declare function forceWindowsVtInput(opts?: VtForceOptions): VtForceResult;