mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-13 11:39:41 +03:00
fix(tui): only re-highlight blocks whose session link changed (#63247)
This commit is contained in:
@@ -19,7 +19,13 @@ import { stringWidth } from "../util/string-width"
|
||||
import { useDialog } from "./dialog"
|
||||
import { plainClick } from "./link"
|
||||
|
||||
type Rendered = { text: string; ranges: { start: number; end: number; sessionID: string }[] }
|
||||
// `sessionIDs` holds every ID the block mentions, linked or not, so a label change re-highlights only
|
||||
// the blocks that show it.
|
||||
type Rendered = {
|
||||
text: string
|
||||
ranges: { start: number; end: number; sessionID: string }[]
|
||||
sessionIDs: Set<string>
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a `<markdown>` ref that shows session IDs as links titled after their sessions and
|
||||
@@ -62,6 +68,7 @@ export function useSessionLinks() {
|
||||
return text !== undefined && !parseSessionMention(text) ? [{ start, end: position }] : []
|
||||
})
|
||||
position = 0
|
||||
const sessionIDs = new Set<string>()
|
||||
const parts = detectLinks(chunks, context).flatMap((chunk): { chunk: TextChunk; sessionID?: string }[] => {
|
||||
const start = position
|
||||
position += chunk.text.length
|
||||
@@ -74,6 +81,7 @@ export function useSessionLinks() {
|
||||
const text = chunk.text.slice(cuts[index], end)
|
||||
const mention = index % 2 ? mentions[(index - 1) / 2]! : undefined
|
||||
const label = mention && labelOf(mention.id)
|
||||
if (mention) sessionIDs.add(mention.id)
|
||||
if (mention && (!used.has(mention.id) || used.get(mention.id) !== label)) {
|
||||
used.set(mention.id, label)
|
||||
setVersion((value) => value + 1)
|
||||
@@ -95,17 +103,24 @@ export function useSessionLinks() {
|
||||
offset += part.chunk.text.length
|
||||
return part.sessionID ? [{ start, end: offset, sessionID: part.sessionID }] : []
|
||||
})
|
||||
rendered.set(this, { text: parts.map((part) => part.chunk.text).join(""), ranges })
|
||||
rendered.set(this, { text: parts.map((part) => part.chunk.text).join(""), ranges, sessionIDs })
|
||||
return parts.map((part) => part.chunk)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
version()
|
||||
if (!markdown || [...used].every(([sessionID, label]) => labelOf(sessionID) === label)) return
|
||||
// Toggling the callback marks each block's highlight dirty while keeping the identity OpenTUI
|
||||
// checks when terminal capabilities refresh link styling.
|
||||
const changed = [...used].flatMap(([sessionID, label]) => (labelOf(sessionID) === label ? [] : [sessionID]))
|
||||
if (!markdown || changed.length === 0) return
|
||||
changed.forEach((sessionID) => used.set(sessionID, labelOf(sessionID)))
|
||||
// Toggling the callback marks the block's highlight dirty while keeping the identity OpenTUI
|
||||
// checks when terminal capabilities refresh link styling. Dirty blocks highlight on the next
|
||||
// frame, so labels that change within one frame share a single re-highlight.
|
||||
const refresh = (renderable: Renderable): void => {
|
||||
if (renderable instanceof CodeRenderable && renderable.onChunks === linkChunks) {
|
||||
if (
|
||||
renderable instanceof CodeRenderable &&
|
||||
renderable.onChunks === linkChunks &&
|
||||
changed.some((sessionID) => rendered.get(renderable)?.sessionIDs.has(sessionID))
|
||||
) {
|
||||
renderable.onChunks = undefined
|
||||
renderable.onChunks = linkChunks
|
||||
renderable.requestRender()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @jsxImportSource @opentui/solid */
|
||||
import { expect, test } from "bun:test"
|
||||
import { SyntaxStyle, TextAttributes } from "@opentui/core"
|
||||
import { CodeRenderable, SyntaxStyle, TextAttributes, type MarkdownRenderable, type Renderable } from "@opentui/core"
|
||||
import { testRender, useRenderer } from "@opentui/solid"
|
||||
import { onCleanup, onMount, type JSX } from "solid-js"
|
||||
import { ConfigProvider } from "../../src/config"
|
||||
@@ -241,6 +241,65 @@ test.each([true, false])("code spans link only when they are exactly an ID, conc
|
||||
}
|
||||
})
|
||||
|
||||
test("a label change re-highlights only the blocks that mention that session", async () => {
|
||||
await using state = await tmpdir()
|
||||
const calls = createFetch(async () => undefined)
|
||||
const other = session(id("other"), "Other work")
|
||||
let data!: ReturnType<typeof useData>
|
||||
let markdown!: MarkdownRenderable
|
||||
|
||||
function Transcript() {
|
||||
data = useData()
|
||||
data.session.remember({ ...known })
|
||||
data.session.remember({ ...other })
|
||||
const links = useSessionLinks()
|
||||
const renderer = useRenderer()
|
||||
const syntaxStyle = SyntaxStyle.fromStyles({ default: { fg: "#ffffff" } }, renderer.nativeScene)
|
||||
onCleanup(() => syntaxStyle.destroy())
|
||||
return (
|
||||
<markdown
|
||||
ref={(renderable: MarkdownRenderable) => {
|
||||
links(renderable)
|
||||
markdown = renderable
|
||||
}}
|
||||
internalBlockMode="top-level"
|
||||
syntaxStyle={syntaxStyle}
|
||||
content={`First ${known.id}\n\nSecond ${other.id}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const app = await mount(state.path, calls.fetch, () => <Transcript />, 80, 6)
|
||||
|
||||
try {
|
||||
await waitForHighlightedFrame(app, (frame) => frame.includes("@Fix") && frame.includes("@Other"))
|
||||
// Record which blocks get their chunk callback reset, the refresh's re-highlight trigger.
|
||||
const reset: string[] = []
|
||||
const descriptor = Object.getOwnPropertyDescriptor(CodeRenderable.prototype, "onChunks")!
|
||||
const blocks = (renderable: Renderable): CodeRenderable[] => [
|
||||
...(renderable instanceof CodeRenderable ? [renderable] : []),
|
||||
...renderable.getChildren().flatMap(blocks),
|
||||
]
|
||||
blocks(markdown).forEach((block) =>
|
||||
Object.defineProperty(block, "onChunks", {
|
||||
configurable: true,
|
||||
get: () => descriptor.get!.call(block),
|
||||
set: (value) => {
|
||||
reset.push(block.plainText)
|
||||
descriptor.set!.call(block, value)
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
data.session.remember({ ...known, title: "Renamed" })
|
||||
await waitForHighlightedFrame(app, (frame) => frame.includes("@Renamed"))
|
||||
expect(reset.length).toBeGreaterThan(0)
|
||||
expect(reset.every((text) => text.startsWith("First"))).toBe(true)
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
async function mount(
|
||||
state: string,
|
||||
fetch: typeof globalThis.fetch,
|
||||
|
||||
Reference in New Issue
Block a user