Skip to content

Commit f66b290

Browse files
committed
Merge release/tui-bugfixes and paint the whole retained transcript, not a smaller window
PR #338 already caps shell.streamLog at MAX_RETAINED_STREAM_ROWS (600) and threads an absolute streamLogBase through every touch point, so the paint tree no longer needs a second, smaller window on top of that cap — painting every retained row is what makes all of it reachable by scrolling, and it composes with the retention machinery instead of duplicating it. Drop the LONG_LOG_WINDOW/collapse-marker path from shell.ts: repaintTranscriptWindow now paints shell.streamLog directly, paintAppendStreamRow adds one node per append and removes only what trimRetainedLog actually evicted, and replaceStreamRowAt's cheap single-node retext no longer has a windowing condition gating it off past 500 rows.
2 parents 2a89028 + d864400 commit f66b290

6 files changed

Lines changed: 329 additions & 261 deletions

File tree

src/tui-opentui/long-log.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,19 @@ export const LONG_LOG_WINDOW = 200
1919
*/
2020
export const LONG_LOG_COLLAPSE_THRESHOLD = 500
2121

22+
/**
23+
* Retained tail of a stream log. Display-only state — the agent's own context
24+
* 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.
27+
*/
28+
export const MAX_RETAINED_STREAM_ROWS = 600
29+
30+
/** Rows to drop from the front of a log of this length to fit the cap. */
31+
export function retentionOverflow(length: number): number {
32+
return Math.max(0, length - MAX_RETAINED_STREAM_ROWS)
33+
}
34+
2235
export type LongLogWindow = {
2336
/** Inclusive start index into the full row log. */
2437
readonly start: number

src/tui-opentui/mention-popup.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,39 @@ describe("@ popup narrows as you type", () => {
123123
})
124124
})
125125

126+
test("quitting mid-lookup does not write into the disposed shell", async () => {
127+
await withTestRenderer(
128+
async (h) => {
129+
const shell = createAppShell(h.renderer, {
130+
terminal: { columns: 80, rows: 24 },
131+
wireKeys: false,
132+
run: "idle",
133+
})
134+
let resolveLookup: (entries: readonly string[]) => void = () => {}
135+
setMentionSuggestionSource(
136+
shell,
137+
() =>
138+
new Promise<readonly string[]>((resolve) => {
139+
resolveLookup = resolve
140+
}),
141+
)
142+
143+
shell.prompt.value = "read @"
144+
shell.prompt.cursorOffset = shell.prompt.value.length
145+
const pending = openAtMentionSuggestions(shell)
146+
147+
// The operator quits before the filesystem lookup answers.
148+
shell.dispose()
149+
resolveLookup(["AGENTS.md", "README.md"])
150+
151+
await expect(pending).resolves.toBe(false)
152+
expect(shell.overlayKind).toBeNull()
153+
expect(isMentionPopupOpen(shell)).toBe(false)
154+
},
155+
{ width: 80, height: 24 },
156+
)
157+
})
158+
126159
test("no match closes the popup and leaves the typed text", async () => {
127160
await withShell(async (shell) => {
128161
await openAt(shell, "@")

src/tui-opentui/prompt-features.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,37 @@ describe("image attachments", () => {
7171
})
7272
})
7373

74+
test("quitting mid-read does not attach into the disposed shell", async () => {
75+
await withTestRenderer(
76+
async (h) => {
77+
const shell = createAppShell(h.renderer, {
78+
terminal: { columns: 80, rows: 24 },
79+
wireKeys: true,
80+
run: "idle",
81+
})
82+
let resolveRead: (r: { ok: true; attachment: PendingImageAttachment }) => void =
83+
() => {}
84+
setPromptImageSource(
85+
shell,
86+
() =>
87+
new Promise((resolve) => {
88+
resolveRead = resolve
89+
}),
90+
)
91+
92+
const pending = attachClipboardImage(shell)
93+
94+
// The operator quits before the clipboard read answers.
95+
shell.dispose()
96+
resolveRead({ ok: true, attachment: CLIP })
97+
98+
expect(await pending).toBe(false)
99+
expect(shell.pendingAttachments).toEqual([])
100+
},
101+
{ width: 80, height: 24 },
102+
)
103+
})
104+
74105
// Raw control bytes, not a synthetic KeyEvent: a binding that never matches
75106
// what the terminal actually writes looks correct in the catalog and fails
76107
// silently in use.

0 commit comments

Comments
 (0)