diff options
| author | Adam <[email protected]> | 2026-02-12 11:26:19 -0600 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-02-12 11:26:19 -0600 |
| commit | da952135cabba2926698298797cd301e7adaf48c (patch) | |
| tree | 78635fe4f7d656266ad3cc1c353b04b56969515c /packages/app/src/components | |
| parent | 789705ea96ae28af7e30801fd6039ce89b6ac48e (diff) | |
| download | opencode-da952135cabba2926698298797cd301e7adaf48c.tar.gz opencode-da952135cabba2926698298797cd301e7adaf48c.zip | |
chore(app): refactor for better solidjs hygiene (#13344)
Diffstat (limited to 'packages/app/src/components')
| -rw-r--r-- | packages/app/src/components/prompt-input.tsx | 3 | ||||
| -rw-r--r-- | packages/app/src/components/question-dock.tsx | 42 | ||||
| -rw-r--r-- | packages/app/src/components/session/session-context-tab.tsx | 53 | ||||
| -rw-r--r-- | packages/app/src/components/session/session-header.tsx | 38 | ||||
| -rw-r--r-- | packages/app/src/components/status-popover.tsx | 41 |
5 files changed, 78 insertions, 99 deletions
diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index d591b22c7..146f1b64e 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -345,6 +345,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => { .filter((agent) => !agent.hidden && agent.mode !== "primary") .map((agent): AtOption => ({ type: "agent", name: agent.name, display: agent.name })), ) + const agentNames = createMemo(() => local.agent.list().map((agent) => agent.name)) const handleAtSelect = (option: AtOption | undefined) => { if (!option) return @@ -1038,7 +1039,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => { keybind={command.keybind("agent.cycle")} > <Select - options={local.agent.list().map((agent) => agent.name)} + options={agentNames()} current={local.agent.current()?.name ?? ""} onSelect={local.agent.set} class={`capitalize ${local.model.variant.list().length > 0 ? "max-w-full" : "max-w-[120px]"}`} diff --git a/packages/app/src/components/question-dock.tsx b/packages/app/src/components/question-dock.tsx index 1ab184535..5054253b8 100644 --- a/packages/app/src/components/question-dock.tsx +++ b/packages/app/src/components/question-dock.tsx @@ -7,32 +7,6 @@ import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2" import { useLanguage } from "@/context/language" import { useSDK } from "@/context/sdk" -const writeAt = <T,>(list: T[], index: number, value: T) => { - const next = [...list] - next[index] = value - return next -} - -const pickAnswer = (list: QuestionAnswer[], index: number, value: string) => { - return writeAt(list, index, [value]) -} - -const toggleAnswer = (list: QuestionAnswer[], index: number, value: string) => { - const current = list[index] ?? [] - const next = current.includes(value) ? current.filter((item) => item !== value) : [...current, value] - return writeAt(list, index, next) -} - -const appendAnswer = (list: QuestionAnswer[], index: number, value: string) => { - const current = list[index] ?? [] - if (current.includes(value)) return list - return writeAt(list, index, [...current, value]) -} - -const writeCustom = (list: string[], index: number, value: string) => { - return writeAt(list, index, value) -} - export const QuestionDock: Component<{ request: QuestionRequest }> = (props) => { const sdk = useSDK() const language = useLanguage() @@ -95,10 +69,10 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) => } const pick = (answer: string, custom: boolean = false) => { - setStore("answers", pickAnswer(store.answers, store.tab, answer)) + setStore("answers", store.tab, [answer]) if (custom) { - setStore("custom", writeCustom(store.custom, store.tab, answer)) + setStore("custom", store.tab, answer) } if (single()) { @@ -110,7 +84,10 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) => } const toggle = (answer: string) => { - setStore("answers", toggleAnswer(store.answers, store.tab, answer)) + setStore("answers", store.tab, (current = []) => { + if (current.includes(answer)) return current.filter((item) => item !== answer) + return [...current, answer] + }) } const selectTab = (index: number) => { @@ -146,7 +123,10 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) => } if (multi()) { - setStore("answers", appendAnswer(store.answers, store.tab, value)) + setStore("answers", store.tab, (current = []) => { + if (current.includes(value)) return current + return [...current, value] + }) setStore("editing", false) return } @@ -239,7 +219,7 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) => value={input()} disabled={store.sending} onInput={(e) => { - setStore("custom", writeCustom(store.custom, store.tab, e.currentTarget.value)) + setStore("custom", store.tab, e.currentTarget.value) }} /> <Button type="submit" variant="primary" size="small" disabled={store.sending}> diff --git a/packages/app/src/components/session/session-context-tab.tsx b/packages/app/src/components/session/session-context-tab.tsx index eb5b4197d..81220b3ad 100644 --- a/packages/app/src/components/session/session-context-tab.tsx +++ b/packages/app/src/components/session/session-context-tab.tsx @@ -168,34 +168,27 @@ export function SessionContextTab(props: SessionContextTabProps) { return language.t("context.breakdown.other") } - const stats = createMemo(() => { - const c = ctx() - const count = counts() - return [ - { label: language.t("context.stats.session"), value: props.info()?.title ?? params.id ?? "—" }, - { label: language.t("context.stats.messages"), value: count.all.toLocaleString(language.locale()) }, - { label: language.t("context.stats.provider"), value: providerLabel() }, - { label: language.t("context.stats.model"), value: modelLabel() }, - { label: language.t("context.stats.limit"), value: formatter().number(c?.limit) }, - { label: language.t("context.stats.totalTokens"), value: formatter().number(c?.total) }, - { label: language.t("context.stats.usage"), value: formatter().percent(c?.usage) }, - { label: language.t("context.stats.inputTokens"), value: formatter().number(c?.input) }, - { label: language.t("context.stats.outputTokens"), value: formatter().number(c?.output) }, - { label: language.t("context.stats.reasoningTokens"), value: formatter().number(c?.reasoning) }, - { - label: language.t("context.stats.cacheTokens"), - value: `${formatter().number(c?.cacheRead)} / ${formatter().number(c?.cacheWrite)}`, - }, - { label: language.t("context.stats.userMessages"), value: count.user.toLocaleString(language.locale()) }, - { - label: language.t("context.stats.assistantMessages"), - value: count.assistant.toLocaleString(language.locale()), - }, - { label: language.t("context.stats.totalCost"), value: cost() }, - { label: language.t("context.stats.sessionCreated"), value: formatter().time(props.info()?.time.created) }, - { label: language.t("context.stats.lastActivity"), value: formatter().time(c?.message.time.created) }, - ] satisfies { label: string; value: JSX.Element }[] - }) + const stats = [ + { label: "context.stats.session", value: () => props.info()?.title ?? params.id ?? "—" }, + { label: "context.stats.messages", value: () => counts().all.toLocaleString(language.locale()) }, + { label: "context.stats.provider", value: providerLabel }, + { label: "context.stats.model", value: modelLabel }, + { label: "context.stats.limit", value: () => formatter().number(ctx()?.limit) }, + { label: "context.stats.totalTokens", value: () => formatter().number(ctx()?.total) }, + { label: "context.stats.usage", value: () => formatter().percent(ctx()?.usage) }, + { label: "context.stats.inputTokens", value: () => formatter().number(ctx()?.input) }, + { label: "context.stats.outputTokens", value: () => formatter().number(ctx()?.output) }, + { label: "context.stats.reasoningTokens", value: () => formatter().number(ctx()?.reasoning) }, + { + label: "context.stats.cacheTokens", + value: () => `${formatter().number(ctx()?.cacheRead)} / ${formatter().number(ctx()?.cacheWrite)}`, + }, + { label: "context.stats.userMessages", value: () => counts().user.toLocaleString(language.locale()) }, + { label: "context.stats.assistantMessages", value: () => counts().assistant.toLocaleString(language.locale()) }, + { label: "context.stats.totalCost", value: cost }, + { label: "context.stats.sessionCreated", value: () => formatter().time(props.info()?.time.created) }, + { label: "context.stats.lastActivity", value: () => formatter().time(ctx()?.message.time.created) }, + ] satisfies { label: string; value: () => JSX.Element }[] let scroll: HTMLDivElement | undefined let frame: number | undefined @@ -257,7 +250,9 @@ export function SessionContextTab(props: SessionContextTabProps) { > <div class="px-6 pt-4 flex flex-col gap-10"> <div class="grid grid-cols-1 @[32rem]:grid-cols-2 gap-4"> - <For each={stats()}>{(stat) => <Stat label={stat.label} value={stat.value} />}</For> + <For each={stats}> + {(stat) => <Stat label={language.t(stat.label as Parameters<typeof language.t>[0])} value={stat.value()} />} + </For> </div> <Show when={breakdown().length > 0}> diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index c1468ce37..274398ee0 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -1,4 +1,4 @@ -import { createEffect, createMemo, onCleanup, Show } from "solid-js" +import { createEffect, createMemo, For, onCleanup, Show } from "solid-js" import { createStore } from "solid-js/store" import { Portal } from "solid-js/web" import { useParams } from "@solidjs/router" @@ -404,23 +404,25 @@ export function SessionHeader() { setPrefs("app", value as OpenApp) }} > - {options().map((o) => ( - <DropdownMenu.RadioItem - value={o.id} - onSelect={() => { - setMenu("open", false) - openDir(o.id) - }} - > - <div class="flex size-5 shrink-0 items-center justify-center"> - <AppIcon id={o.icon} class={openIconSize(o.icon)} /> - </div> - <DropdownMenu.ItemLabel>{o.label}</DropdownMenu.ItemLabel> - <DropdownMenu.ItemIndicator> - <Icon name="check-small" size="small" class="text-icon-weak" /> - </DropdownMenu.ItemIndicator> - </DropdownMenu.RadioItem> - ))} + <For each={options()}> + {(o) => ( + <DropdownMenu.RadioItem + value={o.id} + onSelect={() => { + setMenu("open", false) + openDir(o.id) + }} + > + <div class="flex size-5 shrink-0 items-center justify-center"> + <AppIcon id={o.icon} class={openIconSize(o.icon)} /> + </div> + <DropdownMenu.ItemLabel>{o.label}</DropdownMenu.ItemLabel> + <DropdownMenu.ItemIndicator> + <Icon name="check-small" size="small" class="text-icon-weak" /> + </DropdownMenu.ItemIndicator> + </DropdownMenu.RadioItem> + )} + </For> </DropdownMenu.RadioGroup> </DropdownMenu.Group> <DropdownMenu.Separator /> diff --git a/packages/app/src/components/status-popover.tsx b/packages/app/src/components/status-popover.tsx index 26ee2d070..38152b823 100644 --- a/packages/app/src/components/status-popover.tsx +++ b/packages/app/src/components/status-popover.tsx @@ -173,12 +173,9 @@ export function StatusPopover() { const sortedServers = createMemo(() => listServersByHealth(servers(), server.url, health)) const mcp = useMcpToggle({ sync, sdk, language }) const defaultServer = useDefaultServerUrl(platform.getDefaultServerUrl) - const mcpItems = createMemo(() => - Object.entries(sync.data.mcp ?? {}) - .map(([name, status]) => ({ name, status: status.status })) - .sort((a, b) => a.name.localeCompare(b.name)), - ) - const mcpConnected = createMemo(() => mcpItems().filter((item) => item.status === "connected").length) + const mcpNames = createMemo(() => Object.keys(sync.data.mcp ?? {}).sort((a, b) => a.localeCompare(b))) + const mcpStatus = (name: string) => sync.data.mcp?.[name]?.status + const mcpConnected = createMemo(() => mcpNames().filter((name) => mcpStatus(name) === "connected").length) const lspItems = createMemo(() => sync.data.lsp ?? []) const lspCount = createMemo(() => lspItems().length) const plugins = createMemo(() => sync.data.config.plugin ?? []) @@ -186,7 +183,10 @@ export function StatusPopover() { const pluginEmpty = createMemo(() => pluginEmptyMessage(language.t("dialog.plugins.empty"), "opencode.json")) const overallHealthy = createMemo(() => { const serverHealthy = server.healthy() === true - const anyMcpIssue = mcpItems().some((item) => item.status !== "connected" && item.status !== "disabled") + const anyMcpIssue = mcpNames().some((name) => { + const status = mcpStatus(name) + return status !== "connected" && status !== "disabled" + }) return serverHealthy && !anyMcpIssue }) @@ -306,39 +306,40 @@ export function StatusPopover() { <div class="flex flex-col px-2 pb-2"> <div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14"> <Show - when={mcpItems().length > 0} + when={mcpNames().length > 0} fallback={ <div class="text-14-regular text-text-base text-center my-auto"> {language.t("dialog.mcp.empty")} </div> } > - <For each={mcpItems()}> - {(item) => { - const enabled = () => item.status === "connected" + <For each={mcpNames()}> + {(name) => { + const status = () => mcpStatus(name) + const enabled = () => status() === "connected" return ( <button type="button" class="flex items-center gap-2 w-full h-8 pl-3 pr-2 py-1 rounded-md hover:bg-surface-raised-base-hover transition-colors text-left" - onClick={() => mcp.toggle(item.name)} - disabled={mcp.loading() === item.name} + onClick={() => mcp.toggle(name)} + disabled={mcp.loading() === name} > <div classList={{ "size-1.5 rounded-full shrink-0": true, - "bg-icon-success-base": item.status === "connected", - "bg-icon-critical-base": item.status === "failed", - "bg-border-weak-base": item.status === "disabled", + "bg-icon-success-base": status() === "connected", + "bg-icon-critical-base": status() === "failed", + "bg-border-weak-base": status() === "disabled", "bg-icon-warning-base": - item.status === "needs_auth" || item.status === "needs_client_registration", + status() === "needs_auth" || status() === "needs_client_registration", }} /> - <span class="text-14-regular text-text-base truncate flex-1">{item.name}</span> + <span class="text-14-regular text-text-base truncate flex-1">{name}</span> <div onClick={(event) => event.stopPropagation()}> <Switch checked={enabled()} - disabled={mcp.loading() === item.name} - onChange={() => mcp.toggle(item.name)} + disabled={mcp.loading() === name} + onChange={() => mcp.toggle(name)} /> </div> </button> |
