@aragon-agent/cli 0.6.28 → 0.6.31

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 (80) hide show
  1. package/CHANGELOG.md +2123 -1975
  2. package/README.md +81 -14
  3. package/dist/agent/project-guidance-prompt.js +20 -20
  4. package/dist/agent/reducer.js +47 -2
  5. package/dist/cli.js +169 -56
  6. package/dist/commands/builtins.js +4 -0
  7. package/dist/commands/perf.d.ts +25 -1
  8. package/dist/commands/perf.js +26 -2
  9. package/dist/commands/registry.d.ts +6 -0
  10. package/dist/config/env.js +4 -0
  11. package/dist/config/load.d.ts +3 -1
  12. package/dist/config/load.js +12 -1
  13. package/dist/config/schema.d.ts +27 -0
  14. package/dist/config/schema.js +9 -0
  15. package/dist/config/ui-state.d.ts +9 -0
  16. package/dist/config/ui-state.js +9 -0
  17. package/dist/input/limits.d.ts +20 -0
  18. package/dist/input/limits.js +20 -0
  19. package/dist/input/stdin-filter.js +68 -5
  20. package/dist/ui/App.d.ts +11 -1
  21. package/dist/ui/App.js +155 -67
  22. package/dist/ui/AutocompletePopup.js +1 -1
  23. package/dist/ui/Header.js +7 -2
  24. package/dist/ui/Markdown.js +30 -3
  25. package/dist/ui/PromptInput.js +41 -3
  26. package/dist/ui/ScrollbackShell.d.ts +28 -0
  27. package/dist/ui/ScrollbackShell.js +10 -0
  28. package/dist/ui/Transcript.js +7 -0
  29. package/dist/ui/frame-differ.d.ts +12 -0
  30. package/dist/ui/layout/Gutter.js +1 -1
  31. package/dist/ui/layout/OverlayFrame.d.ts +15 -0
  32. package/dist/ui/layout/OverlayFrame.js +18 -0
  33. package/dist/ui/layout/ScrollIndicator.js +10 -3
  34. package/dist/ui/layout/budget.d.ts +15 -0
  35. package/dist/ui/layout/budget.js +15 -0
  36. package/dist/ui/layout/header-hints.d.ts +9 -0
  37. package/dist/ui/layout/header-hints.js +14 -1
  38. package/dist/ui/layout/status-layout.js +63 -2
  39. package/dist/ui/markdown-blocks.d.ts +26 -5
  40. package/dist/ui/markdown-blocks.js +110 -33
  41. package/dist/ui/overlays/HelpOverlay.d.ts +1 -1
  42. package/dist/ui/overlays/HelpOverlay.js +21 -3
  43. package/dist/ui/screen.d.ts +16 -0
  44. package/dist/ui/screen.js +32 -0
  45. package/dist/ui/scrollback/budget.d.ts +43 -0
  46. package/dist/ui/scrollback/budget.js +44 -0
  47. package/dist/ui/scrollback/drain.d.ts +32 -0
  48. package/dist/ui/scrollback/drain.js +64 -0
  49. package/dist/ui/scrollback/exit-summary.d.ts +23 -0
  50. package/dist/ui/scrollback/exit-summary.js +25 -0
  51. package/dist/ui/scrollback/limits.d.ts +25 -0
  52. package/dist/ui/scrollback/limits.js +25 -0
  53. package/dist/ui/scrollback/live-preview.d.ts +26 -0
  54. package/dist/ui/scrollback/live-preview.js +106 -0
  55. package/dist/ui/scrollback/renderer.d.ts +38 -0
  56. package/dist/ui/scrollback/renderer.js +158 -0
  57. package/dist/ui/scrollback/scrollback-writer.d.ts +74 -0
  58. package/dist/ui/scrollback/scrollback-writer.js +263 -0
  59. package/dist/ui/scrollback/settled-boundary.d.ts +42 -0
  60. package/dist/ui/scrollback/settled-boundary.js +106 -0
  61. package/dist/ui/scrollback/use-scrollback-view.d.ts +92 -0
  62. package/dist/ui/scrollback/use-scrollback-view.js +263 -0
  63. package/dist/ui/scrollback/writer-shapes.d.ts +61 -0
  64. package/dist/ui/scrollback/writer-shapes.js +114 -0
  65. package/dist/ui/stdout-frame-writer.js +12 -2
  66. package/dist/ui/terminal-plan.d.ts +52 -0
  67. package/dist/ui/terminal-plan.js +49 -0
  68. package/dist/ui/transcript-text.d.ts +11 -0
  69. package/dist/ui/transcript-text.js +21 -7
  70. package/dist/ui/use-startup-notices.d.ts +12 -1
  71. package/dist/ui/use-startup-notices.js +28 -2
  72. package/dist/ui/use-wheel-routing.d.ts +10 -7
  73. package/dist/ui/use-wheel-routing.js +11 -6
  74. package/package.json +2 -2
  75. package/skills/project-indexer/SKILL.md +163 -163
  76. package/skills/project-indexer/templates/cleancode-overrides.md +46 -46
  77. package/skills/project-indexer/templates/cleancode-template.md +94 -94
  78. package/skills/project-indexer/templates/config-template.md +69 -69
  79. package/skills/project-indexer/templates/index-template.md +129 -129
  80. package/skills/skill-creator/SKILL.md +96 -96
