Skip to content

Commit 4d40cd3

Browse files
committed
Clear stale float offsets when the overlay host returns to flow
The overlay host floats absolutely over the landing screen, and the un-float path only flipped position back to relative. Yoga treats the leftover top inset as a relative offset, so after any overlay had opened on the landing, every later in-session overlay — the slash popup, the model picker — rendered that many rows below its flow slot, ending up under the prompt and clipped off the screen.
1 parent 50d0409 commit 4d40cd3

2 files changed

Lines changed: 63 additions & 0 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { expect, test } from "bun:test"
2+
3+
import { withTestRenderer } from "./harness"
4+
import { appendStreamRow, closeInsetOverlay, createAppShell } from "./shell"
5+
import { openModelPickerOverlay } from "./overlays"
6+
import type { PaletteCommand } from "./command-catalog"
7+
8+
const CATALOG: readonly PaletteCommand[] = [
9+
{ id: "model", label: "/model" },
10+
{ id: "mcp", label: "/mcp" },
11+
]
12+
13+
// Regression: the overlay host floats absolutely over the landing (top set to
14+
// a large row offset) but is an in-flow band once a transcript exists.
15+
// Un-floating used to leave the absolute insets behind, and under relative
16+
// positioning a stale top offsets the band downward — the slash popup rendered
17+
// below the prompt, clipped off the bottom of the screen.
18+
test("slash popup stays above the prompt after a landing-floated overlay", async () => {
19+
await withTestRenderer(
20+
async (h) => {
21+
const shell = createAppShell(h.renderer, {
22+
terminal: { columns: 120, rows: 50 },
23+
wireKeys: true,
24+
run: "idle",
25+
paletteCatalog: CATALOG,
26+
})
27+
try {
28+
// Landing: overlay floats absolutely with a large top offset.
29+
openModelPickerOverlay(shell, { items: ["codex/def / gpt-5.6-sol"] })
30+
await h.renderOnce()
31+
closeInsetOverlay(shell)
32+
await h.renderOnce()
33+
34+
// Transcript starts; overlays are in-flow bands from here on.
35+
appendStreamRow(shell, { role: "user", text: "hi" })
36+
appendStreamRow(shell, { role: "assistant", text: "Hi! What can I help you with?" })
37+
await h.renderOnce()
38+
39+
h.pressKey("/")
40+
h.pressKey("m")
41+
h.pressKey("o")
42+
await h.renderOnce()
43+
const frame = h.captureCharFrame()
44+
const lines = frame.split("\n")
45+
const popupRow = lines.findIndex((l) => l.includes("/model"))
46+
const promptRow = lines.findIndex((l) => l.includes("/mo") && !l.includes("/model"))
47+
expect(popupRow).toBeGreaterThanOrEqual(0)
48+
expect(promptRow).toBeGreaterThanOrEqual(0)
49+
expect(popupRow).toBeLessThan(promptRow)
50+
} finally {
51+
shell.dispose()
52+
}
53+
},
54+
{ width: 120, height: 50 },
55+
)
56+
})

src/tui/shell.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1193,6 +1193,13 @@ function floatOverlayHost(
11931193
if (!floating) {
11941194
host.position = "relative"
11951195
host.zIndex = 0
1196+
// A previous landing float left absolute insets behind. Under relative
1197+
// positioning those same values act as offsets from the in-flow slot, so
1198+
// a stale top pushes the band that many rows below the prompt — clear
1199+
// them so the band sits where the flow put it.
1200+
host.top = 0
1201+
host.left = 0
1202+
host.width = "100%"
11961203
return
11971204
}
11981205
host.position = "absolute"

0 commit comments

Comments
 (0)