fix: key stale show callbacks

pull/16347/head
Shoubhit Dash 2026-03-06 18:09:58 +05:30
parent 326c70184d
commit 05789d29d2
17 changed files with 110 additions and 49 deletions

View File

@ -74,7 +74,9 @@ export const DialogSelectProvider: Component = () => {
<Show when={i.id === "opencode"}>
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
</Show>
<Show when={note(i.id)}>{(value) => <div class="text-14-regular text-text-weak">{value()}</div>}</Show>
<Show when={note(i.id)} keyed>
{(value) => <div class="text-14-regular text-text-weak">{value}</div>}
</Show>
<Show when={i.id === "opencode-go"}>
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
</Show>

View File

@ -77,10 +77,10 @@ export const ModelTooltip: Component<{ model: ModelInfo; latest?: boolean; free?
return (
<div class="flex flex-col gap-1 py-1">
<div class="text-13-medium">{title()}</div>
<Show when={inputs()}>
<Show when={inputs()} keyed>
{(value) => (
<div class="text-12-regular text-text-invert-base">
{language.t("model.tooltip.allows", { inputs: value() })}
{language.t("model.tooltip.allows", { inputs: value })}
</div>
)}
</Show>

View File

@ -52,12 +52,10 @@ export const PromptContextItems: Component<ContextItemsProps> = (props) => {
<FileIcon node={{ path: item.path, type: "file" }} class="shrink-0 size-3.5" />
<div class="flex items-center text-11-regular min-w-0 font-medium">
<span class="text-text-strong whitespace-nowrap">{label}</span>
<Show when={item.selection}>
<Show when={item.selection} keyed>
{(sel) => (
<span class="text-text-weak whitespace-nowrap shrink-0">
{sel().startLine === sel().endLine
? `:${sel().startLine}`
: `:${sel().startLine}-${sel().endLine}`}
{sel.startLine === sel.endLine ? `:${sel.startLine}` : `:${sel.startLine}-${sel.endLine}`}
</span>
)}
</Show>
@ -74,8 +72,8 @@ export const PromptContextItems: Component<ContextItemsProps> = (props) => {
aria-label={props.t("prompt.context.removeFile")}
/>
</div>
<Show when={item.comment}>
{(comment) => <div class="text-12-regular text-text-strong ml-5 pr-1 truncate">{comment()}</div>}
<Show when={item.comment} keyed>
{(comment) => <div class="text-12-regular text-text-strong ml-5 pr-1 truncate">{comment}</div>}
</Show>
</div>
</Tooltip>

View File

@ -78,6 +78,7 @@ export function ServerRow(props: ServerRowProps) {
</span>
<Show
when={badge()}
keyed
fallback={
<Show when={props.status?.version}>
<span ref={versionRef} class={props.versionClass ?? "text-text-weak text-14-regular truncate"}>
@ -86,20 +87,20 @@ export function ServerRow(props: ServerRowProps) {
</Show>
}
>
{(badge) => badge()}
{(badge) => badge}
</Show>
</div>
<Show when={props.showCredentials && props.conn.type === "http" && props.conn}>
<Show when={props.showCredentials && props.conn.type === "http" && props.conn} keyed>
{(conn) => (
<div class="flex flex-row gap-3">
<span>
{conn().http.username ? (
<span class="text-text-weak">{conn().http.username}</span>
{conn.http.username ? (
<span class="text-text-weak">{conn.http.username}</span>
) : (
<span class="text-text-weaker">no username</span>
)}
</span>
{conn().http.password && <span class="text-text-weak"></span>}
{conn.http.password && <span class="text-text-weak"></span>}
</div>
)}
</Show>

View File

@ -73,15 +73,15 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
const tooltipValue = () => (
<div>
<Show when={context()}>
<Show when={context()} keyed>
{(ctx) => (
<>
<div class="flex items-center gap-2">
<span class="text-text-invert-strong">{ctx().total.toLocaleString(language.intl())}</span>
<span class="text-text-invert-strong">{ctx.total.toLocaleString(language.intl())}</span>
<span class="text-text-invert-base">{language.t("context.usage.tokens")}</span>
</div>
<div class="flex items-center gap-2">
<span class="text-text-invert-strong">{ctx().usage ?? 0}%</span>
<span class="text-text-invert-strong">{ctx.usage ?? 0}%</span>
<span class="text-text-invert-base">{language.t("context.usage.usage")}</span>
</div>
</>

View File

@ -316,12 +316,12 @@ export function SessionContextTab() {
</div>
</Show>
<Show when={systemPrompt()}>
<Show when={systemPrompt()} keyed>
{(prompt) => (
<div class="flex flex-col gap-2">
<div class="text-12-regular text-text-weak">{language.t("context.systemPrompt.title")}</div>
<div class="border border-border-base rounded-md bg-surface-base px-3 py-2">
<Markdown text={prompt()} class="text-12-regular" />
<Markdown text={prompt} class="text-12-regular" />
</div>
</div>
)}

View File

@ -356,9 +356,9 @@ export function SessionHeader() {
return (
<>
<Show when={centerMount()}>
<Show when={centerMount()} keyed>
{(mount) => (
<Portal mount={mount()}>
<Portal mount={mount}>
<Button
type="button"
variant="ghost"
@ -376,18 +376,16 @@ export function SessionHeader() {
</span>
</div>
<Show when={hotkey()}>
{(keybind) => (
<Keybind class="shrink-0 !border-0 !bg-transparent !shadow-none px-0">{keybind()}</Keybind>
)}
<Show when={hotkey()} keyed>
{(keybind) => <Keybind class="shrink-0 !border-0 !bg-transparent !shadow-none px-0">{keybind}</Keybind>}
</Show>
</Button>
</Portal>
)}
</Show>
<Show when={rightMount()}>
<Show when={rightMount()} keyed>
{(mount) => (
<Portal mount={mount()}>
<Portal mount={mount}>
<div class="flex items-center gap-2">
<StatusPopover />
<Show when={projectDirectory()}>

View File

@ -62,14 +62,14 @@ export function NewSessionView(props: NewSessionViewProps) {
<Icon name="branch" size="small" class="mt-0.5 shrink-0" />
<div class="text-12-medium text-text-weak select-text leading-5">{label(current())}</div>
</div>
<Show when={sync.project}>
<Show when={sync.project} keyed>
{(project) => (
<div class="flex justify-center items-start gap-3 min-h-5">
<Icon name="pencil-line" size="small" class="mt-0.5 shrink-0" />
<div class="text-12-medium text-text-weak leading-5">
{language.t("session.new.lastModified")}&nbsp;
<span class="text-text-strong">
{DateTime.fromMillis(project().time.updated ?? project().time.created)
{DateTime.fromMillis(project.time.updated ?? project.time.created)
.setLocale(language.intl())
.toRelative()}
</span>

View File

@ -62,7 +62,9 @@ export function SortableTab(props: { tab: string; onTabClose: (tab: string) => v
hideCloseButton
onMiddleClick={() => props.onTabClose(props.tab)}
>
<Show when={content()}>{(value) => value()}</Show>
<Show when={content()} keyed>
{(value) => value}
</Show>
</Tabs.Trigger>
</div>
</div>

View File

@ -473,7 +473,7 @@ export const SettingsGeneral: Component = () => {
<SoundsSection />
{/*<Show when={platform.platform === "desktop" && platform.os === "windows" && platform.getWslEnabled}>
{/*<Show when={platform.platform === "desktop" && platform.os === "windows" && platform.getWslEnabled} keyed>
{(_) => {
const [enabledResource, actions] = createResource(() => platform.getWslEnabled?.())
const enabled = () => (enabledResource.state === "pending" ? undefined : enabledResource.latest)
@ -503,7 +503,7 @@ export const SettingsGeneral: Component = () => {
<UpdatesSection />
<Show when={linux()}>
<Show when={linux()} keyed>
{(_) => {
const [valueResource, actions] = createResource(() => platform.getDisplayBackend?.())
const value = () => (valueResource.state === "pending" ? undefined : valueResource.latest)

View File

@ -189,8 +189,8 @@ export const SettingsProviders: Component = () => {
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
</Show>
</div>
<Show when={note(item.id)}>
{(key) => <span class="text-12-regular text-text-weak pl-8">{language.t(key())}</span>}
<Show when={note(item.id)} keyed>
{(key) => <span class="text-12-regular text-text-weak pl-8">{language.t(key)}</span>}
</Show>
</div>
<Button

View File

@ -290,8 +290,8 @@ export const ErrorPage: Component<ErrorPageProps> = (props) => {
</Show>
</Show>
</div>
<Show when={store.actionError}>
{(message) => <p class="text-xs text-text-danger-base text-center max-w-2xl">{message()}</p>}
<Show when={store.actionError} keyed>
{(message) => <p class="text-xs text-text-danger-base text-center max-w-2xl">{message}</p>}
</Show>
<div class="flex flex-col items-center gap-2">
<div class="flex items-center justify-center gap-1">
@ -305,10 +305,8 @@ export const ErrorPage: Component<ErrorPageProps> = (props) => {
<Icon name="discord" class="text-text-interactive-base" />
</button>
</div>
<Show when={platform.version}>
{(version) => (
<p class="text-xs text-text-weak">{language.t("error.page.version", { version: version() })}</p>
)}
<Show when={platform.version} keyed>
{(version) => <p class="text-xs text-text-weak">{language.t("error.page.version", { version })}</p>}
</Show>
</div>
</div>

View File

@ -43,10 +43,10 @@ export const ProjectDragOverlay = (props: {
}): JSX.Element => {
const project = createMemo(() => props.projects().find((p) => p.worktree === props.activeProject()))
return (
<Show when={project()}>
<Show when={project()} keyed>
{(p) => (
<div class="bg-background-base rounded-xl p-1">
<ProjectIcon project={p()} />
<ProjectIcon project={p} />
</div>
)}
</Show>

View File

@ -76,8 +76,8 @@ export const WorkspaceDragOverlay = (props: {
})
return (
<Show when={label()}>
{(value) => <div class="bg-background-base rounded-md px-2 py-1 text-14-medium text-text-strong">{value()}</div>}
<Show when={label()} keyed>
{(value) => <div class="bg-background-base rounded-md px-2 py-1 text-14-medium text-text-strong">{value}</div>}
</Show>
)
}

View File

@ -731,12 +731,12 @@ export function MessageTimeline(props: {
class="size-3.5 shrink-0"
/>
<span class="truncate">{getFilename(comment().path)}</span>
<Show when={comment().selection}>
<Show when={comment().selection} keyed>
{(selection) => (
<span class="shrink-0 text-text-weak">
{selection().startLine === selection().endLine
? `:${selection().startLine}`
: `:${selection().startLine}-${selection().endLine}`}
{selection.startLine === selection.endLine
? `:${selection.startLine}`
: `:${selection.startLine}-${selection.endLine}`}
</span>
)}
</Show>

View File

@ -0,0 +1,61 @@
import { describe, test } from "bun:test"
import { join, relative, resolve } from "node:path"
import * as ts from "typescript"
const scan = async (dir: string) =>
Promise.all(
Array.from(new Bun.Glob("**/*.tsx").scanSync({ cwd: dir })).map(async (file) => {
const full = join(dir, file)
return {
file: full,
text: await Bun.file(full).text(),
}
}),
)
const find = (file: string, text: string, root: string) => {
const hits: string[] = []
const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX)
const walk = (node: ts.Node): void => {
if (ts.isJsxElement(node) && node.openingElement.tagName.getText(source) === "Show") {
const keyed = node.openingElement.attributes.properties.some(
(prop) => ts.isJsxAttribute(prop) && prop.name.getText(source) === "keyed",
)
const child = node.children.find((child) => {
if (ts.isJsxText(child)) return child.getText(source).trim() !== ""
if (ts.isJsxExpression(child)) return child.expression !== undefined
return true
})
if (
!keyed &&
child &&
ts.isJsxExpression(child) &&
child.expression &&
ts.isArrowFunction(child.expression) &&
child.expression.parameters.length > 0
) {
hits.push(`${relative(root, file)}:${source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1}`)
}
}
ts.forEachChild(node, walk)
}
walk(source)
return hits
}
describe("show keyed guard", () => {
test("app and desktop show callbacks are keyed", async () => {
const root = resolve(import.meta.dir, "../../..")
const hits = (await Promise.all([scan(import.meta.dir), scan(resolve(import.meta.dir, "../../desktop/src"))]))
.flat()
.flatMap((item) => find(item.file, item.text, root))
if (hits.length > 0) {
throw new Error(`non-keyed <Show> callbacks found:\n${hits.join("\n")}`)
}
})
})

View File

@ -482,6 +482,7 @@ function ServerGate(props: { children: (data: ServerReadyData) => JSX.Element })
return (
<Show
when={serverData.state !== "pending" && serverData()}
keyed
fallback={
<div class="h-screen w-screen flex flex-col items-center justify-center bg-background-base">
<Splash class="w-16 h-20 opacity-50 animate-pulse" />
@ -489,7 +490,7 @@ function ServerGate(props: { children: (data: ServerReadyData) => JSX.Element })
</div>
}
>
{(data) => props.children(data())}
{(data) => props.children(data)}
</Show>
)
}