@@ -84,6 +84,12 @@ export interface UiState {
84
84
  * older file, which is the behaviour wanted here anyway.
85
85
  */
86
86
  vtInputNoticeVersion: number;
87
+ /**
88
+ * Which revision of the "scrolling now uses your terminal" notice this user has
89
+ * been shown (`0` = none; tui-native-terminal-scroll D12). A scalar beside the
90
+ * others for the same reasons the two above record, and NOT a schema bump.
91
+ */
92
+ scrollModeNoticeVersion: number;
87
93
  }
88
94
  /** Read the state, caching it for the life of the process. Never throws. */
89
95
  export declare function loadUiState(): UiState;
@@ -98,6 +104,9 @@ export declare function setMouseNoticeVersion(version: number): void;
98
104
  /** `0` on a file written before the field existed — see the field's comment. */
99
105
  export declare function getVtInputNoticeVersion(): number;
100
106
  export declare function setVtInputNoticeVersion(version: number): void;
107
+ /** `0` on a file written before the field existed. */
108
+ export declare function getScrollModeNoticeVersion(): number;
109
+ export declare function setScrollModeNoticeVersion(version: number): void;
101
110
  /**
102
111
  * Migration-only: fill in fields that are still at their default, and never
103
112
  * overwrite a value already here. A second import — after a half-finished
@@ -37,6 +37,7 @@ const DEFAULT_UI_STATE = {
37
37
  mouseNoticeSeen: false,
38
38
  mouseNoticeVersion: 0,
39
39
  vtInputNoticeVersion: 0,
40
+ scrollModeNoticeVersion: 0,
40
41
  };
41
42
  let memory = null;
42
43
  /** Non-negative integer or `fallback`; shared by both numeric fields. */
@@ -69,6 +70,7 @@ function readFromDisk() {
69
70
  : DEFAULT_UI_STATE.mouseNoticeSeen,
70
71
  mouseNoticeVersion: coerceCount(parsed.mouseNoticeVersion, DEFAULT_UI_STATE.mouseNoticeVersion),
71
72
  vtInputNoticeVersion: coerceCount(parsed.vtInputNoticeVersion, DEFAULT_UI_STATE.vtInputNoticeVersion),
73
+ scrollModeNoticeVersion: coerceCount(parsed.scrollModeNoticeVersion, DEFAULT_UI_STATE.scrollModeNoticeVersion),
72
74
  };
73
75
  }
