@aragon-agent/cli 0.6.32 → 0.6.34

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.
@@ -0,0 +1,74 @@
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; what a single entry
14
+ * taller than the region used to do is the whole subject of
15
+ * `overflow-print.ts` (tui-final-reply-truncated-unscrollable). The box still clips it
16
+ * from the top with `justifyContent="flex-end"`, but the rows it hides are now PRINTED
17
+ * into the terminal's scrollback, where the wheel already reaches, instead of waiting for
18
+ * `K` further entries (README, known limitation 2 - rewritten by that fix).
19
+ *
20
+ * PURE: no Ink, no I/O, no clock. `promote()` (the writer's only feeder) takes
21
+ * `printBoundary`'s result and keeps its own `printed` authority, so "printed is
22
+ * forward-only" (RV-7) is unchanged.
23
+ *
24
+ * PRINTABILITY != VISIBILITY. `isExcluded` entries (a non-terminal `service` card) never
25
+ * print and never take a slot in the window; an entry that is in the window may still be
26
+ * scrolled out of sight by a taller neighbour. That second gap is what the row-level plan
27
+ * closes, and `/perf`'s `window=<start>/<n> head=<rows>` is how it stays observable.
28
+ */
29
+ import { isExcluded, settledCount } from './settled-boundary.js';
30
+ /**
31
+ * The index of the first entry that is still on screen: count backwards up to
32
+ * `maxEntries` PRINTABLE entries. Everything before it has left the region and is the
33
+ * writer's business.
34
+ *
35
+ * `maxEntries <= 0` returns `entries.length` - an empty window, which is the old
36
+ * behaviour ("everything printable may print") and the reason the guard is written as
37
+ * an early return rather than as a loop bound.
38
+ */
39
+ export function tailWindowStart(entries, maxEntries) {
40
+ const limit = Number.isFinite(maxEntries) ? Math.floor(maxEntries) : 0;
41
+ if (limit <= 0)
42
+ return entries.length;
43
+ let seen = 0;
44
+ for (let i = entries.length - 1; i >= 0; i -= 1) {
45
+ if (isExcluded(entries[i]))
46
+ continue;
47
+ seen += 1;
48
+ if (seen === limit)
49
+ return i;
50
+ }
51
+ // Fewer printable entries than the window holds: the whole conversation is on screen.
52
+ return 0;
53
+ }
54
+ /** `entries.slice(start)` with the entries that never print dropped. */
55
+ export function selectTailWindow(entries, maxEntries) {
56
+ const start = tailWindowStart(entries, maxEntries);
57
+ return { start, entries: entries.slice(start).filter((entry) => !isExcluded(entry)) };
58
+ }
59
+ /**
60
+ * The print boundary: never print what the region is still showing.
61
+ *
62
+ * THIS IS THE ONLY PLACE THAT COMBINES "settled" WITH "in the window" (RV-P2-13), which
63
+ * is why the composition lives here instead of at the pump: `promote()` must keep
64
+ * receiving one number, and the two authorities it is derived from - `settledCount`
65
+ * (what may print at all) and `tailWindowStart` (what is still visible) - then cannot
66
+ * drift apart in a future edit.
67
+ *
68
+ * `settleAllEntries` is the OTHER authority, not a duplicate: it answers "what must be
69
+ * printed when the conversation is being torn down", while this answers "how far may
70
+ * this frame print".
71
+ */
72
+ export function printBoundary(entries, runActive, maxEntries) {
73
+ return Math.min(settledCount(entries, runActive), tailWindowStart(entries, maxEntries));
74
+ }
@@ -1,31 +1,55 @@
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
+ * tui-final-reply-truncated-unscrollable A-prime).
4
5
  *
5
6
  * `App` calls it ALWAYS (hooks cannot be conditional); `enabled: false` makes every
6
7
  * effect a no-op and keeps `scrollMode: 'app'` byte-identical to the pre-feature
7
- * build (I-S9).
8
+ * build (I-S9 / I-F10).
8
9
  *
9
10
  * What it does when enabled:
10
- * 1. Prints the session header (and the opener for an empty session) once.
11
- * 2. On every entries change, promotes each newly SETTLED entry: renders it once
12
- * through the offscreen renderer and enqueues the rows for the writer.
13
- * 3. Computes the live-region budget and the streaming preview rows for the shell.
11
+ * 1. On every entries change, promotes each entry that has LEFT THE TAIL WINDOW:
12
+ * renders it once through the offscreen renderer and enqueues the rows for the
13
+ * writer. Entries still on screen are never printed - at `H = rows - 1` a printed
14
+ * row is pushed back off the top by the next frame, and a second copy in the
15
+ * scrollback would be a duplicate the user meets by scrolling up (I-F8).
16
+ * 2. Gives way at the TOP (tui-final-reply-truncated-unscrollable). The window is still
17
+ * counted in ENTRIES, so a settled answer taller than the box used to sit inside the
18
+ * window forever, clipped by `justifyContent="flex-end"`, in a mode whose wheel only
19
+ * scrolls the terminal's scrollback: the clipped rows existed nowhere the user could
20
+ * reach. The rows the box hides are now measured (`overflow-print.ts`) and printed, so
21
+ * `scrollback + screen` covers every row exactly once. Screen unchanged, reachability
22
+ * fixed.
23
+ * 3. Computes the live-region budget, the window and the tail rows for the shell.
24
+ * 4. Flushes the window before either reset path (`/clear`, `--resume`) and on exit,
25
+ * so "on screen but not printed yet" can never become "lost" (D8 / I-F12).
26
+ *
27
+ * THE HEADER AND THE OPENER ARE NOT PRINTED HERE ANY MORE (D3/D4). They are slots of
28
+ * `ScrollbackShell` now, rendered inside the region: `H = rows - 1` means any printed
29
+ * row is immediately scrolled off the screen, so printing the banner would be the same
30
+ * as not drawing it. Their `renderNode` path, the `startedRef` latch that kept their
31
+ * element identities stable, and the `printBanner()` call in `settleAll()` are gone.
14
32
  *
