|
1 | 1 | /** |
2 | | - * Long-log window strategy — keep multi-thousand-line sessions interactive. |
3 | | - * Pure slice math; shell paints only the window, not the full history. |
4 | | - * |
5 | | - * Budget (Wave 6 defaults; CL-5399 may refine): |
6 | | - * - Painted window: last N rows (or pin around offset) |
7 | | - * - Collapse threshold: when history exceeds this, older rows stay in the |
8 | | - * model but drop from the render tree until scrolled into the window |
| 2 | + * Retention budget for a long-running transcript. The paint tree tracks |
| 3 | + * `streamLog` 1:1 (see shell.ts's repaintTranscriptWindow/paintAppendStreamRow) |
| 4 | + * so every retained row stays reachable by scrolling; this cap is what keeps |
| 5 | + * that array — and so the paint tree — bounded over a long session. |
9 | 6 | */ |
10 | 7 |
|
11 | | -import type { StreamRow } from "./stream.js" |
12 | | - |
13 | | -/** Rows kept in the paint tree under normal follow-tail. */ |
14 | | -export const LONG_LOG_WINDOW = 200 |
15 | | - |
16 | | -/** |
17 | | - * When total rows exceed this, append/scroll paths must use windowSlice |
18 | | - * (never re-paint the full history). |
19 | | - */ |
20 | | -export const LONG_LOG_COLLAPSE_THRESHOLD = 500 |
21 | | - |
22 | 8 | /** |
23 | 9 | * Retained tail of a stream log. Display-only state — the agent's own context |
24 | 10 | * is kept separately — but an unbounded array still costs memory and O(n) |
25 | | - * snapshot/diff work on every append over a long, tool-heavy session. Set |
26 | | - * above the collapse threshold so eviction never fights the paint window. |
| 11 | + * snapshot/diff work on every append over a long, tool-heavy session. |
27 | 12 | */ |
28 | 13 | export const MAX_RETAINED_STREAM_ROWS = 600 |
29 | 14 |
|
30 | 15 | /** Rows to drop from the front of a log of this length to fit the cap. */ |
31 | 16 | export function retentionOverflow(length: number): number { |
32 | 17 | return Math.max(0, length - MAX_RETAINED_STREAM_ROWS) |
33 | 18 | } |
34 | | - |
35 | | -export type LongLogWindow = { |
36 | | - /** Inclusive start index into the full row log. */ |
37 | | - readonly start: number |
38 | | - /** Exclusive end index. */ |
39 | | - readonly end: number |
40 | | - /** Slice of rows to paint. */ |
41 | | - readonly rows: readonly StreamRow[] |
42 | | - /** True when older rows exist above the window. */ |
43 | | - readonly truncatedAbove: boolean |
44 | | - /** True when newer rows exist below the window (pinned). */ |
45 | | - readonly truncatedBelow: boolean |
46 | | - /** Full log length. */ |
47 | | - readonly total: number |
48 | | -} |
49 | | - |
50 | | -export type WindowSliceOpts = { |
51 | | - /** Max rows to include (default LONG_LOG_WINDOW). */ |
52 | | - readonly windowSize?: number |
53 | | - /** |
54 | | - * Pin the window so this index is visible (keep-active-visible style). |
55 | | - * When omitted, follow the tail (last windowSize rows). |
56 | | - */ |
57 | | - readonly pinIndex?: number |
58 | | -} |
59 | | - |
60 | | -/** |
61 | | - * Compute which rows to paint for a long log. |
62 | | - * Follow-tail by default; pinIndex keeps a historical row in view. |
63 | | - */ |
64 | | -export function windowSlice( |
65 | | - log: readonly StreamRow[], |
66 | | - opts?: WindowSliceOpts, |
67 | | -): LongLogWindow { |
68 | | - const total = log.length |
69 | | - const windowSize = Math.max(1, Math.floor(opts?.windowSize ?? LONG_LOG_WINDOW)) |
70 | | - |
71 | | - if (total === 0) { |
72 | | - return { |
73 | | - start: 0, |
74 | | - end: 0, |
75 | | - rows: [], |
76 | | - truncatedAbove: false, |
77 | | - truncatedBelow: false, |
78 | | - total: 0, |
79 | | - } |
80 | | - } |
81 | | - |
82 | | - let end: number |
83 | | - let start: number |
84 | | - |
85 | | - if (opts?.pinIndex !== undefined) { |
86 | | - const pin = Math.max(0, Math.min(total - 1, Math.floor(opts.pinIndex))) |
87 | | - // Center-ish: keep pin in window; prefer showing context after pin when possible. |
88 | | - start = Math.max(0, pin - Math.floor(windowSize / 2)) |
89 | | - end = Math.min(total, start + windowSize) |
90 | | - start = Math.max(0, end - windowSize) |
91 | | - } else { |
92 | | - // Follow tail |
93 | | - end = total |
94 | | - start = Math.max(0, total - windowSize) |
95 | | - } |
96 | | - |
97 | | - return { |
98 | | - start, |
99 | | - end, |
100 | | - rows: log.slice(start, end), |
101 | | - truncatedAbove: start > 0, |
102 | | - truncatedBelow: end < total, |
103 | | - total, |
104 | | - } |
105 | | -} |
106 | | - |
107 | | -/** Whether the log is large enough that windowing is mandatory. */ |
108 | | -export function mustWindow(totalRows: number): boolean { |
109 | | - return totalRows > LONG_LOG_COLLAPSE_THRESHOLD |
110 | | -} |
111 | | - |
112 | | -/** |
113 | | - * Collapse marker line for the paint tree when truncatedAbove. |
114 | | - * Pure string — shell styles it as system chrome. |
115 | | - */ |
116 | | -export function collapseMarker(above: number): string { |
117 | | - const n = Math.max(0, Math.floor(above)) |
118 | | - if (n <= 0) return "" |
119 | | - return `… ${n} earlier line${n === 1 ? "" : "s"} collapsed` |
120 | | -} |
0 commit comments