@aragon-agent/cli 0.5.11 → 0.6.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.
- package/CHANGELOG.md +124 -0
- package/README.md +306 -9
- package/dist/agent/controller.d.ts +37 -1
- package/dist/agent/controller.js +34 -4
- package/dist/agent/reducer.js +14 -4
- package/dist/agent/system-prompt.d.ts +16 -0
- package/dist/agent/system-prompt.js +5 -0
- package/dist/cli.js +71 -2
- package/dist/commands/builtins.js +21 -2
- package/dist/config/ui-state.d.ts +18 -0
- package/dist/config/ui-state.js +10 -0
- package/dist/diagnostics/cli-commands.d.ts +10 -0
- package/dist/diagnostics/cli-commands.js +61 -0
- package/dist/diagnostics/doctor.d.ts +29 -0
- package/dist/diagnostics/doctor.js +167 -0
- package/dist/diagnostics/info.d.ts +30 -0
- package/dist/diagnostics/info.js +99 -0
- package/dist/exec/cli-commands.d.ts +31 -0
- package/dist/exec/cli-commands.js +63 -0
- package/dist/exec/emitter.d.ts +80 -0
- package/dist/exec/emitter.js +127 -0
- package/dist/exec/events.d.ts +226 -0
- package/dist/exec/events.js +43 -0
- package/dist/exec/index.d.ts +56 -0
- package/dist/exec/index.js +435 -0
- package/dist/exec/options.d.ts +76 -0
- package/dist/exec/options.js +221 -0
- package/dist/exec/permission.d.ts +66 -0
- package/dist/exec/permission.js +96 -0
- package/dist/exec/runner.d.ts +204 -0
- package/dist/exec/runner.js +559 -0
- package/dist/exec/stdin-stream.d.ts +46 -0
- package/dist/exec/stdin-stream.js +144 -0
- package/dist/logging/install.d.ts +22 -0
- package/dist/logging/install.js +22 -0
- package/dist/session/cli-commands.d.ts +30 -0
- package/dist/session/cli-commands.js +162 -0
- package/dist/session/persist.d.ts +46 -0
- package/dist/session/persist.js +8 -0
- package/dist/session/store.d.ts +129 -0
- package/dist/session/store.js +385 -0
- package/dist/team/runtime.d.ts +12 -0
- package/dist/team/runtime.js +6 -0
- package/dist/team/subagent.d.ts +12 -0
- package/dist/team/subagent.js +4 -0
- package/dist/todo/store.d.ts +12 -6
- package/dist/todo/store.js +12 -6
- package/dist/todo/types.d.ts +2 -1
- package/dist/tools/index.d.ts +20 -0
- package/dist/tools/index.js +26 -0
- package/dist/ui/ActivityLine.d.ts +32 -0
- package/dist/ui/ActivityLine.js +23 -1
- package/dist/ui/App.d.ts +14 -1
- package/dist/ui/App.js +70 -6
- package/dist/ui/BottomStatusRow.d.ts +33 -1
- package/dist/ui/BottomStatusRow.js +20 -6
- package/dist/ui/Transcript.js +15 -0
- package/dist/ui/entries/FastCard.js +20 -2
- package/dist/ui/overlays/HelpOverlay.js +1 -1
- package/dist/ui/use-startup-notices.d.ts +67 -4
- package/dist/ui/use-startup-notices.js +76 -2
- package/dist/ui/win-vt-input.d.ts +57 -0
- package/dist/ui/win-vt-input.js +62 -0
- package/package.json +7 -3
package/dist/ui/ActivityLine.js
CHANGED
|
@@ -3,6 +3,24 @@ import { Box, Text } from 'ink';
|
|
|
3
3
|
import Spinner from 'ink-spinner';
|
|
4
4
|
import { pickGlyphs } from './glyphs.js';
|
|
5
5
|
import { pickActivityPhrase } from './activity-phrases.js';
|
|
6
|
+
/**
|
|
7
|
+
* The animated spinner this row draws while it is live, or `null` when it would
|
|
8
|
+
* be STATIC — an ASCII terminal, or a user who asked for stillness.
|
|
9
|
+
*
|
|
10
|
+
* A FACTORY, NOT A COMPONENT, and the distinction is load-bearing. A
|
|
11
|
+
* `<LiveSpinner/>` element whose own render returned `null` would still be
|
|
12
|
+
* truthy at the call site — the exact trap `BottomStatusRow`'s `update` prop
|
|
13
|
+
* documents at length. Callers test the RETURN VALUE and get a real answer.
|
|
14
|
+
*
|
|
15
|
+
* It is exported so `BottomStatusRow` can put the bare glyph beside a toast
|
|
16
|
+
* without importing `ink-spinner` itself: `spinner-census.test.ts` enumerates
|
|
17
|
+
* every file under `src/ui/**` that imports it, and that list is the checklist
|
|
18
|
+
* for "who is allowed to animate". One more importer is one more site a future
|
|
19
|
+
* reader has to reason about; one more caller of this function is not.
|
|
20
|
+
*/
|
|
21
|
+
export function liveSpinner(reducedMotion, caps) {
|
|
22
|
+
return caps.unicode && !reducedMotion ? _jsx(Spinner, { type: "dots" }) : null;
|
|
23
|
+
}
|
|
6
24
|
export function ActivityLine({ startedAt, elapsedMs, reducedMotion, runningTool, theme, caps, }) {
|
|
7
25
|
const glyphs = pickGlyphs(caps);
|
|
8
26
|
// PURE AND DERIVED FROM `(startedAt, now)`, so the sequence does not need to
|
|
@@ -17,6 +35,10 @@ export function ActivityLine({ startedAt, elapsedMs, reducedMotion, runningTool,
|
|
|
17
35
|
// The IDENTICAL branch `Transcript.tsx` uses for the assistant role marker, and
|
|
18
36
|
// it must stay identical: braille dots are both an animation AND a Unicode-only
|
|
19
37
|
// glyph, so reduced motion and an ASCII terminal fall back to the same marker.
|
|
20
|
-
|
|
38
|
+
//
|
|
39
|
+
// Through `liveSpinner` rather than inline, so this row and the glyph
|
|
40
|
+
// `BottomStatusRow` composes beside a toast can never disagree about WHEN the
|
|
41
|
+
// spinner is live — the same reason `App` names its mount condition once.
|
|
42
|
+
const spinner = liveSpinner(reducedMotion, caps) ?? glyphs.spinnerStill;
|
|
21
43
|
return (_jsx(Box, { flexShrink: 0, children: _jsxs(Text, { wrap: "truncate", color: theme.thinking, children: [' ', spinner, " ", label] }) }));
|
|
22
44
|
}
|
package/dist/ui/App.d.ts
CHANGED
|
@@ -50,6 +50,19 @@ export interface AppProps {
|
|
|
50
50
|
* is off: inline mode, `--no-mouse`, a non-TTY, or a filter that failed to
|
|
51
51
|
* build (mouse-wheel-region-routing §4.2). */
|
|
52
52
|
mouseSource?: MouseSource;
|
|
53
|
+
/**
|
|
54
|
+
* Present ONLY on a console that can deliver neither `CSI Z` nor a mouse
|
|
55
|
+
* report — a Windows console whose Node predates `UV_TTY_MODE_RAW_VT`
|
|
56
|
+
* (`ui/win-vt-input.ts`). Absent everywhere else, which is what makes every
|
|
57
|
+
* unaffected machine byte-identical to a build without this prop.
|
|
58
|
+
*
|
|
59
|
+
* DECIDED BY `cli.tsx`, not here, and by the SAME expression that gates
|
|
60
|
+
* `mouseSource`: the two must never disagree about whether this terminal
|
|
61
|
+
* reports anything.
|
|
62
|
+
*/
|
|
63
|
+
vtInputWarning?: {
|
|
64
|
+
nodeVersion: string;
|
|
65
|
+
};
|
|
53
66
|
/**
|
|
54
67
|
* The auto-updater's late-arrival channel (cli-auto-update §3.8).
|
|
55
68
|
*
|
|
@@ -60,4 +73,4 @@ export interface AppProps {
|
|
|
60
73
|
*/
|
|
61
74
|
updateBridge?: UpdateBridge;
|
|
62
75
|
}
|
|
63
|
-
export declare function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, updateBridge, }: AppProps): React.ReactElement;
|
|
76
|
+
export declare function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, vtInputWarning, updateBridge, }: AppProps): React.ReactElement;
|
package/dist/ui/App.js
CHANGED
|
@@ -14,7 +14,7 @@ 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 } from '../config/ui-state.js';
|
|
17
|
+
import { bumpSubmitCount, setMouseNoticeSeen, setVtInputNoticeSeen } 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';
|
|
@@ -32,7 +32,7 @@ import { secondsLeft } from '../agent/retry-view.js';
|
|
|
32
32
|
import { TeamPanel } from './TeamPanel.js';
|
|
33
33
|
import { TodoPanel } from './TodoPanel.js';
|
|
34
34
|
import { TodoStrip } from './TodoStrip.js';
|
|
35
|
-
import { ActivityLine } from './ActivityLine.js';
|
|
35
|
+
import { ActivityLine, liveSpinner } from './ActivityLine.js';
|
|
36
36
|
import { BottomStatusRow } from './BottomStatusRow.js';
|
|
37
37
|
import { UpdateLine } from './UpdateLine.js';
|
|
38
38
|
// The updater's two PURE modules. `shouldRenderUpdateLine` decides the row's
|
|
@@ -110,7 +110,7 @@ export function resolveSettingsMaxTokens(raw, previous) {
|
|
|
110
110
|
function seedViewState(thinkingVisible) {
|
|
111
111
|
return initialViewState({ thinkingVisible });
|
|
112
112
|
}
|
|
113
|
-
export function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, updateBridge, }) {
|
|
113
|
+
export function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, vtInputWarning, updateBridge, }) {
|
|
114
114
|
const { exit } = useApp();
|
|
115
115
|
const { stdout } = useStdout();
|
|
116
116
|
const fullscreen = mode === 'fullscreen';
|
|
@@ -693,6 +693,8 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
693
693
|
}, [fullscreen]);
|
|
694
694
|
// `enabled` is `mouseSource`, not `cfg.mouse`: a user whose terminal drops
|
|
695
695
|
// the events must never be shown advice about a mode not in effect (§6.1).
|
|
696
|
+
// `cli.tsx::wantMouse` now folds the platform check into the same expression,
|
|
697
|
+
// so on a console that reports nothing there is no source and no advice.
|
|
696
698
|
useStartupNotices(dispatch, {
|
|
697
699
|
enabled: !!mouseSource,
|
|
698
700
|
// `state.json`, not `config.json`: whether a one-shot notice has been shown
|
|
@@ -703,6 +705,20 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
703
705
|
// `transcriptRetain` to `transcriptWindow` (tui-render-performance §5.1).
|
|
704
706
|
cfg.transcriptRetainRequested !== undefined
|
|
705
707
|
? { requested: cfg.transcriptRetainRequested, resolved: cfg.transcriptRetain }
|
|
708
|
+
: undefined,
|
|
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.
|
|
711
|
+
//
|
|
712
|
+
// `fullscreen` goes with it because only half of the damage is mode-
|
|
713
|
+
// independent: `Shift+Tab` is broken either way, but inline never asked for
|
|
714
|
+
// wheel reports and does not own the scroll keys, so it must not be told
|
|
715
|
+
// about either.
|
|
716
|
+
vtInputWarning
|
|
717
|
+
? {
|
|
718
|
+
nodeVersion: vtInputWarning.nodeVersion,
|
|
719
|
+
fullscreen,
|
|
720
|
+
onSeen: () => setVtInputNoticeSeen(true),
|
|
721
|
+
}
|
|
706
722
|
: undefined);
|
|
707
723
|
// --- Exit snapshot: the only way state reaches cli.tsx (§4.4). ----------
|
|
708
724
|
useEffect(() => {
|
|
@@ -1422,6 +1438,41 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1422
1438
|
const opener = empty ? (_jsx(SessionOpener, { variant: fullscreen ? pickOpenerVariant(viewportBudget, contentCols, caps) : 'none', version: version, cwd: controller.getCwd(), hasKey: hasKey, viewportRows: fullscreen ? viewportBudget : Number.POSITIVE_INFINITY, theme: theme, caps: caps })) : null;
|
|
1423
1439
|
const density = cfg.density;
|
|
1424
1440
|
const transcriptWindow = cfg.transcriptWindow ?? DEFAULT_TRANSCRIPT_WINDOW;
|
|
1441
|
+
// --- One spinner per run (single-spinner-while-running D-1 .. D-5). ------
|
|
1442
|
+
//
|
|
1443
|
+
// THE MOUNT CONDITION, NAMED ONCE AND USED TWICE (D-4). This const both gates
|
|
1444
|
+
// the `<ActivityLine>` element below and feeds the derivation on the next
|
|
1445
|
+
// line, so "the line is up" and "every other site is still" can never
|
|
1446
|
+
// disagree. Two copies of the same boolean expression is exactly how that
|
|
1447
|
+
// invariant becomes false in six months, silently and in only one branch.
|
|
1448
|
+
//
|
|
1449
|
+
// NOT `running` ALONE (D-3). While an overlay owns the screen the activity row
|
|
1450
|
+
// is suppressed, so keying suppression off `running` would leave the frame
|
|
1451
|
+
// with ZERO life signals: the bar shows state, this row shows life. Never
|
|
1452
|
+
// trade one duplicate for one absence.
|
|
1453
|
+
//
|
|
1454
|
+
// THE CLAIM ABOVE WAS ONCE FALSE, AND THAT WAS THE WHOLE OF
|
|
1455
|
+
// `activity-spinner-vanishes-behind-toast`. `BottomStatusRow` — not this const
|
|
1456
|
+
// — decides what the row actually shows, and a toast used to take the row
|
|
1457
|
+
// outright, so for its 2.5 s TTL this said "the line is up" while nothing was
|
|
1458
|
+
// mounted and all seven other sites sat still. The answer was NOT to enumerate
|
|
1459
|
+
// `&& !state.toasts.length` here (the next occupant reopens it, and the seven
|
|
1460
|
+
// sites animating out of phase is the defect this whole feature removed):
|
|
1461
|
+
// `BottomStatusRow` now carries the bare spinner alongside the toast, so the
|
|
1462
|
+
// signal is mounted exactly when this const is true and the claim holds by
|
|
1463
|
+
// construction.
|
|
1464
|
+
const activityVisible = running && !overlayNode;
|
|
1465
|
+
// The FOUR VIEW CONSUMERS (transcript x 2, team panel, todo panel) take this;
|
|
1466
|
+
// the activity line keeps the raw config flag (D-5), because it reads that
|
|
1467
|
+
// flag a second time to gate phrase rotation and the widened value would
|
|
1468
|
+
// freeze the phrase at the first word of every run.
|
|
1469
|
+
//
|
|
1470
|
+
// So below `App` the prop now means "this frame does not animate here" rather
|
|
1471
|
+
// than "the user asked for reduced motion". `cfg.reducedMotion` (`:279`) stays
|
|
1472
|
+
// the sole config source, and `spinner-census.test.ts` pins this wiring —
|
|
1473
|
+
// threading the raw flag into a fifth consumer is the one silent way back to
|
|
1474
|
+
// two spinners (R-2).
|
|
1475
|
+
const viewReducedMotion = reducedMotion || activityVisible;
|
|
1425
1476
|
// Assigned during render so `/perf` reads the CURRENT frame (see `perfRef`).
|
|
1426
1477
|
// `mountedEntries` is the same number `render-budget.test.tsx` asserts (AC-9).
|
|
1427
1478
|
perfRef.current = {
|
|
@@ -1440,20 +1491,33 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1440
1491
|
viewportRows: fullscreen ? viewportBudget : rows,
|
|
1441
1492
|
offset: scrolledLines,
|
|
1442
1493
|
};
|
|
1443
|
-
const viewport = fullscreen ? (overlayNode ? (_jsx(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: overlayNode })) : (_jsxs(ScrollViewport, { intent: scrollIntent, pinToBottomNonce: pinToBottomNonce, onScrolledLinesChange: setScrolledLines, showScrollIndicator: true, cols: contentCols, theme: theme, caps: caps, children: [opener, _jsx(TranscriptList, { entries: state.entries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion:
|
|
1494
|
+
const viewport = fullscreen ? (overlayNode ? (_jsx(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: overlayNode })) : (_jsxs(ScrollViewport, { intent: scrollIntent, pinToBottomNonce: pinToBottomNonce, onScrolledLinesChange: setScrolledLines, showScrollIndicator: true, cols: contentCols, theme: theme, caps: caps, children: [opener, _jsx(TranscriptList, { entries: state.entries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: viewReducedMotion, density: density, mode: mode, nowSec: nowSec, theme: theme, caps: caps, windowSize: transcriptWindow, cols: contentCols, heights: heights, mountedSink: mountedCount })] }))) : (_jsxs(_Fragment, { children: [opener, _jsx(Transcript, { entries: state.entries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: viewReducedMotion, density: density, mode: mode, nowSec: nowSec, theme: theme, caps: caps }), overlayNode] }));
|
|
1444
1495
|
// The completion popup lives inside the bottom chrome and can add up to 9
|
|
1445
1496
|
// rows; unbounded, it pushes the status bar past the frame height where
|
|
1446
1497
|
// `overflow: hidden` clips it away (R-14 / M-14). Cap it against the viewport.
|
|
1447
1498
|
const popupMaxRows = fullscreen ? Math.max(1, viewportBudget - 4) : undefined;
|
|
1448
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' }) }) }));
|
|
1449
|
-
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:
|
|
1500
|
+
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,
|
|
1450
1501
|
// `!overlayNode` suppresses the line while an overlay owns the screen:
|
|
1451
1502
|
// the full-screen branch replaces the whole viewport with the overlay,
|
|
1452
1503
|
// and a working line under a settings screen is noise attached to a
|
|
1453
1504
|
// surface the run is not visible on. (Inline overlays are part of the
|
|
1454
1505
|
// document flow, so there the suppression is conservative rather than
|
|
1455
1506
|
// necessary — P2-5.)
|
|
1456
|
-
|
|
1507
|
+
//
|
|
1508
|
+
// THE CONDITION IS `activityVisible`, THE SAME CONST THE SUPPRESSION
|
|
1509
|
+
// SIGNAL IS DERIVED FROM (D-4): inlining it here again is how the two
|
|
1510
|
+
// drift apart, and the drift is silent in whichever branch is not
|
|
1511
|
+
// edited.
|
|
1512
|
+
activity: activityVisible ? (_jsx(ActivityLine, { startedAt: runStartedAt.current, elapsedMs: elapsedMs, reducedMotion: reducedMotion, runningTool: runningTool, theme: theme, caps: caps })) : null,
|
|
1513
|
+
// THE FORM THE LIFE SIGNAL TAKES WHEN A TOAST HAS THE ROW
|
|
1514
|
+
// (`activity-spinner-vanishes-behind-toast`). Passed UNCONDITIONALLY:
|
|
1515
|
+
// `BottomStatusRow` reads it only when `activity` above is non-null,
|
|
1516
|
+
// so `activityVisible` stays the one mount condition and this cannot
|
|
1517
|
+
// become a second copy of it that drifts. `null` here means the
|
|
1518
|
+
// spinner would be static anyway (ASCII tier, or reduced motion), and
|
|
1519
|
+
// those builds must come out unchanged.
|
|
1520
|
+
activityGlyph: liveSpinner(reducedMotion, caps),
|
|
1457
1521
|
// PRESENCE IS DECIDED HERE, NOT BY `UpdateLine` RETURNING `null`
|
|
1458
1522
|
// (C-15 / cli-auto-update P0-1 / D-19). `BottomStatusRow` tests this
|
|
1459
1523
|
// prop for truthiness, and a React element is truthy however it
|
|
@@ -33,6 +33,23 @@
|
|
|
33
33
|
* end, whereas an activity line deferred is an activity line that never renders.
|
|
34
34
|
* That also means a user who is actively working never sees the updater at all
|
|
35
35
|
* until they stop, which is the whole of the silence requirement.
|
|
36
|
+
*
|
|
37
|
+
* THE TOAST TAKES THE ROW BUT NOT THE SPINNER
|
|
38
|
+
* (`activity-spinner-vanishes-behind-toast`). "Toast wins" used to mean the
|
|
39
|
+
* activity line was never mounted while an ack was up — and because `App`
|
|
40
|
+
* suppresses the other seven animated sites for exactly as long as it BELIEVES
|
|
41
|
+
* this row is showing one, a mid-run toast left the whole frame with ZERO
|
|
42
|
+
* animations for its 2.5 s TTL, indistinguishable from a hung process. Steering
|
|
43
|
+
* and plan approval both ack mid-run, so a long run hit it repeatedly.
|
|
44
|
+
*
|
|
45
|
+
* The row still belongs to the toast: the spinner joins it as a bare glyph, no
|
|
46
|
+
* phrase and no clock, so `bottom-status-row.test.tsx`'s "the ack keeps its
|
|
47
|
+
* words" assertion and the toast's own width are both untouched. A row-direction
|
|
48
|
+
* box holding two one-row children is STILL ONE ROW, which is what the note
|
|
49
|
+
* above actually forbids disturbing — `budget.ts` and `AppShell.tsx` stay out of
|
|
50
|
+
* it. And because the glyph is mounted under precisely the condition `App` uses
|
|
51
|
+
* to suppress the others, "the line is up" and "every other site is still" agree
|
|
52
|
+
* again by construction rather than by enumeration of what can pre-empt the row.
|
|
36
53
|
*/
|
|
37
54
|
import React from 'react';
|
|
38
55
|
import type { Theme } from './theme.js';
|
|
@@ -43,6 +60,21 @@ export interface BottomStatusRowProps {
|
|
|
43
60
|
toasts: Toast[];
|
|
44
61
|
/** `<ActivityLine>` while a run owns the screen, else `null`. */
|
|
45
62
|
activity: React.ReactNode;
|
|
63
|
+
/**
|
|
64
|
+
* The bare animated spinner (`liveSpinner`), or `null` when it would be a
|
|
65
|
+
* STATIC glyph — an ASCII terminal, or `reducedMotion`.
|
|
66
|
+
*
|
|
67
|
+
* IT IS NOT A SECOND MOUNT CONDITION. `activity` alone decides whether a life
|
|
68
|
+
* signal belongs on this row; this prop only says which FORM the signal takes,
|
|
69
|
+
* and it is read only when `activity` is non-null. So a caller cannot make the
|
|
70
|
+
* two disagree, which is the failure mode that produced this prop.
|
|
71
|
+
*
|
|
72
|
+
* `null` means PREFIX NOTHING, not "prefix the still glyph". A static `·` in
|
|
73
|
+
* front of every toast would be a visible change to ASCII terminals and to
|
|
74
|
+
* users who asked for stillness — neither of whom ever had the dead-frame bug,
|
|
75
|
+
* and both of whom must come out byte-identical.
|
|
76
|
+
*/
|
|
77
|
+
activityGlyph?: React.ReactElement | null;
|
|
46
78
|
/**
|
|
47
79
|
* `<UpdateLine>` when the updater has something to say, else `null` — AND THE
|
|
48
80
|
* CALLER DECIDES WHICH (C-15 / cli-auto-update P0-1).
|
|
@@ -62,4 +94,4 @@ export interface BottomStatusRowProps {
|
|
|
62
94
|
update?: React.ReactNode;
|
|
63
95
|
theme: Theme;
|
|
64
96
|
}
|
|
65
|
-
export declare function BottomStatusRow({ mode, toasts, activity, update, theme, }: BottomStatusRowProps): React.ReactElement | null;
|
|
97
|
+
export declare function BottomStatusRow({ mode, toasts, activity, activityGlyph, update, theme, }: BottomStatusRowProps): React.ReactElement | null;
|
|
@@ -1,10 +1,24 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { Box } from 'ink';
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
3
|
import { ToastStack } from './ToastStack.js';
|
|
4
|
-
export function BottomStatusRow({ mode, toasts, activity, update, theme, }) {
|
|
5
|
-
// A toast wins the row outright.
|
|
6
|
-
if (toasts.length > 0)
|
|
7
|
-
|
|
4
|
+
export function BottomStatusRow({ mode, toasts, activity, activityGlyph, update, theme, }) {
|
|
5
|
+
// A toast wins the row outright — but a run in flight keeps its spinner on it.
|
|
6
|
+
if (toasts.length > 0) {
|
|
7
|
+
const spinner = activity ? activityGlyph : null;
|
|
8
|
+
// Byte-identical to the old branch whenever there is no live spinner to
|
|
9
|
+
// carry: no run, an ASCII terminal, or `reducedMotion`.
|
|
10
|
+
if (!spinner)
|
|
11
|
+
return _jsx(ToastStack, { toasts: toasts, theme: theme, mode: mode });
|
|
12
|
+
const fullscreen = mode === 'fullscreen';
|
|
13
|
+
return (
|
|
14
|
+
// `flexDirection` SPELLED OUT even though `row` is Yoga's default: it is
|
|
15
|
+
// what keeps two children on ONE row, and the header above turns on that
|
|
16
|
+
// being true. Left implicit, it reads as an oversight next to the child
|
|
17
|
+
// below that declares `column` — and "make them match" is a one-word edit
|
|
18
|
+
// that doubles the row, takes it from the transcript, and desynchronises
|
|
19
|
+
// every `viewportRows()` consumer without failing a type or a lint.
|
|
20
|
+
_jsxs(Box, { flexDirection: "row", flexShrink: 0, children: [_jsx(Box, { flexShrink: 0, marginTop: fullscreen ? 0 : 1, children: _jsxs(Text, { color: theme.thinking, children: [fullscreen ? ' ' : '', spinner, fullscreen ? '' : ' '] }) }), _jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(ToastStack, { toasts: toasts, theme: theme, mode: mode }) })] }));
|
|
21
|
+
}
|
|
8
22
|
// Inline has no frame and therefore nothing to protect: `ToastStack`'s inline
|
|
9
23
|
// branch is already a conditional 0-3 row stack, and these are simply one more
|
|
10
24
|
// conditional row above it.
|
package/dist/ui/Transcript.js
CHANGED
|
@@ -138,6 +138,21 @@ function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion,
|
|
|
138
138
|
// While text streams, the spinner IS the role marker. Braille dots are
|
|
139
139
|
// both an animation and a Unicode-only glyph, so reduced motion and an
|
|
140
140
|
// ASCII terminal fall back to the same static marker (P2-11 / A-10).
|
|
141
|
+
//
|
|
142
|
+
// `reducedMotion` ARRIVES PRE-WIDENED FROM `App`: it is now
|
|
143
|
+
// `cfg.reducedMotion || activityVisible`, so this marker is static for the
|
|
144
|
+
// whole of every run and the activity row above the composer carries the
|
|
145
|
+
// one animation (single-spinner-while-running D-2). Nothing here changes —
|
|
146
|
+
// the expression already meant "do not animate here".
|
|
147
|
+
//
|
|
148
|
+
// THAT IS SAFE FOR `<Static>` BY CONSTRUCTION, not by luck: a settled
|
|
149
|
+
// prefix is printed once and can never be re-printed, so a boolean that
|
|
150
|
+
// changed how an already-printed entry renders would tear the history.
|
|
151
|
+
// `computeSettledCount` below breaks on EVERY condition that makes an
|
|
152
|
+
// entry animate (`:84` streaming, `:85` running tool, `:91` active team,
|
|
153
|
+
// `:104` retry, `:109` live fast), so every animating entry is in the live
|
|
154
|
+
// region and every entry in `<Static>` already has `animate === false` on
|
|
155
|
+
// both sides of the flip. Weakening a break clause breaks that proof.
|
|
141
156
|
const animate = entry.streaming && !reducedMotion && caps.unicode;
|
|
142
157
|
return frame(animate ? _jsx(Spinner, { type: "dots" }) : glyphs.assistant, theme.primary, _jsx(AssistantEntry, { text: entry.text, thinking: entry.thinking, thinkingOpen: entry.thinkingOpen, thinkingVisible: thinkingVisible, streaming: entry.streaming, aborted: entry.aborted,
|
|
143
158
|
// `EntryViewImpl` hands each renderer EXPLICIT SCALAR PROPS, so
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* FastCard — one fast-model review in the transcript (fast-model-tier §6).
|
|
4
4
|
*
|
|
@@ -76,7 +76,25 @@ function FastCardImpl(props) {
|
|
|
76
76
|
const duration = durationMs === undefined ? '' : ` ${glyphs.midDot} ${formatDuration(durationMs)}`;
|
|
77
77
|
const head = `fast review #${reviewIndex} ${glyphs.midDot} ${model} ${glyphs.midDot} turn ${turn}${duration}`;
|
|
78
78
|
const showBody = status === 'advice' && text !== undefined && text.trim().length > 0;
|
|
79
|
-
|
|
79
|
+
// COLOUR TRACKS `live`, MOTION TRACKS `animate` — one headline, two separate
|
|
80
|
+
// decisions (single-spinner-while-running C-1 / D-7).
|
|
81
|
+
//
|
|
82
|
+
// These used to be two whole `<Text>` elements, so the still branch also
|
|
83
|
+
// demoted the head to `theme.muted`. That was harmless while stillness meant
|
|
84
|
+
// "the user set reducedMotion", but `App` now widens the prop to "this frame
|
|
85
|
+
// does not animate here" for the whole of every run — and under the old shape
|
|
86
|
+
// that would have greyed out every live fast review for the whole run, losing
|
|
87
|
+
// the status colour along with the motion.
|
|
88
|
+
//
|
|
89
|
+
// Three things are load-bearing and all three are silent if dropped: the
|
|
90
|
+
// leading two-space gutter that separates the head from the bold `fast` label,
|
|
91
|
+
// the single space after the marker (the still form bakes it into its template
|
|
92
|
+
// string), and `head` itself. `wrap="truncate"` now applies to the animated
|
|
93
|
+
// path too, which it did not before: it is what the still branch always did,
|
|
94
|
+
// it is the behaviour this card has in every state that is now common, and an
|
|
95
|
+
// unbounded wrap inside a one-row headline is the worse failure.
|
|
96
|
+
const animate = live && !reducedMotion && caps.unicode;
|
|
97
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.accent, bold: true, children: "fast" }), _jsxs(Text, { color: live ? color : theme.muted, wrap: "truncate", children: [' ', animate ? (_jsxs(_Fragment, { children: [_jsx(Spinner, { type: "dots" }), ' '] })) : live ? (`${glyphs.spinnerStill} `) : (''), head] })] }), showBody ? (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: textRows(text).map((line, index) => (_jsx(Text, { color: theme.primary, children: line }, index))) })) : (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, children: _jsx(Text, { color: color, wrap: "truncate", children: statusLine(props) }) }))] }));
|
|
80
98
|
}
|
|
81
99
|
/**
|
|
82
100
|
* `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
|
|
@@ -33,7 +33,7 @@ const COMMANDS = [
|
|
|
33
33
|
'Todo planning: status, switches, the rail, follow-through',
|
|
34
34
|
],
|
|
35
35
|
['/tools', 'List active tools'],
|
|
36
|
-
['/clear', 'Clear the visible transcript'],
|
|
36
|
+
['/clear', 'Clear the visible transcript and the todo panel'],
|
|
37
37
|
['/reset', 'New conversation'],
|
|
38
38
|
['/cwd [dir]', 'Show or change the tool working directory'],
|
|
39
39
|
['/save [file]', 'Save the session to JSON'],
|
|
@@ -12,6 +12,43 @@
|
|
|
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 `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.
|
|
20
|
+
*
|
|
21
|
+
* IT NAMES WHAT THE KEY DOES NOW, not merely that it is unavailable, and that
|
|
22
|
+
* is the load-bearing half of the text. `Shift+Tab` on these machines is not
|
|
23
|
+
* inert: the modifier is lost inside libuv, so Ink sees an ordinary `Tab` and
|
|
24
|
+
* the `/` palette or an `@` popup completes its selection over the user's
|
|
25
|
+
* draft. A user told only "this key does not work" keeps pressing it and keeps
|
|
26
|
+
* losing text without ever connecting the two.
|
|
27
|
+
*
|
|
28
|
+
* `Shift+Up` / `Shift+Down` is the scroll workaround named here because it is
|
|
29
|
+
* the only one PROVEN to survive: both Node versions emit `\x1b[1;2A` for it,
|
|
30
|
+
* byte for byte, in the probe data. `PgUp` / `PgDn` is expected to work too but
|
|
31
|
+
* was never injected, so it is left out rather than promised.
|
|
32
|
+
*
|
|
33
|
+
* THE WHEEL HALF IS FULL-SCREEN ONLY, and dropping it inline is not a detail.
|
|
34
|
+
* `Shift+Tab` is broken in both modes — the modifier dies inside libuv, well
|
|
35
|
+
* below anything this app chooses — but the wheel is only ours to lose in
|
|
36
|
+
* full-screen: inline never asks the console to report it (`mode` is a conjunct
|
|
37
|
+
* of `cli.tsx::wantMouse`), so the wheel goes on scrolling the terminal's own
|
|
38
|
+
* scrollback exactly as it always has. And `Shift+Up` / `Shift+Down` are handled
|
|
39
|
+
* inside `App`'s `if (fullscreen)` branch, so recommending them inline names a
|
|
40
|
+
* key that does nothing. Telling an inline user their working wheel is dead and
|
|
41
|
+
* then handing them an inert workaround is the same failure this notice exists
|
|
42
|
+
* to end — advice about a mode that is not running — merely pointed the other
|
|
43
|
+
* way.
|
|
44
|
+
*
|
|
45
|
+
* EXPORTED FOR ITS TEST, unlike the two notices above it. Asserting this text
|
|
46
|
+
* through a rendered frame does not work: the transcript wraps a notice at two
|
|
47
|
+
* columns wider than it displays it, so any phrase landing on a wrap boundary
|
|
48
|
+
* loses a character or two on the way to `lastFrame()` and the assertion fails
|
|
49
|
+
* for a reason that has nothing to do with the text.
|
|
50
|
+
*/
|
|
51
|
+
export declare function vtInputDeadNotice(nodeVersion: string, fullscreen: boolean): string;
|
|
15
52
|
/**
|
|
16
53
|
* `{ requested, resolved }` when the raise happened, `undefined` otherwise —
|
|
17
54
|
* i.e. `cfg.transcriptRetainRequested` and `cfg.transcriptRetain`.
|
|
@@ -22,12 +59,38 @@ export interface RetainNoticeOptions {
|
|
|
22
59
|
}
|
|
23
60
|
export interface MouseNoticeOptions {
|
|
24
61
|
/**
|
|
25
|
-
* Whether reporting is ACTUALLY in effect
|
|
26
|
-
*
|
|
27
|
-
* must never be given advice about a mode that is not
|
|
62
|
+
* Whether reporting is ACTUALLY in effect: a filter was installed AND the
|
|
63
|
+
* platform can deliver the reports it would filter. A user on a terminal
|
|
64
|
+
* where R-1 fires must never be given advice about a mode that is not
|
|
65
|
+
* running.
|
|
66
|
+
*
|
|
67
|
+
* THE TWO HALVES ARE NOT THE SAME CLAIM, and this comment used to define the
|
|
68
|
+
* flag as only the first of them ("i.e. a filter was installed") — which is
|
|
69
|
+
* how the second sentence came to be violated by an implementation that
|
|
70
|
+
* satisfied the first. Constructing the filter proves nothing about whether
|
|
71
|
+
* the terminal will ever send a report; on a Windows console without VT
|
|
72
|
+
* input it succeeds and then sits in front of a stream no report can reach.
|
|
73
|
+
* The caller now derives both from one expression (`cli.tsx::wantMouse`), so
|
|
74
|
+
* re-deriving either one here would put them back out of step.
|
|
28
75
|
*/
|
|
29
76
|
enabled: boolean;
|
|
30
77
|
/** Record that it was shown (`state.json`). Called once, after dispatch. */
|
|
31
78
|
onSeen: () => void;
|
|
32
79
|
}
|
|
33
|
-
export
|
|
80
|
+
export interface VtInputNoticeOptions {
|
|
81
|
+
/** `process.versions.node`, quoted back so the user can act on it. */
|
|
82
|
+
nodeVersion: string;
|
|
83
|
+
/**
|
|
84
|
+
* Whether this run owns the alternate screen, i.e. `App`'s own `fullscreen`.
|
|
85
|
+
*
|
|
86
|
+
* Selects which half of the text applies: inline keeps the wheel and loses the
|
|
87
|
+
* full-screen scroll keys, so it is told about `Shift+Tab` and nothing else
|
|
88
|
+
* (see `vtInputDeadNotice`). Taken from the caller rather than re-derived here
|
|
89
|
+
* for the same reason the version is — one source, no chance of the notice and
|
|
90
|
+
* the renderer disagreeing about which mode is running.
|
|
91
|
+
*/
|
|
92
|
+
fullscreen: boolean;
|
|
93
|
+
/** Record that it was shown (`state.json`). Called once, after dispatch. */
|
|
94
|
+
onSeen: () => void;
|
|
95
|
+
}
|
|
96
|
+
export declare function useStartupNotices(dispatch: (action: ViewAction) => void, mouseNotice?: MouseNoticeOptions, retainNotice?: RetainNoticeOptions, vtInputNotice?: VtInputNoticeOptions): void;
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { useEffect, useRef } from 'react';
|
|
15
15
|
import { readConfigFile } from '../config/store.js';
|
|
16
|
-
import { getMouseNoticeSeen } from '../config/ui-state.js';
|
|
16
|
+
import { getMouseNoticeSeen, getVtInputNoticeSeen } from '../config/ui-state.js';
|
|
17
17
|
import { getLogger } from '../logging/logger.js';
|
|
18
18
|
/**
|
|
19
19
|
* The §6.1 notice (mouse-wheel-region-routing). Shown at most once, ever.
|
|
@@ -39,7 +39,55 @@ function retainRaisedNotice(requested, resolved) {
|
|
|
39
39
|
'so the whole scroll horizon stays reachable. Lower transcriptWindow too if you meant ' +
|
|
40
40
|
'to cap memory.');
|
|
41
41
|
}
|
|
42
|
-
|
|
42
|
+
/**
|
|
43
|
+
* 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.
|
|
47
|
+
*
|
|
48
|
+
* IT NAMES WHAT THE KEY DOES NOW, not merely that it is unavailable, and that
|
|
49
|
+
* is the load-bearing half of the text. `Shift+Tab` on these machines is not
|
|
50
|
+
* inert: the modifier is lost inside libuv, so Ink sees an ordinary `Tab` and
|
|
51
|
+
* the `/` palette or an `@` popup completes its selection over the user's
|
|
52
|
+
* draft. A user told only "this key does not work" keeps pressing it and keeps
|
|
53
|
+
* losing text without ever connecting the two.
|
|
54
|
+
*
|
|
55
|
+
* `Shift+Up` / `Shift+Down` is the scroll workaround named here because it is
|
|
56
|
+
* the only one PROVEN to survive: both Node versions emit `\x1b[1;2A` for it,
|
|
57
|
+
* byte for byte, in the probe data. `PgUp` / `PgDn` is expected to work too but
|
|
58
|
+
* was never injected, so it is left out rather than promised.
|
|
59
|
+
*
|
|
60
|
+
* THE WHEEL HALF IS FULL-SCREEN ONLY, and dropping it inline is not a detail.
|
|
61
|
+
* `Shift+Tab` is broken in both modes — the modifier dies inside libuv, well
|
|
62
|
+
* below anything this app chooses — but the wheel is only ours to lose in
|
|
63
|
+
* full-screen: inline never asks the console to report it (`mode` is a conjunct
|
|
64
|
+
* of `cli.tsx::wantMouse`), so the wheel goes on scrolling the terminal's own
|
|
65
|
+
* scrollback exactly as it always has. And `Shift+Up` / `Shift+Down` are handled
|
|
66
|
+
* inside `App`'s `if (fullscreen)` branch, so recommending them inline names a
|
|
67
|
+
* key that does nothing. Telling an inline user their working wheel is dead and
|
|
68
|
+
* then handing them an inert workaround is the same failure this notice exists
|
|
69
|
+
* to end — advice about a mode that is not running — merely pointed the other
|
|
70
|
+
* way.
|
|
71
|
+
*
|
|
72
|
+
* EXPORTED FOR ITS TEST, unlike the two notices above it. Asserting this text
|
|
73
|
+
* through a rendered frame does not work: the transcript wraps a notice at two
|
|
74
|
+
* columns wider than it displays it, so any phrase landing on a wrap boundary
|
|
75
|
+
* loses a character or two on the way to `lastFrame()` and the assertion fails
|
|
76
|
+
* for a reason that has nothing to do with the text.
|
|
77
|
+
*/
|
|
78
|
+
export function vtInputDeadNotice(nodeVersion, fullscreen) {
|
|
79
|
+
const cause = `Node ${nodeVersion} on Windows does not turn on the console's VT input mode, so `;
|
|
80
|
+
const cost = 'it will not switch mode, and with the / palette or an @ popup open it completes that ' +
|
|
81
|
+
'instead, overwriting your draft. ';
|
|
82
|
+
if (!fullscreen) {
|
|
83
|
+
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.');
|
|
85
|
+
}
|
|
86
|
+
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. ` +
|
|
88
|
+
'Node 22.17.0+ (or 24.2.0+) restores both.');
|
|
89
|
+
}
|
|
90
|
+
export function useStartupNotices(dispatch, mouseNotice, retainNotice, vtInputNotice) {
|
|
43
91
|
// The sink gives up after repeated write failures (a read-only log directory,
|
|
44
92
|
// a full disk). Silently logging nothing is exactly the state a user must not
|
|
45
93
|
// discover later, from an empty file, while trying to explain a bug.
|
|
@@ -76,6 +124,32 @@ export function useStartupNotices(dispatch, mouseNotice, retainNotice) {
|
|
|
76
124
|
dispatch({ type: 'notice', level: 'info', text: MOUSE_SELECTION_NOTICE });
|
|
77
125
|
noticeRef.current?.onSeen();
|
|
78
126
|
}, [mouseEnabled, dispatch]);
|
|
127
|
+
// The R-1 notice. `warn`, not `info`: unlike the one above it reports a
|
|
128
|
+
// capability the user was promised and does not have, plus a key that is
|
|
129
|
+
// actively editing their draft.
|
|
130
|
+
//
|
|
131
|
+
// DEPENDS ON THE SCALAR, never on the options object, for the reason spelled
|
|
132
|
+
// out on the retain effect below — `App` builds that literal during render.
|
|
133
|
+
// Its one-shot key is checked only AFTER the presence test, so a machine that
|
|
134
|
+
// is not affected never touches `state.json` at all.
|
|
135
|
+
const vtNoticeRef = useRef(vtInputNotice);
|
|
136
|
+
vtNoticeRef.current = vtInputNotice;
|
|
137
|
+
const vtNodeVersion = vtInputNotice?.nodeVersion;
|
|
138
|
+
const vtFullscreen = vtInputNotice?.fullscreen ?? false;
|
|
139
|
+
const vtNoticeShown = useRef(false);
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (vtNodeVersion === undefined || vtNoticeShown.current)
|
|
142
|
+
return;
|
|
143
|
+
if (getVtInputNoticeSeen())
|
|
144
|
+
return;
|
|
145
|
+
vtNoticeShown.current = true;
|
|
146
|
+
dispatch({
|
|
147
|
+
type: 'notice',
|
|
148
|
+
level: 'warn',
|
|
149
|
+
text: vtInputDeadNotice(vtNodeVersion, vtFullscreen),
|
|
150
|
+
});
|
|
151
|
+
vtNoticeRef.current?.onSeen();
|
|
152
|
+
}, [vtNodeVersion, vtFullscreen, dispatch]);
|
|
79
153
|
// A `warn` notice rather than a toast: it is about a value the user typed, so
|
|
80
154
|
// it belongs in the transcript where it can be scrolled back to, not in a row
|
|
81
155
|
// that expires. DEPENDS ON THE TWO SCALARS, never on the options object —
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Will this console ever deliver `CSI Z` and SGR mouse reports?
|
|
3
|
+
* (shift-tab-and-mouse-wheel-dead-on-some-terminals, F1')
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY - this tree is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* On Windows the answer is decided by NODE, not by the terminal emulator and not
|
|
8
|
+
* by anything this package writes. `Shift+Tab` and SGR mouse reporting both
|
|
9
|
+
* exist only while the console input handle has `ENABLE_VIRTUAL_TERMINAL_INPUT`
|
|
10
|
+
* set, and Node only sets it from v22.17.0 / v24.2.0 onward, where
|
|
11
|
+
* `SetRawMode` switched from `UV_TTY_MODE_RAW` to `UV_TTY_MODE_RAW_VT`
|
|
12
|
+
* (`nodejs/node` `src/tty_wrap.cc`). Before that libuv translates console input
|
|
13
|
+
* records into ANSI itself, and that translation
|
|
14
|
+
*
|
|
15
|
+
* - has no `VK_TAB` entry, and takes the character branch for it because
|
|
16
|
+
* `uChar.UnicodeChar` is 0x09, where SHIFT_PRESSED is never read. So
|
|
17
|
+
* `Shift+Tab` arrives as a PLAIN TAB, byte-identical to `Tab`;
|
|
18
|
+
* - `continue`s past every record that is not a `KEY_EVENT`, so a mouse
|
|
19
|
+
* report cannot reach the process at all.
|
|
20
|
+
*
|
|
21
|
+
* Measured rather than reasoned: injecting the same keystrokes into the same
|
|
22
|
+
* console, Node 20.19.0 yields `09` for `Shift+Tab` and Node 22.18.0 yields
|
|
23
|
+
* `1b 5b 5a`; console mode samples `0x0008` and `0x0208` respectively. The full
|
|
24
|
+
* probe data is in `docs/diagnoses/shift-tab-and-mouse-wheel-dead-on-some-terminals/`.
|
|
25
|
+
*
|
|
26
|
+
* PURE: the platform and the version are arguments, so the whole table is
|
|
27
|
+
* testable without a Windows box and without a second Node install.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* The versions whose `setRawMode(true)` reaches `UV_TTY_MODE_RAW_VT`.
|
|
31
|
+
*
|
|
32
|
+
* `<23` IS LOAD-BEARING: 23.x branched before the change landed and never got
|
|
33
|
+
* it, and 24.0/24.1 did not have it either - the fix reached the 24 line only in
|
|
34
|
+
* 24.2.0. A range of `>=22.17.0` alone would call both of those supported.
|
|
35
|
+
*
|
|
36
|
+
* A TYPO HERE DISABLES THE WHOLE GATE SILENTLY, because `satisfiesNodeRange`
|
|
37
|
+
* fails open: a clause it cannot parse makes the range unknown and the answer
|
|
38
|
+
* `true`. `win-vt-input.test.ts` pins concrete version strings against concrete
|
|
39
|
+
* booleans for exactly that reason - it is the only guard this constant has.
|
|
40
|
+
*/
|
|
41
|
+
export declare const WINDOWS_VT_INPUT_NODE_RANGE = ">=22.17.0 <23 || >=24.2.0";
|
|
42
|
+
/**
|
|
43
|
+
* `false` only for the combination that is known to swallow both inputs:
|
|
44
|
+
* `win32` running a Node outside `WINDOWS_VT_INPUT_NODE_RANGE`.
|
|
45
|
+
*
|
|
46
|
+
* FAILS OPEN, deliberately, in both directions (inherited from
|
|
47
|
+
* `satisfiesNodeRange`, whose own reasoning is written down at its definition):
|
|
48
|
+
* a version string that cannot be parsed is assumed supported. Guessing wrong
|
|
49
|
+
* that way costs nothing beyond today's behaviour; guessing wrong the other way
|
|
50
|
+
* would turn the mouse off on a machine where it works, which is a regression
|
|
51
|
+
* this bug does not justify risking.
|
|
52
|
+
*
|
|
53
|
+
* Every non-Windows platform is `true` unconditionally. macOS and Linux go
|
|
54
|
+
* through a real pty, where the terminal - not Node - encodes the keys, so no
|
|
55
|
+
* version of Node has ever been able to affect this.
|
|
56
|
+
*/
|
|
57
|
+
export declare function supportsWindowsVtInput(platform: string, nodeVersion: string): boolean;
|