@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.
- package/CHANGELOG.md +2223 -2103
- package/README.md +2957 -2759
- package/dist/agent/controller.d.ts +46 -0
- package/dist/agent/controller.js +175 -7
- package/dist/agent/job-notices.d.ts +143 -0
- package/dist/agent/job-notices.js +182 -0
- package/dist/agent/project-guidance-prompt.js +20 -20
- package/dist/agent/reducer.d.ts +21 -1
- package/dist/agent/reducer.js +7 -0
- package/dist/agent/system-prompt.d.ts +12 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/cli.js +399 -62
- package/dist/commands/builtins.js +27 -8
- package/dist/commands/perf.d.ts +79 -2
- package/dist/commands/perf.js +51 -3
- package/dist/commands/registry.d.ts +6 -0
- package/dist/config/cli-commands.d.ts +27 -0
- package/dist/config/cli-commands.js +54 -1
- package/dist/config/env.js +42 -0
- package/dist/config/load.d.ts +3 -1
- package/dist/config/load.js +61 -1
- package/dist/config/schema.d.ts +100 -0
- package/dist/config/schema.js +43 -0
- package/dist/config/store.js +14 -1
- package/dist/config/ui-state.d.ts +9 -0
- package/dist/config/ui-state.js +9 -0
- package/dist/exec/events.d.ts +29 -3
- package/dist/exec/events.js +3 -12
- package/dist/exec/runner.d.ts +24 -0
- package/dist/exec/runner.js +79 -0
- package/dist/input/cursor-report.d.ts +61 -0
- package/dist/input/cursor-report.js +97 -0
- package/dist/input/stdin-filter.d.ts +22 -0
- package/dist/input/stdin-filter.js +48 -3
- package/dist/proc/limits.d.ts +76 -2
- package/dist/proc/limits.js +83 -2
- package/dist/proc/log-mirror.d.ts +59 -0
- package/dist/proc/log-mirror.js +203 -0
- package/dist/proc/log-ring.d.ts +22 -0
- package/dist/proc/log-ring.js +20 -1
- package/dist/proc/notices.d.ts +57 -0
- package/dist/proc/notices.js +101 -0
- package/dist/proc/progress.d.ts +40 -0
- package/dist/proc/progress.js +135 -0
- package/dist/proc/prompt.d.ts +24 -2
- package/dist/proc/prompt.js +51 -2
- package/dist/proc/supervisor.d.ts +35 -2
- package/dist/proc/supervisor.js +142 -43
- package/dist/proc/types.d.ts +89 -1
- package/dist/proc/wait.d.ts +57 -0
- package/dist/proc/wait.js +133 -0
- package/dist/session/persist.js +3 -1
- package/dist/session/validate-session.js +17 -0
- package/dist/tools/bash-tool.d.ts +12 -1
- package/dist/tools/bash-tool.js +100 -13
- package/dist/tools/index.d.ts +26 -2
- package/dist/tools/index.js +43 -2
- package/dist/tools/job-tools.d.ts +54 -0
- package/dist/tools/job-tools.js +329 -0
- package/dist/tools/proc-tools.d.ts +1 -1
- package/dist/tools/proc-tools.js +49 -17
- package/dist/ui/ActivityLine.js +7 -0
- package/dist/ui/App.d.ts +11 -1
- package/dist/ui/App.js +181 -63
- package/dist/ui/ScrollbackShell.d.ts +54 -0
- package/dist/ui/ScrollbackShell.js +15 -0
- package/dist/ui/StatusBar.d.ts +5 -0
- package/dist/ui/StatusBar.js +1 -0
- package/dist/ui/Transcript.d.ts +14 -0
- package/dist/ui/Transcript.js +14 -2
- package/dist/ui/entries/ServiceCard.d.ts +17 -1
- package/dist/ui/entries/ServiceCard.js +39 -4
- package/dist/ui/frame-differ.d.ts +12 -0
- package/dist/ui/layout/status-layout.d.ts +13 -1
- package/dist/ui/layout/status-layout.js +10 -0
- package/dist/ui/layout/virtual-window.js +14 -1
- package/dist/ui/overlays/HelpOverlay.d.ts +1 -1
- package/dist/ui/overlays/HelpOverlay.js +21 -3
- package/dist/ui/run-status-row.d.ts +12 -0
- package/dist/ui/screen.d.ts +29 -0
- package/dist/ui/screen.js +40 -0
- package/dist/ui/scrollback/LiveTail.d.ts +64 -0
- package/dist/ui/scrollback/LiveTail.js +27 -0
- package/dist/ui/scrollback/band.d.ts +105 -0
- package/dist/ui/scrollback/band.js +109 -0
- package/dist/ui/scrollback/budget.d.ts +72 -0
- package/dist/ui/scrollback/budget.js +81 -0
- package/dist/ui/scrollback/canvas.d.ts +99 -0
- package/dist/ui/scrollback/canvas.js +113 -0
- package/dist/ui/scrollback/cursor-probe.d.ts +71 -0
- package/dist/ui/scrollback/cursor-probe.js +133 -0
- package/dist/ui/scrollback/drain.d.ts +32 -0
- package/dist/ui/scrollback/drain.js +64 -0
- package/dist/ui/scrollback/exit-summary.d.ts +23 -0
- package/dist/ui/scrollback/exit-summary.js +25 -0
- package/dist/ui/scrollback/limits.d.ts +74 -0
- package/dist/ui/scrollback/limits.js +74 -0
- package/dist/ui/scrollback/live-preview.d.ts +26 -0
- package/dist/ui/scrollback/live-preview.js +106 -0
- package/dist/ui/scrollback/renderer.d.ts +38 -0
- package/dist/ui/scrollback/renderer.js +158 -0
- package/dist/ui/scrollback/scrollback-writer.d.ts +162 -0
- package/dist/ui/scrollback/scrollback-writer.js +549 -0
- package/dist/ui/scrollback/settled-boundary.d.ts +42 -0
- package/dist/ui/scrollback/settled-boundary.js +106 -0
- package/dist/ui/scrollback/use-scrollback-view.d.ts +103 -0
- package/dist/ui/scrollback/use-scrollback-view.js +294 -0
- package/dist/ui/scrollback/writer-shapes.d.ts +127 -0
- package/dist/ui/scrollback/writer-shapes.js +159 -0
- package/dist/ui/stdout-frame-writer.js +12 -2
- package/dist/ui/terminal-plan.d.ts +52 -0
- package/dist/ui/terminal-plan.js +49 -0
- package/dist/ui/transcript-text.d.ts +11 -0
- package/dist/ui/transcript-text.js +21 -7
- package/dist/ui/use-startup-notices.d.ts +12 -1
- package/dist/ui/use-startup-notices.js +28 -2
- package/package.json +3 -3
- package/skills/project-indexer/SKILL.md +163 -163
- package/skills/project-indexer/templates/cleancode-overrides.md +46 -46
- package/skills/project-indexer/templates/cleancode-template.md +94 -94
- package/skills/project-indexer/templates/config-template.md +69 -69
- package/skills/project-indexer/templates/index-template.md +129 -129
- package/skills/skill-creator/SKILL.md +96 -96
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exit summary for native-scroll mode (tui-native-terminal-scroll §4.8 / D9).
|
|
3
|
+
*
|
|
4
|
+
* The conversation is already in the terminal's scrollback, so exit does NOT replay
|
|
5
|
+
* it (that would print every message twice). It prints at most four short lines and
|
|
6
|
+
* leaves the cursor on a fresh line.
|
|
7
|
+
*
|
|
8
|
+
* PURE and ASCII-safe: glyphs come from `pickGlyphs`, and the figures come from the
|
|
9
|
+
* same `formatSessionStats` the full-screen replay uses.
|
|
10
|
+
*/
|
|
11
|
+
import type { Entry, UsageTotal } from '../../agent/reducer.js';
|
|
12
|
+
import type { Glyphs } from '../glyphs.js';
|
|
13
|
+
export interface ExitSummaryInput {
|
|
14
|
+
entries: readonly Entry[];
|
|
15
|
+
usageTotal: UsageTotal;
|
|
16
|
+
provider: string;
|
|
17
|
+
model: string;
|
|
18
|
+
elapsedMs: number;
|
|
19
|
+
/** The session id, when this process has one worth resuming. */
|
|
20
|
+
sessionId?: string;
|
|
21
|
+
glyphs: Pick<Glyphs, 'midDot' | 'arrowUp' | 'arrowDown'>;
|
|
22
|
+
}
|
|
23
|
+
export declare function buildExitSummary(input: ExitSummaryInput): string[];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exit summary for native-scroll mode (tui-native-terminal-scroll §4.8 / D9).
|
|
3
|
+
*
|
|
4
|
+
* The conversation is already in the terminal's scrollback, so exit does NOT replay
|
|
5
|
+
* it (that would print every message twice). It prints at most four short lines and
|
|
6
|
+
* leaves the cursor on a fresh line.
|
|
7
|
+
*
|
|
8
|
+
* PURE and ASCII-safe: glyphs come from `pickGlyphs`, and the figures come from the
|
|
9
|
+
* same `formatSessionStats` the full-screen replay uses.
|
|
10
|
+
*/
|
|
11
|
+
import { formatDuration } from '../../agent/usage.js';
|
|
12
|
+
import { formatSessionStats } from '../transcript-text.js';
|
|
13
|
+
import { SCROLLBACK_LIMITS } from './limits.js';
|
|
14
|
+
export function buildExitSummary(input) {
|
|
15
|
+
const { glyphs: g } = input;
|
|
16
|
+
const stats = formatSessionStats(input.entries, input.usageTotal, g);
|
|
17
|
+
const rows = [
|
|
18
|
+
['Session ended', `${input.provider}:${input.model}`, formatDuration(Math.max(0, input.elapsedMs))]
|
|
19
|
+
.join(` ${g.midDot} `),
|
|
20
|
+
[stats.turns, stats.tokens, stats.cost].join(` ${g.midDot} `),
|
|
21
|
+
];
|
|
22
|
+
if (input.sessionId)
|
|
23
|
+
rows.push(`Resume: aragon --resume ${input.sessionId}`);
|
|
24
|
+
return rows.slice(0, SCROLLBACK_LIMITS.exitSummaryMaxRows);
|
|
25
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural limits of native-scroll mode (tui-native-terminal-scroll §4.3-§4.6).
|
|
3
|
+
*
|
|
4
|
+
* Structural caps live here; tunable policy lives in `config/schema.ts`. None of
|
|
5
|
+
* these is a user preference: each one bounds a failure (a runaway write, a live
|
|
6
|
+
* region that eats the screen) rather than expressing a taste.
|
|
7
|
+
*/
|
|
8
|
+
export declare const SCROLLBACK_LIMITS: {
|
|
9
|
+
/** Pending static entries the drain holds before folding the oldest into a marker. */
|
|
10
|
+
readonly feedCapacity: 32;
|
|
11
|
+
/** Rows one entry may print; the rest is replaced by one ASCII notice row. */
|
|
12
|
+
readonly maxRowsPerEntry: 400;
|
|
13
|
+
/** Static rows per `stdout.write`; larger feeds are split into consecutive batches. */
|
|
14
|
+
readonly maxRowsPerBatch: 2000;
|
|
15
|
+
/** tall-frame swallows within this window after a resize are expected and uncounted. */
|
|
16
|
+
readonly guardResizeGraceMs: 250;
|
|
17
|
+
/** Consecutive out-of-resize tall frames that degrade the app to the placeholder. */
|
|
18
|
+
readonly guardTripCount: 3;
|
|
19
|
+
/** Session summary rows printed on exit. */
|
|
20
|
+
readonly exitSummaryMaxRows: 4;
|
|
21
|
+
/** Streaming preview rows, absolute cap. */
|
|
22
|
+
readonly maxLiveTailRows: 12;
|
|
23
|
+
/** ... and as a share of the terminal height. */
|
|
24
|
+
readonly liveTailRowShare: 0.4;
|
|
25
|
+
/**
|
|
26
|
+
* Live-region `EntryView` instances per frame (release-gate-and-native-panels §6.5).
|
|
27
|
+
*
|
|
28
|
+
* A STRUCTURAL CAP of the same kind as `maxLiveTailRows`: a screenful is at most 12
|
|
29
|
+
* rows plus 4 component instances, so the band costs the same order of magnitude as
|
|
30
|
+
* the full-screen live tail and the render governor and every `memo` boundary keep
|
|
31
|
+
* working. Raising it makes the per-frame cost grow linearly with no windowing.
|
|
32
|
+
*/
|
|
33
|
+
readonly liveTailMaxEntries: 4;
|
|
34
|
+
/**
|
|
35
|
+
* Rows the rail needs before the band will show it: `TODO_LIMITS.panelMinRows + 1`.
|
|
36
|
+
*
|
|
37
|
+
* The `+ 1` is not decoration - the rail takes its rows from the same pool as one
|
|
38
|
+
* preview row, and a band that seats the rail but no preview loses the streaming
|
|
39
|
+
* answer entirely (band.ts step 3 / AC-12).
|
|
40
|
+
*/
|
|
41
|
+
readonly railBandMinRows: 4;
|
|
42
|
+
/**
|
|
43
|
+
* How long the cursor-position probe waits for `ESC[<row>;<col>R`.
|
|
44
|
+
*
|
|
45
|
+
* A STRUCTURAL CAP, not a preference (tui-bottom-anchored-live-region §4.4): it
|
|
46
|
+
* bounds "how much startup latency a terminal that does not answer may cost". The
|
|
47
|
+
* round trip is under 5 ms locally and under 60 ms over SSH, so 150 ms is
|
|
48
|
+
* generous, and the probe runs once per session — never per frame, never per
|
|
49
|
+
* keystroke. Anything longer and a terminal without CPR support would pay it on
|
|
50
|
+
* every launch.
|
|
51
|
+
*/
|
|
52
|
+
readonly anchorProbeTimeoutMs: 150;
|
|
53
|
+
/**
|
|
54
|
+
* Settle window before re-measuring the cursor after a resize.
|
|
55
|
+
*
|
|
56
|
+
* A STRUCTURAL CAP for the same reason: a drag fires dozens of resize events, and
|
|
57
|
+
* one CPR per event would both flood the input channel and make the region jitter.
|
|
58
|
+
* 200 ms is below the point a user perceives as "the input box is stuck".
|
|
59
|
+
*/
|
|
60
|
+
readonly anchorReprobeDelayMs: 200;
|
|
61
|
+
/**
|
|
62
|
+
* Structural ceiling on the height of a STARTING canvas.
|
|
63
|
+
*
|
|
64
|
+
* It bounds the bytes a batch may write and the rows the content cache may hold on
|
|
65
|
+
* an extreme terminal (`rows` 3000). Two limits on how it may be applied, both
|
|
66
|
+
* arithmetic rather than taste:
|
|
67
|
+
* - the rows a canvas leaves unused do not disappear, they stay above it as the
|
|
68
|
+
* permanent region, so a smaller starting canvas never loses content;
|
|
69
|
+
* - it must NEVER clip a live canvas. A canvas that has overflowed and scrolled
|
|
70
|
+
* grows to `rows - 1`, and shrinking one back would make the next erase reach
|
|
71
|
+
* above its own top: host output erased, every visible row printed twice.
|
|
72
|
+
*/
|
|
73
|
+
readonly maxCanvasRows: 200;
|
|
74
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural limits of native-scroll mode (tui-native-terminal-scroll §4.3-§4.6).
|
|
3
|
+
*
|
|
4
|
+
* Structural caps live here; tunable policy lives in `config/schema.ts`. None of
|
|
5
|
+
* these is a user preference: each one bounds a failure (a runaway write, a live
|
|
6
|
+
* region that eats the screen) rather than expressing a taste.
|
|
7
|
+
*/
|
|
8
|
+
export const SCROLLBACK_LIMITS = {
|
|
9
|
+
/** Pending static entries the drain holds before folding the oldest into a marker. */
|
|
10
|
+
feedCapacity: 32,
|
|
11
|
+
/** Rows one entry may print; the rest is replaced by one ASCII notice row. */
|
|
12
|
+
maxRowsPerEntry: 400,
|
|
13
|
+
/** Static rows per `stdout.write`; larger feeds are split into consecutive batches. */
|
|
14
|
+
maxRowsPerBatch: 2000,
|
|
15
|
+
/** tall-frame swallows within this window after a resize are expected and uncounted. */
|
|
16
|
+
guardResizeGraceMs: 250,
|
|
17
|
+
/** Consecutive out-of-resize tall frames that degrade the app to the placeholder. */
|
|
18
|
+
guardTripCount: 3,
|
|
19
|
+
/** Session summary rows printed on exit. */
|
|
20
|
+
exitSummaryMaxRows: 4,
|
|
21
|
+
/** Streaming preview rows, absolute cap. */
|
|
22
|
+
maxLiveTailRows: 12,
|
|
23
|
+
/** ... and as a share of the terminal height. */
|
|
24
|
+
liveTailRowShare: 0.4,
|
|
25
|
+
/**
|
|
26
|
+
* Live-region `EntryView` instances per frame (release-gate-and-native-panels §6.5).
|
|
27
|
+
*
|
|
28
|
+
* A STRUCTURAL CAP of the same kind as `maxLiveTailRows`: a screenful is at most 12
|
|
29
|
+
* rows plus 4 component instances, so the band costs the same order of magnitude as
|
|
30
|
+
* the full-screen live tail and the render governor and every `memo` boundary keep
|
|
31
|
+
* working. Raising it makes the per-frame cost grow linearly with no windowing.
|
|
32
|
+
*/
|
|
33
|
+
liveTailMaxEntries: 4,
|
|
34
|
+
/**
|
|
35
|
+
* Rows the rail needs before the band will show it: `TODO_LIMITS.panelMinRows + 1`.
|
|
36
|
+
*
|
|
37
|
+
* The `+ 1` is not decoration - the rail takes its rows from the same pool as one
|
|
38
|
+
* preview row, and a band that seats the rail but no preview loses the streaming
|
|
39
|
+
* answer entirely (band.ts step 3 / AC-12).
|
|
40
|
+
*/
|
|
41
|
+
railBandMinRows: 4,
|
|
42
|
+
/**
|
|
43
|
+
* How long the cursor-position probe waits for `ESC[<row>;<col>R`.
|
|
44
|
+
*
|
|
45
|
+
* A STRUCTURAL CAP, not a preference (tui-bottom-anchored-live-region §4.4): it
|
|
46
|
+
* bounds "how much startup latency a terminal that does not answer may cost". The
|
|
47
|
+
* round trip is under 5 ms locally and under 60 ms over SSH, so 150 ms is
|
|
48
|
+
* generous, and the probe runs once per session — never per frame, never per
|
|
49
|
+
* keystroke. Anything longer and a terminal without CPR support would pay it on
|
|
50
|
+
* every launch.
|
|
51
|
+
*/
|
|
52
|
+
anchorProbeTimeoutMs: 150,
|
|
53
|
+
/**
|
|
54
|
+
* Settle window before re-measuring the cursor after a resize.
|
|
55
|
+
*
|
|
56
|
+
* A STRUCTURAL CAP for the same reason: a drag fires dozens of resize events, and
|
|
57
|
+
* one CPR per event would both flood the input channel and make the region jitter.
|
|
58
|
+
* 200 ms is below the point a user perceives as "the input box is stuck".
|
|
59
|
+
*/
|
|
60
|
+
anchorReprobeDelayMs: 200,
|
|
61
|
+
/**
|
|
62
|
+
* Structural ceiling on the height of a STARTING canvas.
|
|
63
|
+
*
|
|
64
|
+
* It bounds the bytes a batch may write and the rows the content cache may hold on
|
|
65
|
+
* an extreme terminal (`rows` 3000). Two limits on how it may be applied, both
|
|
66
|
+
* arithmetic rather than taste:
|
|
67
|
+
* - the rows a canvas leaves unused do not disappear, they stay above it as the
|
|
68
|
+
* permanent region, so a smaller starting canvas never loses content;
|
|
69
|
+
* - it must NEVER clip a live canvas. A canvas that has overflowed and scrolled
|
|
70
|
+
* grows to `rows - 1`, and shrinking one back would make the next erase reach
|
|
71
|
+
* above its own top: host output erased, every visible row printed twice.
|
|
72
|
+
*/
|
|
73
|
+
maxCanvasRows: 200,
|
|
74
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming preview rows for the native-scroll live region
|
|
3
|
+
* (tui-native-terminal-scroll §4.6).
|
|
4
|
+
*
|
|
5
|
+
* Entries that are not yet final cannot be printed, but the user must still see the
|
|
6
|
+
* answer arrive. This builds a few PLAIN-TEXT rows from the unsettled tail of the
|
|
7
|
+
* transcript; the shell colours them uniformly (`theme.muted`). The preview is a
|
|
8
|
+
* visual buffer only: when an entry settles it is printed in its real, formatted
|
|
9
|
+
* shape and the preview rows disappear (the "settle jump", documented in the README).
|
|
10
|
+
*
|
|
11
|
+
* PURE: no Ink, no theme, no I/O. Every returned row fits `cols` display columns.
|
|
12
|
+
*/
|
|
13
|
+
import type { Entry } from '../../agent/reducer.js';
|
|
14
|
+
export interface LivePreviewInput {
|
|
15
|
+
/** `entries.slice(settledCount)` — may include excluded entries; they are skipped. */
|
|
16
|
+
unsettled: readonly Entry[];
|
|
17
|
+
/** Content width (`cols - 1`). */
|
|
18
|
+
cols: number;
|
|
19
|
+
/** Rows granted by the budget. */
|
|
20
|
+
rows: number;
|
|
21
|
+
thinkingVisible: boolean;
|
|
22
|
+
}
|
|
23
|
+
/** The last `rows` preview rows, oldest first. `[]` when `rows <= 0`. */
|
|
24
|
+
export declare function buildLivePreview(input: LivePreviewInput): string[];
|
|
25
|
+
/** How many preview rows the unsettled entries would like, before budget clamping. */
|
|
26
|
+
export declare function wantedPreviewRows(input: Omit<LivePreviewInput, 'rows'>, cap: number): number;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming preview rows for the native-scroll live region
|
|
3
|
+
* (tui-native-terminal-scroll §4.6).
|
|
4
|
+
*
|
|
5
|
+
* Entries that are not yet final cannot be printed, but the user must still see the
|
|
6
|
+
* answer arrive. This builds a few PLAIN-TEXT rows from the unsettled tail of the
|
|
7
|
+
* transcript; the shell colours them uniformly (`theme.muted`). The preview is a
|
|
8
|
+
* visual buffer only: when an entry settles it is printed in its real, formatted
|
|
9
|
+
* shape and the preview rows disappear (the "settle jump", documented in the README).
|
|
10
|
+
*
|
|
11
|
+
* PURE: no Ink, no theme, no I/O. Every returned row fits `cols` display columns.
|
|
12
|
+
*/
|
|
13
|
+
import { clampLiveText } from '../live-clamp.js';
|
|
14
|
+
import { wrapToRows } from '../layout/wrap-rows.js';
|
|
15
|
+
import { isExcluded } from './settled-boundary.js';
|
|
16
|
+
import { clipLine } from './writer-shapes.js';
|
|
17
|
+
const MAX_TEAM_RUN_ROWS = 3;
|
|
18
|
+
const TOOL_LIVE_ROWS = 6;
|
|
19
|
+
function firstLine(text) {
|
|
20
|
+
const line = text.split('\n').find((candidate) => candidate.trim().length > 0);
|
|
21
|
+
return (line ?? '').trim();
|
|
22
|
+
}
|
|
23
|
+
function fit(text, cols) {
|
|
24
|
+
return clipLine(text, Math.max(1, cols)).text;
|
|
25
|
+
}
|
|
26
|
+
function assistantRows(entry, cols, rows, thinkingVisible) {
|
|
27
|
+
const out = [];
|
|
28
|
+
const thinking = (entry.thinking ?? '').trim();
|
|
29
|
+
if (entry.text.trim().length === 0) {
|
|
30
|
+
if (thinking.length > 0 && thinkingVisible) {
|
|
31
|
+
const last = thinking.split('\n').filter((line) => line.trim().length > 0).pop() ?? '';
|
|
32
|
+
out.push(fit(`thinking: ${last.trim()}`, cols));
|
|
33
|
+
}
|
|
34
|
+
else if (entry.streaming) {
|
|
35
|
+
out.push('thinking...');
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
const clamped = clampLiveText(entry.text, rows * 2);
|
|
40
|
+
out.push(...wrapToRows(clamped.text, cols).map((row) => fit(row, cols)));
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
function toolRows(entry, cols) {
|
|
44
|
+
const head = entry.label && entry.label.length > 0 ? entry.label : entry.name;
|
|
45
|
+
const out = [fit(`${entry.status === 'running' ? '>' : '.'} ${head}`, cols)];
|
|
46
|
+
const live = (entry.live ?? []).slice(-TOOL_LIVE_ROWS);
|
|
47
|
+
for (const row of live)
|
|
48
|
+
out.push(fit(` ${row}`, cols));
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
function teamRows(entry, cols) {
|
|
52
|
+
const running = entry.runs.filter((run) => !['queued', 'done', 'failed', 'aborted'].includes(run.phase)).length;
|
|
53
|
+
const out = [fit(`Team: ${running}/${entry.runs.length} running`, cols)];
|
|
54
|
+
for (const run of entry.runs.slice(0, MAX_TEAM_RUN_ROWS)) {
|
|
55
|
+
out.push(fit(` ${run.label} ${run.phase}`, cols));
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
function entryRows(entry, input) {
|
|
60
|
+
const { cols, rows, thinkingVisible } = input;
|
|
61
|
+
switch (entry.kind) {
|
|
62
|
+
case 'assistant':
|
|
63
|
+
return assistantRows(entry, cols, rows, thinkingVisible);
|
|
64
|
+
case 'tool':
|
|
65
|
+
return toolRows(entry, cols);
|
|
66
|
+
case 'team':
|
|
67
|
+
return teamRows(entry, cols);
|
|
68
|
+
case 'todo':
|
|
69
|
+
return [fit(`Todo: ${entry.doneCount}/${entry.total}`, cols)];
|
|
70
|
+
case 'compaction':
|
|
71
|
+
return [fit(`Compacting context...`, cols)];
|
|
72
|
+
case 'retry':
|
|
73
|
+
return [fit(`Retrying (${entry.attempt}/${entry.maxRetries})...`, cols)];
|
|
74
|
+
case 'fast':
|
|
75
|
+
return [fit(`Fast review ${entry.reviewIndex}...`, cols)];
|
|
76
|
+
case 'queued':
|
|
77
|
+
return [fit(`Queued: ${firstLine(entry.text)}`, cols)];
|
|
78
|
+
case 'user':
|
|
79
|
+
return [fit(`> ${firstLine(entry.text)}`, cols)];
|
|
80
|
+
case 'notice':
|
|
81
|
+
return [fit(firstLine(entry.text), cols)];
|
|
82
|
+
case 'service':
|
|
83
|
+
return [];
|
|
84
|
+
default: {
|
|
85
|
+
const exhaustive = entry;
|
|
86
|
+
return exhaustive;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** The last `rows` preview rows, oldest first. `[]` when `rows <= 0`. */
|
|
91
|
+
export function buildLivePreview(input) {
|
|
92
|
+
const rows = Math.floor(input.rows);
|
|
93
|
+
if (rows <= 0)
|
|
94
|
+
return [];
|
|
95
|
+
const all = [];
|
|
96
|
+
for (const entry of input.unsettled) {
|
|
97
|
+
if (isExcluded(entry))
|
|
98
|
+
continue;
|
|
99
|
+
all.push(...entryRows(entry, input));
|
|
100
|
+
}
|
|
101
|
+
return all.slice(-rows);
|
|
102
|
+
}
|
|
103
|
+
/** How many preview rows the unsettled entries would like, before budget clamping. */
|
|
104
|
+
export function wantedPreviewRows(input, cap) {
|
|
105
|
+
return buildLivePreview({ ...input, rows: cap }).length;
|
|
106
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offscreen entry renderer (tui-native-terminal-scroll §4.5 / D4).
|
|
3
|
+
*
|
|
4
|
+
* Settled entries are rendered ONCE, to ANSI text, by a resident Ink instance that
|
|
5
|
+
* writes to a fake stdout (`debug: true`: Ink then emits the complete layout in one
|
|
6
|
+
* write per render and never touches log-update). The resulting rows go to the feed
|
|
7
|
+
* and are printed; they are never mounted in the live React tree, so the per-frame
|
|
8
|
+
* render cost stays proportional to what is on screen, not to the session length.
|
|
9
|
+
*
|
|
10
|
+
* Failure isolation (I-S12): one entry that throws while rendering degrades to a
|
|
11
|
+
* single placeholder row. It must never block the queue or take the session down.
|
|
12
|
+
*/
|
|
13
|
+
import React from 'react';
|
|
14
|
+
import type { Entry } from '../../agent/reducer.js';
|
|
15
|
+
import type { TermCapabilities } from '../capabilities.js';
|
|
16
|
+
import type { DensityMode } from '../density.js';
|
|
17
|
+
import type { Theme } from '../theme.js';
|
|
18
|
+
export interface EntryRenderOptions {
|
|
19
|
+
theme: Theme;
|
|
20
|
+
caps: TermCapabilities;
|
|
21
|
+
density: DensityMode;
|
|
22
|
+
thinkingVisible: boolean;
|
|
23
|
+
reducedMotion: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface EntryRenderer {
|
|
26
|
+
/** Rows for one entry at `cols` columns; `[]` when it renders as nothing. */
|
|
27
|
+
renderEntry(entry: Entry, prev: Entry | undefined, cols: number, expanded?: boolean): string[];
|
|
28
|
+
/** Rows for an arbitrary node (the startup header and opener). */
|
|
29
|
+
renderNode(node: React.ReactNode, cols: number): string[];
|
|
30
|
+
/** Options for LATER renders; already printed rows never change. */
|
|
31
|
+
setOptions(partial: Partial<EntryRenderOptions>): void;
|
|
32
|
+
stats(): {
|
|
33
|
+
elidedRows: number;
|
|
34
|
+
failures: number;
|
|
35
|
+
};
|
|
36
|
+
dispose(): void;
|
|
37
|
+
}
|
|
38
|
+
export declare function createEntryRenderer(initial: EntryRenderOptions): EntryRenderer;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Offscreen entry renderer (tui-native-terminal-scroll §4.5 / D4).
|
|
4
|
+
*
|
|
5
|
+
* Settled entries are rendered ONCE, to ANSI text, by a resident Ink instance that
|
|
6
|
+
* writes to a fake stdout (`debug: true`: Ink then emits the complete layout in one
|
|
7
|
+
* write per render and never touches log-update). The resulting rows go to the feed
|
|
8
|
+
* and are printed; they are never mounted in the live React tree, so the per-frame
|
|
9
|
+
* render cost stays proportional to what is on screen, not to the session length.
|
|
10
|
+
*
|
|
11
|
+
* Failure isolation (I-S12): one entry that throws while rendering degrades to a
|
|
12
|
+
* single placeholder row. It must never block the queue or take the session down.
|
|
13
|
+
*/
|
|
14
|
+
import React from 'react';
|
|
15
|
+
import { EventEmitter } from 'node:events';
|
|
16
|
+
import { Box, render } from 'ink';
|
|
17
|
+
import { EntryView } from '../Transcript.js';
|
|
18
|
+
import { getLogger } from '../../logging/logger.js';
|
|
19
|
+
import { SCROLLBACK_LIMITS } from './limits.js';
|
|
20
|
+
/**
|
|
21
|
+
* Catches a throwing entry INSIDE the offscreen tree.
|
|
22
|
+
*
|
|
23
|
+
* Without it Ink's own boundary would print an error overview into the frame (which
|
|
24
|
+
* would then be printed into the user's scrollback, forever) and unmount the
|
|
25
|
+
* instance, so every later entry would fail too. With it the failure is a flag the
|
|
26
|
+
* caller reads synchronously and the instance stays healthy (I-S12).
|
|
27
|
+
*/
|
|
28
|
+
class RenderGuard extends React.Component {
|
|
29
|
+
state = { failed: false };
|
|
30
|
+
static getDerivedStateFromError() {
|
|
31
|
+
return { failed: true };
|
|
32
|
+
}
|
|
33
|
+
componentDidCatch(error) {
|
|
34
|
+
this.props.failure.error = error;
|
|
35
|
+
}
|
|
36
|
+
render() {
|
|
37
|
+
return this.state.failed ? null : this.props.children;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const NO_FAILURE = Symbol('no-failure');
|
|
41
|
+
function createOffscreen(cols) {
|
|
42
|
+
const last = { value: null };
|
|
43
|
+
const stdout = Object.assign(new EventEmitter(), {
|
|
44
|
+
columns: cols,
|
|
45
|
+
rows: 1000,
|
|
46
|
+
isTTY: false,
|
|
47
|
+
write(chunk) {
|
|
48
|
+
last.value = chunk;
|
|
49
|
+
return true;
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
const stdin = Object.assign(new EventEmitter(), {
|
|
53
|
+
isTTY: false,
|
|
54
|
+
setEncoding() { },
|
|
55
|
+
resume() { },
|
|
56
|
+
pause() { },
|
|
57
|
+
ref() { },
|
|
58
|
+
unref() { },
|
|
59
|
+
});
|
|
60
|
+
const instance = render(null, {
|
|
61
|
+
stdout, stdin, stderr: stdout, debug: true, exitOnCtrlC: false, patchConsole: false,
|
|
62
|
+
});
|
|
63
|
+
// A rejected exit promise nobody awaits is an unhandled rejection.
|
|
64
|
+
void instance.waitUntilExit().catch(() => undefined);
|
|
65
|
+
return { cols, stdout, stdin, instance, last, failure: { error: NO_FAILURE } };
|
|
66
|
+
}
|
|
67
|
+
/** A function, so the compiler does not narrow the mutable `last` box to `null`. */
|
|
68
|
+
function readFrame(target) {
|
|
69
|
+
return target.last.value;
|
|
70
|
+
}
|
|
71
|
+
function trimTrailingBlank(rows) {
|
|
72
|
+
let end = rows.length;
|
|
73
|
+
while (end > 0 && rows[end - 1].trim().length === 0)
|
|
74
|
+
end -= 1;
|
|
75
|
+
return end === rows.length ? rows : rows.slice(0, end);
|
|
76
|
+
}
|
|
77
|
+
export function createEntryRenderer(initial) {
|
|
78
|
+
let options = initial;
|
|
79
|
+
let offscreen = null;
|
|
80
|
+
let elidedRows = 0;
|
|
81
|
+
let failures = 0;
|
|
82
|
+
let guardKey = 0;
|
|
83
|
+
const dispose = () => {
|
|
84
|
+
if (!offscreen)
|
|
85
|
+
return;
|
|
86
|
+
const current = offscreen;
|
|
87
|
+
offscreen = null;
|
|
88
|
+
try {
|
|
89
|
+
current.instance.unmount();
|
|
90
|
+
current.instance.cleanup();
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Tearing down a fake stream must never throw into an exit path.
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
/** Ink reads `columns` once, at creation, so a width change rebuilds the instance. */
|
|
97
|
+
const ensure = (cols) => {
|
|
98
|
+
if (offscreen && offscreen.cols === cols)
|
|
99
|
+
return offscreen;
|
|
100
|
+
dispose();
|
|
101
|
+
offscreen = createOffscreen(cols);
|
|
102
|
+
return offscreen;
|
|
103
|
+
};
|
|
104
|
+
const capRows = (rows) => {
|
|
105
|
+
const cap = SCROLLBACK_LIMITS.maxRowsPerEntry;
|
|
106
|
+
if (rows.length <= cap)
|
|
107
|
+
return rows;
|
|
108
|
+
const hidden = rows.length - (cap - 1);
|
|
109
|
+
elidedRows += hidden;
|
|
110
|
+
return [...rows.slice(0, cap - 1), `[+${hidden} rows not printed - scrollback entry cap]`];
|
|
111
|
+
};
|
|
112
|
+
const renderNode = (node, cols) => {
|
|
113
|
+
const target = ensure(Math.max(1, Math.floor(cols)));
|
|
114
|
+
target.last.value = null;
|
|
115
|
+
target.failure.error = NO_FAILURE;
|
|
116
|
+
guardKey += 1;
|
|
117
|
+
target.instance.rerender(_jsx(Box, { flexDirection: "column", width: target.cols, children: _jsx(RenderGuard, { failure: target.failure, children: node }, guardKey) }));
|
|
118
|
+
const frame = readFrame(target);
|
|
119
|
+
const failure = target.failure.error;
|
|
120
|
+
target.instance.rerender(null);
|
|
121
|
+
if (failure !== NO_FAILURE)
|
|
122
|
+
throw failure;
|
|
123
|
+
if (frame === null)
|
|
124
|
+
throw new Error('offscreen Ink instance produced no frame');
|
|
125
|
+
return capRows(trimTrailingBlank(frame.split('\n')));
|
|
126
|
+
};
|
|
127
|
+
const failureRow = (label) => {
|
|
128
|
+
failures += 1;
|
|
129
|
+
return [`[aragon] ${label} could not be rendered`];
|
|
130
|
+
};
|
|
131
|
+
return {
|
|
132
|
+
renderEntry(entry, prev, cols, expanded = false) {
|
|
133
|
+
try {
|
|
134
|
+
return renderNode(_jsx(EntryView, { entry: entry, prev: prev, expanded: expanded, thinkingVisible: options.thinkingVisible, reducedMotion: options.reducedMotion, density: options.density, theme: options.theme, caps: options.caps }), cols);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
getLogger().warn('cli', 'entry_render_failed', {
|
|
138
|
+
id: entry.id, kind: entry.kind, error: String(error),
|
|
139
|
+
});
|
|
140
|
+
return failureRow(`entry ${entry.id} (${entry.kind})`);
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
renderNode(node, cols) {
|
|
144
|
+
try {
|
|
145
|
+
return renderNode(node, cols);
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
getLogger().warn('cli', 'node_render_failed', { error: String(error) });
|
|
149
|
+
return failureRow('startup banner');
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
setOptions(partial) {
|
|
153
|
+
options = { ...options, ...partial };
|
|
154
|
+
},
|
|
155
|
+
stats: () => ({ elidedRows, failures }),
|
|
156
|
+
dispose,
|
|
157
|
+
};
|
|
158
|
+
}
|