@aragon-agent/cli 0.6.29 → 0.6.32

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 (123) hide show
  1. package/CHANGELOG.md +2223 -2103
  2. package/README.md +2957 -2759
  3. package/dist/agent/controller.d.ts +46 -0
  4. package/dist/agent/controller.js +175 -7
  5. package/dist/agent/job-notices.d.ts +143 -0
  6. package/dist/agent/job-notices.js +182 -0
  7. package/dist/agent/project-guidance-prompt.js +20 -20
  8. package/dist/agent/reducer.d.ts +21 -1
  9. package/dist/agent/reducer.js +7 -0
  10. package/dist/agent/system-prompt.d.ts +12 -0
  11. package/dist/agent/system-prompt.js +2 -0
  12. package/dist/cli.js +399 -62
  13. package/dist/commands/builtins.js +27 -8
  14. package/dist/commands/perf.d.ts +79 -2
  15. package/dist/commands/perf.js +51 -3
  16. package/dist/commands/registry.d.ts +6 -0
  17. package/dist/config/cli-commands.d.ts +27 -0
  18. package/dist/config/cli-commands.js +54 -1
  19. package/dist/config/env.js +42 -0
  20. package/dist/config/load.d.ts +3 -1
  21. package/dist/config/load.js +61 -1
  22. package/dist/config/schema.d.ts +100 -0
  23. package/dist/config/schema.js +43 -0
  24. package/dist/config/store.js +14 -1
  25. package/dist/config/ui-state.d.ts +9 -0
  26. package/dist/config/ui-state.js +9 -0
  27. package/dist/exec/events.d.ts +29 -3
  28. package/dist/exec/events.js +3 -12
  29. package/dist/exec/runner.d.ts +24 -0
  30. package/dist/exec/runner.js +79 -0
  31. package/dist/input/cursor-report.d.ts +61 -0
  32. package/dist/input/cursor-report.js +97 -0
  33. package/dist/input/stdin-filter.d.ts +22 -0
  34. package/dist/input/stdin-filter.js +48 -3
  35. package/dist/proc/limits.d.ts +76 -2
  36. package/dist/proc/limits.js +83 -2
  37. package/dist/proc/log-mirror.d.ts +59 -0
  38. package/dist/proc/log-mirror.js +203 -0
  39. package/dist/proc/log-ring.d.ts +22 -0
  40. package/dist/proc/log-ring.js +20 -1
  41. package/dist/proc/notices.d.ts +57 -0
  42. package/dist/proc/notices.js +101 -0
  43. package/dist/proc/progress.d.ts +40 -0
  44. package/dist/proc/progress.js +135 -0
  45. package/dist/proc/prompt.d.ts +24 -2
  46. package/dist/proc/prompt.js +51 -2
  47. package/dist/proc/supervisor.d.ts +35 -2
  48. package/dist/proc/supervisor.js +142 -43
  49. package/dist/proc/types.d.ts +89 -1
  50. package/dist/proc/wait.d.ts +57 -0
  51. package/dist/proc/wait.js +133 -0
  52. package/dist/session/persist.js +3 -1
  53. package/dist/session/validate-session.js +17 -0
  54. package/dist/tools/bash-tool.d.ts +12 -1
  55. package/dist/tools/bash-tool.js +100 -13
  56. package/dist/tools/index.d.ts +26 -2
  57. package/dist/tools/index.js +43 -2
  58. package/dist/tools/job-tools.d.ts +54 -0
  59. package/dist/tools/job-tools.js +329 -0
  60. package/dist/tools/proc-tools.d.ts +1 -1
  61. package/dist/tools/proc-tools.js +49 -17
  62. package/dist/ui/ActivityLine.js +7 -0
  63. package/dist/ui/App.d.ts +11 -1
  64. package/dist/ui/App.js +181 -63
  65. package/dist/ui/ScrollbackShell.d.ts +54 -0
  66. package/dist/ui/ScrollbackShell.js +15 -0
  67. package/dist/ui/StatusBar.d.ts +5 -0
  68. package/dist/ui/StatusBar.js +1 -0
  69. package/dist/ui/Transcript.d.ts +14 -0
  70. package/dist/ui/Transcript.js +14 -2
  71. package/dist/ui/entries/ServiceCard.d.ts +17 -1
  72. package/dist/ui/entries/ServiceCard.js +39 -4
  73. package/dist/ui/frame-differ.d.ts +12 -0
  74. package/dist/ui/layout/status-layout.d.ts +13 -1
  75. package/dist/ui/layout/status-layout.js +10 -0
  76. package/dist/ui/layout/virtual-window.js +14 -1
  77. package/dist/ui/overlays/HelpOverlay.d.ts +1 -1
  78. package/dist/ui/overlays/HelpOverlay.js +21 -3
  79. package/dist/ui/run-status-row.d.ts +12 -0
  80. package/dist/ui/screen.d.ts +29 -0
  81. package/dist/ui/screen.js +40 -0
  82. package/dist/ui/scrollback/LiveTail.d.ts +64 -0
  83. package/dist/ui/scrollback/LiveTail.js +27 -0
  84. package/dist/ui/scrollback/band.d.ts +105 -0
  85. package/dist/ui/scrollback/band.js +109 -0
  86. package/dist/ui/scrollback/budget.d.ts +72 -0
  87. package/dist/ui/scrollback/budget.js +81 -0
  88. package/dist/ui/scrollback/canvas.d.ts +99 -0
  89. package/dist/ui/scrollback/canvas.js +113 -0
  90. package/dist/ui/scrollback/cursor-probe.d.ts +71 -0
  91. package/dist/ui/scrollback/cursor-probe.js +133 -0
  92. package/dist/ui/scrollback/drain.d.ts +32 -0
  93. package/dist/ui/scrollback/drain.js +64 -0
  94. package/dist/ui/scrollback/exit-summary.d.ts +23 -0
  95. package/dist/ui/scrollback/exit-summary.js +25 -0
  96. package/dist/ui/scrollback/limits.d.ts +74 -0
  97. package/dist/ui/scrollback/limits.js +74 -0
  98. package/dist/ui/scrollback/live-preview.d.ts +26 -0
  99. package/dist/ui/scrollback/live-preview.js +106 -0
  100. package/dist/ui/scrollback/renderer.d.ts +38 -0
  101. package/dist/ui/scrollback/renderer.js +158 -0
  102. package/dist/ui/scrollback/scrollback-writer.d.ts +162 -0
  103. package/dist/ui/scrollback/scrollback-writer.js +549 -0
  104. package/dist/ui/scrollback/settled-boundary.d.ts +42 -0
  105. package/dist/ui/scrollback/settled-boundary.js +106 -0
  106. package/dist/ui/scrollback/use-scrollback-view.d.ts +103 -0
  107. package/dist/ui/scrollback/use-scrollback-view.js +294 -0
  108. package/dist/ui/scrollback/writer-shapes.d.ts +127 -0
  109. package/dist/ui/scrollback/writer-shapes.js +159 -0
  110. package/dist/ui/stdout-frame-writer.js +12 -2
  111. package/dist/ui/terminal-plan.d.ts +52 -0
  112. package/dist/ui/terminal-plan.js +49 -0
  113. package/dist/ui/transcript-text.d.ts +11 -0
  114. package/dist/ui/transcript-text.js +21 -7
  115. package/dist/ui/use-startup-notices.d.ts +12 -1
  116. package/dist/ui/use-startup-notices.js +28 -2
  117. package/package.json +3 -3
  118. package/skills/project-indexer/SKILL.md +163 -163
  119. package/skills/project-indexer/templates/cleancode-overrides.md +46 -46
  120. package/skills/project-indexer/templates/cleancode-template.md +94 -94
  121. package/skills/project-indexer/templates/config-template.md +69 -69
  122. package/skills/project-indexer/templates/index-template.md +129 -129
  123. package/skills/skill-creator/SKILL.md +96 -96
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Byte shapes of the native-scroll writer (tui-native-terminal-scroll §4.3 /
3
+ * Appendix A). Stateless string assembly only — the state machine that decides
4
+ * WHICH shape to emit lives in `scrollback-writer.ts`.
5
+ *
6
+ * THE WHITELIST (I-S5 / I-S6). Every byte produced here is drawn from: SGR, CUU,
7
+ * CUD, CR, CRLF, ED0, EL0, DEC 2026 and DECTCEM. There is no absolute CUP, no
8
+ * scroll region, no `CSI S/T`, no `ED2/ED3`. Relative addressing is what makes the
9
+ * geometry independent of the terminal scrolling underneath it (spec appendix B).
10
+ *
11
+ * Each row is `SGR0 · text · SGR0`: the leading reset stops one row's style leaking
12
+ * into the next, and the trailing one stops a background-colour-erase terminal from
13
+ * painting the following ED0/EL0 in the row's last colour.
14
+ */
15
+ import stringWidth from 'string-width';
16
+ import stripAnsi from 'strip-ansi';
17
+ const CSI = '\x1b[';
18
+ export const SGR0 = `${CSI}0m`;
19
+ const ED0 = `${CSI}J`;
20
+ const EL0 = `${CSI}K`;
21
+ const SYNC_BEGIN = `${CSI}?2026h`;
22
+ const SYNC_END = `${CSI}?2026l`;
23
+ const SHOW_CURSOR = `${CSI}?25h`;
24
+ const OSC8_CLOSE = '\x1b]8;;\x07';
25
+ const up = (n) => (n > 0 ? `${CSI}${n}A` : '');
26
+ const down = (n) => (n > 0 ? `${CSI}${n}B` : '');
27
+ const row = (text) => SGR0 + text + SGR0 + '\r\n';
28
+ const rows = (list) => list.map(row).join('');
29
+ const wrapSync = (payload, sync) => sync ? SYNC_BEGIN + payload + SYNC_END : payload;
30
+ /** First write of a session: the cursor is already on a fresh line at column 0. */
31
+ export function seedBytes(input) {
32
+ const lead = input.reseed ? '\r\n' : '';
33
+ return wrapSync(lead + '\r' + SGR0 + rows(input.statics) + rows(input.lines), input.sync);
34
+ }
35
+ /**
36
+ * CUD(n): move down n rows WITHOUT writing a single cell.
37
+ *
38
+ * This is how the canvas spends its `gap` (D3). A blank row that is never written
39
+ * cannot be pushed into the scrollback by a later overflow, which is the whole
40
+ * reason the gap is not a run of `\r\n` (I-B3). The sequence is already whitelisted
41
+ * (`diffBytes` uses it to step between changed rows); this is the same CUD used to
42
+ * skip empty space instead.
43
+ */
44
+ export function skipRows(rows) {
45
+ return down(Math.max(0, Math.floor(Number.isFinite(rows) ? rows : 0)));
46
+ }
47
+ /**
48
+ * First canvas batch (or the first one after a reset): the screen below the cursor
49
+ * is assumed empty, so there is nothing to erase — only `[gap][content][region]` to
50
+ * write, ending with the cursor parked on the last row.
51
+ */
52
+ export function canvasSeedBytes(input) {
53
+ const lead = input.reseed ? '\r\n' : '';
54
+ const skip = skipRows(input.downToTop) + skipRows(input.gap);
55
+ return wrapSync(lead + '\r' + SGR0 + skip + rows(input.held) + rows(input.lines), input.sync);
56
+ }
57
+ /**
58
+ * Redraw the whole canvas: erase the previous one, then write `[gap][content][region]`.
59
+ *
60
+ * ORDER IS SEMANTICS: `ED0`, then the gap, then the content, then the region. Moving
61
+ * the gap after the content puts the blank space back between the reply and the
62
+ * composer, which is the exact defect this feature fixes (M1 in the spec).
63
+ *
64
+ * `held` is the FULL content sequence, not the part that will still be on screen: the
65
+ * rows that do not fit are supposed to leave through the bottom, and the terminal can
66
+ * only scroll away what it was given.
67
+ */
68
+ export function canvasBytes(input) {
69
+ return wrapSync(up(input.eraseUp) + '\r' + SGR0 + ED0 + skipRows(input.gap) +
70
+ rows(input.held) + rows(input.lines), input.sync);
71
+ }
72
+ /** Erase exactly the previous live region, print static rows, redraw the region. */
73
+ export function fullBytes(input) {
74
+ return wrapSync(up(input.eraseUp) + '\r' + SGR0 + ED0 + rows(input.statics) + rows(input.lines), input.sync);
75
+ }
76
+ /**
77
+ * Fill the screen down to the LAST row before the first batch (D4 rung 2 / §4.3).
78
+ *
79
+ * This is the fallback for a terminal that never answers `ESC[6n`: from any row, a
80
+ * run of `rows - 1` line feeds is guaranteed to leave the cursor on the bottom row
81
+ * (extra feeds only scroll, which is why the count is the caller's business). It is
82
+ * written ONCE, before anything else, so the rows it scrolls away are the host's old
83
+ * output — which is still in the scrollback — and never this session's content.
84
+ *
85
+ * The argument is the NUMBER OF LINE FEEDS, not the terminal height: `cli.tsx` passes
86
+ * `rows - 1`. Naming it after the height instead made the caller subtract twice and
87
+ * left the cursor one row short of the bottom.
88
+ */
89
+ export function fillBytes(count) {
90
+ return '\r\n'.repeat(Math.max(0, Math.floor(count)));
91
+ }
92
+ /** Same height, no static rows: rewrite only the rows that changed. `''` if none did. */
93
+ export function diffBytes(input) {
94
+ const { lines, prev } = input;
95
+ const height = lines.length;
96
+ let out = '';
97
+ let cursor = -1;
98
+ for (let i = 0; i < height; i += 1) {
99
+ if (lines[i] === prev[i])
100
+ continue;
101
+ // Paint, THEN clear the tail: never ESC[2K first, which would blank the row
102
+ // the user is looking at for the instant between the two writes.
103
+ out += (cursor < 0 ? up(height - i) : down(i - cursor)) + '\r' + SGR0 + lines[i] + SGR0 + EL0;
104
+ cursor = i;
105
+ }
106
+ if (cursor < 0)
107
+ return '';
108
+ // Park on the row below the region, column 0 (I-S4).
109
+ return wrapSync(out + down(height - cursor) + '\r', input.sync);
110
+ }
111
+ /**
112
+ * Exit: erase the live region, flush leftover static rows, print the summary.
113
+ * `eraseUp < 0` means the geometry is unknown (a foreign write intervened), in
114
+ * which case the only safe move is to start on a fresh line and erase nothing.
115
+ */
116
+ export function finalizeBytes(input) {
117
+ const head = input.eraseUp < 0 ? '\r\n' : up(input.eraseUp) + '\r' + SGR0 + ED0;
118
+ return head + rows(input.statics) + rows(input.summary) + SHOW_CURSOR;
119
+ }
120
+ const ESCAPE = /(\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\))/;
121
+ const segmenter = typeof Intl !== 'undefined' && 'Segmenter' in Intl
122
+ ? new Intl.Segmenter(undefined, { granularity: 'grapheme' })
123
+ : null;
124
+ function graphemes(text) {
125
+ return segmenter ? Array.from(segmenter.segment(text), (part) => part.segment) : Array.from(text);
126
+ }
127
+ /**
128
+ * Cut a row to `maxCols` DISPLAY columns (I-S13), keeping SGR / OSC 8 intact.
129
+ *
130
+ * Returns the input unchanged (same reference) when it already fits, which is the
131
+ * overwhelmingly common case. Width is measured the way Ink measures it
132
+ * (`string-width` over the ANSI-stripped text): that is the number Ink used to
133
+ * wrap, so a disagreement with the terminal is the only way a row can exceed it.
134
+ */
135
+ export function clipLine(line, maxCols) {
136
+ if (maxCols < 1)
137
+ return { text: '', clipped: line.length > 0 };
138
+ if (stringWidth(stripAnsi(line)) <= maxCols)
139
+ return { text: line, clipped: false };
140
+ let out = '';
141
+ let width = 0;
142
+ let linkOpen = false;
143
+ for (const part of line.split(ESCAPE)) {
144
+ if (part.startsWith('\x1b')) {
145
+ out += part;
146
+ if (part.startsWith('\x1b]8;'))
147
+ linkOpen = !/^\x1b\]8;[^;]*;(?:\x07|\x1b\\)$/.test(part);
148
+ continue;
149
+ }
150
+ for (const glyph of graphemes(part)) {
151
+ const glyphWidth = stringWidth(glyph);
152
+ if (width + glyphWidth > maxCols)
153
+ return { text: out + SGR0 + (linkOpen ? OSC8_CLOSE : ''), clipped: true };
154
+ out += glyph;
155
+ width += glyphWidth;
156
+ }
157
+ }
158
+ return { text: out, clipped: false };
159
+ }
@@ -97,7 +97,14 @@ export function wrapStdoutForFrames(stdout, differ) {
97
97
  }
