fix(tui): prevent duplicate renderable IDs (#32110)
This commit is contained in:
@@ -784,7 +784,6 @@ function DiffViewer(props: { api: TuiPluginApi }) {
|
||||
<Panel flexGrow={1} minHeight={0} border="none">
|
||||
<Separator axis="x" start={showFileTree() ? "edge-out" : undefined} />
|
||||
<scrollbox
|
||||
id="diff-viewer-patches"
|
||||
ref={(element: ScrollBoxRenderable) => (scroll = element)}
|
||||
flexGrow={1}
|
||||
minHeight={0}
|
||||
@@ -824,7 +823,6 @@ function DiffViewer(props: { api: TuiPluginApi }) {
|
||||
{(patch) => (
|
||||
<box border={patchLeftBorder()} borderColor={theme().border}>
|
||||
<diff
|
||||
id={`diff-viewer-patch-${entry.fileIndex}`}
|
||||
ref={(element: DiffRenderable) => diffNodeByFileIndex.set(entry.fileIndex, element)}
|
||||
diff={patch()}
|
||||
view={view()}
|
||||
|
||||
@@ -1607,7 +1607,13 @@ function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: Ass
|
||||
|
||||
return (
|
||||
<Show when={content()}>
|
||||
<box id={"text-" + props.part.id} paddingLeft={3} marginTop={1} flexDirection="column" flexShrink={0}>
|
||||
<box
|
||||
id={`text-${props.part.messageID}-${props.part.id}`}
|
||||
paddingLeft={3}
|
||||
marginTop={1}
|
||||
flexDirection="column"
|
||||
flexShrink={0}
|
||||
>
|
||||
<box onMouseUp={toggle}>
|
||||
<ReasoningHeader
|
||||
toggleable={inMinimal()}
|
||||
@@ -1684,7 +1690,7 @@ function TextPart(props: { last: boolean; part: TextPart; message: AssistantMess
|
||||
const { theme, syntax } = useTheme()
|
||||
return (
|
||||
<Show when={props.part.text.trim()}>
|
||||
<box id={"text-" + props.part.id} paddingLeft={3} marginTop={1} flexShrink={0}>
|
||||
<box id={`text-${props.part.messageID}-${props.part.id}`} paddingLeft={3} marginTop={1} flexShrink={0}>
|
||||
<markdown
|
||||
syntaxStyle={syntax()}
|
||||
streaming={true}
|
||||
@@ -1875,7 +1881,7 @@ function InlineTool(props: {
|
||||
|
||||
return (
|
||||
<InlineToolRow
|
||||
id={`tool-inline-${props.subagent ? "subagent-" : ""}${props.part.id}`}
|
||||
id={`tool-inline-${props.subagent ? "subagent-" : ""}${props.part.messageID}-${props.part.id}`}
|
||||
icon={props.icon}
|
||||
iconColor={props.iconColor}
|
||||
color={fg()}
|
||||
@@ -2007,7 +2013,7 @@ function BlockTool(props: {
|
||||
const error = createMemo(() => (props.part?.state.status === "error" ? props.part.state.error : undefined))
|
||||
return (
|
||||
<box
|
||||
id={props.part ? "tool-block-" + props.part.id : undefined}
|
||||
id={props.part ? `tool-block-${props.part.messageID}-${props.part.id}` : undefined}
|
||||
border={["left"]}
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
@@ -2174,7 +2180,7 @@ function Read(props: ToolProps) {
|
||||
</InlineTool>
|
||||
<For each={loaded()}>
|
||||
{(filepath, index) => (
|
||||
<box id={`tool-inline-loaded-${props.part.id}-${index()}`} paddingLeft={3}>
|
||||
<box id={`tool-inline-loaded-${props.part.messageID}-${props.part.id}-${index()}`} paddingLeft={3}>
|
||||
<text paddingLeft={3} fg={theme.textMuted}>
|
||||
↳ Loaded {pathFormatter.format(filepath)}
|
||||
</text>
|
||||
|
||||
@@ -238,9 +238,19 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
const option = selected()
|
||||
if (option) props.onMove?.(option)
|
||||
if (!scroll) return
|
||||
const target = scroll.getChildren().find((child: { id?: string }) => {
|
||||
return child.id === JSON.stringify(selected()?.value)
|
||||
})
|
||||
let remaining = store.selected
|
||||
let index = 0
|
||||
// Locate the row by position because a unique renderable ID cannot currently be ensured.
|
||||
for (const [category, options] of grouped()) {
|
||||
if (category) index++
|
||||
if (remaining < options.length) {
|
||||
index += remaining
|
||||
break
|
||||
}
|
||||
index += options.length
|
||||
remaining -= options.length
|
||||
}
|
||||
const target = scroll.getChildren()[index]
|
||||
if (!target) return
|
||||
const y = target.y - scroll.y
|
||||
if (center) {
|
||||
@@ -553,7 +563,6 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
const current = createMemo(() => isDeepEqual(option.value, props.current))
|
||||
return (
|
||||
<box
|
||||
id={JSON.stringify(option.value)}
|
||||
flexDirection="column"
|
||||
position="relative"
|
||||
onMouseMove={() => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/** @jsxImportSource @opentui/solid */
|
||||
import { expect, test } from "bun:test"
|
||||
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"
|
||||
import type { Renderable, ScrollBoxRenderable } from "@opentui/core"
|
||||
import { DiffRenderable, type Renderable, ScrollBoxRenderable } from "@opentui/core"
|
||||
import { testRender, useRenderer } from "@opentui/solid"
|
||||
import type { TuiPluginApi, TuiPluginMeta, TuiRouteCurrent, TuiRouteDefinition } from "@opencode-ai/plugin/tui"
|
||||
import type { Session } from "@opencode-ai/sdk/v2"
|
||||
@@ -63,9 +63,9 @@ test("brackets navigate diff hunks", async () => {
|
||||
)
|
||||
try {
|
||||
await viewer.app.waitForFrame((frame) => frame.includes("const first"))
|
||||
await viewer.app.waitFor(() => Boolean(findRenderable(viewer.app.renderer.root, "diff-viewer-patches")))
|
||||
await viewer.app.waitFor(() => Boolean(findScrollBox(viewer.app.renderer.root)))
|
||||
await viewer.app.flush()
|
||||
const scroll = findRenderable(viewer.app.renderer.root, "diff-viewer-patches") as ScrollBoxRenderable
|
||||
const scroll = findScrollBox(viewer.app.renderer.root)!
|
||||
const initial = scroll.scrollTop
|
||||
|
||||
expect(TuiKeybind.defaultValue("diff_next_hunk")).toBe("]")
|
||||
@@ -178,14 +178,19 @@ async function renderDiffViewer(vcsDiff: unknown[], height = 20) {
|
||||
|
||||
const startRoute: TuiRouteCurrent = { name: "session", params: { sessionID: "session-1" } }
|
||||
|
||||
function findRenderable(root: Renderable, id: string): Renderable | undefined {
|
||||
if (root.id === id) return root
|
||||
function findScrollBox(root: Renderable): ScrollBoxRenderable | undefined {
|
||||
if (root instanceof ScrollBoxRenderable && containsDiff(root)) return root
|
||||
return root
|
||||
.getChildren()
|
||||
.map((child) => findRenderable(child, id))
|
||||
.map(findScrollBox)
|
||||
.find(Boolean)
|
||||
}
|
||||
|
||||
function containsDiff(root: Renderable): boolean {
|
||||
if (root instanceof DiffRenderable) return true
|
||||
return root.getChildren().some(containsDiff)
|
||||
}
|
||||
|
||||
const session = {
|
||||
id: "session-1",
|
||||
slug: "session-1",
|
||||
|
||||
Reference in New Issue
Block a user