+
+
+
{
+ const item = project()
+ if (!item) return
+ renameProject(item, next)
+ }}
+ class="text-14-medium text-text-strong truncate"
+ displayClass="text-14-medium text-text-strong truncate"
+ stopPropagation
+ />
-
-
-
-
-
-
-
-
- >
- }
- >
+
+
+ {worktree().replace(homedir(), "~")}
+
+
+
+
+
+
+
+
+ {
+ const item = project()
+ if (!item) return
+ showEditProjectDialog(item)
+ }}
+ >
+ {language.t("common.edit")}
+
+ {
+ const item = project()
+ if (!item) return
+ toggleProjectWorkspaces(item)
+ }}
+ >
+
+ {workspacesEnabled()
+ ? language.t("sidebar.workspaces.disable")
+ : language.t("sidebar.workspaces.enable")}
+
+
+
+
+ {language.t("sidebar.project.clearNotifications")}
+
+
+
+ {
+ const dir = worktree()
+ if (!dir) return
+ closeProject(dir)
+ }}
+ >
+ {language.t("common.close")}
+
+
+
+
+
+
+
+
+
-
-
-
-
-
- {
- if (!panelProps.mobile) scrollContainerRef = el
- }}
- class="size-full flex flex-col py-2 gap-4 overflow-y-auto no-scrollbar [overflow-anchor:none]"
- >
-
-
- {(directory) => (
-
- )}
-
-
-
-
- store.activeWorkspace}
- workspaceLabel={workspaceLabel}
- />
-
-
+
+
>
-
-
- >
- )}
+ }
+ >
+ <>
+
+ {
+ const item = project()
+ if (!item) return
+ createWorkspace(item)
+ }}
+ >
+ {language.t("workspace.new")}
+
+
+
+
+
+
+ {
+ if (!panelProps.mobile) scrollContainerRef = el
+ }}
+ class="size-full flex flex-col py-2 gap-4 overflow-y-auto no-scrollbar [overflow-anchor:none]"
+ >
+
+
+ {(directory) => (
+
+ )}
+
+
+
+
+ store.activeWorkspace}
+ workspaceLabel={workspaceLabel}
+ />
+
+
+
+ >
+
+
+ >
language.t("sidebar.help")}
onOpenHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
renderPanel={() =>
- mobile ? (
-
- ) : (
-
-
-
- )
+ mobile ? :
}
/>
)
@@ -2300,7 +2394,7 @@ export default function Layout(props: ParentProps) {
"size-full overflow-x-hidden flex flex-col items-start contain-strict border-t border-border-weak-base bg-background-base xl:border-l xl:rounded-tl-[12px]": true,
}}
>
- }>
+ }>
{props.children}
@@ -2325,8 +2419,8 @@ export default function Layout(props: ParentProps) {
arm()
}}
>
-
-
+
+
diff --git a/packages/app/src/pages/layout/helpers.test.ts b/packages/app/src/pages/layout/helpers.test.ts
index 9dbc6c72d2..1fe52d47a0 100644
--- a/packages/app/src/pages/layout/helpers.test.ts
+++ b/packages/app/src/pages/layout/helpers.test.ts
@@ -104,14 +104,14 @@ describe("layout deep links", () => {
describe("layout workspace helpers", () => {
test("normalizes trailing slash in workspace key", () => {
expect(workspaceKey("/tmp/demo///")).toBe("/tmp/demo")
- expect(workspaceKey("C:\\tmp\\demo\\\\")).toBe("C:\\tmp\\demo")
+ expect(workspaceKey("C:\\tmp\\demo\\\\")).toBe("C:/tmp/demo")
})
test("preserves posix and drive roots in workspace key", () => {
expect(workspaceKey("/")).toBe("/")
expect(workspaceKey("///")).toBe("/")
- expect(workspaceKey("C:\\")).toBe("C:\\")
- expect(workspaceKey("C:\\\\\\")).toBe("C:\\")
+ expect(workspaceKey("C:\\")).toBe("C:/")
+ expect(workspaceKey("C://")).toBe("C:/")
expect(workspaceKey("C:///")).toBe("C:/")
})
diff --git a/packages/app/src/pages/layout/helpers.ts b/packages/app/src/pages/layout/helpers.ts
index be4ce9f574..226098c1cd 100644
--- a/packages/app/src/pages/layout/helpers.ts
+++ b/packages/app/src/pages/layout/helpers.ts
@@ -1,11 +1,17 @@
import { getFilename } from "@opencode-ai/util/path"
import { type Session } from "@opencode-ai/sdk/v2/client"
+type SessionStore = {
+ session?: Session[]
+ path: { directory: string }
+}
+
export const workspaceKey = (directory: string) => {
- const drive = directory.match(/^([A-Za-z]:)[\\/]+$/)
- if (drive) return `${drive[1]}${directory.includes("\\") ? "\\" : "/"}`
- if (/^[\\/]+$/.test(directory)) return directory.includes("\\") ? "\\" : "/"
- return directory.replace(/[\\/]+$/, "")
+ const value = directory.replaceAll("\\", "/")
+ const drive = value.match(/^([A-Za-z]:)\/+$/)
+ if (drive) return `${drive[1]}/`
+ if (/^\/+$/i.test(value)) return "/"
+ return value.replace(/\/+$/, "")
}
function sortSessions(now: number) {
@@ -25,24 +31,24 @@ function sortSessions(now: number) {
const isRootVisibleSession = (session: Session, directory: string) =>
workspaceKey(session.directory) === workspaceKey(directory) && !session.parentID && !session.time?.archived
-export const sortedRootSessions = (store: { session: Session[]; path: { directory: string } }, now: number) =>
- store.session.filter((session) => isRootVisibleSession(session, store.path.directory)).sort(sortSessions(now))
+const roots = (store: SessionStore) =>
+ (store.session ?? []).filter((session) => isRootVisibleSession(session, store.path.directory))
-export const latestRootSession = (stores: { session: Session[]; path: { directory: string } }[], now: number) =>
- stores
- .flatMap((store) => store.session.filter((session) => isRootVisibleSession(session, store.path.directory)))
- .sort(sortSessions(now))[0]
+export const sortedRootSessions = (store: SessionStore, now: number) => roots(store).sort(sortSessions(now))
+
+export const latestRootSession = (stores: SessionStore[], now: number) =>
+ stores.flatMap(roots).sort(sortSessions(now))[0]
export function hasProjectPermissions
(
- request: Record,
+ request: Record | undefined,
include: (item: T) => boolean = () => true,
) {
- return Object.values(request).some((list) => list?.some(include))
+ return Object.values(request ?? {}).some((list) => list?.some(include))
}
-export const childMapByParent = (sessions: Session[]) => {
+export const childMapByParent = (sessions: Session[] | undefined) => {
const map = new Map()
- for (const session of sessions) {
+ for (const session of sessions ?? []) {
if (!session.parentID) continue
const existing = map.get(session.parentID)
if (existing) {
diff --git a/packages/app/src/pages/layout/sidebar-items.tsx b/packages/app/src/pages/layout/sidebar-items.tsx
index 5ce526103e..f8e16f3e12 100644
--- a/packages/app/src/pages/layout/sidebar-items.tsx
+++ b/packages/app/src/pages/layout/sidebar-items.tsx
@@ -384,7 +384,7 @@ export const NewSessionItem = (props: {
>
-
+
{label}
diff --git a/packages/app/src/pages/layout/sidebar-project.tsx b/packages/app/src/pages/layout/sidebar-project.tsx
index a26bc18311..2528264561 100644
--- a/packages/app/src/pages/layout/sidebar-project.tsx
+++ b/packages/app/src/pages/layout/sidebar-project.tsx
@@ -15,6 +15,7 @@ import { childMapByParent, displayName, sortedRootSessions } from "./helpers"
export type ProjectSidebarContext = {
currentDir: Accessor
+ currentProject: Accessor
sidebarOpened: Accessor
sidebarHovering: Accessor
hoverProject: Accessor
@@ -278,11 +279,7 @@ export const SortableProject = (props: {
const globalSync = useGlobalSync()
const language = useLanguage()
const sortable = createSortable(props.project.worktree)
- const selected = createMemo(
- () =>
- props.project.worktree === props.ctx.currentDir() ||
- props.project.sandboxes?.includes(props.ctx.currentDir()) === true,
- )
+ const selected = createMemo(() => props.ctx.currentProject()?.worktree === props.project.worktree)
const workspaces = createMemo(() => props.ctx.workspaceIds(props.project).slice(0, 2))
const workspaceEnabled = createMemo(() => props.ctx.workspacesEnabled(props.project))
const dirs = createMemo(() => props.ctx.workspaceIds(props.project))
diff --git a/packages/app/src/pages/layout/sidebar-workspace.tsx b/packages/app/src/pages/layout/sidebar-workspace.tsx
index 73d3d2141a..3bf00ea424 100644
--- a/packages/app/src/pages/layout/sidebar-workspace.tsx
+++ b/packages/app/src/pages/layout/sidebar-workspace.tsx
@@ -17,7 +17,7 @@ import { type LocalProject } from "@/context/layout"
import { useGlobalSync } from "@/context/global-sync"
import { useLanguage } from "@/context/language"
import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items"
-import { childMapByParent, sortedRootSessions } from "./helpers"
+import { childMapByParent, sortedRootSessions, workspaceKey } from "./helpers"
type InlineEditorComponent = (props: {
id: string
@@ -217,7 +217,7 @@ const WorkspaceActions = (props: {
sortedRootSessions(workspaceStore, props.sortNow()))
const children = createMemo(() => childMapByParent(workspaceStore.session))
const local = createMemo(() => props.directory === props.project.worktree)
- const active = createMemo(() => props.ctx.currentDir() === props.directory)
+ const active = createMemo(() => workspaceKey(props.ctx.currentDir()) === workspaceKey(props.directory))
const workspaceValue = createMemo(() => {
const branch = workspaceStore.vcs?.branch
const name = branch ?? getFilename(props.directory)
@@ -332,12 +332,13 @@ export const SortableWorkspace = (props: {
const open = createMemo(() => props.ctx.workspaceExpanded(props.directory, local()))
const boot = createMemo(() => open() || active())
const booted = createMemo((prev) => prev || workspaceStore.status === "complete", false)
- const hasMore = createMemo(() => workspaceStore.sessionTotal > sessions().length)
+ const count = createMemo(() => sessions()?.length ?? 0)
+ const hasMore = createMemo(() => workspaceStore.sessionTotal > count())
const busy = createMemo(() => props.ctx.isBusy(props.directory))
const wasBusy = createMemo((prev) => prev || busy(), false)
- const loading = createMemo(() => open() && !booted() && sessions().length === 0 && !wasBusy())
+ const loading = createMemo(() => open() && !booted() && count() === 0 && !wasBusy())
const touch = createMediaQuery("(hover: none)")
- const showNew = createMemo(() => !loading() && (touch() || sessions().length === 0 || (active() && !params.id)))
+ const showNew = createMemo(() => !loading() && (touch() || count() === 0 || (active() && !params.id)))
const loadMore = async () => {
setWorkspaceStore("limit", (limit) => (limit ?? 0) + 5)
await globalSync.project.loadSessions(props.directory)
@@ -472,8 +473,9 @@ export const LocalWorkspace = (props: {
const sessions = createMemo(() => sortedRootSessions(workspace().store, props.sortNow()))
const children = createMemo(() => childMapByParent(workspace().store.session))
const booted = createMemo((prev) => prev || workspace().store.status === "complete", false)
- const loading = createMemo(() => !booted() && sessions().length === 0)
- const hasMore = createMemo(() => workspace().store.sessionTotal > sessions().length)
+ const count = createMemo(() => sessions()?.length ?? 0)
+ const loading = createMemo(() => !booted() && count() === 0)
+ const hasMore = createMemo(() => workspace().store.sessionTotal > count())
const loadMore = async () => {
workspace().setStore("limit", (limit) => (limit ?? 0) + 5)
await globalSync.project.loadSessions(props.project.worktree)
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index 8399a13678..6d29170081 100644
--- a/packages/app/src/pages/session.tsx
+++ b/packages/app/src/pages/session.tsx
@@ -44,7 +44,7 @@ import { createOpenReviewFile, createSessionTabs, createSizing, focusTerminalByI
import { MessageTimeline } from "@/pages/session/message-timeline"
import { type DiffStyle, SessionReviewTab, type SessionReviewTabProps } from "@/pages/session/review-tab"
import { useSessionLayout } from "@/pages/session/session-layout"
-import { resetSessionModel, syncSessionModel } from "@/pages/session/session-model-helpers"
+import { syncSessionModel } from "@/pages/session/session-model-helpers"
import { SessionSidePanel } from "@/pages/session/session-side-panel"
import { TerminalPanel } from "@/pages/session/terminal-panel"
import { useSessionCommands } from "@/pages/session/use-session-commands"
@@ -490,7 +490,7 @@ export default function Page() {
(next, prev) => {
if (!prev) return
if (next.dir === prev.dir && next.id === prev.id) return
- if (!next.id) resetSessionModel(local)
+ if (prev.id && !next.id) local.session.reset()
},
{ defer: true },
),
diff --git a/packages/app/src/pages/session/session-model-helpers.test.ts b/packages/app/src/pages/session/session-model-helpers.test.ts
index 5f554dcd36..319db805d2 100644
--- a/packages/app/src/pages/session/session-model-helpers.test.ts
+++ b/packages/app/src/pages/session/session-model-helpers.test.ts
@@ -14,145 +14,38 @@ const message = (input?: Partial {
- test("restores the last message model and variant", () => {
+ test("restores the last message through session state", () => {
const calls: unknown[] = []
syncSessionModel(
{
- agent: {
- current() {
- return undefined
- },
- set(value) {
- calls.push(["agent", value])
- },
- },
- model: {
- set(value) {
- calls.push(["model", value])
- },
- current() {
- return { id: "claude-sonnet-4", provider: { id: "anthropic" } }
- },
- variant: {
- set(value) {
- calls.push(["variant", value])
- },
+ session: {
+ restore(value) {
+ calls.push(value)
},
+ reset() {},
},
},
message({ variant: "high" }),
)
- expect(calls).toEqual([
- ["agent", "build"],
- ["model", { providerID: "anthropic", modelID: "claude-sonnet-4" }],
- ["variant", "high"],
- ])
- })
-
- test("skips variant when the model falls back", () => {
- const calls: unknown[] = []
-
- syncSessionModel(
- {
- agent: {
- current() {
- return undefined
- },
- set(value) {
- calls.push(["agent", value])
- },
- },
- model: {
- set(value) {
- calls.push(["model", value])
- },
- current() {
- return { id: "gpt-5", provider: { id: "openai" } }
- },
- variant: {
- set(value) {
- calls.push(["variant", value])
- },
- },
- },
- },
- message({ variant: "high" }),
- )
-
- expect(calls).toEqual([
- ["agent", "build"],
- ["model", { providerID: "anthropic", modelID: "claude-sonnet-4" }],
- ])
+ expect(calls).toEqual([message({ variant: "high" })])
})
})
describe("resetSessionModel", () => {
- test("restores the current agent defaults", () => {
- const calls: unknown[] = []
+ test("clears draft session state", () => {
+ const calls: string[] = []
resetSessionModel({
- agent: {
- current() {
- return {
- model: { providerID: "anthropic", modelID: "claude-sonnet-4" },
- variant: "high",
- }
- },
- set() {},
- },
- model: {
- set(value) {
- calls.push(["model", value])
- },
- current() {
- return undefined
- },
- variant: {
- set(value) {
- calls.push(["variant", value])
- },
+ session: {
+ reset() {
+ calls.push("reset")
},
+ restore() {},
},
})
- expect(calls).toEqual([
- ["model", { providerID: "anthropic", modelID: "claude-sonnet-4" }],
- ["variant", "high"],
- ])
- })
-
- test("clears the variant when the agent has none", () => {
- const calls: unknown[] = []
-
- resetSessionModel({
- agent: {
- current() {
- return {
- model: { providerID: "anthropic", modelID: "claude-sonnet-4" },
- }
- },
- set() {},
- },
- model: {
- set(value) {
- calls.push(["model", value])
- },
- current() {
- return undefined
- },
- variant: {
- set(value) {
- calls.push(["variant", value])
- },
- },
- },
- })
-
- expect(calls).toEqual([
- ["model", { providerID: "anthropic", modelID: "claude-sonnet-4" }],
- ["variant", undefined],
- ])
+ expect(calls).toEqual(["reset"])
})
})
diff --git a/packages/app/src/pages/session/session-model-helpers.ts b/packages/app/src/pages/session/session-model-helpers.ts
index 7600f16d5c..c9e2e1dbd2 100644
--- a/packages/app/src/pages/session/session-model-helpers.ts
+++ b/packages/app/src/pages/session/session-model-helpers.ts
@@ -1,48 +1,16 @@
import type { UserMessage } from "@opencode-ai/sdk/v2"
-import { batch } from "solid-js"
type Local = {
- agent: {
- current():
- | {
- model?: UserMessage["model"]
- variant?: string
- }
- | undefined
- set(name: string | undefined): void
- }
- model: {
- set(model: UserMessage["model"] | undefined): void
- current():
- | {
- id: string
- provider: { id: string }
- }
- | undefined
- variant: {
- set(value: string | undefined): void
- }
+ session: {
+ reset(): void
+ restore(msg: UserMessage): void
}
}
export const resetSessionModel = (local: Local) => {
- const agent = local.agent.current()
- if (!agent) return
- batch(() => {
- local.model.set(agent.model)
- local.model.variant.set(agent.variant)
- })
+ local.session.reset()
}
export const syncSessionModel = (local: Local, msg: UserMessage) => {
- batch(() => {
- local.agent.set(msg.agent)
- local.model.set(msg.model)
- })
-
- const model = local.model.current()
- if (!model) return
- if (model.provider.id !== msg.model.providerID) return
- if (model.id !== msg.model.modelID) return
- local.model.variant.set(msg.variant)
+ local.session.restore(msg)
}
diff --git a/packages/app/src/pages/session/terminal-panel.tsx b/packages/app/src/pages/session/terminal-panel.tsx
index e78ebecfc4..c663d7d671 100644
--- a/packages/app/src/pages/session/terminal-panel.tsx
+++ b/packages/app/src/pages/session/terminal-panel.tsx
@@ -18,8 +18,10 @@ import { terminalTabLabel } from "@/pages/session/terminal-label"
import { createSizing, focusTerminalById } from "@/pages/session/helpers"
import { getTerminalHandoff, setTerminalHandoff } from "@/pages/session/handoff"
import { useSessionLayout } from "@/pages/session/session-layout"
+import { terminalProbe } from "@/testing/terminal"
export function TerminalPanel() {
+ const delays = [120, 240]
const layout = useLayout()
const terminal = useTerminal()
const language = useLanguage()
@@ -79,16 +81,20 @@ export function TerminalPanel() {
)
const focus = (id: string) => {
+ const probe = terminalProbe(id)
+ probe.focus(delays.length + 1)
focusTerminalById(id)
const frame = requestAnimationFrame(() => {
+ probe.step()
if (!opened()) return
if (terminal.active() !== id) return
focusTerminalById(id)
})
- const timers = [120, 240].map((ms) =>
+ const timers = delays.map((ms) =>
window.setTimeout(() => {
+ probe.step()
if (!opened()) return
if (terminal.active() !== id) return
focusTerminalById(id)
@@ -96,6 +102,7 @@ export function TerminalPanel() {
)
return () => {
+ probe.focus(0)
cancelAnimationFrame(frame)
for (const timer of timers) clearTimeout(timer)
}
@@ -273,21 +280,24 @@ export function TerminalPanel() {
- {(id) => (
- pty.id === id)}>
- {(pty) => (
-
- terminal.trim(id)}
- onCleanup={terminal.update}
- onConnectError={() => terminal.clone(id)}
- />
-
- )}
-
- )}
+ {(id) => {
+ const ops = terminal.bind()
+ return (
+ pty.id === id)}>
+ {(pty) => (
+
+ ops.trim(id)}
+ onCleanup={ops.update}
+ onConnectError={() => ops.clone(id)}
+ />
+
+ )}
+
+ )
+ }}
diff --git a/packages/app/src/pages/session/use-session-commands.tsx b/packages/app/src/pages/session/use-session-commands.tsx
index f5a4c05764..1a2e777f52 100644
--- a/packages/app/src/pages/session/use-session-commands.tsx
+++ b/packages/app/src/pages/session/use-session-commands.tsx
@@ -351,7 +351,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
description: language.t("command.model.choose.description"),
keybind: "mod+'",
slash: "model",
- onSelect: () => dialog.show(() => ),
+ onSelect: () => dialog.show(() => ),
}),
mcpCommand({
id: "mcp.toggle",
diff --git a/packages/app/src/testing/model-selection.ts b/packages/app/src/testing/model-selection.ts
new file mode 100644
index 0000000000..a5ea199ac1
--- /dev/null
+++ b/packages/app/src/testing/model-selection.ts
@@ -0,0 +1,80 @@
+type ModelKey = {
+ providerID: string
+ modelID: string
+}
+
+type State = {
+ agent?: string
+ model?: ModelKey | null
+ variant?: string | null
+}
+
+export type ModelProbeState = {
+ dir?: string
+ sessionID?: string
+ last?: {
+ type: "agent" | "model" | "variant"
+ agent?: string
+ model?: ModelKey | null
+ variant?: string | null
+ }
+ agent?: string
+ model?: (ModelKey & { name?: string }) | undefined
+ variant?: string | null
+ selected?: string | null
+ configured?: string
+ pick?: State
+ base?: State
+ current?: string
+}
+
+export type ModelWindow = Window & {
+ __opencode_e2e?: {
+ model?: {
+ enabled?: boolean
+ current?: ModelProbeState
+ }
+ }
+}
+
+const clone = (state?: State) => {
+ if (!state) return undefined
+ return {
+ ...state,
+ model: state.model ? { ...state.model } : state.model,
+ }
+}
+
+export const modelEnabled = () => {
+ if (typeof window === "undefined") return false
+ return (window as ModelWindow).__opencode_e2e?.model?.enabled === true
+}
+
+const root = () => {
+ if (!modelEnabled()) return
+ return (window as ModelWindow).__opencode_e2e?.model
+}
+
+export const modelProbe = {
+ set(input: ModelProbeState) {
+ const state = root()
+ if (!state) return
+ state.current = {
+ ...input,
+ model: input.model ? { ...input.model } : undefined,
+ last: input.last
+ ? {
+ ...input.last,
+ model: input.last.model ? { ...input.last.model } : input.last.model,
+ }
+ : undefined,
+ pick: clone(input.pick),
+ base: clone(input.base),
+ }
+ },
+ clear() {
+ const state = root()
+ if (!state) return
+ state.current = undefined
+ },
+}
diff --git a/packages/app/src/testing/prompt.ts b/packages/app/src/testing/prompt.ts
new file mode 100644
index 0000000000..e11462f301
--- /dev/null
+++ b/packages/app/src/testing/prompt.ts
@@ -0,0 +1,56 @@
+import type { E2EWindow } from "./terminal"
+
+export type PromptProbeState = {
+ popover: "at" | "slash" | null
+ slash: {
+ active: string | null
+ ids: string[]
+ }
+ selected: string | null
+ selects: number
+}
+
+export const promptEnabled = () => {
+ if (typeof window === "undefined") return false
+ return (window as E2EWindow).__opencode_e2e?.prompt?.enabled === true
+}
+
+const root = () => {
+ if (!promptEnabled()) return
+ return (window as E2EWindow).__opencode_e2e?.prompt
+}
+
+export const promptProbe = {
+ set(input: Omit) {
+ const state = root()
+ if (!state) return
+ state.current = {
+ popover: input.popover,
+ slash: {
+ active: input.slash.active,
+ ids: [...input.slash.ids],
+ },
+ selected: state.current?.selected ?? null,
+ selects: state.current?.selects ?? 0,
+ }
+ },
+ select(id: string) {
+ const state = root()
+ if (!state) return
+ const prev = state.current
+ state.current = {
+ popover: prev?.popover ?? null,
+ slash: {
+ active: prev?.slash.active ?? null,
+ ids: [...(prev?.slash.ids ?? [])],
+ },
+ selected: id,
+ selects: (prev?.selects ?? 0) + 1,
+ }
+ },
+ clear() {
+ const state = root()
+ if (!state) return
+ state.current = undefined
+ },
+}
diff --git a/packages/app/src/testing/terminal.ts b/packages/app/src/testing/terminal.ts
index 4c179dee3c..2bca39b31c 100644
--- a/packages/app/src/testing/terminal.ts
+++ b/packages/app/src/testing/terminal.ts
@@ -1,3 +1,5 @@
+import type { ModelProbeState } from "./model-selection"
+
export const terminalAttr = "data-pty-id"
export type TerminalProbeState = {
@@ -5,6 +7,7 @@ export type TerminalProbeState = {
connects: number
rendered: string
settled: number
+ focusing: number
}
type TerminalProbeControl = {
@@ -13,6 +16,14 @@ type TerminalProbeControl = {
export type E2EWindow = Window & {
__opencode_e2e?: {
+ model?: {
+ enabled?: boolean
+ current?: ModelProbeState
+ }
+ prompt?: {
+ enabled?: boolean
+ current?: import("./prompt").PromptProbeState
+ }
terminal?: {
enabled?: boolean
terminals?: Record
@@ -26,6 +37,7 @@ const seed = (): TerminalProbeState => ({
connects: 0,
rendered: "",
settled: 0,
+ focusing: 0,
})
const root = () => {
@@ -82,6 +94,15 @@ export const terminalProbe = (id: string) => {
const prev = state[id] ?? seed()
state[id] = { ...prev, settled: prev.settled + 1 }
},
+ focus(count: number) {
+ set({ focusing: Math.max(0, count) })
+ },
+ step() {
+ const state = terms()
+ if (!state) return
+ const prev = state[id] ?? seed()
+ state[id] = { ...prev, focusing: Math.max(0, prev.focusing - 1) }
+ },
control(next: Partial) {
const state = controls()
if (!state) return
diff --git a/packages/app/src/utils/persist.ts b/packages/app/src/utils/persist.ts
index bee2f3e7d1..3dcbeb7d36 100644
--- a/packages/app/src/utils/persist.ts
+++ b/packages/app/src/utils/persist.ts
@@ -5,7 +5,12 @@ import { createResource, type Accessor } from "solid-js"
import type { SetStoreFunction, Store } from "solid-js/store"
type InitType = Promise | string | null
-type PersistedWithReady = [Store, SetStoreFunction, InitType, Accessor]
+type PersistedWithReady = [
+ Store,
+ SetStoreFunction,
+ InitType,
+ Accessor & { promise: undefined | Promise },
+]
type PersistTarget = {
storage?: string
@@ -460,5 +465,12 @@ export function persisted(
{ initialValue: !isAsync },
)
- return [state, setState, init, () => ready() === true]
+ return [
+ state,
+ setState,
+ init,
+ Object.assign(() => ready() === true, {
+ promise: init instanceof Promise ? init : undefined,
+ }),
+ ]
}
diff --git a/packages/console/app/package.json b/packages/console/app/package.json
index a07b628569..cb0f91a64f 100644
--- a/packages/console/app/package.json
+++ b/packages/console/app/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
- "version": "1.2.25",
+ "version": "1.2.27",
"type": "module",
"license": "MIT",
"scripts": {
diff --git a/packages/console/app/src/component/icon.tsx b/packages/console/app/src/component/icon.tsx
index fc50b489b9..0aaa302b3e 100644
--- a/packages/console/app/src/component/icon.tsx
+++ b/packages/console/app/src/component/icon.tsx
@@ -76,6 +76,19 @@ export function IconAlipay(props: JSX.SvgSVGAttributes) {
)
}
+export function IconUpi(props: JSX.SvgSVGAttributes) {
+ return (
+
+ )
+}
+
export function IconWechat(props: JSX.SvgSVGAttributes) {
return (