fix: key stale show callbacks
parent
326c70184d
commit
05789d29d2
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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()}>
|
||||
|
|
|
|||
|
|
@ -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")}
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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")}`)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue