fix(tui): prevent duplicate renderable IDs (#32110)

This commit is contained in:
Sebastian 2026-06-13 00:51:32 +02:00 committed by GitHub
commit fff0ec294c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 98 additions and 95 deletions

View file

@ -1,6 +1,6 @@
/** @jsxImportSource @opentui/solid */
import { expect, test } from "bun:test"
import { RGBA, type BoxRenderable } from "@opentui/core"
import { BoxRenderable, RGBA, type RootRenderable } from "@opentui/core"
import { testRender, useRenderer } from "@opentui/solid"
import { createSignal } from "solid-js"
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"
@ -248,6 +248,45 @@ function expectPaletteList(list: BoxRenderable, selectedIndex: number) {
)
}
function child(root: BoxRenderable | RootRenderable, index: number) {
return root.getChildren()[index] as BoxRenderable
}
function boxPath(root: BoxRenderable | RootRenderable, name: string): BoxRenderable[] | undefined {
for (const item of root.getChildren()) {
if (item.constructor.name === name) return root instanceof BoxRenderable ? [root] : []
if (!(item instanceof BoxRenderable)) continue
const path = boxPath(item, name)
if (path) return root instanceof BoxRenderable ? [root, ...path] : path
}
}
function footerComposerFrame(root: BoxRenderable | RootRenderable) {
return boxPath(root, "TextareaRenderable")!.at(-5)!
}
function footerStatusline(root: BoxRenderable | RootRenderable) {
const status = (RUN_THEME_FALLBACK.footer.status as RGBA).toInts()
const accent = (RUN_THEME_FALLBACK.footer.statusAccent as RGBA).toInts()
const boxes = root.getChildren().filter((item): item is BoxRenderable => item instanceof BoxRenderable)
for (const box of boxes) {
const first = box.getChildren().find((item): item is BoxRenderable => item instanceof BoxRenderable)
if (
box.backgroundColor?.toInts().every((value, index) => value === status[index]) &&
first?.backgroundColor?.toInts().every((value, index) => value === accent[index])
)
return box
boxes.push(...box.getChildren().filter((item): item is BoxRenderable => item instanceof BoxRenderable))
}
throw new Error("Footer statusline not found")
}
function panelMenu(root: BoxRenderable | RootRenderable) {
const panel = child(child(root, 0), 0)
const content = child(panel, 0)
return child(content.getChildren().at(-1) as BoxRenderable, 0)
}
test("direct footer composer area does not adopt footer surface", async () => {
const surface = RGBA.fromHex("#123456")
const [theme, setTheme] = createSignal(RUN_THEME_FALLBACK)
@ -255,7 +294,7 @@ test("direct footer composer area does not adopt footer surface", async () => {
try {
await app.renderOnce()
const area = app.renderer.root.findDescendantById("run-direct-footer-composer-area") as BoxRenderable
const area = child(footerComposerFrame(app.renderer.root), 0)
expect(area.backgroundColor.toInts()).not.toEqual(surface.toInts())
setTheme({
@ -588,7 +627,7 @@ test("direct subagent panel renders active subagents", async () => {
try {
await app.renderOnce()
const frame = app.captureCharFrame()
const list = app.renderer.root.findDescendantById("run-direct-footer-subagent-list") as BoxRenderable
const list = panelMenu(app.renderer.root)
expect(frame).toContain("Select subagent")
expect(frame).toContain("Inspect auth flow")
@ -626,7 +665,7 @@ test("direct queued prompt panel renders pending prompt actions", async () => {
try {
await app.renderOnce()
const frame = app.captureCharFrame()
const list = app.renderer.root.findDescendantById("run-direct-footer-queued-list") as BoxRenderable
const list = panelMenu(app.renderer.root)
expect(frame).toContain("Queued prompts")
expect(frame).toContain("fix the auth test")
@ -648,12 +687,12 @@ test.skip("direct footer recreates the frame across command panel transitions",
await app.renderOnce()
for (let index = 0; index < 3; index++) {
const composerFrame = app.renderer.root.findDescendantById("run-direct-footer-composer-frame") as BoxRenderable
const composerFrame = footerComposerFrame(app.renderer.root)
app.mockInput.pressKey("p", { ctrl: true })
await app.renderOnce()
expect(app.captureCharFrame()).toContain("Commands")
expect(app.renderer.root.findDescendantById("run-direct-footer-composer-frame")).not.toBe(composerFrame)
expect(footerComposerFrame(app.renderer.root)).not.toBe(composerFrame)
app.mockInput.pressKey("c", { ctrl: true })
await app.renderOnce()
expect(app.captureCharFrame()).not.toContain("Commands")
@ -952,13 +991,14 @@ test("direct footer shows editable prompts and additional queued work while runn
const transparent = RGBA.fromValues(0, 0, 0, 0).toInts()
const tinted = (RUN_THEME_FALLBACK.footer.status as RGBA).toInts()
const accent = (RUN_THEME_FALLBACK.footer.statusAccent as RGBA).toInts()
const statusline = app.renderer.root.findDescendantById("run-direct-footer-statusline") as BoxRenderable
const mode = app.renderer.root.findDescendantById("run-direct-footer-statusline-mode") as BoxRenderable
const main = app.renderer.root.findDescendantById("run-direct-footer-statusline-main") as BoxRenderable
const spinner = app.renderer.root.findDescendantById("run-direct-footer-status-spinner")
const model = app.renderer.root.findDescendantById("run-direct-footer-statusline-model") as BoxRenderable
const queued = app.renderer.root.findDescendantById("run-direct-footer-statusline-queued") as BoxRenderable
const hint = app.renderer.root.findDescendantById("run-direct-footer-statusline-hint") as BoxRenderable
const statusline = footerStatusline(app.renderer.root)
const statusItems = statusline.getChildren().filter((item): item is BoxRenderable => item instanceof BoxRenderable)
const mode = statusItems[0]
const main = statusItems[1]
const spinner = main.getChildren()[0]
const model = statusItems[2]
const queued = statusItems[3]
const hint = statusItems.at(-1)!
expect(spinner).toBeDefined()
expect(frame).toContain("a-model-name-long-enough-to-force-responsive-truncation")
@ -1276,7 +1316,7 @@ test("direct model panel renders current model selector", async () => {
try {
await app.renderOnce()
const frame = app.captureCharFrame()
const list = app.renderer.root.findDescendantById("run-direct-footer-model-list") as BoxRenderable
const list = panelMenu(app.renderer.root)
expect(frame).toContain("Select model")
expect(frame).toContain("Search")
@ -1319,7 +1359,7 @@ test("direct variant panel renders current variant selector", async () => {
try {
await app.renderOnce()
const frame = app.captureCharFrame()
const list = app.renderer.root.findDescendantById("run-direct-footer-variant-list") as BoxRenderable
const list = panelMenu(app.renderer.root)
expect(frame).toContain("Select variant")
expect(frame).toContain("Default")