@aragon-agent/cli 0.6.32 → 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 +19 -0
- package/README.md +44 -31
- package/dist/cli.js +4 -0
- package/dist/commands/perf.d.ts +24 -1
- package/dist/commands/perf.js +11 -3
- package/dist/ui/App.js +39 -11
- package/dist/ui/ScrollbackShell.d.ts +37 -12
- package/dist/ui/ScrollbackShell.js +6 -3
- package/dist/ui/scrollback/LiveTail.d.ts +27 -12
- package/dist/ui/scrollback/LiveTail.js +15 -10
- package/dist/ui/scrollback/band.d.ts +33 -24
- package/dist/ui/scrollback/band.js +43 -22
- package/dist/ui/scrollback/budget.d.ts +31 -19
- package/dist/ui/scrollback/budget.js +37 -25
- package/dist/ui/scrollback/limits.d.ts +18 -10
- package/dist/ui/scrollback/limits.js +18 -10
- package/dist/ui/scrollback/live-preview.d.ts +18 -10
- package/dist/ui/scrollback/live-preview.js +14 -11
- 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 +26 -18
- package/dist/ui/scrollback/use-scrollback-view.js +85 -65
- package/package.json +2 -2
|
@@ -0,0 +1,60 @@
|
|
|
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 type { Entry } from '../../agent/reducer.js';
|
|
29
|
+
export interface TailWindow {
|
|
30
|
+
/** First index of the window inside the full entry list. */
|
|
31
|
+
start: number;
|
|
32
|
+
/** Entries inside the window, oldest first (excluded entries already dropped). */
|
|
33
|
+
entries: readonly Entry[];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The index of the first entry that is still on screen: count backwards up to
|
|
37
|
+
* `maxEntries` PRINTABLE entries. Everything before it has left the region and is the
|
|
38
|
+
* writer's business.
|
|
39
|
+
*
|
|
40
|
+
* `maxEntries <= 0` returns `entries.length` - an empty window, which is the old
|
|
41
|
+
* behaviour ("everything printable may print") and the reason the guard is written as
|
|
42
|
+
* an early return rather than as a loop bound.
|
|
43
|
+
*/
|
|
44
|
+
export declare function tailWindowStart(entries: readonly Entry[], maxEntries: number): number;
|
|
45
|
+
/** `entries.slice(start)` with the entries that never print dropped. */
|
|
46
|
+
export declare function selectTailWindow(entries: readonly Entry[], maxEntries: number): TailWindow;
|
|
47
|
+
/**
|
|
48
|
+
* The print boundary: never print what the region is still showing.
|
|
49
|
+
*
|
|
50
|
+
* THIS IS THE ONLY PLACE THAT COMBINES "settled" WITH "in the window" (RV-P2-13), which
|
|
51
|
+
* is why the composition lives here instead of at the pump: `promote()` must keep
|
|
52
|
+
* receiving one number, and the two authorities it is derived from - `settledCount`
|
|
53
|
+
* (what may print at all) and `tailWindowStart` (what is still visible) - then cannot
|
|
54
|
+
* drift apart in a future edit.
|
|
55
|
+
*
|
|
56
|
+
* `settleAllEntries` is the OTHER authority, not a duplicate: it answers "what must be
|
|
57
|
+
* printed when the conversation is being torn down", while this answers "how far may
|
|
58
|
+
* this frame print".
|
|
59
|
+
*/
|
|
60
|
+
export declare function printBoundary(entries: readonly Entry[], runActive: boolean, maxEntries: number): number;
|
|
@@ -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,10 +77,6 @@ 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;
|
|
@@ -89,13 +94,16 @@ export interface ScrollbackView {
|
|
|
89
94
|
budget: ScrollbackBudget | null;
|
|
90
95
|
liveTailLines: string[];
|
|
91
96
|
/**
|
|
92
|
-
* The
|
|
93
|
-
*
|
|
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).
|
|
94
101
|
*/
|
|
95
|
-
|
|
102
|
+
tailEntries: readonly Entry[];
|
|
103
|
+
windowStart: number;
|
|
96
104
|
/** Print a fresh EXPANDED copy of an entry; false when it cannot be found / rendered. */
|
|
97
105
|
reprintExpanded(entryId: string): boolean;
|
|
98
|
-
/** Promote everything
|
|
106
|
+
/** Promote everything still unprinted (shutdown) - `flushUnprinted(latest entries)`. */
|
|
99
107
|
settleAll(): void;
|
|
100
108
|
/** Called by `App`'s dispatch wrapper BEFORE a reset action reaches the reducer. */
|
|
101
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,10 +91,9 @@ 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
|
-
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
// band that shrank for two frames would make everything above the composer jump.
|
|
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.
|
|
92
97
|
const heldRailRows = useRef(0);
|
|
93
98
|
const heldTeamRows = useRef(0);
|
|
94
99
|
const heldGeometry = useRef({ rows, cols });
|
|
@@ -137,25 +142,45 @@ export function useScrollbackView(args) {
|
|
|
137
142
|
remember();
|
|
138
143
|
return false;
|
|
139
144
|
}, [ensureRenderer, enqueue]);
|
|
140
|
-
|
|
141
|
-
|
|
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)
|
|
142
162
|
return;
|
|
143
|
-
startedRef.current = true;
|
|
144
163
|
const renderer = ensureRenderer();
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
enqueue(renderer.
|
|
149
|
-
|
|
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]);
|
|
150
171
|
const applyPendingReset = useCallback(() => {
|
|
151
172
|
const reset = pendingReset.current;
|
|
152
173
|
if (!reset || latest.current.entries === reset.captured)
|
|
153
174
|
return;
|
|
154
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);
|
|
155
180
|
printedIds.current = new Set();
|
|
156
181
|
cursorRef.current = null;
|
|
157
182
|
enqueue([reset.kind === 'clear' ? CLEARED_ROW : RESTORED_ROW], `reset:${Date.now()}`, 'notice');
|
|
158
|
-
}, [enqueue]);
|
|
183
|
+
}, [enqueue, flushUnprinted]);
|
|
159
184
|
const pumpRef = useRef(() => undefined);
|
|
160
185
|
const schedulePump = useCallback(() => {
|
|
161
186
|
if (pumpQueued.current)
|
|
@@ -168,11 +193,12 @@ export function useScrollbackView(args) {
|
|
|
168
193
|
pumpQueued.current = false;
|
|
169
194
|
if (!mountedRef.current || !bridge)
|
|
170
195
|
return;
|
|
171
|
-
printBanner();
|
|
172
196
|
applyPendingReset();
|
|
173
197
|
const { entries: list, runActive: active } = latest.current;
|
|
174
198
|
const limit = Math.max(1, SCROLLBACK_LIMITS.feedCapacity - bridge.drain.pendingEntries() - 1);
|
|
175
|
-
|
|
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);
|
|
176
202
|
pruneEntryIds(printedIds.current, list);
|
|
177
203
|
if (more) {
|
|
178
204
|
// Continue once the writer has consumed what is queued (a restored transcript
|
|
@@ -189,19 +215,8 @@ export function useScrollbackView(args) {
|
|
|
189
215
|
schedulePump();
|
|
190
216
|
}, [enabled, bridge, entries, runActive, schedulePump]);
|
|
191
217
|
const settleAll = useCallback(() => {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const list = latest.current.entries;
|
|
195
|
-
const renderer = ensureRenderer();
|
|
196
|
-
printBanner();
|
|
197
|
-
// Shutdown must not leave anything behind, so no per-pass limit.
|
|
198
|
-
for (const entry of settleAllEntries(list)) {
|
|
199
|
-
if (printedIds.current.has(entry.id))
|
|
200
|
-
continue;
|
|
201
|
-
enqueue(renderer.renderEntry(entry, printableBefore(list, list.indexOf(entry)), latest.current.contentCols), entry.id, entry.kind);
|
|
202
|
-
printedIds.current.add(entry.id);
|
|
203
|
-
}
|
|
204
|
-
}, [bridge, ensureRenderer, printBanner, enqueue]);
|
|
218
|
+
flushUnprinted(latest.current.entries);
|
|
219
|
+
}, [flushUnprinted]);
|
|
205
220
|
// Shutdown hook for the exit paths. `restore()` runs it AFTER the tree is gone
|
|
206
221
|
// (normal exit) or while it is alive (signal), so it renders synchronously and then
|
|
207
222
|
// releases the offscreen instance. The unmount cleanup deliberately renders nothing:
|
|
@@ -223,16 +238,14 @@ export function useScrollbackView(args) {
|
|
|
223
238
|
};
|
|
224
239
|
return () => { mountedRef.current = false; };
|
|
225
240
|
}, [enabled, bridge]);
|
|
226
|
-
// --- Live region budget and
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
// D11 · the preview height only grows while anything is unsettled, then resets.
|
|
235
|
-
heldTailRows.current = unsettled.length === 0 ? 0 : Math.max(heldTailRows.current, wanted);
|
|
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;
|
|
236
249
|
// The band's hysteresis has exactly THREE reset points and no others: the plan
|
|
237
250
|
// emptied, the dispatch finished, or the geometry changed (a new size is a new
|
|
238
251
|
// measurement - the same treatment `App` gives `guardTripped`).
|
|
@@ -248,29 +261,32 @@ export function useScrollbackView(args) {
|
|
|
248
261
|
const budget = useMemo(() => enabled
|
|
249
262
|
? buildScrollbackBudget({
|
|
250
263
|
rows, cols, draftRows, popupRows, statusExpanded, overlayOpen,
|
|
251
|
-
|
|
264
|
+
forceInactive,
|
|
252
265
|
band: {
|
|
253
266
|
itemCount, panelEnabled, teamRowsNatural,
|
|
254
267
|
heldRailRows: heldRailRows.current, heldTeamRows: heldTeamRows.current,
|
|
255
268
|
},
|
|
256
269
|
})
|
|
257
270
|
: null,
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
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.
|
|
261
274
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
262
|
-
[enabled, rows, cols, draftRows, popupRows, statusExpanded, overlayOpen,
|
|
263
|
-
|
|
275
|
+
[enabled, rows, cols, draftRows, popupRows, statusExpanded, overlayOpen,
|
|
276
|
+
forceInactive, itemCount, panelEnabled, teamRowsNatural]);
|
|
264
277
|
if (budget) {
|
|
265
278
|
// Monotone memory, fed back into the memo above on the next frame.
|
|
266
279
|
heldRailRows.current = budget.band.heldRailRows;
|
|
267
280
|
heldTeamRows.current = budget.band.heldTeamRows;
|
|
268
281
|
}
|
|
269
|
-
// Print keeps `contentCols`; only the live
|
|
282
|
+
// Print keeps `contentCols`; only the live tail gives up the rail's columns (I-5).
|
|
270
283
|
const previewCols = budget ? budget.previewCols : contentCols;
|
|
271
284
|
const liveTailLines = useMemo(() => enabled && budget && budget.liveTailRows > 0
|
|
272
|
-
? buildLivePreview({
|
|
273
|
-
|
|
285
|
+
? buildLivePreview({
|
|
286
|
+
unsettled: tailEntries, cols: previewCols, rows: budget.liveTailRows,
|
|
287
|
+
thinkingVisible,
|
|
288
|
+
})
|
|
289
|
+
: [], [enabled, budget, tailEntries, previewCols, thinkingVisible]);
|
|
274
290
|
const reprintExpanded = useCallback((entryId) => {
|
|
275
291
|
if (!enabled || !bridge)
|
|
276
292
|
return false;
|
|
@@ -288,7 +304,11 @@ export function useScrollbackView(args) {
|
|
|
288
304
|
const notifyReset = useCallback((kind) => {
|
|
289
305
|
if (!enabled)
|
|
290
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.
|
|
291
309
|
pendingReset.current = { kind, captured: latest.current.entries };
|
|
292
310
|
}, [enabled]);
|
|
293
|
-
return {
|
|
311
|
+
return {
|
|
312
|
+
budget, liveTailLines, tailEntries, windowStart, reprintExpanded, settleAll, notifyReset,
|
|
313
|
+
};
|
|
294
314
|
}
|
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",
|