98
98
  return writeReal(out, encoding, cb);
99
99
  };
100
- const onResize = () => differ.invalidate();
100
+ // Optional differ hooks keep the full-screen path byte-identical: a differ that
101
+ // does not implement them is invalidated exactly as before.
102
+ const onResize = () => {
103
+ if (differ.onResize)
104
+ differ.onResize();
105
+ else
106
+ differ.invalidate();
107
+ };
101
108
  stdout.on('resize', onResize);
102
109
  let disposed = false;
103
110
  const proxy = new Proxy(stdout, {
@@ -130,7 +137,10 @@ export function wrapStdoutForFrames(stdout, differ) {
130
137
  return true;
131
138
  },
132
139
  writeForeign(text) {
133
- differ.invalidate();
140
+ if (differ.cursorNeutralWrite)
141
+ differ.cursorNeutralWrite();
142
+ else
143
+ differ.invalidate();
134
144
  if (disposed || text.length === 0)
135
145
  return;
136
146
  writeReal(text);
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Terminal plan — the ONE decision point for how the interactive TUI occupies the
3
+ * terminal (tui-native-terminal-scroll §4.1 / D10).
4
+ *
5
+ * `cli.tsx::runInteractive` used to derive `wantMouse`, `mouseOn`, `wantSelect`,
6
+ * `pasteOn` and `enhancedKeysOn` inline, each with its own comment about why the
7
+ * others must agree. Adding a second screen mode multiplies that surface, so the
8
+ * agreement is computed here, once, and the call site only READS the result.
9
+ *
10
+ * PURE: no I/O, no `process` access, so every row of the rules below is a table
11
+ * test with plain literals.
12
+ *
13
+ * I-S9 · `app` reproduces the pre-feature expressions term for term. The
14
+ * regression test diffs this function against a verbatim copy of those
15
+ * expressions, because "the same by inspection" is how mouse and paste drifted
16
+ * apart the last time.
17
+ */
18
+ import type { ScrollMode } from '../config/schema.js';
19
+ export type { ScrollMode } from '../config/schema.js';
20
+ export interface TerminalPlanInput {
21
+ /** The resolved `scrollMode` config value. */
22
+ scrollMode: ScrollMode;
23
+ configMouse: boolean;
24
+ configMouseSelect: boolean;
25
+ configPaste: boolean;
26
+ configKeyboardEnhancement: boolean;
27
+ configDiffRender: boolean;
28
+ stdinIsTTY: boolean;
29
+ stdoutIsTTY: boolean;
30
+ platform: NodeJS.Platform;
31
+ /** Measured on win32 (`forceVtInputForThisConsole`); irrelevant elsewhere. */
32
+ vtInputSupported: boolean;
33
+ }
34
+ export interface TerminalPlan {
35
+ scrollMode: ScrollMode;
36
+ /** `app`: enter the alternate screen. `native`: never. */
37
+ altScreen: boolean;
38
+ /** Whether the terminal is asked for SGR mouse reports (and a mouse filter is wanted). */
39
+ mouseReporting: boolean;
40
+ /** Application-drawn drag selection. Always `false` in native mode (the terminal selects). */
41
+ dragSelect: boolean;
42
+ bracketedPaste: boolean;
43
+ enhancedKeys: boolean;
44
+ /**
45
+ * Which frame writer fronts Ink's stdout. `native` ALWAYS needs the scrollback
46
+ * writer, even with `diffRender` off: static rows are injected through it.
47
+ */
48
+ differ: 'fullscreen' | 'scrollback' | 'none';
49
+ /** Native + `diffRender:false` — every frame rewrites the whole live region. */
50
+ fullEveryFrame: boolean;
51
+ }
52
+ export declare function resolveTerminalPlan(input: TerminalPlanInput): TerminalPlan;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Terminal plan — the ONE decision point for how the interactive TUI occupies the
3
+ * terminal (tui-native-terminal-scroll §4.1 / D10).
4
+ *
5
+ * `cli.tsx::runInteractive` used to derive `wantMouse`, `mouseOn`, `wantSelect`,
6
+ * `pasteOn` and `enhancedKeysOn` inline, each with its own comment about why the
7
+ * others must agree. Adding a second screen mode multiplies that surface, so the
8
+ * agreement is computed here, once, and the call site only READS the result.
9
+ *
10
+ * PURE: no I/O, no `process` access, so every row of the rules below is a table
11
+ * test with plain literals.
12
+ *
13
+ * I-S9 · `app` reproduces the pre-feature expressions term for term. The
14
+ * regression test diffs this function against a verbatim copy of those
15
+ * expressions, because "the same by inspection" is how mouse and paste drifted
16
+ * apart the last time.
17
+ */
18
+ export function resolveTerminalPlan(input) {
19
+ const bothTty = input.stdinIsTTY && input.stdoutIsTTY;
20
+ // Paste and keyboard enhancement ride the stdin filter and say nothing about
21
+ // scrolling, so the two modes agree on them (rule 3).
22
+ const bracketedPaste = input.configPaste && input.stdinIsTTY;
23
+ const enhancedKeys = input.configKeyboardEnhancement &&
24
+ input.stdinIsTTY &&
25
+ (input.platform !== 'win32' || input.vtInputSupported);
26
+ if (input.scrollMode === 'native') {
27
+ return {
28
+ scrollMode: 'native',
29
+ altScreen: false,
30
+ mouseReporting: false,
31
+ dragSelect: false,
32
+ bracketedPaste,
33
+ enhancedKeys,
34
+ differ: 'scrollback',
35
+ fullEveryFrame: !input.configDiffRender,
36
+ };
37
+ }
38
+ const mouseReporting = input.configMouse && bothTty && input.vtInputSupported;
39
+ return {
40
+ scrollMode: 'app',
41
+ altScreen: true,
42
+ mouseReporting,
43
+ dragSelect: mouseReporting && input.configMouseSelect,
44
+ bracketedPaste,
45
+ enhancedKeys,
46
+ differ: input.configDiffRender ? 'fullscreen' : 'none',
47
+ fullEveryFrame: false,
48
+ };
49
+ }
@@ -26,5 +26,16 @@ export interface TranscriptTextOptions {
26
26
  */
27
27
  droppedEntries?: number;
28
28
  }
29
+ /**
30
+ * The three figures every session summary carries: turns, tokens, cost.
31
+ *
32
+ * One implementation for the exit replay and the native-scroll exit summary
33
+ * (tui-native-terminal-scroll §4.8), so the two can never disagree.
34
+ */
35
+ export declare function formatSessionStats(entries: readonly Entry[], usageTotal: UsageTotal, g: Pick<Glyphs, 'arrowUp' | 'arrowDown'>): {
36
+ turns: string;
37
+ tokens: string;
38
+ cost: string;
39
+ };
29
40
  /** Render the whole session (summary line + entries) as plain text. */
30
41
  export declare function renderTranscriptText(entries: Entry[], opts: TranscriptTextOptions): string;
@@ -198,21 +198,35 @@ function renderEntry(entry, glyphs) {
198
198
  return [];
199
199
  }
200
200
  }
201
- /** Render the whole session (summary line + entries) as plain text. */
202
- export function renderTranscriptText(entries, opts) {
201
+ /**
202
+ * The three figures every session summary carries: turns, tokens, cost.
203
+ *
204
+ * One implementation for the exit replay and the native-scroll exit summary
205
+ * (tui-native-terminal-scroll §4.8), so the two can never disagree.
206
+ */
207
+ export function formatSessionStats(entries, usageTotal, g) {
203
208
  const turns = entries.filter((e) => e.kind === 'user').length;
204
- const g = opts.glyphs;
205
209
  // THE SAME PROMPT-SIDE TOTAL THE STATUS BAR SHOWS (context-usage-gauge-accuracy
206
210
  // §3.6). This line is what a user compares against the bar they were watching a
207
211
  // second earlier, so a different unit here would look like the exit summary
208
212
  // losing tokens.
209
- const tokens = `${formatTokens(promptTokensOf(opts.usageTotal))}${g.arrowUp} ${formatTokens(opts.usageTotal.outputTokens)}${g.arrowDown}`;
213
+ const tokens = `${formatTokens(promptTokensOf(usageTotal))}${g.arrowUp} ${formatTokens(usageTotal.outputTokens)}${g.arrowDown}`;
214
+ return {
215
+ turns: `${turns} ${turns === 1 ? 'turn' : 'turns'}`,
216
+ tokens,
217
+ cost: formatCost(usageTotal.costUsd),
218
+ };
219
+ }
220
+ /** Render the whole session (summary line + entries) as plain text. */
221
+ export function renderTranscriptText(entries, opts) {
222
+ const g = opts.glyphs;
223
+ const stats = formatSessionStats(entries, opts.usageTotal, g);
210
224
  const summary = [
211
225
  'aragon',
212
226
  `${opts.provider}:${opts.model}`,
213
- `${turns} ${turns === 1 ? 'turn' : 'turns'}`,
214
- tokens,
215
- formatCost(opts.usageTotal.costUsd),
227
+ stats.turns,
228
+ stats.tokens,
229
+ stats.cost,
216
230
  formatDuration(Math.max(0, opts.elapsedMs)),
217
231
  ].join(` ${g.midDot} `);
218
232
  const max = opts.maxEntries ?? DEFAULT_MAX_ENTRIES;
@@ -53,6 +53,17 @@ export interface RetainNoticeOptions {
53
53
  requested: number;
54
54
  resolved: number;
55
55
  }
56
+ /**
57
+ * The revision of the native-scroll notice a user must have seen for it to stay
58
+ * quiet (`state.json::scrollModeNoticeVersion`). BUMP IT WHENEVER THE ADVICE
59
+ * CHANGES, never for a rewording (tui-native-terminal-scroll D12).
60
+ */
61
+ export declare const SCROLL_MODE_NOTICE_VERSION = 1;
62
+ export declare function scrollModeNoticeText(): string;
63
+ export interface ScrollModeNoticeOptions {
64
+ /** Record that `SCROLL_MODE_NOTICE_VERSION` was shown (`state.json`). */
65
+ onSeen: () => void;
66
+ }
56
67
  export interface MouseNoticeOptions {
57
68
  /**
58
69
  * Whether reporting is ACTUALLY in effect: a filter was installed AND the
@@ -98,4 +109,4 @@ export interface VtInputNoticeOptions {
98
109
  */
99
110
  onSeen: () => void;
100
111
  }
101
- export declare function useStartupNotices(dispatch: (action: ViewAction) => void, mouseNotice?: MouseNoticeOptions, retainNotice?: RetainNoticeOptions, vtInputNotice?: VtInputNoticeOptions): void;
112
+ export declare function useStartupNotices(dispatch: (action: ViewAction) => void, mouseNotice?: MouseNoticeOptions, retainNotice?: RetainNoticeOptions, vtInputNotice?: VtInputNoticeOptions, scrollModeNotice?: ScrollModeNoticeOptions): void;
@@ -14,7 +14,7 @@
14
14
  import { useEffect, useRef } from 'react';
15
15
  import { MODE_TOGGLE_KEYS } from '../agent/agent-mode.js';
16
16
  import { readConfigFile } from '../config/store.js';
17
- import { getMouseNoticeVersion, getVtInputNoticeVersion } from '../config/ui-state.js';
17
+ import { getMouseNoticeVersion, getScrollModeNoticeVersion, getVtInputNoticeVersion, } from '../config/ui-state.js';
18
18
  import { getLogger } from '../logging/logger.js';
19
19
  /**
20
20
  * The revision of the mouse notice a user must have seen for it to stay quiet
@@ -93,7 +93,17 @@ export function vtInputDeadNotice(nodeVersion) {
93
93
  `as a PLAIN TAB - ${cost}${toggle}, and Shift+Up / Shift+Down to scroll. ` +
94
94
  'Node 22.17.0+ (or 24.2.0+) restores both.');
95
95
  }
96
- export function useStartupNotices(dispatch, mouseNotice, retainNotice, vtInputNotice) {
96
+ /**
97
+ * The revision of the native-scroll notice a user must have seen for it to stay
98
+ * quiet (`state.json::scrollModeNoticeVersion`). BUMP IT WHENEVER THE ADVICE
99
+ * CHANGES, never for a rewording (tui-native-terminal-scroll D12).
100
+ */
101
+ export const SCROLL_MODE_NOTICE_VERSION = 1;
102
+ export function scrollModeNoticeText() {
103
+ return ('Scrolling now uses your terminal: wheel, scrollbar, selection and search all work as ' +
104
+ 'usual. Prefer the old full-screen view? Run: aragon config set scrollMode app');
105
+ }
106
+ export function useStartupNotices(dispatch, mouseNotice, retainNotice, vtInputNotice, scrollModeNotice) {
97
107
  // The sink gives up after repeated write failures (a read-only log directory,
98
108
  // a full disk). Silently logging nothing is exactly the state a user must not
99
109
  // discover later, from an empty file, while trying to explain a bug.
@@ -167,6 +177,22 @@ export function useStartupNotices(dispatch, mouseNotice, retainNotice, vtInputNo
167
177
  });
168
178
  vtNoticeRef.current?.onSeen();
169
179
  }, [vtNodeVersion, dispatch]);
180
+ // The one-shot native-scroll notice (D12): a default-behaviour change has to be
181
+ // discoverable and reversible. `>=`, like the notices above: a state file written
182
+ // by a newer build has already told this user.
183
+ const scrollNoticeRef = useRef(scrollModeNotice);
184
+ scrollNoticeRef.current = scrollModeNotice;
185
+ const scrollNoticeWanted = scrollModeNotice !== undefined;
186
+ const scrollNoticeShown = useRef(false);
187
+ useEffect(() => {
188
+ if (!scrollNoticeWanted || scrollNoticeShown.current)
189
+ return;
190
+ if (getScrollModeNoticeVersion() >= SCROLL_MODE_NOTICE_VERSION)
191
+ return;
192
+ scrollNoticeShown.current = true;
193
+ dispatch({ type: 'notice', level: 'info', text: scrollModeNoticeText() });
194
+ scrollNoticeRef.current?.onSeen();
195
+ }, [scrollNoticeWanted, dispatch]);
170
196
  // A `warn` notice rather than a toast: it is about a value the user typed, so
171
197
  // it belongs in the transcript where it can be scrolled back to, not in a row
172
198
  // that expires. DEPENDS ON THE TWO SCALARS, never on the options object —
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aragon-agent/cli",
3
- "version": "0.6.29",
3
+ "version": "0.6.32",
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",
@@ -56,7 +56,7 @@
56
56
  "prepublishOnly": "npm run build"
57
57
  },
58
58
  "dependencies": {
59
- "@aragon-agent/core": "^0.2.41",
59
+ "@aragon-agent/core": "^0.2.44",
60
60
  "chalk": "^5.6.2",
61
61
  "cli-highlight": "^2.0.0",
62
62
  "commander": "^12.1.0",
@@ -76,6 +76,6 @@
76
76
  "@types/react": "^18.3.0",
77
77
  "ink-testing-library": "^4.0.0",
78
78
  "typescript": "^5.9.3",
79
- "vitest": "^4.1.0"
79
+ "vitest": "4.1.9"
80
80
  }
81
81
  }