@aragon-agent/cli 0.6.31 → 0.6.33
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 +119 -0
- package/README.md +2970 -2826
- 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/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 +236 -8
- package/dist/commands/builtins.js +23 -8
- package/dist/commands/perf.d.ts +77 -1
- package/dist/commands/perf.js +37 -5
- package/dist/config/cli-commands.d.ts +27 -0
- package/dist/config/cli-commands.js +54 -1
- package/dist/config/env.js +38 -0
- package/dist/config/load.js +50 -1
- package/dist/config/schema.d.ts +73 -0
- package/dist/config/schema.js +34 -0
- package/dist/config/store.js +14 -1
- 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.js +98 -12
- package/dist/ui/ScrollbackShell.d.ts +62 -11
- package/dist/ui/ScrollbackShell.js +11 -3
- 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/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/run-status-row.d.ts +12 -0
- package/dist/ui/screen.d.ts +17 -4
- package/dist/ui/screen.js +14 -6
- package/dist/ui/scrollback/LiveTail.d.ts +79 -0
- package/dist/ui/scrollback/LiveTail.js +32 -0
- package/dist/ui/scrollback/band.d.ts +114 -0
- package/dist/ui/scrollback/band.js +130 -0
- package/dist/ui/scrollback/budget.d.ts +51 -10
- package/dist/ui/scrollback/budget.js +69 -20
- 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/limits.d.ts +61 -4
- package/dist/ui/scrollback/limits.js +61 -4
- package/dist/ui/scrollback/live-preview.d.ts +18 -10
- package/dist/ui/scrollback/live-preview.js +14 -11
- package/dist/ui/scrollback/scrollback-writer.d.ts +104 -16
- package/dist/ui/scrollback/scrollback-writer.js +368 -82
- package/dist/ui/scrollback/tail-window.d.ts +60 -0
- package/dist/ui/scrollback/tail-window.js +73 -0
- package/dist/ui/scrollback/use-scrollback-view.d.ts +34 -15
- package/dist/ui/scrollback/use-scrollback-view.js +110 -59
- package/dist/ui/scrollback/writer-shapes.d.ts +68 -2
- package/dist/ui/scrollback/writer-shapes.js +53 -8
- package/package.json +3 -3
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The TAIL WINDOW of the native-scroll live region
|
|
3
|
+
* (tui-full-height-live-region D6 / D7, I-F8 / I-F9).
|
|
4
|
+
*
|
|
5
|
+
* WHAT CHANGED AND WHY. The region used to show only the entries the reducer had not
|
|
6
|
+
* finished writing (`entries.slice(settledCount)`), and everything settled was printed
|
|
7
|
+
* into the terminal's scrollback the moment it settled. Once the region is the WHOLE
|
|
8
|
+
* screen (`liveRows = rows - 1`), a printed row is pushed off the top by the very next
|
|
9
|
+
* frame, so "print on settle" would leave the user staring at a screen of blanks with
|
|
10
|
+
* only the unfinished cards on it. So the region now shows the last `K` PRINTABLE
|
|
11
|
+
* entries - settled or not - and an entry is printed when it LEAVES that window.
|
|
12
|
+
*
|
|
13
|
+
* The window is defined by ENTRY COUNT, never by measured height: a single entry taller
|
|
14
|
+
* than the region is clipped from the top by `justifyContent="flex-end"` and stays in
|
|
15
|
+
* the window until `K` further entries arrive (README, known limitation 2). Measuring
|
|
16
|
+
* heights here would need the offscreen renderer on every frame, which is the cost this
|
|
17
|
+
* module exists to avoid.
|
|
18
|
+
*
|
|
19
|
+
* PURE: no Ink, no I/O, no clock. `promote()` (the writer's only feeder) takes
|
|
20
|
+
* `printBoundary`'s result and keeps its own `printedIds` authority, so "printed is
|
|
21
|
+
* forward-only" (RV-7) is unchanged.
|
|
22
|
+
*
|
|
23
|
+
* PRINTABILITY != VISIBILITY. `isExcluded` entries (a non-terminal `service` card) never
|
|
24
|
+
* print and never take a slot in the window; an entry that is in the window may still be
|
|
25
|
+
* scrolled out of sight by a taller neighbour (README, known limitation 5). The second
|
|
26
|
+
* gap is disclosed by `/perf`'s `window=<start>/<n>`, not by a second measurement.
|
|
27
|
+
*/
|
|
28
|
+
import { isExcluded, settledCount } from './settled-boundary.js';
|
|
29
|
+
/**
|
|
30
|
+
* The index of the first entry that is still on screen: count backwards up to
|
|
31
|
+
* `maxEntries` PRINTABLE entries. Everything before it has left the region and is the
|
|
32
|
+
* writer's business.
|
|
33
|
+
*
|
|
34
|
+
* `maxEntries <= 0` returns `entries.length` - an empty window, which is the old
|
|
35
|
+
* behaviour ("everything printable may print") and the reason the guard is written as
|
|
36
|
+
* an early return rather than as a loop bound.
|
|
37
|
+
*/
|
|
38
|
+
export function tailWindowStart(entries, maxEntries) {
|
|
39
|
+
const limit = Number.isFinite(maxEntries) ? Math.floor(maxEntries) : 0;
|
|
40
|
+
if (limit <= 0)
|
|
41
|
+
return entries.length;
|
|
42
|
+
let seen = 0;
|
|
43
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
44
|
+
if (isExcluded(entries[i]))
|
|
45
|
+
continue;
|
|
46
|
+
seen += 1;
|
|
47
|
+
if (seen === limit)
|
|
48
|
+
return i;
|
|
49
|
+
}
|
|
50
|
+
// Fewer printable entries than the window holds: the whole conversation is on screen.
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
/** `entries.slice(start)` with the entries that never print dropped. */
|
|
54
|
+
export function selectTailWindow(entries, maxEntries) {
|
|
55
|
+
const start = tailWindowStart(entries, maxEntries);
|
|
56
|
+
return { start, entries: entries.slice(start).filter((entry) => !isExcluded(entry)) };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The print boundary: never print what the region is still showing.
|
|
60
|
+
*
|
|
61
|
+
* THIS IS THE ONLY PLACE THAT COMBINES "settled" WITH "in the window" (RV-P2-13), which
|
|
62
|
+
* is why the composition lives here instead of at the pump: `promote()` must keep
|
|
63
|
+
* receiving one number, and the two authorities it is derived from - `settledCount`
|
|
64
|
+
* (what may print at all) and `tailWindowStart` (what is still visible) - then cannot
|
|
65
|
+
* drift apart in a future edit.
|
|
66
|
+
*
|
|
67
|
+
* `settleAllEntries` is the OTHER authority, not a duplicate: it answers "what must be
|
|
68
|
+
* printed when the conversation is being torn down", while this answers "how far may
|
|
69
|
+
* this frame print".
|
|
70
|
+
*/
|
|
71
|
+
export function printBoundary(entries, runActive, maxEntries) {
|
|
72
|
+
return Math.min(settledCount(entries, runActive), tailWindowStart(entries, maxEntries));
|
|
73
|
+
}
|
|
@@ -1,23 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `useScrollbackView` — the promotion pipeline of native-scroll mode
|
|
3
|
-
* (tui-native-terminal-scroll §4.4 / §6.2).
|
|
3
|
+
* (tui-native-terminal-scroll §4.4 / §6.2, tui-full-height-live-region D3/D4/D7/D8).
|
|
4
4
|
*
|
|
5
5
|
* `App` calls it ALWAYS (hooks cannot be conditional); `enabled: false` makes every
|
|
6
6
|
* effect a no-op and keeps `scrollMode: 'app'` byte-identical to the pre-feature
|
|
7
|
-
* build (I-S9).
|
|
7
|
+
* build (I-S9 / I-F10).
|
|
8
8
|
*
|
|
9
9
|
* What it does when enabled:
|
|
10
|
-
* 1.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* 1. On every entries change, promotes each entry that has LEFT THE TAIL WINDOW:
|
|
11
|
+
* renders it once through the offscreen renderer and enqueues the rows for the
|
|
12
|
+
* writer. Entries still on screen are never printed - at `H = rows - 1` a printed
|
|
13
|
+
* row is pushed back off the top by the next frame, and a second copy in the
|
|
14
|
+
* scrollback would be a duplicate the user meets by scrolling up (I-F8).
|
|
15
|
+
* 2. Computes the live-region budget, the window and the tail rows for the shell.
|
|
16
|
+
* 3. Flushes the window before either reset path (`/clear`, `--resume`) and on exit,
|
|
17
|
+
* so "on screen but not printed yet" can never become "lost" (D8 / I-F12).
|
|
18
|
+
*
|
|
19
|
+
* THE HEADER AND THE OPENER ARE NOT PRINTED HERE ANY MORE (D3/D4). They are slots of
|
|
20
|
+
* `ScrollbackShell` now, rendered inside the region: `H = rows - 1` means any printed
|
|
21
|
+
* row is immediately scrolled off the screen, so printing the banner would be the same
|
|
22
|
+
* as not drawing it. Their `renderNode` path, the `startedRef` latch that kept their
|
|
23
|
+
* element identities stable, and the `printBanner()` call in `settleAll()` are gone.
|
|
14
24
|
*
|
|
15
25
|
* THE PROMOTION RUNS IN A MICROTASK, NOT IN THE EFFECT. The offscreen renderer is a
|
|
16
26
|
* second Ink instance, and both instances share one React reconciler. A `rerender`
|
|
17
27
|
* issued from inside the main tree's commit (a layout effect) is deferred until that
|
|
18
|
-
* commit finishes, so its frame would not exist when the effect reads it. A
|
|
19
|
-
*
|
|
20
|
-
*
|
|
28
|
+
* commit finishes, so its frame would not exist when the effect reads it. A microtask
|
|
29
|
+
* runs after the commit has unwound, where the offscreen render is synchronous again.
|
|
30
|
+
* Everything the pass needs is read from refs for that reason.
|
|
21
31
|
*
|
|
22
32
|
* `printedIds` is the ONLY authority on "already printed" (RV-7). The cursor is just
|
|
23
33
|
* a hint for where to resume scanning: entries leave the list by retain-ring
|
|
@@ -25,7 +35,6 @@
|
|
|
25
35
|
* cards), so an id-keyed cursor can dangle, and then the only correct answer is to
|
|
26
36
|
* rescan and let `printedIds` skip.
|
|
27
37
|
*/
|
|
28
|
-
import React from 'react';
|
|
29
38
|
import type { Entry } from '../../agent/reducer.js';
|
|
30
39
|
import type { TermCapabilities } from '../capabilities.js';
|
|
31
40
|
import type { DensityMode } from '../density.js';
|
|
@@ -68,23 +77,33 @@ export interface UseScrollbackViewArgs {
|
|
|
68
77
|
/** Terminal size. */
|
|
69
78
|
rows: number;
|
|
70
79
|
cols: number;
|
|
71
|
-
/** Printed first, once. */
|
|
72
|
-
headerNode: React.ReactNode;
|
|
73
|
-
/** Printed after the header for an empty session. */
|
|
74
|
-
openerNode: React.ReactNode | null;
|
|
75
80
|
overlayOpen: boolean;
|
|
76
81
|
draftRows: number;
|
|
77
82
|
popupRows: number;
|
|
78
83
|
statusExpanded: boolean;
|
|
79
84
|
/** Degrade the region to the placeholder (the writer's tall-frame guard tripped). */
|
|
80
85
|
forceInactive?: boolean;
|
|
86
|
+
/** `state.todos.items.length` - 0 means there is no plan, so the rail has no work to show. */
|
|
87
|
+
itemCount: number;
|
|
88
|
+
/** `cfg.todo.panel`: the user can switch the rail off entirely. */
|
|
89
|
+
panelEnabled: boolean;
|
|
90
|
+
/** `buildTeamPanelLayout().rowCount`; 0 when nothing is dispatched. */
|
|
91
|
+
teamRowsNatural: number;
|
|
81
92
|
}
|
|
82
93
|
export interface ScrollbackView {
|
|
83
94
|
budget: ScrollbackBudget | null;
|
|
84
95
|
liveTailLines: string[];
|
|
96
|
+
/**
|
|
97
|
+
* The entries the region is showing, in order (excluded kinds already dropped).
|
|
98
|
+
* `windowStart` is their index in the full list - together they are what `/perf`
|
|
99
|
+
* reports as `window=<start>/<n>`, the number that makes the "visible vs printable"
|
|
100
|
+
* gap observable instead of a guess (known limitation 5).
|
|
101
|
+
*/
|
|
102
|
+
tailEntries: readonly Entry[];
|
|
103
|
+
windowStart: number;
|
|
85
104
|
/** Print a fresh EXPANDED copy of an entry; false when it cannot be found / rendered. */
|
|
86
105
|
reprintExpanded(entryId: string): boolean;
|
|
87
|
-
/** Promote everything
|
|
106
|
+
/** Promote everything still unprinted (shutdown) - `flushUnprinted(latest entries)`. */
|
|
88
107
|
settleAll(): void;
|
|
89
108
|
/** Called by `App`'s dispatch wrapper BEFORE a reset action reaches the reducer. */
|
|
90
109
|
notifyReset(kind: ScrollbackResetKind): void;
|
|
@@ -1,23 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `useScrollbackView` — the promotion pipeline of native-scroll mode
|
|
3
|
-
* (tui-native-terminal-scroll §4.4 / §6.2).
|
|
3
|
+
* (tui-native-terminal-scroll §4.4 / §6.2, tui-full-height-live-region D3/D4/D7/D8).
|
|
4
4
|
*
|
|
5
5
|
* `App` calls it ALWAYS (hooks cannot be conditional); `enabled: false` makes every
|
|
6
6
|
* effect a no-op and keeps `scrollMode: 'app'` byte-identical to the pre-feature
|
|
7
|
-
* build (I-S9).
|
|
7
|
+
* build (I-S9 / I-F10).
|
|
8
8
|
*
|
|
9
9
|
* What it does when enabled:
|
|
10
|
-
* 1.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* 1. On every entries change, promotes each entry that has LEFT THE TAIL WINDOW:
|
|
11
|
+
* renders it once through the offscreen renderer and enqueues the rows for the
|
|
12
|
+
* writer. Entries still on screen are never printed - at `H = rows - 1` a printed
|
|
13
|
+
* row is pushed back off the top by the next frame, and a second copy in the
|
|
14
|
+
* scrollback would be a duplicate the user meets by scrolling up (I-F8).
|
|
15
|
+
* 2. Computes the live-region budget, the window and the tail rows for the shell.
|
|
16
|
+
* 3. Flushes the window before either reset path (`/clear`, `--resume`) and on exit,
|
|
17
|
+
* so "on screen but not printed yet" can never become "lost" (D8 / I-F12).
|
|
18
|
+
*
|
|
19
|
+
* THE HEADER AND THE OPENER ARE NOT PRINTED HERE ANY MORE (D3/D4). They are slots of
|
|
20
|
+
* `ScrollbackShell` now, rendered inside the region: `H = rows - 1` means any printed
|
|
21
|
+
* row is immediately scrolled off the screen, so printing the banner would be the same
|
|
22
|
+
* as not drawing it. Their `renderNode` path, the `startedRef` latch that kept their
|
|
23
|
+
* element identities stable, and the `printBanner()` call in `settleAll()` are gone.
|
|
14
24
|
*
|
|
15
25
|
* THE PROMOTION RUNS IN A MICROTASK, NOT IN THE EFFECT. The offscreen renderer is a
|
|
16
26
|
* second Ink instance, and both instances share one React reconciler. A `rerender`
|
|
17
27
|
* issued from inside the main tree's commit (a layout effect) is deferred until that
|
|
18
|
-
* commit finishes, so its frame would not exist when the effect reads it. A
|
|
19
|
-
*
|
|
20
|
-
*
|
|
28
|
+
* commit finishes, so its frame would not exist when the effect reads it. A microtask
|
|
29
|
+
* runs after the commit has unwound, where the offscreen render is synchronous again.
|
|
30
|
+
* Everything the pass needs is read from refs for that reason.
|
|
21
31
|
*
|
|
22
32
|
* `printedIds` is the ONLY authority on "already printed" (RV-7). The cursor is just
|
|
23
33
|
* a hint for where to resume scanning: entries leave the list by retain-ring
|
|
@@ -30,7 +40,8 @@ import { buildScrollbackBudget } from './budget.js';
|
|
|
30
40
|
import { buildLivePreview } from './live-preview.js';
|
|
31
41
|
import { SCROLLBACK_LIMITS } from './limits.js';
|
|
32
42
|
import { createEntryRenderer } from './renderer.js';
|
|
33
|
-
import { isExcluded, settleAllEntries
|
|
43
|
+
import { isExcluded, settleAllEntries } from './settled-boundary.js';
|
|
44
|
+
import { printBoundary, selectTailWindow } from './tail-window.js';
|
|
34
45
|
export function createScrollbackBridge(parts) {
|
|
35
46
|
return {
|
|
36
47
|
writer: parts.writer,
|
|
@@ -64,20 +75,15 @@ function pruneEntryIds(printed, entries) {
|
|
|
64
75
|
printed.delete(id);
|
|
65
76
|
}
|
|
66
77
|
export function useScrollbackView(args) {
|
|
67
|
-
const { enabled, bridge, entries, runActive, theme, caps, density, thinkingVisible, reducedMotion, rows, cols,
|
|
78
|
+
const { enabled, bridge, entries, runActive, theme, caps, density, thinkingVisible, reducedMotion, rows, cols, overlayOpen, draftRows, popupRows, statusExpanded, forceInactive = false, itemCount, panelEnabled, teamRowsNatural, } = args;
|
|
68
79
|
const contentCols = Math.max(1, cols - 1);
|
|
69
|
-
const startedRef = useRef(false);
|
|
70
80
|
// Everything the asynchronous pass reads travels here, refreshed on every render.
|
|
71
81
|
const latest = useRef({
|
|
72
|
-
entries, runActive, contentCols,
|
|
82
|
+
entries, runActive, contentCols,
|
|
73
83
|
options: { theme, caps, density, thinkingVisible, reducedMotion },
|
|
74
84
|
});
|
|
75
85
|
latest.current = {
|
|
76
86
|
entries, runActive, contentCols,
|
|
77
|
-
// The banner nodes only matter until the banner is printed; afterwards keep the
|
|
78
|
-
// old references so a changing element identity is not mistaken for new input.
|
|
79
|
-
headerNode: startedRef.current ? latest.current.headerNode : headerNode,
|
|
80
|
-
openerNode: startedRef.current ? latest.current.openerNode : openerNode,
|
|
81
87
|
options: { theme, caps, density, thinkingVisible, reducedMotion },
|
|
82
88
|
};
|
|
83
89
|
const rendererRef = useRef(null);
|
|
@@ -85,7 +91,12 @@ export function useScrollbackView(args) {
|
|
|
85
91
|
/** A resume hint only (RV-7): the id of the last entry the previous pass covered. */
|
|
86
92
|
const cursorRef = useRef(null);
|
|
87
93
|
const pendingReset = useRef(null);
|
|
88
|
-
|
|
94
|
+
// Band hysteresis (release-gate-and-native-panels §6.7): the writer re-lays out the
|
|
95
|
+
// canvas on every batch, so a band that shrank for two frames would make everything
|
|
96
|
+
// above the composer jump.
|
|
97
|
+
const heldRailRows = useRef(0);
|
|
98
|
+
const heldTeamRows = useRef(0);
|
|
99
|
+
const heldGeometry = useRef({ rows, cols });
|
|
89
100
|
const pumpQueued = useRef(false);
|
|
90
101
|
const mountedRef = useRef(true);
|
|
91
102
|
const ensureRenderer = useCallback(() => {
|
|
@@ -131,25 +142,45 @@ export function useScrollbackView(args) {
|
|
|
131
142
|
remember();
|
|
132
143
|
return false;
|
|
133
144
|
}, [ensureRenderer, enqueue]);
|
|
134
|
-
|
|
135
|
-
|
|
145
|
+
/**
|
|
146
|
+
* THE ONE PRINT LOOP (RV-P0-2 / RV-P2-12): every not-yet-printed entry of `list`,
|
|
147
|
+
* oldest first, with no per-pass limit.
|
|
148
|
+
*
|
|
149
|
+
* `list` is a PARAMETER on purpose. `applyPendingReset` must pass `reset.captured` -
|
|
150
|
+
* the snapshot taken BEFORE the reducer reset - because by the time the microtask
|
|
151
|
+
* runs, `latest.current.entries` is already the NEW list and flushing that would
|
|
152
|
+
* print zero rows: the entries that only ever existed on screen would vanish with
|
|
153
|
+
* the reset. That is real data loss, not a cosmetic gap.
|
|
154
|
+
*
|
|
155
|
+
* `settleAllEntries` is the exit path's authority (everything that can ever print,
|
|
156
|
+
* including an entry still streaming at this instant - printed is forever, and the
|
|
157
|
+
* conversation is being destroyed right now, so a snapshot beats a hole). The drain
|
|
158
|
+
* is FIFO, so the ORDER of the calls below is the order on screen.
|
|
159
|
+
*/
|
|
160
|
+
const flushUnprinted = useCallback((list) => {
|
|
161
|
+
if (!bridge)
|
|
136
162
|
return;
|
|
137
|
-
startedRef.current = true;
|
|
138
163
|
const renderer = ensureRenderer();
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
enqueue(renderer.
|
|
143
|
-
|
|
164
|
+
for (const entry of settleAllEntries(list)) {
|
|
165
|
+
if (printedIds.current.has(entry.id))
|
|
166
|
+
continue;
|
|
167
|
+
enqueue(renderer.renderEntry(entry, printableBefore(list, list.indexOf(entry)), latest.current.contentCols), entry.id, entry.kind);
|
|
168
|
+
printedIds.current.add(entry.id);
|
|
169
|
+
}
|
|
170
|
+
}, [bridge, ensureRenderer, enqueue]);
|
|
144
171
|
const applyPendingReset = useCallback(() => {
|
|
145
172
|
const reset = pendingReset.current;
|
|
146
173
|
if (!reset || latest.current.entries === reset.captured)
|
|
147
174
|
return;
|
|
148
175
|
pendingReset.current = null;
|
|
176
|
+
// ORDER IS THE CONTRACT (AC-F16): flush the OLD list, then the separator, then
|
|
177
|
+
// forget what was printed. Clearing `printedIds` first would make the flush print
|
|
178
|
+
// the whole conversation a second time.
|
|
179
|
+
flushUnprinted(reset.captured);
|
|
149
180
|
printedIds.current = new Set();
|
|
150
181
|
cursorRef.current = null;
|
|
151
182
|
enqueue([reset.kind === 'clear' ? CLEARED_ROW : RESTORED_ROW], `reset:${Date.now()}`, 'notice');
|
|
152
|
-
}, [enqueue]);
|
|
183
|
+
}, [enqueue, flushUnprinted]);
|
|
153
184
|
const pumpRef = useRef(() => undefined);
|
|
154
185
|
const schedulePump = useCallback(() => {
|
|
155
186
|
if (pumpQueued.current)
|
|
@@ -162,11 +193,12 @@ export function useScrollbackView(args) {
|
|
|
162
193
|
pumpQueued.current = false;
|
|
163
194
|
if (!mountedRef.current || !bridge)
|
|
164
195
|
return;
|
|
165
|
-
printBanner();
|
|
166
196
|
applyPendingReset();
|
|
167
197
|
const { entries: list, runActive: active } = latest.current;
|
|
168
198
|
const limit = Math.max(1, SCROLLBACK_LIMITS.feedCapacity - bridge.drain.pendingEntries() - 1);
|
|
169
|
-
|
|
199
|
+
// `printBoundary` = min(settled, window start): the region keeps what it shows.
|
|
200
|
+
const end = printBoundary(list, active, SCROLLBACK_LIMITS.liveTailMaxEntries);
|
|
201
|
+
const more = promote(list, end, limit);
|
|
170
202
|
pruneEntryIds(printedIds.current, list);
|
|
171
203
|
if (more) {
|
|
172
204
|
// Continue once the writer has consumed what is queued (a restored transcript
|
|
@@ -183,19 +215,8 @@ export function useScrollbackView(args) {
|
|
|
183
215
|
schedulePump();
|
|
184
216
|
}, [enabled, bridge, entries, runActive, schedulePump]);
|
|
185
217
|
const settleAll = useCallback(() => {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const list = latest.current.entries;
|
|
189
|
-
const renderer = ensureRenderer();
|
|
190
|
-
printBanner();
|
|
191
|
-
// Shutdown must not leave anything behind, so no per-pass limit.
|
|
192
|
-
for (const entry of settleAllEntries(list)) {
|
|
193
|
-
if (printedIds.current.has(entry.id))
|
|
194
|
-
continue;
|
|
195
|
-
enqueue(renderer.renderEntry(entry, printableBefore(list, list.indexOf(entry)), latest.current.contentCols), entry.id, entry.kind);
|
|
196
|
-
printedIds.current.add(entry.id);
|
|
197
|
-
}
|
|
198
|
-
}, [bridge, ensureRenderer, printBanner, enqueue]);
|
|
218
|
+
flushUnprinted(latest.current.entries);
|
|
219
|
+
}, [flushUnprinted]);
|
|
199
220
|
// Shutdown hook for the exit paths. `restore()` runs it AFTER the tree is gone
|
|
200
221
|
// (normal exit) or while it is alive (signal), so it renders synchronously and then
|
|
201
222
|
// releases the offscreen instance. The unmount cleanup deliberately renders nothing:
|
|
@@ -217,29 +238,55 @@ export function useScrollbackView(args) {
|
|
|
217
238
|
};
|
|
218
239
|
return () => { mountedRef.current = false; };
|
|
219
240
|
}, [enabled, bridge]);
|
|
220
|
-
// --- Live region budget and
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
//
|
|
229
|
-
|
|
241
|
+
// --- Live region budget and the tail window. ------------------------------
|
|
242
|
+
// The window is the last K printable entries, settled or not (D6/D7). `slice` +
|
|
243
|
+
// `tailWindowStart` is `tail-window.ts`'s definition, and `printBoundary` uses the
|
|
244
|
+
// same start - so "what is on screen" and "what is withheld from the writer" cannot
|
|
245
|
+
// disagree about where the boundary is.
|
|
246
|
+
const window = useMemo(() => selectTailWindow(entries, SCROLLBACK_LIMITS.liveTailMaxEntries), [entries]);
|
|
247
|
+
const tailEntries = window.entries;
|
|
248
|
+
const windowStart = window.start;
|
|
249
|
+
// The band's hysteresis has exactly THREE reset points and no others: the plan
|
|
250
|
+
// emptied, the dispatch finished, or the geometry changed (a new size is a new
|
|
251
|
+
// measurement - the same treatment `App` gives `guardTripped`).
|
|
252
|
+
if (heldGeometry.current.rows !== rows || heldGeometry.current.cols !== cols) {
|
|
253
|
+
heldGeometry.current = { rows, cols };
|
|
254
|
+
heldRailRows.current = 0;
|
|
255
|
+
heldTeamRows.current = 0;
|
|
256
|
+
}
|
|
257
|
+
if (itemCount === 0)
|
|
258
|
+
heldRailRows.current = 0;
|
|
259
|
+
if (teamRowsNatural === 0)
|
|
260
|
+
heldTeamRows.current = 0;
|
|
230
261
|
const budget = useMemo(() => enabled
|
|
231
262
|
? buildScrollbackBudget({
|
|
232
263
|
rows, cols, draftRows, popupRows, statusExpanded, overlayOpen,
|
|
233
|
-
|
|
264
|
+
forceInactive,
|
|
265
|
+
band: {
|
|
266
|
+
itemCount, panelEnabled, teamRowsNatural,
|
|
267
|
+
heldRailRows: heldRailRows.current, heldTeamRows: heldTeamRows.current,
|
|
268
|
+
},
|
|
234
269
|
})
|
|
235
270
|
: null,
|
|
236
|
-
//
|
|
271
|
+
// The two band refs are read at memo time; `itemCount` and `teamRowsNatural` are
|
|
272
|
+
// what move them. The budget no longer depends on CONTENT at all (the band fills the
|
|
273
|
+
// pool, D5), so nothing about the transcript belongs in this dependency list.
|
|
237
274
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
238
|
-
[enabled, rows, cols, draftRows, popupRows, statusExpanded, overlayOpen,
|
|
239
|
-
|
|
275
|
+
[enabled, rows, cols, draftRows, popupRows, statusExpanded, overlayOpen,
|
|
276
|
+
forceInactive, itemCount, panelEnabled, teamRowsNatural]);
|
|
277
|
+
if (budget) {
|
|
278
|
+
// Monotone memory, fed back into the memo above on the next frame.
|
|
279
|
+
heldRailRows.current = budget.band.heldRailRows;
|
|
280
|
+
heldTeamRows.current = budget.band.heldTeamRows;
|
|
281
|
+
}
|
|
282
|
+
// Print keeps `contentCols`; only the live tail gives up the rail's columns (I-5).
|
|
283
|
+
const previewCols = budget ? budget.previewCols : contentCols;
|
|
240
284
|
const liveTailLines = useMemo(() => enabled && budget && budget.liveTailRows > 0
|
|
241
|
-
? buildLivePreview({
|
|
242
|
-
|
|
285
|
+
? buildLivePreview({
|
|
286
|
+
unsettled: tailEntries, cols: previewCols, rows: budget.liveTailRows,
|
|
287
|
+
thinkingVisible,
|
|
288
|
+
})
|
|
289
|
+
: [], [enabled, budget, tailEntries, previewCols, thinkingVisible]);
|
|
243
290
|
const reprintExpanded = useCallback((entryId) => {
|
|
244
291
|
if (!enabled || !bridge)
|
|
245
292
|
return false;
|
|
@@ -257,7 +304,11 @@ export function useScrollbackView(args) {
|
|
|
257
304
|
const notifyReset = useCallback((kind) => {
|
|
258
305
|
if (!enabled)
|
|
259
306
|
return;
|
|
307
|
+
// The PRE-reset snapshot: see `flushUnprinted`'s note on why this cannot be read
|
|
308
|
+
// from `latest` when the microtask finally runs.
|
|
260
309
|
pendingReset.current = { kind, captured: latest.current.entries };
|
|
261
310
|
}, [enabled]);
|
|
262
|
-
return {
|
|
311
|
+
return {
|
|
312
|
+
budget, liveTailLines, tailEntries, windowStart, reprintExpanded, settleAll, notifyReset,
|
|
313
|
+
};
|
|
263
314
|
}
|
|
@@ -21,6 +21,60 @@ export declare function seedBytes(input: {
|
|
|
21
21
|
/** After a foreign write the cursor may sit mid-line; start on a new one (RV-11). */
|
|
22
22
|
reseed: boolean;
|
|
23
23
|
}): string;
|
|
24
|
+
/**
|
|
25
|
+
* CUD(n): move down n rows WITHOUT writing a single cell.
|
|
26
|
+
*
|
|
27
|
+
* This is how the canvas spends its `gap` (D3). A blank row that is never written
|
|
28
|
+
* cannot be pushed into the scrollback by a later overflow, which is the whole
|
|
29
|
+
* reason the gap is not a run of `\r\n` (I-B3). The sequence is already whitelisted
|
|
30
|
+
* (`diffBytes` uses it to step between changed rows); this is the same CUD used to
|
|
31
|
+
* skip empty space instead.
|
|
32
|
+
*/
|
|
33
|
+
export declare function skipRows(rows: number): string;
|
|
34
|
+
/**
|
|
35
|
+
* First canvas batch (or the first one after a reset): the screen below the cursor
|
|
36
|
+
* is assumed empty, so there is nothing to erase — only `[gap][content][region]` to
|
|
37
|
+
* write, ending with the cursor parked on the last row.
|
|
38
|
+
*/
|
|
39
|
+
export declare function canvasSeedBytes(input: {
|
|
40
|
+
/**
|
|
41
|
+
* Rows to descend from the cursor to the canvas top, when the canvas is shorter
|
|
42
|
+
* than the space measured below the cursor.
|
|
43
|
+
*
|
|
44
|
+
* Non-zero only on a terminal taller than the start-of-canvas cap: the canvas then
|
|
45
|
+
* begins BELOW the cursor, and a seed that started writing at the cursor would park
|
|
46
|
+
* the region low and leave the top of the canvas empty for the whole session.
|
|
47
|
+
*/
|
|
48
|
+
downToTop: number;
|
|
49
|
+
/** Blank rows to skip before the first content row. */
|
|
50
|
+
gap: number;
|
|
51
|
+
/** Content rows, already clipped, oldest first. */
|
|
52
|
+
held: readonly string[];
|
|
53
|
+
/** The live region. */
|
|
54
|
+
lines: readonly string[];
|
|
55
|
+
sync: boolean;
|
|
56
|
+
/** After a foreign write the cursor may sit mid-line; start on a new one. */
|
|
57
|
+
reseed: boolean;
|
|
58
|
+
}): string;
|
|
59
|
+
/**
|
|
60
|
+
* Redraw the whole canvas: erase the previous one, then write `[gap][content][region]`.
|
|
61
|
+
*
|
|
62
|
+
* ORDER IS SEMANTICS: `ED0`, then the gap, then the content, then the region. Moving
|
|
63
|
+
* the gap after the content puts the blank space back between the reply and the
|
|
64
|
+
* composer, which is the exact defect this feature fixes (M1 in the spec).
|
|
65
|
+
*
|
|
66
|
+
* `held` is the FULL content sequence, not the part that will still be on screen: the
|
|
67
|
+
* rows that do not fit are supposed to leave through the bottom, and the terminal can
|
|
68
|
+
* only scroll away what it was given.
|
|
69
|
+
*/
|
|
70
|
+
export declare function canvasBytes(input: {
|
|
71
|
+
/** Previous canvas height: the erase distance. */
|
|
72
|
+
eraseUp: number;
|
|
73
|
+
gap: number;
|
|
74
|
+
held: readonly string[];
|
|
75
|
+
lines: readonly string[];
|
|
76
|
+
sync: boolean;
|
|
77
|
+
}): string;
|
|
24
78
|
/** Erase exactly the previous live region, print static rows, redraw the region. */
|
|
25
79
|
export declare function fullBytes(input: {
|
|
26
80
|
lines: readonly string[];
|
|
@@ -29,6 +83,20 @@ export declare function fullBytes(input: {
|
|
|
29
83
|
eraseUp: number;
|
|
30
84
|
sync: boolean;
|
|
31
85
|
}): string;
|
|
86
|
+
/**
|
|
87
|
+
* Fill the screen down to the LAST row before the first batch (D4 rung 2 / §4.3).
|
|
88
|
+
*
|
|
89
|
+
* This is the fallback for a terminal that never answers `ESC[6n`: from any row, a
|
|
90
|
+
* run of `rows - 1` line feeds is guaranteed to leave the cursor on the bottom row
|
|
91
|
+
* (extra feeds only scroll, which is why the count is the caller's business). It is
|
|
92
|
+
* written ONCE, before anything else, so the rows it scrolls away are the host's old
|
|
93
|
+
* output — which is still in the scrollback — and never this session's content.
|
|
94
|
+
*
|
|
95
|
+
* The argument is the NUMBER OF LINE FEEDS, not the terminal height: `cli.tsx` passes
|
|
96
|
+
* `rows - 1`. Naming it after the height instead made the caller subtract twice and
|
|
97
|
+
* left the cursor one row short of the bottom.
|
|
98
|
+
*/
|
|
99
|
+
export declare function fillBytes(count: number): string;
|
|
32
100
|
/** Same height, no static rows: rewrite only the rows that changed. `''` if none did. */
|
|
33
101
|
export declare function diffBytes(input: {
|
|
34
102
|
lines: readonly string[];
|
|
@@ -57,5 +125,3 @@ export declare function clipLine(line: string, maxCols: number): {
|
|
|
57
125
|
text: string;
|
|
58
126
|
clipped: boolean;
|
|
59
127
|
};
|
|
60
|
-
/** Rows the previous region occupies after the terminal re-flowed it to `cols` (§4.10). */
|
|
61
|
-
export declare function estimateReflowRows(lines: readonly string[], cols: number): number;
|
|
@@ -32,10 +32,63 @@ export function seedBytes(input) {
|
|
|
32
32
|
const lead = input.reseed ? '\r\n' : '';
|
|
33
33
|
return wrapSync(lead + '\r' + SGR0 + rows(input.statics) + rows(input.lines), input.sync);
|
|
34
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
|
+
}
|
|
35
72
|
/** Erase exactly the previous live region, print static rows, redraw the region. */
|
|
36
73
|
export function fullBytes(input) {
|
|
37
74
|
return wrapSync(up(input.eraseUp) + '\r' + SGR0 + ED0 + rows(input.statics) + rows(input.lines), input.sync);
|
|
38
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
|
+
}
|
|
39
92
|
/** Same height, no static rows: rewrite only the rows that changed. `''` if none did. */
|
|
40
93
|
export function diffBytes(input) {
|
|
41
94
|
const { lines, prev } = input;
|
|
@@ -104,11 +157,3 @@ export function clipLine(line, maxCols) {
|
|
|
104
157
|
}
|
|
105
158
|
return { text: out, clipped: false };
|
|
106
159
|
}
|
|
107
|
-
/** Rows the previous region occupies after the terminal re-flowed it to `cols` (§4.10). */
|
|
108
|
-
export function estimateReflowRows(lines, cols) {
|
|
109
|
-
const width = Math.max(1, cols);
|
|
110
|
-
let total = 0;
|
|
111
|
-
for (const line of lines)
|
|
112
|
-
total += Math.max(1, Math.ceil(stringWidth(stripAnsi(line)) / width));
|
|
113
|
-
return total;
|
|
114
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aragon-agent/cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.33",
|
|
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.
|
|
59
|
+
"@aragon-agent/core": "^0.2.45",
|
|
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": "
|
|
79
|
+
"vitest": "4.1.9"
|
|
80
80
|
}
|
|
81
81
|
}
|