74
76
  catch {
@@ -142,6 +144,13 @@ export function getVtInputNoticeVersion() {
142
144
  export function setVtInputNoticeVersion(version) {
143
145
  update({ vtInputNoticeVersion: version });
144
146
  }
147
+ /** `0` on a file written before the field existed. */
148
+ export function getScrollModeNoticeVersion() {
149
+ return loadUiState().scrollModeNoticeVersion;
150
+ }
151
+ export function setScrollModeNoticeVersion(version) {
152
+ update({ scrollModeNoticeVersion: version });
153
+ }
145
154
  /**
146
155
  * Migration-only: fill in fields that are still at their default, and never
147
156
  * overwrite a value already here. A second import — after a half-finished
@@ -39,6 +39,26 @@ export declare const PASTE_BURST_MS = 15;
39
39
  * which is the rule `MAX_PENDING_MOUSE_CHARS` already encodes for mouse reports.
40
40
  */
41
41
  export declare const PASTE_ASSEMBLY_MAX_MS = 2000;
42
+ /**
43
+ * How long a paste-marker head that was ALREADY released to the pass-through as
44
+ * ordinary text may still be recognised as the head of a marker (defect 2: the
45
+ * torn-marker rescue in `stdin-filter.ts`).
46
+ *
47
+ * STRUCTURAL, NOT POLICY: no config key exposes it, because it bounds what the
48
+ * byte stream can physically do -- a marker split across two reads -- and not
49
+ * what a user prefers. What it does NOT do is delay a keystroke: the 12 ms
50
+ * pending-flush window in `stdin-filter.ts` still releases a lone ESC exactly as
51
+ * before, and this window only governs whether that release may be spliced back
52
+ * in front of the next chunk so an assembled marker is consumed, not typed.
53
+ *
54
+ * THE FALSE POSITIVE IS THE TRADE-OFF, AND IT IS ACCEPTED. Firing the rescue
55
+ * wrongly needs the user to press Esc and then TYPE the literal characters
56
+ * `[200~` or `[201~` inside this window -- a sequence no keyboard produces, and
57
+ * at worst five characters of lost typing. Paying that buys a fix for visible
58
+ * `[200~` / `[201~` garbage in the draft, which is the failure the whole paste
59
+ * feature exists to prevent.
60
+ */
61
+ export declare const PASTE_MARKER_RESCAN_TTL_MS = 250;
42
62
  /**
43
63
  * The inline frame the filter writes into the stream Ink reads (D-3 / I-5).
44
64
  *
@@ -39,6 +39,26 @@ export const PASTE_BURST_MS = 15;
39
39
  * which is the rule `MAX_PENDING_MOUSE_CHARS` already encodes for mouse reports.
40
40
  */
41
41
  export const PASTE_ASSEMBLY_MAX_MS = 2_000;
42
+ /**
43
+ * How long a paste-marker head that was ALREADY released to the pass-through as
44
+ * ordinary text may still be recognised as the head of a marker (defect 2: the
45
+ * torn-marker rescue in `stdin-filter.ts`).
46
+ *
47
+ * STRUCTURAL, NOT POLICY: no config key exposes it, because it bounds what the
48
+ * byte stream can physically do -- a marker split across two reads -- and not
49
+ * what a user prefers. What it does NOT do is delay a keystroke: the 12 ms
50
+ * pending-flush window in `stdin-filter.ts` still releases a lone ESC exactly as
51
+ * before, and this window only governs whether that release may be spliced back
52
+ * in front of the next chunk so an assembled marker is consumed, not typed.
53
+ *
54
+ * THE FALSE POSITIVE IS THE TRADE-OFF, AND IT IS ACCEPTED. Firing the rescue
55
+ * wrongly needs the user to press Esc and then TYPE the literal characters
56
+ * `[200~` or `[201~` inside this window -- a sequence no keyboard produces, and
57
+ * at worst five characters of lost typing. Paying that buys a fix for visible
58
+ * `[200~` / `[201~` garbage in the draft, which is the failure the whole paste
59
+ * feature exists to prevent.
60
+ */
61
+ export const PASTE_MARKER_RESCAN_TTL_MS = 250;
42
62
  /**
43
63
  * The inline frame the filter writes into the stream Ink reads (D-3 / I-5).
44
64
  *
@@ -33,8 +33,8 @@
33
33
  */
34
34
  import { PassThrough } from 'node:stream';
35
35
  import { MAX_PENDING_MOUSE_CHARS, splitMouseEvents, isMousePrefix, } from './mouse-events.js';
36
- import { ENTER_NEWLINE_FRAME, INDEX_KEY_FRAME, PASTE_ASSEMBLY_MAX_MS, PASTE_BURST_MS, PASTE_CLOSE, PASTE_MAX_BYTES, PASTE_OPEN, formatPasteSize, } from './limits.js';
37
- import { PASTE_BEGIN_MARK, PASTE_END_MARK, classifyChunk, sanitisePaste, trailingPastePrefixLength, } from './paste-parse.js';
36
+ import { ENTER_NEWLINE_FRAME, INDEX_KEY_FRAME, PASTE_ASSEMBLY_MAX_MS, PASTE_BURST_MS, PASTE_CLOSE, PASTE_MARKER_RESCAN_TTL_MS, PASTE_MAX_BYTES, PASTE_OPEN, formatPasteSize, } from './limits.js';
37
+ import { PASTE_BEGIN_MARK, PASTE_END_MARK, classifyChunk, isPastePrefix, sanitisePaste, trailingPastePrefixLength, } from './paste-parse.js';
38
38
  import { hasPasteOnlySequence } from './control-sequences.js';
39
39
  import { splitEnterSequences, trailingEnterPrefixLength } from './enter-sequences.js';
40
40
  import { splitCsiUKeys, trailingCsiUPrefixLength, translateCsiUKey } from './csiu-keys.js';
@@ -97,6 +97,23 @@ export function createStdinFilter(real, features, bridge, onWarn) {
97
97
  let paste = { kind: 'idle' };
98
98
  let discardBracketed = false;
99
99
  let outputBatch = null;
100
+ /**
101
+ * A marker head that has ALREADY been released to Ink, kept for one more
102
+ * chunk (defect 2: the torn-marker rescue in `onData`).
103
+ *
104
+ * WHY IT EXISTS. `flushPending` releases a lone ESC after 12 ms because the
105
+ * Esc key must not be held hostage. When the split point lands inside a paste
106
+ * marker -- `ESC` in one read and `[200~hello` in the next, which is ordinary
107
+ * for a Windows console, SSH or tmux under load -- `feed` can no longer find
108
+ * the marker and `[200~` is typed into the draft. Nothing downstream can undo
109
+ * that: the marker has to be recognised BEFORE its bytes become text.
110
+ *
111
+ * WHAT IT CANNOT DO, STATED HONESTLY. The released ESC has already reached Ink
112
+ * as an Esc key press and this filter cannot retract a write. The rescue
113
+ * guarantees the marker CHARACTERS never become draft text; it does NOT undo
114
+ * that keypress (an armed interrupt gesture, a dismissed popup).
115
+ */
116
+ let rescuedPrefix = null;
100
117
  const writeInput = (text) => {
101
118
  if (disposed || outputBatch?.refused)
102
119
  return;
@@ -417,6 +434,18 @@ export function createStdinFilter(real, features, bridge, onWarn) {
417
434
  rest = rest.slice(first + PASTE_BEGIN_MARK.length);
418
435
  }
419
436
  };
437
+ /**
438
+ * Hand a held tail to the ordinary text path, remembering it when it could
439
+ * still be the head of a paste marker.
440
+ *
441
+ * THE TAIL IS RELEASED EXACTLY AS BEFORE. Remembering it costs no latency: a
442
+ * real Esc keypress still reaches Ink after `PENDING_FLUSH_MS`, and the
443
+ * remembered head is only ever consulted for the NEXT chunk.
444
+ */
445
+ const flushTailAsText = (text) => {
446
+ rescuedPrefix = isPastePrefix(text) ? { text, at: Date.now() } : null;
447
+ handleOutsidePaste(text, false);
448
+ };
420
449
  const flushPending = () => {
421
450
  flushTimer = clearTimer(flushTimer);
422
451
  if (pending.length === 0 || disposed)
@@ -425,20 +454,54 @@ export function createStdinFilter(real, features, bridge, onWarn) {
425
454
  pending = '';
426
455
  // A tail that never completed is ordinary text; it goes through the same
427
456
  // scan so a burst already open keeps coalescing it.
428
- handleOutsidePaste(text, false);
457
+ flushTailAsText(text);
458
+ };
459
+ /**
460
+ * One-shot rescue of a marker whose ESC was already flushed as text.
461
+ *
462
+ * Returns the string `feed` should scan -- the remembered head followed by
463
+ * this chunk -- when the chunk COMPLETES that head into a whole marker, and
464
+ * `null` otherwise. On `null` the chunk travels the normal path: the head has
465
+ * already been emitted, so re-feeding it would type its bytes twice, and
466
+ * holding the chunk instead would swallow characters the user really typed.
467
+ *
468
+ * STARTS WITH, NOT CONTAINS. A marker completed at the junction starts at
469
+ * index 0 of the joined text; one starting anywhere later lies wholly inside
470
+ * `chunk` (the head holds an ESC at index 0 and nothing else that could begin
471
+ * a marker), and `feed` finds that on its own. Only the junction case needs
472
+ * re-scanning, and it is the only case where prepending cannot duplicate a
473
+ * byte that Ink has already seen.
474
+ *
475
+ * ONE-SHOT BY CONSTRUCTION: the remembered head is cleared here on every call,
476
+ * whether or not the rescue fired, so at most one chunk is ever re-scanned
477
+ * against it and no stale head can outlive its `PASTE_MARKER_RESCAN_TTL_MS`.
478
+ */
479
+ const takeRescuedPrefix = (carried, chunk) => {
480
+ const rescue = rescuedPrefix;
481
+ rescuedPrefix = null;
482
+ if (rescue === null || carried.length > 0 || !features.paste)
483
+ return null;
484
+ if (Date.now() - rescue.at > PASTE_MARKER_RESCAN_TTL_MS)
485
+ return null;
486
+ const joined = rescue.text + chunk;
487
+ const completes = joined.startsWith(PASTE_BEGIN_MARK) || joined.startsWith(PASTE_END_MARK);
488
+ return completes ? joined : null;
429
489
  };
430
490
  const onData = (chunk) => {
431
491
  if (disposed || finished)
432
492
  return;
433
493
  flushTimer = clearTimer(flushTimer);
434
494
  const decoded = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
495
+ // Stripped on BOTH paths, because a NUL in a payload must never be able to
496
+ // forge a `PASTE_OPEN` frame boundary.
497
+ const cleaned = decoded.replace(/\u0000/g, '');
435
498
  const carried = pending;
436
499
  pending = '';
437
500
  // A rejected paste must not leave earlier text or a later Enter executable.
438
501
  const batch = { chunks: [], refused: false };
439
502
  outputBatch = batch;
440
503
  try {
441
- feed(carried + decoded.replace(/\u0000/g, ''));
504
+ feed(takeRescuedPrefix(carried, cleaned) ?? (carried + cleaned));
442
505
  }
443
506
  finally {
444
507
  outputBatch = null;
@@ -468,7 +531,7 @@ export function createStdinFilter(real, features, bridge, onWarn) {
468
531
  // input: past the ceiling the tail becomes ordinary text.
469
532
  const text = pending;
470
533
  pending = '';
471
- handleOutsidePaste(text, false);
534
+ flushTailAsText(text);
472
535
  return;
473
536
  }
474
537
  flushTimer = arm(flushPending, PENDING_FLUSH_MS);
package/dist/ui/App.d.ts CHANGED
@@ -10,6 +10,8 @@ import type { AgentController } from '../agent/controller.js';
10
10
  import type { ConfirmRequest } from '../tools/index.js';
11
11
  import { type Entry, type Overlay, type ToastLevel } from '../agent/reducer.js';
12
12
  import type { UpdateBridge } from '../update/types.js';
13
+ import { type ScrollbackBridge } from './scrollback/use-scrollback-view.js';
14
+ import type { ScrollMode } from '../config/schema.js';
13
15
  import type { MouseSource } from '../input/stdin-filter.js';
14
16
  import { type PasteBridge } from '../input/limits.js';
15
17
  import type { CheckpointResult } from '../session/checkpoint.js';
@@ -86,6 +88,14 @@ export interface AppProps {
86
88
  */
87
89
  pasteBridge?: PasteBridge;
88
90
  terminal?: TerminalBridge;
91
+ /**
92
+ * `'native'` prints settled conversation into the terminal's own scrollback and
93
+ * renders only a bounded live region (tui-native-terminal-scroll). DEFAULTS TO
94
+ * `'app'` so every existing render test and call site is untouched (I-S9).
95
+ */
96
+ scrollMode?: ScrollMode;
97
+ /** The writer / feed channel `cli.tsx` shares with the tree; required for `'native'`. */
98
+ scrollback?: ScrollbackBridge;
89
99
  /**
90
100
  * The transcript to seed the first frame with, from a resumed session
91
101
  * (tui-session-id-resume 3.12).
@@ -148,4 +158,4 @@ export interface TerminalBridge {
148
158
  writeForeign?: (text: string) => void;
149
159
  selection?: SelectionBridge;
150
160
  }
151
- export declare function App({ controller, version, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, vtInputWarning, updateBridge, pasteBridge, terminal, initialEntries, session, sessionBridge, sessionNotice, }: AppProps): React.ReactElement;
161
+ export declare function App({ controller, version, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, vtInputWarning, updateBridge, pasteBridge, terminal, scrollMode, scrollback, initialEntries, session, sessionBridge, sessionNotice, }: AppProps): React.ReactElement;