summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/components
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-03-12 11:32:05 -0500
committerGitHub <[email protected]>2026-03-12 11:32:05 -0500
commitdce7eceb2855bc36a41bc49d9c56d5dcc92a8eb2 (patch)
tree31cd7d7aa33733579134e9a6cf3a61762599d8e0 /packages/app/src/components
parent0e077f748352df6d44c811829baff3c26b3436ac (diff)
downloadopencode-dce7eceb2855bc36a41bc49d9c56d5dcc92a8eb2.tar.gz
opencode-dce7eceb2855bc36a41bc49d9c56d5dcc92a8eb2.zip
chore: cleanup (#17197)
Diffstat (limited to 'packages/app/src/components')
-rw-r--r--packages/app/src/components/dialog-custom-provider-form.ts159
-rw-r--r--packages/app/src/components/dialog-custom-provider.test.ts82
-rw-r--r--packages/app/src/components/dialog-custom-provider.tsx268
-rw-r--r--packages/app/src/components/dialog-select-file.tsx10
-rw-r--r--packages/app/src/components/prompt-input.tsx9
-rw-r--r--packages/app/src/components/session-context-usage.tsx10
-rw-r--r--packages/app/src/components/session/session-new-view.tsx1
7 files changed, 345 insertions, 194 deletions
diff --git a/packages/app/src/components/dialog-custom-provider-form.ts b/packages/app/src/components/dialog-custom-provider-form.ts
new file mode 100644
index 000000000..92d235c3b
--- /dev/null
+++ b/packages/app/src/components/dialog-custom-provider-form.ts
@@ -0,0 +1,159 @@
+const PROVIDER_ID = /^[a-z0-9][a-z0-9-_]*$/
+const OPENAI_COMPATIBLE = "@ai-sdk/openai-compatible"
+
+type Translator = (key: string, vars?: Record<string, string | number | boolean>) => string
+
+export type ModelErr = {
+ id?: string
+ name?: string
+}
+
+export type HeaderErr = {
+ key?: string
+ value?: string
+}
+
+export type ModelRow = {
+ row: string
+ id: string
+ name: string
+ err: ModelErr
+}
+
+export type HeaderRow = {
+ row: string
+ key: string
+ value: string
+ err: HeaderErr
+}
+
+export type FormState = {
+ providerID: string
+ name: string
+ baseURL: string
+ apiKey: string
+ models: ModelRow[]
+ headers: HeaderRow[]
+ saving: boolean
+ err: {
+ providerID?: string
+ name?: string
+ baseURL?: string
+ }
+}
+
+type ValidateArgs = {
+ form: FormState
+ t: Translator
+ disabledProviders: string[]
+ existingProviderIDs: Set<string>
+}
+
+export function validateCustomProvider(input: ValidateArgs) {
+ const providerID = input.form.providerID.trim()
+ const name = input.form.name.trim()
+ const baseURL = input.form.baseURL.trim()
+ const apiKey = input.form.apiKey.trim()
+
+ const env = apiKey.match(/^\{env:([^}]+)\}$/)?.[1]?.trim()
+ const key = apiKey && !env ? apiKey : undefined
+
+ const idError = !providerID
+ ? input.t("provider.custom.error.providerID.required")
+ : !PROVIDER_ID.test(providerID)
+ ? input.t("provider.custom.error.providerID.format")
+ : undefined
+
+ const nameError = !name ? input.t("provider.custom.error.name.required") : undefined
+ const urlError = !baseURL
+ ? input.t("provider.custom.error.baseURL.required")
+ : !/^https?:\/\//.test(baseURL)
+ ? input.t("provider.custom.error.baseURL.format")
+ : undefined
+
+ const disabled = input.disabledProviders.includes(providerID)
+ const existsError = idError
+ ? undefined
+ : input.existingProviderIDs.has(providerID) && !disabled
+ ? input.t("provider.custom.error.providerID.exists")
+ : undefined
+
+ const seenModels = new Set<string>()
+ const models = input.form.models.map((m) => {
+ const id = m.id.trim()
+ const idError = !id
+ ? input.t("provider.custom.error.required")
+ : seenModels.has(id)
+ ? input.t("provider.custom.error.duplicate")
+ : (() => {
+ seenModels.add(id)
+ return undefined
+ })()
+ const nameError = !m.name.trim() ? input.t("provider.custom.error.required") : undefined
+ return { id: idError, name: nameError }
+ })
+ const modelsValid = models.every((m) => !m.id && !m.name)
+ const modelConfig = Object.fromEntries(input.form.models.map((m) => [m.id.trim(), { name: m.name.trim() }]))
+
+ const seenHeaders = new Set<string>()
+ const headers = input.form.headers.map((h) => {
+ const key = h.key.trim()
+ const value = h.value.trim()
+
+ if (!key && !value) return {}
+ const keyError = !key
+ ? input.t("provider.custom.error.required")
+ : seenHeaders.has(key.toLowerCase())
+ ? input.t("provider.custom.error.duplicate")
+ : (() => {
+ seenHeaders.add(key.toLowerCase())
+ return undefined
+ })()
+ const valueError = !value ? input.t("provider.custom.error.required") : undefined
+ return { key: keyError, value: valueError }
+ })
+ const headersValid = headers.every((h) => !h.key && !h.value)
+ const headerConfig = Object.fromEntries(
+ input.form.headers
+ .map((h) => ({ key: h.key.trim(), value: h.value.trim() }))
+ .filter((h) => !!h.key && !!h.value)
+ .map((h) => [h.key, h.value]),
+ )
+
+ const err = {
+ providerID: idError ?? existsError,
+ name: nameError,
+ baseURL: urlError,
+ }
+
+ const ok = !idError && !existsError && !nameError && !urlError && modelsValid && headersValid
+ if (!ok) return { err, models, headers }
+
+ return {
+ err,
+ models,
+ headers,
+ result: {
+ providerID,
+ name,
+ key,
+ config: {
+ npm: OPENAI_COMPATIBLE,
+ name,
+ ...(env ? { env: [env] } : {}),
+ options: {
+ baseURL,
+ ...(Object.keys(headerConfig).length ? { headers: headerConfig } : {}),
+ },
+ models: modelConfig,
+ },
+ },
+ }
+}
+
+let row = 0
+
+const nextRow = () => `row-${row++}`
+
+export const modelRow = (): ModelRow => ({ row: nextRow(), id: "", name: "", err: {} })
+export const headerRow = (): HeaderRow => ({ row: nextRow(), key: "", value: "", err: {} })
diff --git a/packages/app/src/components/dialog-custom-provider.test.ts b/packages/app/src/components/dialog-custom-provider.test.ts
new file mode 100644
index 000000000..8cfd78ebe
--- /dev/null
+++ b/packages/app/src/components/dialog-custom-provider.test.ts
@@ -0,0 +1,82 @@
+import { describe, expect, test } from "bun:test"
+import { validateCustomProvider } from "./dialog-custom-provider-form"
+
+const t = (key: string) => key
+
+describe("validateCustomProvider", () => {
+ test("builds trimmed config payload", () => {
+ const result = validateCustomProvider({
+ form: {
+ providerID: "custom-provider",
+ name: " Custom Provider ",
+ baseURL: "https://api.example.com ",
+ apiKey: " {env: CUSTOM_PROVIDER_KEY} ",
+ models: [{ row: "m0", id: " model-a ", name: " Model A ", err: {} }],
+ headers: [
+ { row: "h0", key: " X-Test ", value: " enabled ", err: {} },
+ { row: "h1", key: "", value: "", err: {} },
+ ],
+ saving: false,
+ err: {},
+ },
+ t,
+ disabledProviders: [],
+ existingProviderIDs: new Set(),
+ })
+
+ expect(result.result).toEqual({
+ providerID: "custom-provider",
+ name: "Custom Provider",
+ key: undefined,
+ config: {
+ npm: "@ai-sdk/openai-compatible",
+ name: "Custom Provider",
+ env: ["CUSTOM_PROVIDER_KEY"],
+ options: {
+ baseURL: "https://api.example.com",
+ headers: {
+ "X-Test": "enabled",
+ },
+ },
+ models: {
+ "model-a": { name: "Model A" },
+ },
+ },
+ })
+ })
+
+ test("flags duplicate rows and allows reconnecting disabled providers", () => {
+ const result = validateCustomProvider({
+ form: {
+ providerID: "custom-provider",
+ name: "Provider",
+ baseURL: "https://api.example.com",
+ apiKey: "secret",
+ models: [
+ { row: "m0", id: "model-a", name: "Model A", err: {} },
+ { row: "m1", id: "model-a", name: "Model A 2", err: {} },
+ ],
+ headers: [
+ { row: "h0", key: "Authorization", value: "one", err: {} },
+ { row: "h1", key: "authorization", value: "two", err: {} },
+ ],
+ saving: false,
+ err: {},
+ },
+ t,
+ disabledProviders: ["custom-provider"],
+ existingProviderIDs: new Set(["custom-provider"]),
+ })
+
+ expect(result.result).toBeUndefined()
+ expect(result.err.providerID).toBeUndefined()
+ expect(result.models[1]).toEqual({
+ id: "provider.custom.error.duplicate",
+ name: undefined,
+ })
+ expect(result.headers[1]).toEqual({
+ key: "provider.custom.error.duplicate",
+ value: undefined,
+ })
+ })
+})
diff --git a/packages/app/src/components/dialog-custom-provider.tsx b/packages/app/src/components/dialog-custom-provider.tsx
index 017b85a2c..4d220a0b1 100644
--- a/packages/app/src/components/dialog-custom-provider.tsx
+++ b/packages/app/src/components/dialog-custom-provider.tsx
@@ -5,158 +5,15 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { TextField } from "@opencode-ai/ui/text-field"
import { showToast } from "@opencode-ai/ui/toast"
-import { For } from "solid-js"
-import { createStore } from "solid-js/store"
+import { batch, For } from "solid-js"
+import { createStore, produce } from "solid-js/store"
import { Link } from "@/components/link"
import { useGlobalSDK } from "@/context/global-sdk"
import { useGlobalSync } from "@/context/global-sync"
import { useLanguage } from "@/context/language"
+import { type FormState, headerRow, modelRow, validateCustomProvider } from "./dialog-custom-provider-form"
import { DialogSelectProvider } from "./dialog-select-provider"
-const PROVIDER_ID = /^[a-z0-9][a-z0-9-_]*$/
-const OPENAI_COMPATIBLE = "@ai-sdk/openai-compatible"
-
-type Translator = ReturnType<typeof useLanguage>["t"]
-
-type ModelRow = {
- id: string
- name: string
-}
-
-type HeaderRow = {
- key: string
- value: string
-}
-
-type FormState = {
- providerID: string
- name: string
- baseURL: string
- apiKey: string
- models: ModelRow[]
- headers: HeaderRow[]
- saving: boolean
-}
-
-type FormErrors = {
- providerID: string | undefined
- name: string | undefined
- baseURL: string | undefined
- models: Array<{ id?: string; name?: string }>
- headers: Array<{ key?: string; value?: string }>
-}
-
-type ValidateArgs = {
- form: FormState
- t: Translator
- disabledProviders: string[]
- existingProviderIDs: Set<string>
-}
-
-function validateCustomProvider(input: ValidateArgs) {
- const providerID = input.form.providerID.trim()
- const name = input.form.name.trim()
- const baseURL = input.form.baseURL.trim()
- const apiKey = input.form.apiKey.trim()
-
- const env = apiKey.match(/^\{env:([^}]+)\}$/)?.[1]?.trim()
- const key = apiKey && !env ? apiKey : undefined
-
- const idError = !providerID
- ? input.t("provider.custom.error.providerID.required")
- : !PROVIDER_ID.test(providerID)
- ? input.t("provider.custom.error.providerID.format")
- : undefined
-
- const nameError = !name ? input.t("provider.custom.error.name.required") : undefined
- const urlError = !baseURL
- ? input.t("provider.custom.error.baseURL.required")
- : !/^https?:\/\//.test(baseURL)
- ? input.t("provider.custom.error.baseURL.format")
- : undefined
-
- const disabled = input.disabledProviders.includes(providerID)
- const existsError = idError
- ? undefined
- : input.existingProviderIDs.has(providerID) && !disabled
- ? input.t("provider.custom.error.providerID.exists")
- : undefined
-
- const seenModels = new Set<string>()
- const modelErrors = input.form.models.map((m) => {
- const id = m.id.trim()
- const modelIdError = !id
- ? input.t("provider.custom.error.required")
- : seenModels.has(id)
- ? input.t("provider.custom.error.duplicate")
- : (() => {
- seenModels.add(id)
- return undefined
- })()
- const modelNameError = !m.name.trim() ? input.t("provider.custom.error.required") : undefined
- return { id: modelIdError, name: modelNameError }
- })
- const modelsValid = modelErrors.every((m) => !m.id && !m.name)
- const models = Object.fromEntries(input.form.models.map((m) => [m.id.trim(), { name: m.name.trim() }]))
-
- const seenHeaders = new Set<string>()
- const headerErrors = input.form.headers.map((h) => {
- const key = h.key.trim()
- const value = h.value.trim()
-
- if (!key && !value) return {}
- const keyError = !key
- ? input.t("provider.custom.error.required")
- : seenHeaders.has(key.toLowerCase())
- ? input.t("provider.custom.error.duplicate")
- : (() => {
- seenHeaders.add(key.toLowerCase())
- return undefined
- })()
- const valueError = !value ? input.t("provider.custom.error.required") : undefined
- return { key: keyError, value: valueError }
- })
- const headersValid = headerErrors.every((h) => !h.key && !h.value)
- const headers = Object.fromEntries(
- input.form.headers
- .map((h) => ({ key: h.key.trim(), value: h.value.trim() }))
- .filter((h) => !!h.key && !!h.value)
- .map((h) => [h.key, h.value]),
- )
-
- const errors: FormErrors = {
- providerID: idError ?? existsError,
- name: nameError,
- baseURL: urlError,
- models: modelErrors,
- headers: headerErrors,
- }
-
- const ok = !idError && !existsError && !nameError && !urlError && modelsValid && headersValid
- if (!ok) return { errors }
-
- const options = {
- baseURL,
- ...(Object.keys(headers).length ? { headers } : {}),
- }
-
- return {
- errors,
- result: {
- providerID,
- name,
- key,
- config: {
- npm: OPENAI_COMPATIBLE,
- name,
- ...(env ? { env: [env] } : {}),
- options,
- models,
- },
- },
- }
-}
-
type Props = {
back?: "providers" | "close"
}
@@ -172,17 +29,10 @@ export function DialogCustomProvider(props: Props) {
name: "",
baseURL: "",
apiKey: "",
- models: [{ id: "", name: "" }],
- headers: [{ key: "", value: "" }],
+ models: [modelRow()],
+ headers: [headerRow()],
saving: false,
- })
-
- const [errors, setErrors] = createStore<FormErrors>({
- providerID: undefined,
- name: undefined,
- baseURL: undefined,
- models: [{}],
- headers: [{}],
+ err: {},
})
const goBack = () => {
@@ -194,25 +44,61 @@ export function DialogCustomProvider(props: Props) {
}
const addModel = () => {
- setForm("models", (v) => [...v, { id: "", name: "" }])
- setErrors("models", (v) => [...v, {}])
+ setForm(
+ "models",
+ produce((rows) => {
+ rows.push(modelRow())
+ }),
+ )
}
const removeModel = (index: number) => {
if (form.models.length <= 1) return
- setForm("models", (v) => v.filter((_, i) => i !== index))
- setErrors("models", (v) => v.filter((_, i) => i !== index))
+ setForm(
+ "models",
+ produce((rows) => {
+ rows.splice(index, 1)
+ }),
+ )
}
const addHeader = () => {
- setForm("headers", (v) => [...v, { key: "", value: "" }])
- setErrors("headers", (v) => [...v, {}])
+ setForm(
+ "headers",
+ produce((rows) => {
+ rows.push(headerRow())
+ }),
+ )
}
const removeHeader = (index: number) => {
if (form.headers.length <= 1) return
- setForm("headers", (v) => v.filter((_, i) => i !== index))
- setErrors("headers", (v) => v.filter((_, i) => i !== index))
+ setForm(
+ "headers",
+ produce((rows) => {
+ rows.splice(index, 1)
+ }),
+ )
+ }
+
+ const setField = (key: "providerID" | "name" | "baseURL" | "apiKey", value: string) => {
+ setForm(key, value)
+ if (key === "apiKey") return
+ setForm("err", key, undefined)
+ }
+
+ const setModel = (index: number, key: "id" | "name", value: string) => {
+ batch(() => {
+ setForm("models", index, key, value)
+ setForm("models", index, "err", key, undefined)
+ })
+ }
+
+ const setHeader = (index: number, key: "key" | "value", value: string) => {
+ batch(() => {
+ setForm("headers", index, key, value)
+ setForm("headers", index, "err", key, undefined)
+ })
}
const validate = () => {
@@ -222,7 +108,11 @@ export function DialogCustomProvider(props: Props) {
disabledProviders: globalSync.data.config.disabled_providers ?? [],
existingProviderIDs: new Set(globalSync.data.provider.all.map((p) => p.id)),
})
- setErrors(output.errors)
+ batch(() => {
+ setForm("err", output.err)
+ output.models.forEach((err, index) => setForm("models", index, "err", err))
+ output.headers.forEach((err, index) => setForm("headers", index, "err", err))
+ })
return output.result
}
@@ -305,32 +195,32 @@ export function DialogCustomProvider(props: Props) {
placeholder={language.t("provider.custom.field.providerID.placeholder")}
description={language.t("provider.custom.field.providerID.description")}
value={form.providerID}
- onChange={(v) => setForm("providerID", v)}
- validationState={errors.providerID ? "invalid" : undefined}
- error={errors.providerID}
+ onChange={(v) => setField("providerID", v)}
+ validationState={form.err.providerID ? "invalid" : undefined}
+ error={form.err.providerID}
/>
<TextField
label={language.t("provider.custom.field.name.label")}
placeholder={language.t("provider.custom.field.name.placeholder")}
value={form.name}
- onChange={(v) => setForm("name", v)}
- validationState={errors.name ? "invalid" : undefined}
- error={errors.name}
+ onChange={(v) => setField("name", v)}
+ validationState={form.err.name ? "invalid" : undefined}
+ error={form.err.name}
/>
<TextField
label={language.t("provider.custom.field.baseURL.label")}
placeholder={language.t("provider.custom.field.baseURL.placeholder")}
value={form.baseURL}
- onChange={(v) => setForm("baseURL", v)}
- validationState={errors.baseURL ? "invalid" : undefined}
- error={errors.baseURL}
+ onChange={(v) => setField("baseURL", v)}
+ validationState={form.err.baseURL ? "invalid" : undefined}
+ error={form.err.baseURL}
/>
<TextField
label={language.t("provider.custom.field.apiKey.label")}
placeholder={language.t("provider.custom.field.apiKey.placeholder")}
description={language.t("provider.custom.field.apiKey.description")}
value={form.apiKey}
- onChange={(v) => setForm("apiKey", v)}
+ onChange={(v) => setField("apiKey", v)}
/>
</div>
@@ -338,16 +228,16 @@ export function DialogCustomProvider(props: Props) {
<label class="text-12-medium text-text-weak">{language.t("provider.custom.models.label")}</label>
<For each={form.models}>
{(m, i) => (
- <div class="flex gap-2 items-start">
+ <div class="flex gap-2 items-start" data-row={m.row}>
<div class="flex-1">
<TextField
label={language.t("provider.custom.models.id.label")}
hideLabel
placeholder={language.t("provider.custom.models.id.placeholder")}
value={m.id}
- onChange={(v) => setForm("models", i(), "id", v)}
- validationState={errors.models[i()]?.id ? "invalid" : undefined}
- error={errors.models[i()]?.id}
+ onChange={(v) => setModel(i(), "id", v)}
+ validationState={m.err.id ? "invalid" : undefined}
+ error={m.err.id}
/>
</div>
<div class="flex-1">
@@ -356,9 +246,9 @@ export function DialogCustomProvider(props: Props) {
hideLabel
placeholder={language.t("provider.custom.models.name.placeholder")}
value={m.name}
- onChange={(v) => setForm("models", i(), "name", v)}
- validationState={errors.models[i()]?.name ? "invalid" : undefined}
- error={errors.models[i()]?.name}
+ onChange={(v) => setModel(i(), "name", v)}
+ validationState={m.err.name ? "invalid" : undefined}
+ error={m.err.name}
/>
</div>
<IconButton
@@ -382,16 +272,16 @@ export function DialogCustomProvider(props: Props) {
<label class="text-12-medium text-text-weak">{language.t("provider.custom.headers.label")}</label>
<For each={form.headers}>
{(h, i) => (
- <div class="flex gap-2 items-start">
+ <div class="flex gap-2 items-start" data-row={h.row}>
<div class="flex-1">
<TextField
label={language.t("provider.custom.headers.key.label")}
hideLabel
placeholder={language.t("provider.custom.headers.key.placeholder")}
value={h.key}
- onChange={(v) => setForm("headers", i(), "key", v)}
- validationState={errors.headers[i()]?.key ? "invalid" : undefined}
- error={errors.headers[i()]?.key}
+ onChange={(v) => setHeader(i(), "key", v)}
+ validationState={h.err.key ? "invalid" : undefined}
+ error={h.err.key}
/>
</div>
<div class="flex-1">
@@ -400,9 +290,9 @@ export function DialogCustomProvider(props: Props) {
hideLabel
placeholder={language.t("provider.custom.headers.value.placeholder")}
value={h.value}
- onChange={(v) => setForm("headers", i(), "value", v)}
- validationState={errors.headers[i()]?.value ? "invalid" : undefined}
- error={errors.headers[i()]?.value}
+ onChange={(v) => setHeader(i(), "value", v)}
+ validationState={h.err.value ? "invalid" : undefined}
+ error={h.err.value}
/>
</div>
<IconButton
diff --git a/packages/app/src/components/dialog-select-file.tsx b/packages/app/src/components/dialog-select-file.tsx
index 9b88cff90..3d4dbecbd 100644
--- a/packages/app/src/components/dialog-select-file.tsx
+++ b/packages/app/src/components/dialog-select-file.tsx
@@ -15,6 +15,7 @@ import { useLayout } from "@/context/layout"
import { useFile } from "@/context/file"
import { useLanguage } from "@/context/language"
import { useSessionLayout } from "@/pages/session/session-layout"
+import { createSessionTabs } from "@/pages/session/helpers"
import { decode64 } from "@/utils/base64"
import { getRelativeTime } from "@/utils/time"
@@ -133,9 +134,14 @@ function createFileEntries(props: {
tabs: () => ReturnType<ReturnType<typeof useLayout>["tabs"]>
language: ReturnType<typeof useLanguage>
}) {
+ const tabState = createSessionTabs({
+ tabs: props.tabs,
+ pathFromTab: props.file.pathFromTab,
+ normalizeTab: (tab) => (tab.startsWith("file://") ? props.file.tab(tab) : tab),
+ })
const recent = createMemo(() => {
- const all = props.tabs().all()
- const active = props.tabs().active()
+ const all = tabState.openedTabs()
+ const active = tabState.activeFileTab()
const order = active ? [active, ...all.filter((item) => item !== active)] : all
const seen = new Set<string>()
const category = props.language.t("palette.group.files")
diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx
index f1a33e75f..e129b499a 100644
--- a/packages/app/src/components/prompt-input.tsx
+++ b/packages/app/src/components/prompt-input.tsx
@@ -37,6 +37,7 @@ import { usePermission } from "@/context/permission"
import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform"
import { useSessionLayout } from "@/pages/session/session-layout"
+import { createSessionTabs } from "@/pages/session/helpers"
import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom"
import { createPromptAttachments, ACCEPTED_FILE_TYPES } from "./prompt-input/attachments"
import {
@@ -154,6 +155,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
requestAnimationFrame(scrollCursorIntoView)
}
+ const activeFileTab = createSessionTabs({
+ tabs,
+ pathFromTab: files.pathFromTab,
+ normalizeTab: (tab) => (tab.startsWith("file://") ? files.tab(tab) : tab),
+ }).activeFileTab
+
const commentInReview = (path: string) => {
const sessionID = params.id
if (!sessionID) return false
@@ -205,7 +212,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const recent = createMemo(() => {
const all = tabs().all()
- const active = tabs().active()
+ const active = activeFileTab()
const order = active ? [active, ...all.filter((x) => x !== active)] : all
const seen = new Set<string>()
const paths: string[] = []
diff --git a/packages/app/src/components/session-context-usage.tsx b/packages/app/src/components/session-context-usage.tsx
index 99e6c13a3..7379833f8 100644
--- a/packages/app/src/components/session-context-usage.tsx
+++ b/packages/app/src/components/session-context-usage.tsx
@@ -3,11 +3,13 @@ import { Tooltip, type TooltipProps } from "@opencode-ai/ui/tooltip"
import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
import { Button } from "@opencode-ai/ui/button"
+import { useFile } from "@/context/file"
import { useLayout } from "@/context/layout"
import { useSync } from "@/context/sync"
import { useLanguage } from "@/context/language"
import { getSessionContextMetrics } from "@/components/session/session-context-metrics"
import { useSessionLayout } from "@/pages/session/session-layout"
+import { createSessionTabs } from "@/pages/session/helpers"
interface SessionContextUsageProps {
variant?: "button" | "indicator"
@@ -27,11 +29,17 @@ function openSessionContext(args: {
export function SessionContextUsage(props: SessionContextUsageProps) {
const sync = useSync()
+ const file = useFile()
const layout = useLayout()
const language = useLanguage()
const { params, tabs, view } = useSessionLayout()
const variant = createMemo(() => props.variant ?? "button")
+ const tabState = createSessionTabs({
+ tabs,
+ pathFromTab: file.pathFromTab,
+ normalizeTab: (tab) => (tab.startsWith("file://") ? file.tab(tab) : tab),
+ })
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
const usd = createMemo(
@@ -51,7 +59,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
const openContext = () => {
if (!params.id) return
- if (tabs().active() === "context") {
+ if (tabState.activeTab() === "context") {
tabs().close("context")
return
}
diff --git a/packages/app/src/components/session/session-new-view.tsx b/packages/app/src/components/session/session-new-view.tsx
index 52251dbb2..e4ef36393 100644
--- a/packages/app/src/components/session/session-new-view.tsx
+++ b/packages/app/src/components/session/session-new-view.tsx
@@ -13,7 +13,6 @@ const ROOT_CLASS = "size-full flex flex-col"
interface NewSessionViewProps {
worktree: string
- onWorktreeChange: (value: string) => void
}
export function NewSessionView(props: NewSessionViewProps) {