@aragon-agent/cli 0.5.12 → 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.
Files changed (56) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/README.md +301 -6
  3. package/dist/agent/controller.d.ts +32 -0
  4. package/dist/agent/controller.js +25 -0
  5. package/dist/agent/system-prompt.d.ts +16 -0
  6. package/dist/agent/system-prompt.js +5 -0
  7. package/dist/cli.js +71 -2
  8. package/dist/config/ui-state.d.ts +18 -0
  9. package/dist/config/ui-state.js +10 -0
  10. package/dist/diagnostics/cli-commands.d.ts +10 -0
  11. package/dist/diagnostics/cli-commands.js +61 -0
  12. package/dist/diagnostics/doctor.d.ts +29 -0
  13. package/dist/diagnostics/doctor.js +167 -0
  14. package/dist/diagnostics/info.d.ts +30 -0
  15. package/dist/diagnostics/info.js +99 -0
  16. package/dist/exec/cli-commands.d.ts +31 -0
  17. package/dist/exec/cli-commands.js +63 -0
  18. package/dist/exec/emitter.d.ts +80 -0
  19. package/dist/exec/emitter.js +127 -0
  20. package/dist/exec/events.d.ts +226 -0
  21. package/dist/exec/events.js +43 -0
  22. package/dist/exec/index.d.ts +56 -0
  23. package/dist/exec/index.js +435 -0
  24. package/dist/exec/options.d.ts +76 -0
  25. package/dist/exec/options.js +221 -0
  26. package/dist/exec/permission.d.ts +66 -0
  27. package/dist/exec/permission.js +96 -0
  28. package/dist/exec/runner.d.ts +204 -0
  29. package/dist/exec/runner.js +559 -0
  30. package/dist/exec/stdin-stream.d.ts +46 -0
  31. package/dist/exec/stdin-stream.js +144 -0
  32. package/dist/logging/install.d.ts +22 -0
  33. package/dist/logging/install.js +22 -0
  34. package/dist/session/cli-commands.d.ts +30 -0
  35. package/dist/session/cli-commands.js +162 -0
  36. package/dist/session/persist.d.ts +46 -0
  37. package/dist/session/persist.js +8 -0
  38. package/dist/session/store.d.ts +129 -0
  39. package/dist/session/store.js +385 -0
  40. package/dist/team/runtime.d.ts +12 -0
  41. package/dist/team/runtime.js +6 -0
  42. package/dist/team/subagent.d.ts +12 -0
  43. package/dist/team/subagent.js +4 -0
  44. package/dist/tools/index.d.ts +20 -0
  45. package/dist/tools/index.js +26 -0
  46. package/dist/ui/ActivityLine.d.ts +16 -0
  47. package/dist/ui/ActivityLine.js +23 -1
  48. package/dist/ui/App.d.ts +14 -1
  49. package/dist/ui/App.js +38 -3
  50. package/dist/ui/BottomStatusRow.d.ts +33 -1
  51. package/dist/ui/BottomStatusRow.js +20 -6
  52. package/dist/ui/use-startup-notices.d.ts +67 -4
  53. package/dist/ui/use-startup-notices.js +76 -2
  54. package/dist/ui/win-vt-input.d.ts +57 -0
  55. package/dist/ui/win-vt-input.js +62 -0
  56. package/package.json +7 -3
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(() => {
@@ -1434,6 +1450,17 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
1434
1450
  // is suppressed, so keying suppression off `running` would leave the frame
1435
1451
  // with ZERO life signals: the bar shows state, this row shows life. Never
1436
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.
1437
1464
  const activityVisible = running && !overlayNode;
1438
1465
  // The FOUR VIEW CONSUMERS (transcript x 2, team panel, todo panel) take this;
1439
1466
  // the activity line keeps the raw config flag (D-5), because it reads that
@@ -1483,6 +1510,14 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
1483
1510
  // drift apart, and the drift is silent in whichever branch is not
1484
1511
  // edited.
1485
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),
1486
1521
  // PRESENCE IS DECIDED HERE, NOT BY `UpdateLine` RETURNING `null`
1487
1522
  // (C-15 / cli-auto-update P0-1 / D-19). `BottomStatusRow` tests this
1488
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
- return _jsx(ToastStack, { toasts: toasts, theme: theme, mode: mode });
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.
@@ -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 — i.e. a filter was installed, not
26
- * merely that the config asked for one. A user on a terminal where R-1 fires
27
- * must never be given advice about a mode that is not running.
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 declare function useStartupNotices(dispatch: (action: ViewAction) => void, mouseNotice?: MouseNoticeOptions, retainNotice?: RetainNoticeOptions): void;
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
- export function useStartupNotices(dispatch, mouseNotice, retainNotice) {
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;
@@ -0,0 +1,62 @@
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
+ import { satisfiesNodeRange } from '../update/semver.js';
30
+ /**
31
+ * The versions whose `setRawMode(true)` reaches `UV_TTY_MODE_RAW_VT`.
32
+ *
33
+ * `<23` IS LOAD-BEARING: 23.x branched before the change landed and never got
34
+ * it, and 24.0/24.1 did not have it either - the fix reached the 24 line only in
35
+ * 24.2.0. A range of `>=22.17.0` alone would call both of those supported.
36
+ *
37
+ * A TYPO HERE DISABLES THE WHOLE GATE SILENTLY, because `satisfiesNodeRange`
38
+ * fails open: a clause it cannot parse makes the range unknown and the answer
39
+ * `true`. `win-vt-input.test.ts` pins concrete version strings against concrete
40
+ * booleans for exactly that reason - it is the only guard this constant has.
41
+ */
42
+ export const WINDOWS_VT_INPUT_NODE_RANGE = '>=22.17.0 <23 || >=24.2.0';
43
+ /**
44
+ * `false` only for the combination that is known to swallow both inputs:
45
+ * `win32` running a Node outside `WINDOWS_VT_INPUT_NODE_RANGE`.
46
+ *
47
+ * FAILS OPEN, deliberately, in both directions (inherited from
48
+ * `satisfiesNodeRange`, whose own reasoning is written down at its definition):
49
+ * a version string that cannot be parsed is assumed supported. Guessing wrong
50
+ * that way costs nothing beyond today's behaviour; guessing wrong the other way
51
+ * would turn the mouse off on a machine where it works, which is a regression
52
+ * this bug does not justify risking.
53
+ *
54
+ * Every non-Windows platform is `true` unconditionally. macOS and Linux go
55
+ * through a real pty, where the terminal - not Node - encodes the keys, so no
56
+ * version of Node has ever been able to affect this.
57
+ */
58
+ export function supportsWindowsVtInput(platform, nodeVersion) {
59
+ if (platform !== 'win32')
60
+ return true;
61
+ return satisfiesNodeRange(WINDOWS_VT_INPUT_NODE_RANGE, nodeVersion);
62
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aragon-agent/cli",
3
- "version": "0.5.12",
3
+ "version": "0.6.1",
4
4
  "description": "AragonAgent CLI — a Claude-Code / Codex-style interactive terminal UI (TUI) on top of the @aragon-agent/core engine. Installable and runnable via npm / npx.",
5
5
  "license": "MIT",
6
6
  "author": "AragonAgent contributors",
@@ -15,7 +15,11 @@
15
15
  "anthropic",
16
16
  "openai",
17
17
  "google",
18
- "aragonagent"
18
+ "aragonagent",
19
+ "exec",
20
+ "headless",
21
+ "automation",
22
+ "ci"
19
23
  ],
20
24
  "repository": {
21
25
  "type": "git",
@@ -52,7 +56,7 @@
52
56
  "prepublishOnly": "npm run build"
53
57
  },
54
58
  "dependencies": {
55
- "@aragon-agent/core": "^0.2.12",
59
+ "@aragon-agent/core": "^0.2.13",
56
60
  "chalk": "^5.6.2",
57
61
  "cli-highlight": "^2.0.0",
58
62
  "commander": "^12.1.0",