Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 40 additions & 19 deletions packages/app/src/context/local.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createSimpleContext } from "@opencode-ai/ui/context"
import { base64Encode } from "@opencode-ai/core/util/encode"
import { useParams } from "@solidjs/router"
import { batch, createEffect, createMemo, startTransition } from "solid-js"
import { batch, createEffect, createMemo, createRoot, onCleanup, startTransition } from "solid-js"
import { createStore } from "solid-js/store"
import { useModels } from "@/context/models"
import { useSettings } from "@/context/settings"
Expand All @@ -26,6 +26,13 @@ type Saved = {
session: Record<string, State | undefined>
}

type SavedEntry = {
saved: Saved
setSaved: ReturnType<typeof createStore<Saved>>[1]
ready: ReturnType<typeof persisted<Saved>>[3]
dispose: VoidFunction
}

const WORKSPACE_KEY = "__workspace__"
const handoff = new Map<string, State>()

Expand Down Expand Up @@ -71,15 +78,29 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const agentsVisible = createMemo(() => settings.visibility.customAgents() || hasCustomAgent(list()))
const connected = createMemo(() => new Set(providers.connected().map((item) => item.id)))

const [saved, setSaved, , savedReady] = persisted(
{
...Persist.serverWorkspace(serverSDK().scope, sdk().directory, "model-selection", ["model-selection.v1"]),
migrate,
},
createStore<Saved>({
session: {},
}),
)
const savedCache = new Map<string, SavedEntry>()
const loadSaved = (serverScope: ServerScope, directory: string) => {
const key = ScopedKey.from(serverScope, directory, "model-selection")
const existing = savedCache.get(key)
if (existing) return existing
const entry = createRoot((dispose) => {
const [saved, setSaved, , ready] = persisted(
{
...Persist.serverWorkspace(serverScope, directory, "model-selection", ["model-selection.v1"]),
migrate,
},
createStore<Saved>({ session: {} }),
)
return { saved, setSaved, ready, dispose }
})
savedCache.set(key, entry)
return entry
}
const saved = createMemo(() => loadSaved(serverSDK().scope, sdk().directory))
onCleanup(() => {
for (const entry of savedCache.values()) entry.dispose()
savedCache.clear()
})

const [store, setStore] = createStore<{
current?: string
Expand Down Expand Up @@ -127,7 +148,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const scope = createMemo<State | undefined>(() => {
const session = id()
if (!session) return store.draft ?? store.promoting
return saved.session[session] ?? handoff.get(handoffKey(serverSDK().scope, sdk().directory, session))
return saved().saved.session[session] ?? handoff.get(handoffKey(serverSDK().scope, sdk().directory, session))
})

createEffect(() => {
Expand All @@ -137,13 +158,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const key = handoffKey(serverSDK().scope, sdk().directory, session)
const next = handoff.get(key)
if (!next) return
if (saved.session[session] !== undefined) {
if (saved().saved.session[session] !== undefined) {
handoff.delete(key)
setStore("promoting", undefined)
return
}

setSaved("session", session, clone(next))
saved().setSaved("session", session, clone(next))
handoff.delete(key)
setStore("promoting", undefined)
})
Expand Down Expand Up @@ -209,7 +230,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
} satisfies State
const session = id()
if (session) {
setSaved("session", session, next)
saved().setSaved("session", session, next)
return
}
setStore("draft", next)
Expand Down Expand Up @@ -270,7 +291,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({

const session = id()
if (session) {
setSaved("session", session, state)
saved().setSaved("session", session, state)
return
}
setStore("draft", state)
Expand Down Expand Up @@ -378,7 +399,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
model,
agent,
session: {
ready: savedReady,
ready: () => saved().ready(),
reset() {
setStore({ draft: undefined, promoting: undefined })
},
Expand All @@ -389,7 +410,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
handoff.set(key, next)

if (dir === sdk().directory) {
setSaved("session", session, next)
saved().setSaved("session", session, next)
}

setStore("promoting", next)
Expand All @@ -399,10 +420,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const session = id()
if (!session) return
if (msg.sessionID !== session) return
if (saved.session[session] !== undefined) return
if (saved().saved.session[session] !== undefined) return
if (handoff.has(handoffKey(serverSDK().scope, sdk().directory, session))) return

setSaved("session", session, {
saved().setSaved("session", session, {
agent: msg.agent,
model: msg.model,
variant: msg.model?.variant ?? null,
Expand Down
20 changes: 8 additions & 12 deletions packages/app/src/pages/directory-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,14 @@ export function DirectoryDataProvider(
})

return (
<Show when={directory()} keyed>
{(directory) => (
<DataProvider
data={sync().data}
directory={directory}
onNavigateToSession={(sessionID: string) => navigate(href(sessionID))}
onSessionHref={href}
>
<LocalProvider>{props.children}</LocalProvider>
</DataProvider>
)}
</Show>
<DataProvider
data={sync().data}
directory={directory}
onNavigateToSession={(sessionID: string) => navigate(href(sessionID))}
onSessionHref={href}
>
<LocalProvider>{props.children}</LocalProvider>
</DataProvider>
)
}

Expand Down
22 changes: 3 additions & 19 deletions packages/app/src/pages/session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ function ResolvedTargetSessionRoute() {
() => params.id,
() => sync().session.lineage,
)
const directory = createMemo(() => current()?.session.directory)
const directory = createMemo<string | undefined>((previous) => current()?.session.directory ?? previous)
const targetDirectory = () => directory()!

createEffect(() => {
Expand All @@ -265,33 +265,17 @@ function ResolvedTargetSessionRoute() {
})

return (
// Non-keyed: closes only while the target's directory is unknown (uncached
// lineage mid-resolution), which tears down the workspace subtree including
// the terminal. Same-workspace tab switches keep it open because warm
// targets resolve synchronously from the sync cache.
// Keep the previous workspace mounted while an uncached target resolves.
<Show when={directory()}>
<SDKProvider directory={targetDirectory}>
<DirectoryDataProvider directory={targetDirectory} server={serverKey}>
<TargetSessionPage />
<SessionPage />
</DirectoryDataProvider>
</SDKProvider>
</Show>
)
}

// Owns the workspace-identity remount. Must not include the session ID in the
// key: SessionPage handles session changes reactively, and remounting here
// destroys workspace-scoped state (terminal PTYs, file/prompt providers).
function TargetSessionPage() {
const sdk = useSDK()
const serverSDK = useServerSDK()
return (
<Show when={`${serverSDK().scope}\0${sdk().directory}`} keyed>
<SessionPage />
</Show>
)
}

function TargetServerScopedProviders(
props: ParentProps<{ directory?: () => string | undefined; sessionID?: () => string | undefined }>,
) {
Expand Down
5 changes: 3 additions & 2 deletions packages/session-ui/src/context/data.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Message, Session, Part, SnapshotFileDiff, SessionStatus, Provider
import type { FileDiffInfo } from "@opencode-ai/client/promise"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
import type { Accessor } from "solid-js"

export type NormalizedProviderListResponse = {
all: Map<string, Provider>
Expand Down Expand Up @@ -46,7 +47,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
name: "Data",
init: (props: {
data: Data
directory: string
directory: string | Accessor<string>
onNavigateToSession?: NavigateToSessionFn
onSessionHref?: SessionHrefFn
}) => {
Expand All @@ -55,7 +56,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
return props.data
},
get directory() {
return props.directory
return typeof props.directory === "function" ? props.directory() : props.directory
},
navigateToSession: props.onNavigateToSession,
sessionHref: props.onSessionHref,
Expand Down
Loading