Compare commits

...

2 Commits

Author SHA1 Message Date
Shoubhit Dash 5ec45c03d6
Merge branch 'dev' into fix/16323-keyed-show-callbacks 2026-03-06 18:14:07 +05:30
Shoubhit Dash 05789d29d2 fix: key stale show callbacks 2026-03-06 18:09:58 +05:30
17 changed files with 110 additions and 49 deletions

View File

@ -74,7 +74,9 @@ export const DialogSelectProvider: Component = () => {
<Show when={i.id === "opencode"}> <Show when={i.id === "opencode"}>
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag> <Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
</Show> </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"}> <Show when={i.id === "opencode-go"}>
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag> <Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
</Show> </Show>

View File

@ -77,10 +77,10 @@ export const ModelTooltip: Component<{ model: ModelInfo; latest?: boolean; free?
return ( return (
<div class="flex flex-col gap-1 py-1"> <div class="flex flex-col gap-1 py-1">
<div class="text-13-medium">{title()}</div> <div class="text-13-medium">{title()}</div>
<Show when={inputs()}> <Show when={inputs()} keyed>
{(value) => ( {(value) => (
<div class="text-12-regular text-text-invert-base"> <div class="text-12-regular text-text-invert-base">
{language.t("model.tooltip.allows", { inputs: value() })} {language.t("model.tooltip.allows", { inputs: value })}
</div> </div>
)} )}
</Show> </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" /> <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"> <div class="flex items-center text-11-regular min-w-0 font-medium">
<span class="text-text-strong whitespace-nowrap">{label}</span> <span class="text-text-strong whitespace-nowrap">{label}</span>
<Show when={item.selection}> <Show when={item.selection} keyed>
{(sel) => ( {(sel) => (
<span class="text-text-weak whitespace-nowrap shrink-0"> <span class="text-text-weak whitespace-nowrap shrink-0">
{sel().startLine === sel().endLine {sel.startLine === sel.endLine ? `:${sel.startLine}` : `:${sel.startLine}-${sel.endLine}`}
? `:${sel().startLine}`
: `:${sel().startLine}-${sel().endLine}`}
</span> </span>
)} )}
</Show> </Show>
@ -74,8 +72,8 @@ export const PromptContextItems: Component<ContextItemsProps> = (props) => {
aria-label={props.t("prompt.context.removeFile")} aria-label={props.t("prompt.context.removeFile")}
/> />
</div> </div>
<Show when={item.comment}> <Show when={item.comment} keyed>
{(comment) => <div class="text-12-regular text-text-strong ml-5 pr-1 truncate">{comment()}</div>} {(comment) => <div class="text-12-regular text-text-strong ml-5 pr-1 truncate">{comment}</div>}
</Show> </Show>
</div> </div>
</Tooltip> </Tooltip>

View File

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

View File

@ -73,15 +73,15 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
const tooltipValue = () => ( const tooltipValue = () => (
<div> <div>
<Show when={context()}> <Show when={context()} keyed>
{(ctx) => ( {(ctx) => (
<> <>
<div class="flex items-center gap-2"> <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> <span class="text-text-invert-base">{language.t("context.usage.tokens")}</span>
</div> </div>
<div class="flex items-center gap-2"> <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> <span class="text-text-invert-base">{language.t("context.usage.usage")}</span>
</div> </div>
</> </>

View File

@ -316,12 +316,12 @@ export function SessionContextTab() {
</div> </div>
</Show> </Show>
<Show when={systemPrompt()}> <Show when={systemPrompt()} keyed>
{(prompt) => ( {(prompt) => (
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="text-12-regular text-text-weak">{language.t("context.systemPrompt.title")}</div> <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"> <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>
</div> </div>
)} )}

View File

@ -356,9 +356,9 @@ export function SessionHeader() {
return ( return (
<> <>
<Show when={centerMount()}> <Show when={centerMount()} keyed>
{(mount) => ( {(mount) => (
<Portal mount={mount()}> <Portal mount={mount}>
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
@ -376,18 +376,16 @@ export function SessionHeader() {
</span> </span>
</div> </div>
<Show when={hotkey()}> <Show when={hotkey()} keyed>
{(keybind) => ( {(keybind) => <Keybind class="shrink-0 !border-0 !bg-transparent !shadow-none px-0">{keybind}</Keybind>}
<Keybind class="shrink-0 !border-0 !bg-transparent !shadow-none px-0">{keybind()}</Keybind>
)}
</Show> </Show>
</Button> </Button>
</Portal> </Portal>
)} )}
</Show> </Show>
<Show when={rightMount()}> <Show when={rightMount()} keyed>
{(mount) => ( {(mount) => (
<Portal mount={mount()}> <Portal mount={mount}>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<StatusPopover /> <StatusPopover />
<Show when={projectDirectory()}> <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" /> <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 class="text-12-medium text-text-weak select-text leading-5">{label(current())}</div>
</div> </div>
<Show when={sync.project}> <Show when={sync.project} keyed>
{(project) => ( {(project) => (
<div class="flex justify-center items-start gap-3 min-h-5"> <div class="flex justify-center items-start gap-3 min-h-5">
<Icon name="pencil-line" size="small" class="mt-0.5 shrink-0" /> <Icon name="pencil-line" size="small" class="mt-0.5 shrink-0" />
<div class="text-12-medium text-text-weak leading-5"> <div class="text-12-medium text-text-weak leading-5">
{language.t("session.new.lastModified")}&nbsp; {language.t("session.new.lastModified")}&nbsp;
<span class="text-text-strong"> <span class="text-text-strong">
{DateTime.fromMillis(project().time.updated ?? project().time.created) {DateTime.fromMillis(project.time.updated ?? project.time.created)
.setLocale(language.intl()) .setLocale(language.intl())
.toRelative()} .toRelative()}
</span> </span>

View File

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

View File

@ -473,7 +473,7 @@ export const SettingsGeneral: Component = () => {
<SoundsSection /> <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 [enabledResource, actions] = createResource(() => platform.getWslEnabled?.())
const enabled = () => (enabledResource.state === "pending" ? undefined : enabledResource.latest) const enabled = () => (enabledResource.state === "pending" ? undefined : enabledResource.latest)
@ -503,7 +503,7 @@ export const SettingsGeneral: Component = () => {
<UpdatesSection /> <UpdatesSection />
<Show when={linux()}> <Show when={linux()} keyed>
{(_) => { {(_) => {
const [valueResource, actions] = createResource(() => platform.getDisplayBackend?.()) const [valueResource, actions] = createResource(() => platform.getDisplayBackend?.())
const value = () => (valueResource.state === "pending" ? undefined : valueResource.latest) 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> <Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
</Show> </Show>
</div> </div>
<Show when={note(item.id)}> <Show when={note(item.id)} keyed>
{(key) => <span class="text-12-regular text-text-weak pl-8">{language.t(key())}</span>} {(key) => <span class="text-12-regular text-text-weak pl-8">{language.t(key)}</span>}
</Show> </Show>
</div> </div>
<Button <Button

View File

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

View File

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

View File

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

View File

@ -731,12 +731,12 @@ export function MessageTimeline(props: {
class="size-3.5 shrink-0" class="size-3.5 shrink-0"
/> />
<span class="truncate">{getFilename(comment().path)}</span> <span class="truncate">{getFilename(comment().path)}</span>
<Show when={comment().selection}> <Show when={comment().selection} keyed>
{(selection) => ( {(selection) => (
<span class="shrink-0 text-text-weak"> <span class="shrink-0 text-text-weak">
{selection().startLine === selection().endLine {selection.startLine === selection.endLine
? `:${selection().startLine}` ? `:${selection.startLine}`
: `:${selection().startLine}-${selection().endLine}`} : `:${selection.startLine}-${selection.endLine}`}
</span> </span>
)} )}
</Show> </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 ( return (
<Show <Show
when={serverData.state !== "pending" && serverData()} when={serverData.state !== "pending" && serverData()}
keyed
fallback={ fallback={
<div class="h-screen w-screen flex flex-col items-center justify-center bg-background-base"> <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" /> <Splash class="w-16 h-20 opacity-50 animate-pulse" />
@ -489,7 +490,7 @@ function ServerGate(props: { children: (data: ServerReadyData) => JSX.Element })
</div> </div>
} }
> >
{(data) => props.children(data())} {(data) => props.children(data)}
</Show> </Show>
) )
} }