fix(tui): prevent duplicate renderable IDs (#32110)
This commit is contained in:
parent
cf2d1dd3e9
commit
fff0ec294c
15 changed files with 98 additions and 95 deletions
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue