summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/components
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-02-12 11:26:19 -0600
committerGitHub <[email protected]>2026-02-12 11:26:19 -0600
commitda952135cabba2926698298797cd301e7adaf48c (patch)
tree78635fe4f7d656266ad3cc1c353b04b56969515c /packages/app/src/components
parent789705ea96ae28af7e30801fd6039ce89b6ac48e (diff)
downloadopencode-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.tsx3
-rw-r--r--packages/app/src/components/question-dock.tsx42
-rw-r--r--packages/app/src/components/session/session-context-tab.tsx53
-rw-r--r--packages/app/src/components/session/session-header.tsx38
-rw-r--r--packages/app/src/components/status-popover.tsx41
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>