@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/README.md +40 -19
- package/dist/agent/agent-mode.d.ts +39 -0
- package/dist/agent/agent-mode.js +36 -0
- package/dist/cli.js +73 -1
- package/dist/config/ui-state.d.ts +16 -5
- package/dist/config/ui-state.js +12 -12
- package/dist/diagnostics/doctor.js +41 -1
- package/dist/exec/index.js +8 -4
- package/dist/exec/runner.js +2 -2
- package/dist/session/cli-commands.js +23 -1
- package/dist/session/store.d.ts +18 -0
- package/dist/session/store.js +59 -2
- package/dist/ui/App.js +26 -7
- package/dist/ui/Composer.d.ts +14 -1
- package/dist/ui/Composer.js +5 -5
- package/dist/ui/overlays/HelpOverlay.js +12 -1
- package/dist/ui/use-startup-notices.d.ts +35 -4
- package/dist/ui/use-startup-notices.js +39 -7
- package/dist/ui/win-vt-force.d.ts +136 -0
- package/dist/ui/win-vt-force.js +237 -0
- package/package.json +2 -2
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,
|
|
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: () =>
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
package/dist/ui/Composer.d.ts
CHANGED
|
@@ -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;
|
package/dist/ui/Composer.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
|
18
|
-
* input — see `ui/win-vt-input.ts` for why that is a
|
|
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
|
-
/**
|
|
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,
|
|
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
|
|
45
|
-
* input — see `ui/win-vt-input.ts` for why that is a
|
|
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
|
-
|
|
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}
|
|
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 (
|
|
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;
|