15
33
  * THE PROMOTION RUNS IN A MICROTASK, NOT IN THE EFFECT. The offscreen renderer is a
16
34
  * second Ink instance, and both instances share one React reconciler. A `rerender`
17
35
  * 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
- * microtask runs after the commit has unwound, where the offscreen render is
20
- * synchronous again. Everything the pass needs is read from refs for that reason.
36
+ * commit finishes, so its frame would not exist when the effect reads it. A microtask
37
+ * runs after the commit has unwound, where the offscreen render is synchronous again.
38
+ *
39
+ * THE PASS READS THE COMMITTED FRAME, NOT THE LATEST RENDER. `latest` is refreshed during
40
+ * render, so it can be a frame ahead of the screen; `committed` is written by a layout
41
+ * effect and therefore describes exactly the frame the user is looking at. With
42
+ * entry-granular bookkeeping the difference was harmless (printing was idempotent per
43
+ * entry); with ROW-granular bookkeeping it would slice this frame's picture with another
44
+ * frame's row numbers, and the result is a duplicate or a hole.
21
45
  *
22
- * `printedIds` is the ONLY authority on "already printed" (RV-7). The cursor is just
23
- * a hint for where to resume scanning: entries leave the list by retain-ring
24
- * trimming AND by reducer deletion (empty settled assistants, abandoned tool
25
- * cards), so an id-keyed cursor can dangle, and then the only correct answer is to
26
- * rescan and let `printedIds` skip.
46
+ * `printed` is the ONLY authority on "already printed" (RV-7): a `Map` from entry id to
47
+ * the leading rows already handed to the writer, and the width they were measured at. The
48
+ * cursor is just a hint for where to resume scanning: entries leave the list by retain-ring
49
+ * trimming AND by reducer deletion (empty settled assistants, abandoned tool cards), so an
50
+ * id-keyed cursor can dangle, and then the only correct answer is to rescan and let
51
+ * `printed` skip.
27
52
  */
28
- import React from 'react';
29
53
  import type { Entry } from '../../agent/reducer.js';
30
54
  import type { TermCapabilities } from '../capabilities.js';
31
55
  import type { DensityMode } from '../density.js';
@@ -41,7 +65,7 @@ export interface ScrollbackBridge {
41
65
  requestFlush: () => void;
42
66
  /**
43
67
  * Installed by the hook on mount (reads the latest entries through a ref).
44
- * `restore()` calls it on every exit path; it is idempotent through `printedIds`
68
+ * `restore()` calls it on every exit path; it is idempotent through `printed`
45
69
  * and it releases the offscreen renderer when done.
46
70
  */
47
71
  settleAllNow: () => void;
@@ -68,10 +92,6 @@ export interface UseScrollbackViewArgs {
68
92
  /** Terminal size. */
69
93
  rows: number;
70
94
  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
95
  overlayOpen: boolean;
76
96
  draftRows: number;
77
97
  popupRows: number;
@@ -84,18 +104,34 @@ export interface UseScrollbackViewArgs {
84
104
  panelEnabled: boolean;
85
105
  /** `buildTeamPanelLayout().rowCount`; 0 when nothing is dispatched. */
86
106
  teamRowsNatural: number;
107
+ /**
108
+ * `state.expandedToolIds` - REQUIRED, because the height of a window entry depends on it
109
+ * and a forgotten value would measure an expansion the region is not drawing.
110
+ */
111
+ expandedToolIds: Record<string, true>;
87
112
  }
88
113
  export interface ScrollbackView {
89
114
  budget: ScrollbackBudget | null;
90
115
  liveTailLines: string[];
91
116
  /**
92
- * The exact list `liveTailLines` was built from, so the real-component tail
93
- * (`LiveTail`) and the plain-text fallback can never disagree about their input.
117
+ * The entries the region is showing, in order (excluded kinds already dropped).
118
+ * `windowStart` is their index in the full list - together they are what `/perf`
119
+ * reports as `window=<start>/<n>`, the number that makes the "visible vs printable"
120
+ * gap observable instead of a guess (known limitation 5).
121
+ */
122
+ tailEntries: readonly Entry[];
123
+ windowStart: number;
124
+ /**
125
+ * Measured height of `tailEntries`, once every one of them is stable AND measured at the
126
+ * current geometry; `undefined` otherwise. `LiveTail` prefers it over the estimate - see
127
+ * `tail-anchor.ts::resolveTailAnchor` for why the measured answer has to win.
94
128
  */
95
- unsettledEntries: readonly Entry[];
129
+ measuredTailRows: number | undefined;
130
+ /** Rows the top give-way has printed this session; `/perf`'s `head=`. */
131
+ headRows: number;
96
132
  /** Print a fresh EXPANDED copy of an entry; false when it cannot be found / rendered. */
97
133
  reprintExpanded(entryId: string): boolean;
98
- /** Promote everything printable now (shutdown). */
134
+ /** Promote everything still unprinted (shutdown) - `flushUnprinted(latest entries)`. */
99
135
  settleAll(): void;
100
136
  /** Called by `App`'s dispatch wrapper BEFORE a reset action reaches the reducer. */
101
137
  notifyReset(kind: ScrollbackResetKind): void;