feat(app): scope sdk/sync hooks per-route so /new-session targets its draft server (#32290)

This commit is contained in:
Brendan Allan
2026-06-14 19:24:10 +08:00
committed by GitHub
parent c81cd3202c
commit 010b456dfd
53 changed files with 524 additions and 458 deletions
+91 -48
View File
@@ -69,8 +69,8 @@ const SessionRoute = Object.assign(
createEffect(() => { createEffect(() => {
if (!settings.general.newLayoutDesigns()) return if (!settings.general.newLayoutDesigns()) return
if (params.id || search.draftId) return if (params.id || search.draftId) return
if (!tabs.ready() || !sdk.directory) return if (!tabs.ready() || !sdk().directory) return
tabs.newDraft({ server: server.key, directory: sdk.directory }, search.prompt) tabs.newDraft({ server: server.key, directory: sdk().directory }, search.prompt)
}) })
return ( return (
@@ -82,6 +82,45 @@ const SessionRoute = Object.assign(
{ preload: Session.preload }, { preload: Session.preload },
) )
// Wraps the non-draft routes. They are gated on (and keyed to) the globally selected
// server via ServerKey, then provide the server-scoped shell (Permission/Layout/
// Notification/Models + the visual Layout) for that server.
function SelectedServerLayout(props: ParentProps) {
return (
<ServerKey>
<ServerSDKProvider>
<ServerSyncProvider>
<ServerScopedShell>{props.children}</ServerScopedShell>
</ServerSyncProvider>
</ServerSDKProvider>
</ServerKey>
)
}
// Wraps /new-session. It resolves the draft's target server and provides the
// server-scoped shell for that server — without ServerKey, so the page never depends
// on the globally "selected" server.
function DraftServerLayout(props: ParentProps) {
const server = useServer()
const tabs = useTabs()
const [search] = useSearchParams<{ draftId?: string }>()
const conn = createMemo(() => {
const id = search.draftId
if (!id) return undefined
const draft = tabs.store.find((tab): tab is DraftTab => tab.type === "draft" && tab.draftID === id)
if (!draft) return undefined
return server.list.find((c) => ServerConnection.key(c) === draft.server)
})
return (
<ServerSDKProvider server={conn}>
<ServerSyncProvider server={conn}>
<ServerScopedShell>{props.children}</ServerScopedShell>
</ServerSyncProvider>
</ServerSDKProvider>
)
}
function DraftRoute() { function DraftRoute() {
const [search] = useSearchParams<{ draftId?: string }>() const [search] = useSearchParams<{ draftId?: string }>()
const tabs = useTabs() const tabs = useTabs()
@@ -95,19 +134,15 @@ function DraftRoute() {
} }
function ResolvedDraftRoute(props: { draftID: string }) { function ResolvedDraftRoute(props: { draftID: string }) {
const server = useServer()
const tabs = useTabs() const tabs = useTabs()
const draft = createMemo(() => const draft = createMemo(() =>
tabs.store.find((tab): tab is DraftTab => tab.type === "draft" && tab.draftID === props.draftID), tabs.store.find((tab): tab is DraftTab => tab.type === "draft" && tab.draftID === props.draftID),
) )
createEffect(() => {
const current = draft()
if (current && current.server !== server.key) server.setActive(current.server)
})
// Key on the directory so retargeting the draft's project re-instantiates the // Key on the directory so retargeting the draft's project re-instantiates the
// SDK/data providers for the new directory while keeping the same draft id. // directory-scoped providers while keeping the same draft id. The draft's target
// server is provided by DraftServerLayout, so changing only the server updates the
// SDK/sync hooks without remounting the composer.
const directory = () => draft()?.directory const directory = () => draft()?.directory
return ( return (
@@ -171,27 +206,36 @@ function BodyDesignClass() {
return null return null
} }
function AppShellProviders(props: ParentProps) { // Server-agnostic providers shared across every route. These live in the shared
// shell (router root) so they stay mounted regardless of the active server/route.
function SharedProviders(props: ParentProps) {
return ( return (
<SettingsProvider> <SettingsProvider>
<BodyDesignClass /> <BodyDesignClass />
<PermissionProvider> <CommandProvider>
<LayoutProvider> <HighlightsProvider>{props.children}</HighlightsProvider>
<NotificationProvider> </CommandProvider>
<ModelsProvider>
<CommandProvider>
<HighlightsProvider>
<Layout>{props.children}</Layout>
</HighlightsProvider>
</CommandProvider>
</ModelsProvider>
</NotificationProvider>
</LayoutProvider>
</PermissionProvider>
</SettingsProvider> </SettingsProvider>
) )
} }
// Server-scoped providers plus the visual Layout (tabs/sidebar). These live inside
// each per-route server layout so they resolve to that route's server (selected vs
// draft). The Layout remounts when crossing between those groups.
function ServerScopedShell(props: ParentProps) {
return (
<PermissionProvider>
<LayoutProvider>
<NotificationProvider>
<ModelsProvider>
<Layout>{props.children}</Layout>
</ModelsProvider>
</NotificationProvider>
</LayoutProvider>
</PermissionProvider>
)
}
function SessionProviders(props: ParentProps) { function SessionProviders(props: ParentProps) {
return ( return (
<TerminalProvider> <TerminalProvider>
@@ -216,17 +260,6 @@ function DraftProviders(props: ParentProps) {
) )
} }
function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) {
return (
<AppShellProviders>
{/*<Suspense fallback={<Loading />}>*/}
{props.appChildren}
{props.children}
{/*</Suspense>*/}
</AppShellProviders>
)
}
export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) { export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
return ( return (
<MetaProvider> <MetaProvider>
@@ -385,6 +418,20 @@ export function AppInterface(props: {
router?: Component<BaseRouterProps> router?: Component<BaseRouterProps>
disableHealthCheck?: boolean disableHealthCheck?: boolean
}) { }) {
// The shared shell holds only server-agnostic providers (QueryClient + Settings/
// Command/Highlights) and stays mounted across every route. The server-scoped
// providers and the visual Layout live in the per-route layouts below, so they
// resolve to that route's server (selected for most routes, the draft's server for
// /new-session). appChildren is server-agnostic, so it renders here once.
const ServerShell = (shellProps: ParentProps) => (
<QueryProvider>
<SharedProviders>
{props.children}
{shellProps.children}
</SharedProviders>
</QueryProvider>
)
return ( return (
<ServerProvider <ServerProvider
defaultServer={props.defaultServer} defaultServer={props.defaultServer}
@@ -397,23 +444,19 @@ export function AppInterface(props: {
component={props.router ?? Router} component={props.router ?? Router}
root={(routerProps) => ( root={(routerProps) => (
<TabsProvider> <TabsProvider>
<ServerKey> <ServerShell>{routerProps.children}</ServerShell>
<QueryProvider>
<ServerSDKProvider>
<ServerSyncProvider>
<RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>
</ServerSyncProvider>
</ServerSDKProvider>
</QueryProvider>
</ServerKey>
</TabsProvider> </TabsProvider>
)} )}
> >
<Route path="/" component={HomeRoute} /> <Route component={SelectedServerLayout}>
<Route path="/new-session" component={DraftRoute} /> <Route path="/" component={HomeRoute} />
<Route path="/:dir" component={DirectoryLayout}> <Route path="/:dir" component={DirectoryLayout}>
<Route path="/" component={() => <Navigate href="session" />} /> <Route path="/" component={() => <Navigate href="session" />} />
<Route path="/session/:id?" component={SessionRoute} /> <Route path="/session/:id?" component={SessionRoute} />
</Route>
</Route>
<Route component={DraftServerLayout}>
<Route path="/new-session" component={DraftRoute} />
</Route> </Route>
</Dynamic> </Dynamic>
</ConnectionGate> </ConnectionGate>
@@ -41,7 +41,7 @@ export function DialogConnectProvider(props: { provider: string }) {
}) })
const provider = createMemo( const provider = createMemo(
() => providers.all().get(props.provider) ?? serverSync.data.provider.all.get(props.provider)!, () => providers.all().get(props.provider) ?? serverSync().data.provider.all.get(props.provider)!,
) )
const fallback = createMemo<ProviderAuthMethod[]>(() => [ const fallback = createMemo<ProviderAuthMethod[]>(() => [
{ {
@@ -52,16 +52,16 @@ export function DialogConnectProvider(props: { provider: string }) {
const [auth] = createResource( const [auth] = createResource(
() => props.provider, () => props.provider,
async () => { async () => {
const cached = serverSync.data.provider_auth[props.provider] const cached = serverSync().data.provider_auth[props.provider]
if (cached) return cached if (cached) return cached
const res = await serverSDK.client.provider.auth() const res = await serverSDK().client.provider.auth()
if (!alive.value) return fallback() if (!alive.value) return fallback()
serverSync.set("provider_auth", res.data ?? {}) serverSync().set("provider_auth", res.data ?? {})
return res.data?.[props.provider] ?? fallback() return res.data?.[props.provider] ?? fallback()
}, },
) )
const loading = createMemo(() => auth.loading && !serverSync.data.provider_auth[props.provider]) const loading = createMemo(() => auth.loading && !serverSync().data.provider_auth[props.provider])
const methods = createMemo(() => auth.latest ?? serverSync.data.provider_auth[props.provider] ?? fallback()) const methods = createMemo(() => auth.latest ?? serverSync().data.provider_auth[props.provider] ?? fallback())
const [store, setStore] = createStore({ const [store, setStore] = createStore({
methodIndex: undefined as undefined | number, methodIndex: undefined as undefined | number,
authorization: undefined as undefined | ProviderAuthAuthorization, authorization: undefined as undefined | ProviderAuthAuthorization,
@@ -158,7 +158,7 @@ export function DialogConnectProvider(props: { provider: string }) {
} }
dispatch({ type: "auth.pending" }) dispatch({ type: "auth.pending" })
const start = Date.now() const start = Date.now()
await serverSDK.client.provider.oauth await serverSDK().client.provider.oauth
.authorize( .authorize(
{ {
providerID: props.provider, providerID: props.provider,
@@ -331,7 +331,7 @@ export function DialogConnectProvider(props: { provider: string }) {
}) })
async function complete() { async function complete() {
await serverSDK.client.global.dispose() await serverSDK().client.global.dispose()
dialog.close() dialog.close()
showToast({ showToast({
variant: "success", variant: "success",
@@ -409,7 +409,7 @@ export function DialogConnectProvider(props: { provider: string }) {
} }
setFormStore("error", undefined) setFormStore("error", undefined)
await serverSDK.client.auth.set({ await serverSDK().client.auth.set({
providerID: props.provider, providerID: props.provider,
auth: { auth: {
type: "api", type: "api",
@@ -480,7 +480,7 @@ export function DialogConnectProvider(props: { provider: string }) {
} }
setFormStore("error", undefined) setFormStore("error", undefined)
const result = await serverSDK.client.provider.oauth const result = await serverSDK().client.provider.oauth
.callback({ .callback({
providerID: props.provider, providerID: props.provider,
method: store.methodIndex, method: store.methodIndex,
@@ -533,7 +533,7 @@ export function DialogConnectProvider(props: { provider: string }) {
onMount(() => { onMount(() => {
void (async () => { void (async () => {
const result = await serverSDK.client.provider.oauth const result = await serverSDK().client.provider.oauth
.callback({ .callback({
providerID: props.provider, providerID: props.provider,
method: store.methodIndex, method: store.methodIndex,
@@ -105,8 +105,8 @@ export function DialogCustomProvider(props: Props) {
const output = validateCustomProvider({ const output = validateCustomProvider({
form, form,
t: language.t, t: language.t,
disabledProviders: serverSync.data.config.disabled_providers ?? [], disabledProviders: serverSync().data.config.disabled_providers ?? [],
existingProviderIDs: new Set(serverSync.data.provider.all.keys()), existingProviderIDs: new Set(serverSync().data.provider.all.keys()),
}) })
batch(() => { batch(() => {
setForm("err", output.err) setForm("err", output.err)
@@ -118,11 +118,11 @@ export function DialogCustomProvider(props: Props) {
const saveMutation = useMutation(() => ({ const saveMutation = useMutation(() => ({
mutationFn: async (result: NonNullable<ReturnType<typeof validate>>) => { mutationFn: async (result: NonNullable<ReturnType<typeof validate>>) => {
const disabledProviders = serverSync.data.config.disabled_providers ?? [] const disabledProviders = serverSync().data.config.disabled_providers ?? []
const nextDisabled = disabledProviders.filter((id) => id !== result.providerID) const nextDisabled = disabledProviders.filter((id) => id !== result.providerID)
if (result.key) { if (result.key) {
await serverSDK.client.auth.set({ await serverSDK().client.auth.set({
providerID: result.providerID, providerID: result.providerID,
auth: { auth: {
type: "api", type: "api",
@@ -131,7 +131,7 @@ export function DialogCustomProvider(props: Props) {
}) })
} }
await serverSync.updateConfig({ await serverSync().updateConfig({
provider: { [result.providerID]: result.config }, provider: { [result.providerID]: result.config },
disabled_providers: nextDisabled, disabled_providers: nextDisabled,
}) })
+6 -6
View File
@@ -35,13 +35,13 @@ export const DialogFork: Component = () => {
const sessionID = params.id const sessionID = params.id
if (!sessionID) return [] if (!sessionID) return []
const msgs = sync.data.message[sessionID] ?? [] const msgs = sync().data.message[sessionID] ?? []
const result: ForkableMessage[] = [] const result: ForkableMessage[] = []
for (const message of msgs) { for (const message of msgs) {
if (message.role !== "user") continue if (message.role !== "user") continue
const parts = sync.data.part[message.id] ?? [] const parts = sync().data.part[message.id] ?? []
const textPart = parts.find((x): x is SDKTextPart => x.type === "text" && !x.synthetic && !x.ignored) const textPart = parts.find((x): x is SDKTextPart => x.type === "text" && !x.synthetic && !x.ignored)
if (!textPart) continue if (!textPart) continue
@@ -61,14 +61,14 @@ export const DialogFork: Component = () => {
const sessionID = params.id const sessionID = params.id
if (!sessionID) return if (!sessionID) return
const parts = sync.data.part[item.id] ?? [] const parts = sync().data.part[item.id] ?? []
const restored = extractPromptFromParts(parts, { const restored = extractPromptFromParts(parts, {
directory: sdk.directory, directory: sdk().directory,
attachmentName: language.t("common.attachment"), attachmentName: language.t("common.attachment"),
}) })
const dir = base64Encode(sdk.directory) const dir = base64Encode(sdk().directory)
sdk.client.session sdk().client.session
.fork({ sessionID, messageID: item.id }) .fork({ sessionID, messageID: item.id })
.then((forked) => { .then((forked) => {
if (!forked.data) { if (!forked.data) {
@@ -9,7 +9,7 @@ import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
import { useNavigate } from "@solidjs/router" import { useNavigate } from "@solidjs/router"
import { createMemo, createSignal, Match, onCleanup, Show, Switch } from "solid-js" import { createMemo, createSignal, Match, onCleanup, Show, Switch } from "solid-js"
import { formatKeybind, useCommand, type CommandOption } from "@/context/command" import { formatKeybind, useCommand, type CommandOption } from "@/context/command"
import { useServerSDK } from "@/context/server-sdk" import { useServerSDK, type ServerSDK } from "@/context/server-sdk"
import { useServerSync } from "@/context/server-sync" import { useServerSync } from "@/context/server-sync"
import { useLayout } from "@/context/layout" import { useLayout } from "@/context/layout"
import { useFile } from "@/context/file" import { useFile } from "@/context/file"
@@ -175,7 +175,7 @@ function createFileEntries(props: {
function createSessionEntries(props: { function createSessionEntries(props: {
workspaces: () => string[] workspaces: () => string[]
label: (directory: string) => string label: (directory: string) => string
serverSDK: ReturnType<typeof useServerSDK> serverSDK: ServerSDK
language: ReturnType<typeof useLanguage> language: ReturnType<typeof useLanguage>
}) { }) {
const state: { const state: {
@@ -292,21 +292,21 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
if (directory && !dirs.includes(directory)) return [...dirs, directory] if (directory && !dirs.includes(directory)) return [...dirs, directory]
return dirs return dirs
}) })
const homedir = createMemo(() => serverSync.data.path.home) const homedir = createMemo(() => serverSync().data.path.home)
const label = (directory: string) => { const label = (directory: string) => {
const current = project() const current = project()
const kind = const kind =
current && directory === current.worktree current && directory === current.worktree
? language.t("workspace.type.local") ? language.t("workspace.type.local")
: language.t("workspace.type.sandbox") : language.t("workspace.type.sandbox")
const [store] = serverSync.child(directory, { bootstrap: false }) const [store] = serverSync().child(directory, { bootstrap: false })
const home = homedir() const home = homedir()
const path = home ? directory.replace(home, "~") : directory const path = home ? directory.replace(home, "~") : directory
const name = store.vcs?.branch ?? getFilename(directory) const name = store.vcs?.branch ?? getFilename(directory)
return `${kind} : ${name || path}` return `${kind} : ${name || path}`
} }
const { sessions } = createSessionEntries({ workspaces, label, serverSDK, language }) const { sessions } = createSessionEntries({ workspaces, label, serverSDK: serverSDK(), language })
const items = async (text: string) => { const items = async (text: string) => {
const query = text.trim() const query = text.trim()
@@ -19,7 +19,7 @@ export const DialogSelectMcp: Component = () => {
const language = useLanguage() const language = useLanguage()
const items = createMemo(() => const items = createMemo(() =>
Object.entries(sync.data.mcp ?? {}) Object.entries(sync().data.mcp ?? {})
.map(([name, status]) => ({ name, status: status.status })) .map(([name, status]) => ({ name, status: status.status }))
.sort((a, b) => a.name.localeCompare(b.name)), .sort((a, b) => a.name.localeCompare(b.name)),
) )
@@ -48,7 +48,7 @@ export const DialogSelectMcp: Component = () => {
}} }}
> >
{(i) => { {(i) => {
const mcpStatus = () => sync.data.mcp[i.name] const mcpStatus = () => sync().data.mcp[i.name]
const status = () => mcpStatus()?.status const status = () => mcpStatus()?.status
const statusLabel = () => { const statusLabel = () => {
const key = status() ? statusLabels[status() as keyof typeof statusLabels] : undefined const key = status() ? statusLabels[status() as keyof typeof statusLabels] : undefined
+14 -14
View File
@@ -207,7 +207,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const sessionID = params.id const sessionID = params.id
if (!sessionID) return false if (!sessionID) return false
const diffs = sync.data.session_diff[sessionID] const diffs = sync().data.session_diff[sessionID]
if (!diffs) return false if (!diffs) return false
return diffs.some((diff) => diff.file === path) return diffs.some((diff) => diff.file === path)
} }
@@ -269,8 +269,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
return paths return paths
}) })
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined)) const info = createMemo(() => (params.id ? sync().session.get(params.id) : undefined))
const working = createMemo(() => sync.data.session_working(params.id ?? "")) const working = createMemo(() => sync().data.session_working(params.id ?? ""))
const imageAttachments = createMemo(() => const imageAttachments = createMemo(() =>
prompt.current().filter((part): part is ImageAttachmentPart => part.type === "image"), prompt.current().filter((part): part is ImageAttachmentPart => part.type === "image"),
) )
@@ -349,7 +349,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const hasUserPrompt = createMemo(() => { const hasUserPrompt = createMemo(() => {
const sessionID = params.id const sessionID = params.id
if (!sessionID) return false if (!sessionID) return false
const messages = sync.data.message[sessionID] const messages = sync().data.message[sessionID]
if (!messages) return false if (!messages) return false
return messages.some((m) => m.role === "user") return messages.some((m) => m.role === "user")
}) })
@@ -474,7 +474,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const pick = () => { const pick = () => {
pickAttachmentFiles({ pickAttachmentFiles({
picker: platform.openAttachmentPickerDialog, picker: platform.openAttachmentPickerDialog,
directory: () => sdk.directory, directory: () => sdk().directory,
fallback: () => fileInputRef?.click(), fallback: () => fileInputRef?.click(),
onFile: addAttachment, onFile: addAttachment,
onError: (error) => onError: (error) =>
@@ -603,8 +603,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
} }
const agentList = createMemo(() => const agentList = createMemo(() =>
sync.data.agent sync()
.filter((agent) => !agent.hidden && agent.mode !== "primary") .data.agent.filter((agent) => !agent.hidden && agent.mode !== "primary")
.map((agent): AtOption => ({ type: "agent", name: agent.name, display: agent.name })), .map((agent): AtOption => ({ type: "agent", name: agent.name, display: agent.name })),
) )
const agentNames = createMemo(() => local.agent.list().map((agent) => agent.name)) const agentNames = createMemo(() => local.agent.list().map((agent) => agent.name))
@@ -673,7 +673,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
type: "builtin" as const, type: "builtin" as const,
})) }))
const custom = sync.data.command.map((cmd) => ({ const custom = sync().data.command.map((cmd) => ({
id: `custom.${cmd.name}`, id: `custom.${cmd.name}`,
trigger: cmd.name, trigger: cmd.name,
title: cmd.name, title: cmd.name,
@@ -1127,8 +1127,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const showVariantControl = createMemo(() => local.model.variant.list().length > 0) const showVariantControl = createMemo(() => local.model.variant.list().length > 0)
const accepting = createMemo(() => { const accepting = createMemo(() => {
const id = params.id const id = params.id
if (!id) return permission.isAutoAcceptingDirectory(sdk.directory) if (!id) return permission.isAutoAcceptingDirectory(sdk().directory)
return permission.isAutoAccepting(id, sdk.directory) return permission.isAutoAccepting(id, sdk().directory)
}) })
const { abort, handleSubmit } = createPromptSubmit({ const { abort, handleSubmit } = createPromptSubmit({
@@ -1319,9 +1319,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const [agentsQuery, globalProvidersQuery, providersQuery] = useQueries(() => ({ const [agentsQuery, globalProvidersQuery, providersQuery] = useQueries(() => ({
queries: [ queries: [
queryOptions.agents(pathKey(sdk.directory)), queryOptions().agents(pathKey(sdk().directory)),
queryOptions.providers(null), queryOptions().providers(null),
queryOptions.providers(pathKey(sdk.directory)), queryOptions().providers(pathKey(sdk().directory)),
], ],
})) }))
@@ -1366,7 +1366,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
(project) => pathKey(project.worktree) === key || project.sandboxes?.some((sandbox) => pathKey(sandbox) === key), (project) => pathKey(project.worktree) === key || project.sandboxes?.some((sandbox) => pathKey(sandbox) === key),
) )
} }
const selectedProject = createMemo(() => projectForDirectory(sdk.directory)) const selectedProject = createMemo(() => projectForDirectory(sdk().directory))
const projectResults = createMemo(() => { const projectResults = createMemo(() => {
const search = picker.projectSearch.trim().toLowerCase() const search = picker.projectSearch.trim().toLowerCase()
if (!search) return projects() if (!search) return projects()
@@ -147,12 +147,12 @@ beforeAll(async () => {
return clientFor(opts.directory) return clientFor(opts.directory)
}, },
} }
return sdk return () => sdk
}, },
})) }))
mock.module("@/context/sync", () => ({ mock.module("@/context/sync", () => ({
useSync: () => ({ useSync: () => () => ({
data: { command: [] }, data: { command: [] },
session: { session: {
optimistic: { optimistic: {
@@ -176,7 +176,7 @@ beforeAll(async () => {
})) }))
mock.module("@/context/server-sync", () => ({ mock.module("@/context/server-sync", () => ({
useServerSync: () => ({ useServerSync: () => () => ({
child: (directory: string) => { child: (directory: string) => {
syncedDirectories.push(directory) syncedDirectories.push(directory)
storedSessions[directory] ??= [] storedSessions[directory] ??= []
@@ -7,14 +7,14 @@ import { batch, type Accessor } from "solid-js"
import type { FileSelection } from "@/context/file" import type { FileSelection } from "@/context/file"
import { useServer } from "@/context/server" import { useServer } from "@/context/server"
import { useTabs } from "@/context/tabs" import { useTabs } from "@/context/tabs"
import { useServerSync } from "@/context/server-sync" import { useServerSync, type ServerSync } from "@/context/server-sync"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { useLayout } from "@/context/layout" import { useLayout } from "@/context/layout"
import { useLocal } from "@/context/local" import { useLocal } from "@/context/local"
import { usePermission } from "@/context/permission" import { usePermission } from "@/context/permission"
import { type ContextItem, type ImageAttachmentPart, type Prompt, usePrompt } from "@/context/prompt" import { type ContextItem, type ImageAttachmentPart, type Prompt, usePrompt } from "@/context/prompt"
import { useSDK } from "@/context/sdk" import { useSDK, type DirectorySDK } from "@/context/sdk"
import { useSync } from "@/context/sync" import { useSync, type DirectorySync } from "@/context/sync"
import { Identifier } from "@/utils/id" import { Identifier } from "@/utils/id"
import { Worktree as WorktreeState } from "@/utils/worktree" import { Worktree as WorktreeState } from "@/utils/worktree"
import { buildRequestParts } from "./build-request-parts" import { buildRequestParts } from "./build-request-parts"
@@ -40,9 +40,9 @@ export type FollowupDraft = {
} }
type FollowupSendInput = { type FollowupSendInput = {
client: ReturnType<typeof useSDK>["client"] client: DirectorySDK["client"]
serverSync: ReturnType<typeof useServerSync> serverSync: ServerSync
sync: ReturnType<typeof useSync> sync: DirectorySync
draft: FollowupDraft draft: FollowupDraft
messageID?: string messageID?: string
optimisticBusy?: boolean optimisticBusy?: boolean
@@ -218,7 +218,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
const [search] = useSearchParams<{ draftId?: string }>() const [search] = useSearchParams<{ draftId?: string }>()
const server = useServer() const server = useServer()
const tabs = useTabs() const tabs = useTabs()
const pendingKey = (sessionID: string) => ScopedKey.from(sdk.scope, sessionID) const pendingKey = (sessionID: string) => ScopedKey.from(sdk().scope, sessionID)
const errorMessage = (err: unknown) => { const errorMessage = (err: unknown) => {
if (err && typeof err === "object" && "data" in err) { if (err && typeof err === "object" && "data" in err) {
@@ -233,8 +233,8 @@ export function createPromptSubmit(input: PromptSubmitInput) {
const sessionID = params.id const sessionID = params.id
if (!sessionID) return Promise.resolve() if (!sessionID) return Promise.resolve()
serverSync.todo.set(sessionID, []) serverSync().todo.set(sessionID, [])
const [, setStore] = serverSync.child(sdk.directory) const [, setStore] = serverSync().child(sdk().directory)
setStore("todo", sessionID, []) setStore("todo", sessionID, [])
input.onAbort?.() input.onAbort?.()
@@ -247,7 +247,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
pending.delete(key) pending.delete(key)
return Promise.resolve() return Promise.resolve()
} }
return sdk.client.session return sdk().client.session
.abort({ .abort({
sessionID, sessionID,
}) })
@@ -281,7 +281,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
} }
const seed = (dir: string, info: Session) => { const seed = (dir: string, info: Session) => {
const [, setStore] = serverSync.child(dir) const [, setStore] = serverSync().child(dir)
setStore("session", (list: Session[]) => { setStore("session", (list: Session[]) => {
const result = Binary.search(list, info.id, (item) => item.id) const result = Binary.search(list, info.id, (item) => item.id)
const next = [...list] const next = [...list]
@@ -321,13 +321,13 @@ export function createPromptSubmit(input: PromptSubmitInput) {
input.addToHistory(currentPrompt, mode) input.addToHistory(currentPrompt, mode)
input.resetHistoryNavigation() input.resetHistoryNavigation()
const projectDirectory = sdk.directory const projectDirectory = sdk().directory
const isNewSession = !params.id const isNewSession = !params.id
const shouldAutoAccept = isNewSession && input.autoAccept() const shouldAutoAccept = isNewSession && input.autoAccept()
const worktreeSelection = input.newSessionWorktree?.() || "main" const worktreeSelection = input.newSessionWorktree?.() || "main"
let sessionDirectory = projectDirectory let sessionDirectory = projectDirectory
let client = sdk.client let client = sdk().client
if (isNewSession) { if (isNewSession) {
if (worktreeSelection === "create") { if (worktreeSelection === "create") {
@@ -349,7 +349,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
}) })
return return
} }
WorktreeState.pending(sdk.scope, createdWorktree.directory) WorktreeState.pending(sdk().scope, createdWorktree.directory)
sessionDirectory = createdWorktree.directory sessionDirectory = createdWorktree.directory
} }
@@ -358,11 +358,11 @@ export function createPromptSubmit(input: PromptSubmitInput) {
} }
if (sessionDirectory !== projectDirectory) { if (sessionDirectory !== projectDirectory) {
client = sdk.createClient({ client = sdk().createClient({
directory: sessionDirectory, directory: sessionDirectory,
throwOnError: true, throwOnError: true,
}) })
serverSync.child(sessionDirectory) serverSync().child(sessionDirectory)
} }
input.onNewSessionWorktreeReset?.() input.onNewSessionWorktreeReset?.()
@@ -470,7 +470,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
if (text.startsWith("/")) { if (text.startsWith("/")) {
const [cmdName, ...args] = text.split(" ") const [cmdName, ...args] = text.split(" ")
const commandName = cmdName.slice(1) const commandName = cmdName.slice(1)
const customCommand = sync.data.command.find((c) => c.name === commandName) const customCommand = sync().data.command.find((c) => c.name === commandName)
if (customCommand) { if (customCommand) {
clearInput() clearInput()
client.session client.session
@@ -504,7 +504,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
const messageID = Identifier.ascending("message") const messageID = Identifier.ascending("message")
const removeOptimisticMessage = () => { const removeOptimisticMessage = () => {
sync.session.optimistic.remove({ sync().session.optimistic.remove({
directory: sessionDirectory, directory: sessionDirectory,
sessionID: session.id, sessionID: session.id,
messageID, messageID,
@@ -515,17 +515,17 @@ export function createPromptSubmit(input: PromptSubmitInput) {
clearInput() clearInput()
const waitForWorktree = async () => { const waitForWorktree = async () => {
const worktree = WorktreeState.get(sdk.scope, sessionDirectory) const worktree = WorktreeState.get(sdk().scope, sessionDirectory)
if (!worktree || worktree.status !== "pending") return true if (!worktree || worktree.status !== "pending") return true
if (sessionDirectory === projectDirectory) { if (sessionDirectory === projectDirectory) {
sync.set("session_status", session.id, { type: "busy" }) sync().set("session_status", session.id, { type: "busy" })
} }
const controller = new AbortController() const controller = new AbortController()
const cleanup = () => { const cleanup = () => {
if (sessionDirectory === projectDirectory) { if (sessionDirectory === projectDirectory) {
sync.set("session_status", session.id, { type: "idle" }) sync().set("session_status", session.id, { type: "idle" })
} }
removeOptimisticMessage() removeOptimisticMessage()
restoreCommentItems(commentItems) restoreCommentItems(commentItems)
@@ -559,7 +559,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
}, timeoutMs) }, timeoutMs)
}) })
const result = await Promise.race([WorktreeState.wait(sdk.scope, sessionDirectory), abortWait, timeout]).finally( const result = await Promise.race([WorktreeState.wait(sdk().scope, sessionDirectory), abortWait, timeout]).finally(
() => { () => {
if (timer.id === undefined) return if (timer.id === undefined) return
clearTimeout(timer.id) clearTimeout(timer.id)
@@ -573,8 +573,8 @@ export function createPromptSubmit(input: PromptSubmitInput) {
void sendFollowupDraft({ void sendFollowupDraft({
client, client,
sync, sync: sync(),
serverSync, serverSync: serverSync(),
draft, draft,
messageID, messageID,
optimisticBusy: sessionDirectory === projectDirectory, optimisticBusy: sessionDirectory === projectDirectory,
@@ -582,7 +582,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
}).catch((err) => { }).catch((err) => {
pending.delete(pendingKey(session.id)) pending.delete(pendingKey(session.id))
if (sessionDirectory === projectDirectory) { if (sessionDirectory === projectDirectory) {
sync.set("session_status", session.id, { type: "idle" }) sync().set("session_status", session.id, { type: "idle" })
} }
showToast({ showToast({
title: language.t("prompt.toast.promptSendFailed.title"), title: language.t("prompt.toast.promptSendFailed.title"),
@@ -42,7 +42,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
pathFromTab: file.pathFromTab, pathFromTab: file.pathFromTab,
normalizeTab: (tab) => (tab.startsWith("file://") ? file.tab(tab) : tab), normalizeTab: (tab) => (tab.startsWith("file://") ? file.tab(tab) : tab),
}) })
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : [])) const messages = createMemo(() => (params.id ? (sync().data.message[params.id] ?? []) : []))
const usd = createMemo( const usd = createMemo(
() => () =>
@@ -96,13 +96,13 @@ export function SessionContextTab() {
const providers = useProviders() const providers = useProviders()
const { params, view } = useSessionLayout() const { params, view } = useSessionLayout()
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined)) const info = createMemo(() => (params.id ? sync().session.get(params.id) : undefined))
const messages = createMemo( const messages = createMemo(
() => { () => {
const id = params.id const id = params.id
if (!id) return emptyMessages if (!id) return emptyMessages
return (sync.data.message[id] ?? []) as Message[] return (sync().data.message[id] ?? []) as Message[]
}, },
emptyMessages, emptyMessages,
{ equals: same }, { equals: same },
@@ -180,7 +180,7 @@ export function SessionContextTab() {
if (!c?.input) return [] if (!c?.input) return []
return estimateSessionContextBreakdown({ return estimateSessionContextBreakdown({
messages: messages(), messages: messages(),
parts: sync.data.part as Record<string, Part[] | undefined>, parts: sync().data.part as Record<string, Part[] | undefined>,
input: c.input, input: c.input,
systemPrompt: systemPrompt(), systemPrompt: systemPrompt(),
}) })
@@ -221,7 +221,7 @@ export function SessionContextTab() {
let scroll: HTMLDivElement | undefined let scroll: HTMLDivElement | undefined
let frame: number | undefined let frame: number | undefined
let pending: { x: number; y: number } | undefined let pending: { x: number; y: number } | undefined
const getParts = (id: string) => (sync.data.part[id] ?? []) as Part[] const getParts = (id: string) => (sync().data.part[id] ?? []) as Part[]
const restoreScroll = () => { const restoreScroll = () => {
const el = scroll const el = scroll
@@ -229,7 +229,7 @@ export function SessionHeader() {
) )
const opening = createMemo(() => openRequest.app !== undefined) const opening = createMemo(() => openRequest.app !== undefined)
const tint = createMemo(() => const tint = createMemo(() =>
messageAgentColor(params.id ? sync.data.message[params.id] : undefined, sync.data.agent), messageAgentColor(params.id ? sync().data.message[params.id] : undefined, sync().data.agent),
) )
const v2ActionsState = createMemo<SessionHeaderV2ActionsState>(() => ({ const v2ActionsState = createMemo<SessionHeaderV2ActionsState>(() => ({
statusVisible: status(), statusVisible: status(),
@@ -20,24 +20,24 @@ export function NewSessionView(props: NewSessionViewProps) {
const sdk = useSDK() const sdk = useSDK()
const language = useLanguage() const language = useLanguage()
const sandboxes = createMemo(() => sync.project?.sandboxes ?? []) const sandboxes = createMemo(() => sync().project?.sandboxes ?? [])
const options = createMemo(() => [MAIN_WORKTREE, ...sandboxes(), CREATE_WORKTREE]) const options = createMemo(() => [MAIN_WORKTREE, ...sandboxes(), CREATE_WORKTREE])
const current = createMemo(() => { const current = createMemo(() => {
const selection = props.worktree const selection = props.worktree
if (options().includes(selection)) return selection if (options().includes(selection)) return selection
return MAIN_WORKTREE return MAIN_WORKTREE
}) })
const projectRoot = createMemo(() => sync.project?.worktree ?? sdk.directory) const projectRoot = createMemo(() => sync().project?.worktree ?? sdk().directory)
const isWorktree = createMemo(() => { const isWorktree = createMemo(() => {
const project = sync.project const project = sync().project
if (!project) return false if (!project) return false
return sdk.directory !== project.worktree return sdk().directory !== project.worktree
}) })
const label = (value: string) => { const label = (value: string) => {
if (value === MAIN_WORKTREE) { if (value === MAIN_WORKTREE) {
if (isWorktree()) return language.t("session.new.worktree.main") if (isWorktree()) return language.t("session.new.worktree.main")
const branch = sync.data.vcs?.branch const branch = sync().data.vcs?.branch
if (branch) return language.t("session.new.worktree.mainWithBranch", { branch }) if (branch) return language.t("session.new.worktree.mainWithBranch", { branch })
return language.t("session.new.worktree.main") return language.t("session.new.worktree.main")
} }
@@ -69,7 +69,7 @@ export function NewSessionView(props: NewSessionViewProps) {
{label(current())} {label(current())}
</div> </div>
</div> </div>
<Show when={sync.project}> <Show when={sync().project}>
{(project) => ( {(project) => (
<div class="flex items-start justify-center gap-3 min-h-5"> <div class="flex items-start justify-center gap-3 min-h-5">
<div class="text-12-medium text-text-weak leading-5 min-w-0 max-w-160 break-words text-center"> <div class="text-12-medium text-text-weak leading-5 min-w-0 max-w-160 break-words text-center">
@@ -127,7 +127,7 @@ export const SettingsGeneral: Component = () => {
const [shells] = createResource( const [shells] = createResource(
() => () =>
serverSdk.client.pty serverSdk().client.pty
.shells() .shells()
.then((res) => res.data ?? []) .then((res) => res.data ?? [])
.catch(() => [] as ShellOption[]), .catch(() => [] as ShellOption[]),
@@ -151,11 +151,11 @@ export const SettingsGeneral: Component = () => {
}) })
const autoOption = { id: "auto", value: "", label: language.t("settings.general.row.shell.autoDefault") } const autoOption = { id: "auto", value: "", label: language.t("settings.general.row.shell.autoDefault") }
const currentShell = createMemo(() => serverSync.data.config.shell ?? "") const currentShell = createMemo(() => serverSync().data.config.shell ?? "")
const shellOptions = createMemo<ShellSelectOption[]>(() => { const shellOptions = createMemo<ShellSelectOption[]>(() => {
const list = shells.latest const list = shells.latest
const current = serverSync.data.config.shell const current = serverSync().data.config.shell
const nameCounts = new Map<string, number>() const nameCounts = new Map<string, number>()
for (const s of list) { for (const s of list) {
@@ -290,7 +290,7 @@ export const SettingsGeneral: Component = () => {
onSelect={(option) => { onSelect={(option) => {
if (!option) return if (!option) return
if (option.value === currentShell()) return if (option.value === currentShell()) return
serverSync.updateConfig({ shell: option.value }) serverSync().updateConfig({ shell: option.value })
}} }}
variant="secondary" variant="secondary"
size="small" size="small"
@@ -83,7 +83,7 @@ const SettingsProvidersContent: Component = () => {
const note = (id: string) => PROVIDER_NOTES.find((item) => item.match(id))?.key const note = (id: string) => PROVIDER_NOTES.find((item) => item.match(id))?.key
const isConfigCustom = (providerID: string) => { const isConfigCustom = (providerID: string) => {
const provider = serverSync.data.config.provider?.[providerID] const provider = serverSync().data.config.provider?.[providerID]
if (!provider) return false if (!provider) return false
if (provider.npm !== "@ai-sdk/openai-compatible") return false if (provider.npm !== "@ai-sdk/openai-compatible") return false
if (!provider.models || Object.keys(provider.models).length === 0) return false if (!provider.models || Object.keys(provider.models).length === 0) return false
@@ -91,11 +91,11 @@ const SettingsProvidersContent: Component = () => {
} }
const disableProvider = async (providerID: string, name: string) => { const disableProvider = async (providerID: string, name: string) => {
const before = serverSync.data.config.disabled_providers ?? [] const before = serverSync().data.config.disabled_providers ?? []
const next = before.includes(providerID) ? before : [...before, providerID] const next = before.includes(providerID) ? before : [...before, providerID]
serverSync.set("config", "disabled_providers", next) serverSync().set("config", "disabled_providers", next)
await serverSync await serverSync()
.updateConfig({ disabled_providers: next }) .updateConfig({ disabled_providers: next })
.then(() => { .then(() => {
showToast({ showToast({
@@ -106,7 +106,7 @@ const SettingsProvidersContent: Component = () => {
}) })
}) })
.catch((err: unknown) => { .catch((err: unknown) => {
serverSync.set("config", "disabled_providers", before) serverSync().set("config", "disabled_providers", before)
const message = err instanceof Error ? err.message : String(err) const message = err instanceof Error ? err.message : String(err)
showToast({ title: language.t("common.requestFailed"), description: message }) showToast({ title: language.t("common.requestFailed"), description: message })
}) })
@@ -114,14 +114,14 @@ const SettingsProvidersContent: Component = () => {
const disconnect = async (providerID: string, name: string) => { const disconnect = async (providerID: string, name: string) => {
if (isConfigCustom(providerID)) { if (isConfigCustom(providerID)) {
await serverSDK.client.auth.remove({ providerID }).catch(() => undefined) await serverSDK().client.auth.remove({ providerID }).catch(() => undefined)
await disableProvider(providerID, name) await disableProvider(providerID, name)
return return
} }
await serverSDK.client.auth await serverSDK().client.auth
.remove({ providerID }) .remove({ providerID })
.then(async () => { .then(async () => {
await serverSDK.client.global.dispose() await serverSDK().client.global.dispose()
showToast({ showToast({
variant: "success", variant: "success",
icon: "circle-check", icon: "circle-check",
@@ -30,7 +30,7 @@ function SettingsServerDataProviders(props: ParentProps<{ server: ServerConnecti
return ( return (
<QueryClientProvider client={serverCtx().queryClient}> <QueryClientProvider client={serverCtx().queryClient}>
<ServerSDKProvider server={props.server}> <ServerSDKProvider server={() => props.server}>
<ServerSyncProvider> <ServerSyncProvider>
<ModelsProvider>{props.children}</ModelsProvider> <ModelsProvider>{props.children}</ModelsProvider>
</ServerSyncProvider> </ServerSyncProvider>
@@ -126,7 +126,7 @@ export const SettingsGeneralV2: Component = () => {
const [shells] = createResource( const [shells] = createResource(
() => () =>
serverSdk.client.pty serverSdk().client.pty
.shells() .shells()
.then((res) => res.data ?? []) .then((res) => res.data ?? [])
.catch(() => [] as ShellOption[]), .catch(() => [] as ShellOption[]),
@@ -144,11 +144,11 @@ export const SettingsGeneralV2: Component = () => {
}) })
const autoOption = { id: "auto", value: "", label: language.t("settings.general.row.shell.autoDefault") } const autoOption = { id: "auto", value: "", label: language.t("settings.general.row.shell.autoDefault") }
const currentShell = createMemo(() => serverSync.data.config.shell ?? "") const currentShell = createMemo(() => serverSync().data.config.shell ?? "")
const shellOptions = createMemo<ShellSelectOption[]>(() => { const shellOptions = createMemo<ShellSelectOption[]>(() => {
const list = shells.latest const list = shells.latest
const current = serverSync.data.config.shell const current = serverSync().data.config.shell
const nameCounts = new Map<string, number>() const nameCounts = new Map<string, number>()
for (const s of list) { for (const s of list) {
@@ -275,7 +275,7 @@ export const SettingsGeneralV2: Component = () => {
onSelect={(option) => { onSelect={(option) => {
if (!option) return if (!option) return
if (option.value === currentShell()) return if (option.value === currentShell()) return
serverSync.updateConfig({ shell: option.value }) serverSync().updateConfig({ shell: option.value })
}} }}
/> />
</SettingsRowV2> </SettingsRowV2>
@@ -77,7 +77,7 @@ export const SettingsProvidersV2: Component = () => {
const note = (id: string) => PROVIDER_NOTES.find((item) => item.match(id))?.key const note = (id: string) => PROVIDER_NOTES.find((item) => item.match(id))?.key
const isConfigCustom = (providerID: string) => { const isConfigCustom = (providerID: string) => {
const provider = serverSync.data.config.provider?.[providerID] const provider = serverSync().data.config.provider?.[providerID]
if (!provider) return false if (!provider) return false
if (provider.npm !== "@ai-sdk/openai-compatible") return false if (provider.npm !== "@ai-sdk/openai-compatible") return false
if (!provider.models || Object.keys(provider.models).length === 0) return false if (!provider.models || Object.keys(provider.models).length === 0) return false
@@ -85,11 +85,11 @@ export const SettingsProvidersV2: Component = () => {
} }
const disableProvider = async (providerID: string, name: string) => { const disableProvider = async (providerID: string, name: string) => {
const before = serverSync.data.config.disabled_providers ?? [] const before = serverSync().data.config.disabled_providers ?? []
const next = before.includes(providerID) ? before : [...before, providerID] const next = before.includes(providerID) ? before : [...before, providerID]
serverSync.set("config", "disabled_providers", next) serverSync().set("config", "disabled_providers", next)
await serverSync await serverSync()
.updateConfig({ disabled_providers: next }) .updateConfig({ disabled_providers: next })
.then(() => { .then(() => {
showToast({ showToast({
@@ -100,7 +100,7 @@ export const SettingsProvidersV2: Component = () => {
}) })
}) })
.catch((err: unknown) => { .catch((err: unknown) => {
serverSync.set("config", "disabled_providers", before) serverSync().set("config", "disabled_providers", before)
const message = err instanceof Error ? err.message : String(err) const message = err instanceof Error ? err.message : String(err)
showToast({ title: language.t("common.requestFailed"), description: message }) showToast({ title: language.t("common.requestFailed"), description: message })
}) })
@@ -108,14 +108,14 @@ export const SettingsProvidersV2: Component = () => {
const disconnect = async (providerID: string, name: string) => { const disconnect = async (providerID: string, name: string) => {
if (isConfigCustom(providerID)) { if (isConfigCustom(providerID)) {
await serverSdk.client.auth.remove({ providerID }).catch(() => undefined) await serverSdk().client.auth.remove({ providerID }).catch(() => undefined)
await disableProvider(providerID, name) await disableProvider(providerID, name)
return return
} }
await serverSdk.client.auth await serverSdk().client.auth
.remove({ providerID }) .remove({ providerID })
.then(async () => { .then(async () => {
await serverSdk.client.global.dispose() await serverSdk().client.global.dispose()
showToast({ showToast({
variant: "success", variant: "success",
icon: "circle-check", icon: "circle-check",
@@ -279,13 +279,13 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
const sortedServers = createMemo(() => listServersByHealth(global.servers.list(), server.key, global.servers.health)) const sortedServers = createMemo(() => listServersByHealth(global.servers.list(), server.key, global.servers.health))
const toggleMcp = useMcpToggle() const toggleMcp = useMcpToggle()
const defaultServer = useDefaultServerKey(platform.getDefaultServer) const defaultServer = useDefaultServerKey(platform.getDefaultServer)
const mcpNames = createMemo(() => Object.keys(sync.data.mcp ?? {}).sort((a, b) => a.localeCompare(b))) const mcpNames = createMemo(() => Object.keys(sync().data.mcp ?? {}).sort((a, b) => a.localeCompare(b)))
const mcpStatus = (name: string) => sync.data.mcp?.[name]?.status const mcpStatus = (name: string) => sync().data.mcp?.[name]?.status
const mcpConnected = createMemo(() => mcpNames().filter((name) => mcpStatus(name) === "connected").length) const mcpConnected = createMemo(() => mcpNames().filter((name) => mcpStatus(name) === "connected").length)
const lspItems = createMemo(() => sync.data.lsp ?? []) const lspItems = createMemo(() => sync().data.lsp ?? [])
const lspCount = createMemo(() => lspItems().length) const lspCount = createMemo(() => lspItems().length)
const plugins = createMemo(() => const plugins = createMemo(() =>
(sync.data.config.plugin ?? []).map((item) => (typeof item === "string" ? item : item[0])), (sync().data.config.plugin ?? []).map((item) => (typeof item === "string" ? item : item[0])),
) )
const pluginCount = createMemo(() => plugins().length) const pluginCount = createMemo(() => plugins().length)
const pluginEmpty = createMemo(() => pluginEmptyMessage(language.t("dialog.plugins.empty"), "opencode.json")) const pluginEmpty = createMemo(() => pluginEmptyMessage(language.t("dialog.plugins.empty"), "opencode.json"))
@@ -18,9 +18,9 @@ export function StatusPopover() {
const global = useGlobal() const global = useGlobal()
const sync = useSync() const sync = useSync()
const [shown, setShown] = createSignal(false) const [shown, setShown] = createSignal(false)
const ready = createMemo(() => global.servers.health[server.key]?.healthy === false || sync.data.mcp_ready) const ready = createMemo(() => global.servers.health[server.key]?.healthy === false || sync().data.mcp_ready)
const mcpIssue = createMemo(() => { const mcpIssue = createMemo(() => {
const mcp = Object.values(sync.data.mcp ?? {}) const mcp = Object.values(sync().data.mcp ?? {})
const failed = mcp.some((item) => item.status === "failed" || item.status === "needs_client_registration") const failed = mcp.some((item) => item.status === "failed" || item.status === "needs_client_registration")
const warn = mcp.some((item) => item.status === "needs_auth") const warn = mcp.some((item) => item.status === "needs_auth")
if (failed) return "critical" as const if (failed) return "critical" as const
@@ -86,9 +86,9 @@ function DirectoryStatusPopover() {
const sync = useSync() const sync = useSync()
const [shown, setShown] = createSignal(false) const [shown, setShown] = createSignal(false)
const serverHealth = () => global.servers.health[server.key]?.healthy const serverHealth = () => global.servers.health[server.key]?.healthy
const ready = createMemo(() => serverHealth() === false || sync.data.mcp_ready) const ready = createMemo(() => serverHealth() === false || sync().data.mcp_ready)
const mcpIssue = createMemo(() => { const mcpIssue = createMemo(() => {
const mcp = Object.values(sync.data.mcp ?? {}) const mcp = Object.values(sync().data.mcp ?? {})
const failed = mcp.some((item) => item.status === "failed" || item.status === "needs_client_registration") const failed = mcp.some((item) => item.status === "failed" || item.status === "needs_client_registration")
const warn = mcp.some((item) => item.status === "needs_auth") const warn = mcp.some((item) => item.status === "needs_auth")
if (failed) return "critical" as const if (failed) return "critical" as const
+3 -3
View File
@@ -161,9 +161,9 @@ export const Terminal = (props: TerminalProps) => {
const theme = useTheme() const theme = useTheme()
const language = useLanguage() const language = useLanguage()
const server = useServer() const server = useServer()
const directory = sdk.directory const directory = sdk().directory
const client = sdk.client const client = sdk().client
const url = sdk.url const url = sdk().url
const auth = server.current?.http const auth = server.current?.http
const username = auth?.username ?? "opencode" const username = auth?.username ?? "opencode"
const password = auth?.password ?? "" const password = auth?.password ?? ""
+2 -2
View File
@@ -291,7 +291,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
item.type === "session" && item.server === route.server && item.sessionId === route.sessionId, item.type === "session" && item.server === route.server && item.sessionId === route.sessionId,
) )
if (main) return main if (main) return main
const sync = serverSync.createDirSyncContext(route.dir) const sync = serverSync().createDirSyncContext(route.dir)
const session = sync.session.get(route.sessionId) const session = sync.session.get(route.sessionId)
if (session?.parentID) { if (session?.parentID) {
const parentID = session.parentID const parentID = session.parentID
@@ -312,7 +312,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
if (tab) return if (tab) return
if (route.type === "session") { if (route.type === "session") {
const sync = serverSync.createDirSyncContext(route.dir) const sync = serverSync().createDirSyncContext(route.dir)
const session = sync.session.get(route.sessionId) const session = sync.session.get(route.sessionId)
if (!session) return if (!session) return
const sessionId = session.parentID ?? session.id const sessionId = session.parentID ?? session.id
+1 -1
View File
@@ -208,7 +208,7 @@ export const { use: useComments, provider: CommentsProvider } = createSimpleCont
const decoded = decodeSessionKey(key) const decoded = decodeSessionKey(key)
return createRoot((dispose) => ({ return createRoot((dispose) => ({
value: createCommentSession( value: createCommentSession(
serverSDK.scope, serverSDK().scope,
decoded.dir, decoded.dir,
decoded.id === WORKSPACE_KEY ? undefined : decoded.id, decoded.id === WORKSPACE_KEY ? undefined : decoded.id,
), ),
+2 -2
View File
@@ -11,7 +11,7 @@ import {
import type { Message, Part } from "@opencode-ai/sdk/v2/client" import type { Message, Part } from "@opencode-ai/sdk/v2/client"
import { SESSION_CACHE_LIMIT, dropSessionCaches, pickSessionCacheEvictions } from "./global-sync/session-cache" import { SESSION_CACHE_LIMIT, dropSessionCaches, pickSessionCacheEvictions } from "./global-sync/session-cache"
import { diffs as list, message as clean } from "@/utils/diffs" import { diffs as list, message as clean } from "@/utils/diffs"
import { createServerSdkContext, useServerSDK } from "./server-sdk" import { type createServerSdkContext } from "./server-sdk"
import { type createServerSyncContextInner } from "./server-sync" import { type createServerSyncContextInner } from "./server-sync"
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"]) const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
@@ -174,7 +174,7 @@ function setOptimisticRemove(setStore: (...args: unknown[]) => void, input: Opti
export const createDirSyncContext = ( export const createDirSyncContext = (
directory: string, directory: string,
serverSync: ReturnType<typeof createServerSyncContextInner>, serverSync: ReturnType<typeof createServerSyncContextInner>,
serverSDK: ReturnType<typeof createServerSdkContext> = useServerSDK(), serverSDK: ReturnType<typeof createServerSdkContext>,
) => { ) => {
const client = serverSDK.createClient({ directory, throwOnError: true }) const client = serverSDK.createClient({ directory, throwOnError: true })
+7 -7
View File
@@ -62,10 +62,10 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
const language = useLanguage() const language = useLanguage()
const layout = useLayout() const layout = useLayout()
const scope = createMemo(() => sdk.directory) const scope = createMemo(() => sdk().directory)
const path = createPathHelpers(scope) const path = createPathHelpers(scope)
const tabs = layout.tabs(() => const tabs = layout.tabs(() =>
SessionStateKey.from(serverSDK.scope, SessionRouteKey.fromRoute(params.dir, params.id)), SessionStateKey.from(serverSDK().scope, SessionRouteKey.fromRoute(params.dir, params.id)),
) )
const inflight = new Map<string, Promise<void>>() const inflight = new Map<string, Promise<void>>()
@@ -78,7 +78,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
const tree = createFileTreeStore({ const tree = createFileTreeStore({
scope, scope,
normalizeDir: path.normalizeDir, normalizeDir: path.normalizeDir,
list: (dir) => sdk.client.file.list({ path: dir }).then((x) => x.data ?? []), list: (dir) => sdk().client.file.list({ path: dir }).then((x) => x.data ?? []),
onError: (message) => { onError: (message) => {
showToast({ showToast({
variant: "error", variant: "error",
@@ -112,7 +112,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
}) })
}) })
const viewCache = createFileViewCache(serverSDK.scope) const viewCache = createFileViewCache(serverSDK().scope)
const view = createMemo(() => viewCache.load(scope(), params.id)) const view = createMemo(() => viewCache.load(scope(), params.id))
const ensure = (file: string) => { const ensure = (file: string) => {
@@ -176,7 +176,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
setLoading(file) setLoading(file)
const promise = sdk.client.file const promise = sdk().client.file
.read({ path: file }) .read({ path: file })
.then((x) => { .then((x) => {
if (scope() !== directory) return if (scope() !== directory) return
@@ -200,12 +200,12 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
} }
const search = (query: string, dirs: "true" | "false") => const search = (query: string, dirs: "true" | "false") =>
sdk.client.find.files({ query, dirs }).then( sdk().client.find.files({ query, dirs }).then(
(x) => (x.data ?? []).map(path.normalize), (x) => (x.data ?? []).map(path.normalize),
() => [], () => [],
) )
const stop = sdk.event.listen((e) => { const stop = sdk().event.listen((e) => {
invalidateFromWatcher(e.details, { invalidateFromWatcher(e.details, {
normalize: path.normalize, normalize: path.normalize,
hasFile: (file) => Boolean(store.file[file]), hasFile: (file) => Boolean(store.file[file]),
+11 -11
View File
@@ -246,7 +246,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
} }
} }
const target = Persist.serverGlobal(serverSdk.scope, "layout", ["layout.v6"]) const target = Persist.serverGlobal(serverSdk().scope, "layout", ["layout.v6"])
const [store, setStore, _, ready] = persisted( const [store, setStore, _, ready] = persisted(
{ ...target, migrate }, { ...target, migrate },
createStore({ createStore({
@@ -409,11 +409,11 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
} }
function enrich(project: { worktree: string; expanded: boolean }) { function enrich(project: { worktree: string; expanded: boolean }) {
const [childStore] = serverSync.child(project.worktree, { bootstrap: false }) const [childStore] = serverSync().child(project.worktree, { bootstrap: false })
const projectID = childStore.project const projectID = childStore.project
const metadata = projectID const metadata = projectID
? serverSync.data.project.find((x) => x.id === projectID) ? serverSync().data.project.find((x) => x.id === projectID)
: serverSync.data.project.find((x) => x.worktree === project.worktree) : serverSync().data.project.find((x) => x.worktree === project.worktree)
// Preserve local icon override from per-workspace localStorage cache (childStore.icon). // Preserve local icon override from per-workspace localStorage cache (childStore.icon).
// Without this, different subdirectories of the same git repo would share the same // Without this, different subdirectories of the same git repo would share the same
@@ -427,7 +427,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
const roots = createMemo(() => { const roots = createMemo(() => {
const map = new Map<string, string>() const map = new Map<string, string>()
for (const project of serverSync.data.project) { for (const project of serverSync().data.project) {
const sandboxes = project.sandboxes ?? [] const sandboxes = project.sandboxes ?? []
for (const sandbox of sandboxes) { for (const sandbox of sandboxes) {
map.set(sandbox, project.worktree) map.set(sandbox, project.worktree)
@@ -493,12 +493,12 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
createEffect(() => { createEffect(() => {
const projects = enriched() const projects = enriched()
if (projects.length === 0) return if (projects.length === 0) return
if (!serverSync.ready) return if (!serverSync().ready) return
for (const project of projects) { for (const project of projects) {
if (!project.id) continue if (!project.id) continue
if (project.id === "global") continue if (project.id === "global") continue
serverSync.project.icon(project.worktree, project.icon?.override) serverSync().project.icon(project.worktree, project.icon?.override)
} }
}) })
@@ -532,11 +532,11 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
colorRequested.set(worktree, color) colorRequested.set(worktree, color)
if (project.id === "global") { if (project.id === "global") {
serverSync.project.meta(worktree, { icon: { color } }) serverSync().project.meta(worktree, { icon: { color } })
continue continue
} }
void serverSdk.client.project void serverSdk().client.project
.update({ projectID: project.id, directory: worktree, icon: { color } }) .update({ projectID: project.id, directory: worktree, icon: { color } })
.catch(() => { .catch(() => {
if (colorRequested.get(worktree) === color) colorRequested.delete(worktree) if (colorRequested.get(worktree) === color) colorRequested.delete(worktree)
@@ -554,7 +554,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
sessionTimer = undefined sessionTimer = undefined
void Promise.all( void Promise.all(
server.projects.list().map((project) => { server.projects.list().map((project) => {
return serverSync.project.loadSessions(project.worktree) return serverSync().project.loadSessions(project.worktree)
}), }),
) )
}, 0) }, 0)
@@ -584,7 +584,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
open(directory: string) { open(directory: string) {
const root = rootFor(directory) const root = rootFor(directory)
if (server.projects.list().find((x) => x.worktree === root)) return if (server.projects.list().find((x) => x.worktree === root)) return
void serverSync.project.loadSessions(root) void serverSync().project.loadSessions(root)
server.projects.open(root) server.projects.open(root)
}, },
close(directory: string) { close(directory: string) {
+11 -10
View File
@@ -64,12 +64,12 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const models = useModels() const models = useModels()
const id = createMemo(() => params.id || undefined) const id = createMemo(() => params.id || undefined)
const list = createMemo(() => sync.data.agent.filter((item) => item.mode !== "subagent" && !item.hidden)) const list = createMemo(() => sync().data.agent.filter((item) => item.mode !== "subagent" && !item.hidden))
const connected = createMemo(() => new Set(providers.connected().map((item) => item.id))) const connected = createMemo(() => new Set(providers.connected().map((item) => item.id)))
const [saved, setSaved] = persisted( const [saved, setSaved] = persisted(
{ {
...Persist.serverWorkspace(serverSDK.scope, sdk.directory, "model-selection", ["model-selection.v1"]), ...Persist.serverWorkspace(serverSDK().scope, sdk().directory, "model-selection", ["model-selection.v1"]),
migrate, migrate,
}, },
createStore<Saved>({ createStore<Saved>({
@@ -124,14 +124,14 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const scope = createMemo<State | undefined>(() => { const scope = createMemo<State | undefined>(() => {
const session = id() const session = id()
if (!session) return store.draft if (!session) return store.draft
return saved.session[session] ?? handoff.get(handoffKey(serverSDK.scope, sdk.directory, session)) return saved.session[session] ?? handoff.get(handoffKey(serverSDK().scope, sdk().directory, session))
}) })
createEffect(() => { createEffect(() => {
const session = id() const session = id()
if (!session) return if (!session) return
const key = handoffKey(serverSDK.scope, sdk.directory, session) const key = handoffKey(serverSDK().scope, sdk().directory, session)
const next = handoff.get(key) const next = handoff.get(key)
if (!next) return if (!next) return
if (saved.session[session] !== undefined) { if (saved.session[session] !== undefined) {
@@ -144,8 +144,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}) })
const configuredModel = () => { const configuredModel = () => {
if (!sync.data.config.model) return const configured = sync().data.config.model
const [providerID, modelID] = sync.data.config.model.split("/") if (!configured) return
const [providerID, modelID] = configured.split("/")
const model = { providerID, modelID } const model = { providerID, modelID }
if (validModel(model)) return model if (validModel(model)) return model
} }
@@ -363,7 +364,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
} }
const result = { const result = {
slug: createMemo(() => base64Encode(sdk.directory)), slug: createMemo(() => base64Encode(sdk().directory)),
model, model,
agent, agent,
session: { session: {
@@ -374,13 +375,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const next = clone(snapshot()) const next = clone(snapshot())
if (!next) return if (!next) return
if (dir === sdk.directory) { if (dir === sdk().directory) {
setSaved("session", session, next) setSaved("session", session, next)
setStore("draft", undefined) setStore("draft", undefined)
return return
} }
handoff.set(handoffKey(serverSDK.scope, dir, session), next) handoff.set(handoffKey(serverSDK().scope, dir, session), next)
setStore("draft", undefined) setStore("draft", undefined)
}, },
restore(msg: { sessionID: string; agent: string; model: ModelKey }) { restore(msg: { sessionID: string; agent: string; model: ModelKey }) {
@@ -388,7 +389,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
if (!session) return if (!session) return
if (msg.sessionID !== session) return if (msg.sessionID !== session) return
if (saved.session[session] !== undefined) return if (saved.session[session] !== undefined) return
if (handoff.has(handoffKey(serverSDK.scope, sdk.directory, session))) return if (handoff.has(handoffKey(serverSDK().scope, sdk().directory, session))) return
setSaved("session", session, { setSaved("session", session, {
agent: msg.agent, agent: msg.agent,
+1 -1
View File
@@ -8,7 +8,7 @@ export function useMcpToggle() {
const language = useLanguage() const language = useLanguage()
return useMutation(() => ({ return useMutation(() => ({
mutationFn: sync.mcp.toggle, mutationFn: sync().mcp.toggle,
onError: (error) => onError: (error) =>
showToast({ showToast({
variant: "error", variant: "error",
+4 -4
View File
@@ -125,7 +125,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
const currentSession = createMemo(() => params.id) const currentSession = createMemo(() => params.id)
const [store, setStore, _, ready] = persisted( const [store, setStore, _, ready] = persisted(
Persist.serverGlobal(serverSDK.scope, "notification", ["notification.v1"]), Persist.serverGlobal(serverSDK().scope, "notification", ["notification.v1"]),
createStore({ createStore({
list: [] as Notification[], list: [] as Notification[],
}), }),
@@ -208,10 +208,10 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
const lookup = async (directory: string, sessionID?: string) => { const lookup = async (directory: string, sessionID?: string) => {
if (!sessionID) return undefined if (!sessionID) return undefined
const [syncStore] = serverSync.child(directory, { bootstrap: false }) const [syncStore] = serverSync().child(directory, { bootstrap: false })
const match = Binary.search(syncStore.session, sessionID, (s) => s.id) const match = Binary.search(syncStore.session, sessionID, (s) => s.id)
if (match.found) return syncStore.session[match.index] if (match.found) return syncStore.session[match.index]
return serverSDK.client.session return serverSDK().client.session
.get({ directory, sessionID }) .get({ directory, sessionID })
.then((x) => x.data) .then((x) => x.data)
.catch(() => undefined) .catch(() => undefined)
@@ -286,7 +286,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
}) })
} }
const unsub = serverSDK.event.listen((e) => { const unsub = serverSDK().event.listen((e) => {
const event = e.details const event = e.details
if (event.type !== "session.idle" && event.type !== "session.error") return if (event.type !== "session.idle" && event.type !== "session.error") return
+10 -10
View File
@@ -55,13 +55,13 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
const permissionsEnabled = createMemo(() => { const permissionsEnabled = createMemo(() => {
const directory = decode64(params.dir) const directory = decode64(params.dir)
if (!directory) return false if (!directory) return false
const [store] = serverSync.child(directory) const [store] = serverSync().child(directory)
return hasPermissionPromptRules(store.config.permission) return hasPermissionPromptRules(store.config.permission)
}) })
const [store, setStore, _, ready] = persisted( const [store, setStore, _, ready] = persisted(
{ {
...Persist.serverGlobal(serverSDK.scope, "permission", ["permission.v3"]), ...Persist.serverGlobal(serverSDK().scope, "permission", ["permission.v3"]),
migrate(value) { migrate(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) return value if (!value || typeof value !== "object" || Array.isArray(value)) return value
@@ -87,7 +87,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
if (!ready()) return if (!ready()) return
const directory = decode64(params.dir) const directory = decode64(params.dir)
if (!directory) return if (!directory) return
const [childStore] = serverSync.child(directory) const [childStore] = serverSync().child(directory)
const perm = childStore.config.permission const perm = childStore.config.permission
if (typeof perm === "string" && perm === "allow") { if (typeof perm === "string" && perm === "allow") {
const key = directoryAcceptKey(directory) const key = directoryAcceptKey(directory)
@@ -119,7 +119,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
} }
const respond: PermissionRespondFn = (input) => { const respond: PermissionRespondFn = (input) => {
serverSDK.client.permission.respond(input).catch(() => { serverSDK().client.permission.respond(input).catch(() => {
responded.delete(input.permissionID) responded.delete(input.permissionID)
}) })
} }
@@ -140,7 +140,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
} }
function isAutoAccepting(sessionID: string, directory?: string) { function isAutoAccepting(sessionID: string, directory?: string) {
const session = directory ? serverSync.child(directory, { bootstrap: false })[0].session : [] const session = directory ? serverSync().child(directory, { bootstrap: false })[0].session : []
return autoRespondsPermission(store.autoAccept, session, { sessionID }, directory) return autoRespondsPermission(store.autoAccept, session, { sessionID }, directory)
} }
@@ -149,7 +149,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
} }
function shouldAutoRespond(permission: PermissionRequest, directory?: string) { function shouldAutoRespond(permission: PermissionRequest, directory?: string) {
const session = directory ? serverSync.child(directory, { bootstrap: false })[0].session : [] const session = directory ? serverSync().child(directory, { bootstrap: false })[0].session : []
return autoRespondsPermission(store.autoAccept, session, permission, directory) return autoRespondsPermission(store.autoAccept, session, permission, directory)
} }
@@ -160,7 +160,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
return next return next
} }
const unsubscribe = serverSDK.event.listen((e) => { const unsubscribe = serverSDK().event.listen((e) => {
const event = e.details const event = e.details
if (event?.type !== "permission.asked") return if (event?.type !== "permission.asked") return
@@ -179,7 +179,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
}), }),
) )
serverSDK.client.permission serverSDK().client.permission
.list({ directory }) .list({ directory })
.then((x) => { .then((x) => {
if (!isAutoAcceptingDirectory(directory)) return if (!isAutoAcceptingDirectory(directory)) return
@@ -211,7 +211,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
}), }),
) )
serverSDK.client.permission serverSDK().client.permission
.list({ directory }) .list({ directory })
.then((x) => { .then((x) => {
if (enableVersion.get(key) !== version) return if (enableVersion.get(key) !== version) return
@@ -269,7 +269,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
}, },
permissionsEnabled, permissionsEnabled,
isPermissionAllowAll(directory: string) { isPermissionAllowAll(directory: string) {
const [childStore] = serverSync.child(directory) const [childStore] = serverSync().child(directory)
const perm = childStore.config.permission const perm = childStore.config.permission
return typeof perm === "string" && perm === "allow" return typeof perm === "string" && perm === "allow"
}, },
+1 -1
View File
@@ -272,7 +272,7 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
const entry = createRoot( const entry = createRoot(
(dispose) => ({ (dispose) => ({
value: createPromptSession(serverSDK.scope, scope), value: createPromptSession(serverSDK().scope, scope),
dispose, dispose,
}), }),
owner, owner,
+10 -4
View File
@@ -1,11 +1,17 @@
import { createSimpleContext } from "@opencode-ai/ui/context" import { createSimpleContext } from "@opencode-ai/ui/context"
import { useServerSDK } from "./server-sdk" import { type Accessor, createMemo } from "solid-js"
import { type ServerSDK, useServerSDK } from "./server-sdk"
export type DirectorySDK = ReturnType<ServerSDK["createDirSdkContext"]>
export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
name: "SDK", name: "SDK",
init: (props: { directory: string }) => { // Resolves the directory-scoped SDK reactively from the (possibly changing) server.
init: (props: { directory: string | Accessor<string> }) => {
const serverSDK = useServerSDK() const serverSDK = useServerSDK()
return createMemo(() => {
return serverSDK.createDirSdkContext(props.directory) const directory = typeof props.directory === "function" ? props.directory() : props.directory
return serverSDK().createDirSdkContext(directory)
})
}, },
}) })
+21 -11
View File
@@ -2,7 +2,7 @@ import type { Event } from "@opencode-ai/sdk/v2/client"
import { createSimpleContext } from "@opencode-ai/ui/context" import { createSimpleContext } from "@opencode-ai/ui/context"
import { createGlobalEmitter } from "@solid-primitives/event-bus" import { createGlobalEmitter } from "@solid-primitives/event-bus"
import { makeEventListener } from "@solid-primitives/event-listener" import { makeEventListener } from "@solid-primitives/event-listener"
import { batch, onCleanup, onMount } from "solid-js" import { type Accessor, batch, createMemo, onCleanup, onMount } from "solid-js"
import { createSdkForServer } from "@/utils/server" import { createSdkForServer } from "@/utils/server"
import { useLanguage } from "./language" import { useLanguage } from "./language"
import { usePlatform } from "./platform" import { usePlatform } from "./platform"
@@ -21,7 +21,7 @@ export function resumeStreamAfterPageShow(event: PageTransitionEvent, start: ()
start() start()
} }
export function createServerSdkContext(server: ServerConnection.Any, scope: ServerScope) { function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerScope) {
const platform = usePlatform() const platform = usePlatform()
const abort = new AbortController() const abort = new AbortController()
@@ -261,21 +261,31 @@ export function createServerSdkContext(server: ServerConnection.Any, scope: Serv
} }
} }
export type ServerSDK = ReturnType<typeof createServerSdkContext> type ServerSDKBase = ReturnType<typeof createServerSdkContextBase>
export type ServerSDK = ServerSDKBase & {
createDirSdkContext: (directory: string) => ReturnType<typeof createDirSdkContext>
}
export function createServerSdkContext(server: ServerConnection.Any, scope: ServerScope): ServerSDK {
const sdk = createServerSdkContextBase(server, scope)
return Object.assign(sdk, {
createDirSdkContext: createRefCountMap((dir) => createDirSdkContext(dir, sdk)),
})
}
export const { use: useServerSDK, provider: ServerSDKProvider } = createSimpleContext({ export const { use: useServerSDK, provider: ServerSDKProvider } = createSimpleContext({
name: "ServerSDK", name: "ServerSDK",
init: (props: { server?: ServerConnection.Any }) => { // Returns an accessor so the resolved server can change reactively (e.g. a
// /new-session draft retargeting its server) without re-instantiating the subtree.
init: (props: { server?: Accessor<ServerConnection.Any | undefined> }) => {
const global = useGlobal() const global = useGlobal()
const language = useLanguage() const language = useLanguage()
const server = useServer() const server = useServer()
const conn = props.server ?? server.current return createMemo<ServerSDK>(() => {
if (!conn) throw new Error(language.t("error.serverSDK.noServerAvailable")) const conn = props.server?.() ?? server.current
if (!conn) throw new Error(language.t("error.serverSDK.noServerAvailable"))
const ctx = global.createServerCtx(conn) return global.createServerCtx(conn).sdk
return Object.assign(ctx.sdk, {
createDirSdkContext: createRefCountMap((dir) => createDirSdkContext(dir, ctx.sdk)),
}) })
}, },
}) })
@@ -284,7 +294,7 @@ type SDKEventMap = {
[key in Event["type"]]: Extract<Event, { type: key }> [key in Event["type"]]: Extract<Event, { type: key }>
} }
function createDirSdkContext(directory: string, serverSDK: ServerSDK) { function createDirSdkContext(directory: string, serverSDK: ServerSDKBase) {
const client = serverSDK.createClient({ const client = serverSDK.createClient({
directory, directory,
throwOnError: true, throwOnError: true,
+18 -15
View File
@@ -1,11 +1,11 @@
import type { Config, OpencodeClient, Path, Project, ProviderAuthResponse, Todo } from "@opencode-ai/sdk/v2/client" import type { Config, OpencodeClient, Path, Project, ProviderAuthResponse, Todo } from "@opencode-ai/sdk/v2/client"
import { showToast } from "@/utils/toast" import { showToast } from "@/utils/toast"
import { getFilename } from "@opencode-ai/core/util/path" import { getFilename } from "@opencode-ai/core/util/path"
import { batch, getOwner, onCleanup, onMount, untrack } from "solid-js" import { type Accessor, batch, createMemo, getOwner, onCleanup, onMount, untrack } from "solid-js"
import { createStore, produce, reconcile } from "solid-js/store" import { createStore, produce, reconcile } from "solid-js/store"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import type { InitError } from "../pages/error" import type { InitError } from "../pages/error"
import { ServerSDK, useServerSDK } from "./server-sdk" import { ServerSDK } from "./server-sdk"
import { import {
bootstrapDirectory, bootstrapDirectory,
bootstrapGlobal, bootstrapGlobal,
@@ -84,8 +84,7 @@ function makeQueryOptionsApi(
} }
export type QueryOptionsApi = ReturnType<typeof makeQueryOptionsApi> export type QueryOptionsApi = ReturnType<typeof makeQueryOptionsApi>
export function createServerSyncContextInner(_serverSDK?: ServerSDK) { export function createServerSyncContextInner(serverSDK: ServerSDK) {
const serverSDK: ServerSDK = _serverSDK ?? useServerSDK()
const language = useLanguage() const language = useLanguage()
const owner = getOwner() const owner = getOwner()
if (!owner) throw new Error("ServerSync must be created within owner") if (!owner) throw new Error("ServerSync must be created within owner")
@@ -507,33 +506,37 @@ export function createServerSyncContextInner(_serverSDK?: ServerSDK) {
} }
} }
export function createServerSyncContext(_serverSDK?: ServerSDK) { export function createServerSyncContext(serverSDK: ServerSDK) {
const inner = createServerSyncContextInner(_serverSDK) const inner = createServerSyncContextInner(serverSDK)
return Object.assign(inner, { return Object.assign(inner, {
createDirSyncContext: createRefCountMap( createDirSyncContext: createRefCountMap(
(dir) => createDirSyncContext(dir, inner, _serverSDK), (dir) => createDirSyncContext(dir, inner, serverSDK),
(dir) => inner.disableMcp(dir), (dir) => inner.disableMcp(dir),
directoryKey, directoryKey,
), ),
}) })
} }
export type ServerSync = ReturnType<typeof createServerSyncContext>
export const { use: useServerSync, provider: ServerSyncProvider } = createSimpleContext({ export const { use: useServerSync, provider: ServerSyncProvider } = createSimpleContext({
name: "ServerSync", name: "ServerSync",
gate: false, // Returns an accessor so the resolved server can change reactively without
init: (props: { server?: ServerConnection.Any }) => { // re-instantiating the subtree (mirrors useServerSDK).
init: (props: { server?: Accessor<ServerConnection.Any | undefined> }) => {
const global = useGlobal() const global = useGlobal()
const language = useLanguage() const language = useLanguage()
const server = useServer() const server = useServer()
const conn = props.server ?? server.current return createMemo<ServerSync>(() => {
if (!conn) throw new Error(language.t("error.serverSDK.noServerAvailable")) const conn = props.server?.() ?? server.current
const ctx = global.createServerCtx(conn) if (!conn) throw new Error(language.t("error.serverSDK.noServerAvailable"))
return global.createServerCtx(conn).sync
return ctx.sync })
}, },
}) })
export function useQueryOptions() { export function useQueryOptions() {
return useServerSync().queryOptions const sync = useServerSync()
return createMemo(() => sync().queryOptions)
} }
+4 -1
View File
@@ -1,4 +1,5 @@
import { Binary } from "@opencode-ai/core/util/binary" import { Binary } from "@opencode-ai/core/util/binary"
import { createMemo } from "solid-js"
import { useServerSync } from "./server-sync" import { useServerSync } from "./server-sync"
import { useSDK } from "./sdk" import { useSDK } from "./sdk"
import type { Message, Part } from "@opencode-ai/sdk/v2/client" import type { Message, Part } from "@opencode-ai/sdk/v2/client"
@@ -112,5 +113,7 @@ export const useSync = () => {
const serverSync = useServerSync() const serverSync = useServerSync()
const sdk = useSDK() const sdk = useSDK()
return serverSync.createDirSyncContext(sdk.directory) return createMemo(() => serverSync().createDirSyncContext(sdk().directory))
} }
export type DirectorySync = ReturnType<ReturnType<typeof useSync>>
+5 -5
View File
@@ -2,7 +2,7 @@ import { createStore, produce } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context" import { createSimpleContext } from "@opencode-ai/ui/context"
import { batch, createEffect, createMemo, createRoot, on, onCleanup } from "solid-js" import { batch, createEffect, createMemo, createRoot, on, onCleanup } from "solid-js"
import { useParams } from "@solidjs/router" import { useParams } from "@solidjs/router"
import { useSDK } from "./sdk" import { useSDK, type DirectorySDK } from "./sdk"
import type { Platform } from "./platform" import type { Platform } from "./platform"
import { useServer } from "./server" import { useServer } from "./server"
import { defaultTitle, titleNumber } from "./terminal-title" import { defaultTitle, titleNumber } from "./terminal-title"
@@ -143,7 +143,7 @@ export function clearWorkspaceTerminals(
} }
function createWorkspaceTerminalSession( function createWorkspaceTerminalSession(
sdk: ReturnType<typeof useSDK>, sdk: DirectorySDK,
dir: string, dir: string,
scope: ServerScopeValue, scope: ServerScopeValue,
legacySessionID?: string, legacySessionID?: string,
@@ -202,7 +202,7 @@ function createWorkspaceTerminalSession(
}) })
onCleanup(unsub) onCleanup(unsub)
const update = (client: ReturnType<typeof useSDK>["client"], pty: Partial<LocalPTY> & { id: string }) => { const update = (client: DirectorySDK["client"], pty: Partial<LocalPTY> & { id: string }) => {
const index = store.all.findIndex((x) => x.id === pty.id) const index = store.all.findIndex((x) => x.id === pty.id)
const previous = index >= 0 ? store.all[index] : undefined const previous = index >= 0 ? store.all[index] : undefined
if (index >= 0) { if (index >= 0) {
@@ -223,7 +223,7 @@ function createWorkspaceTerminalSession(
}) })
} }
const clone = async (client: ReturnType<typeof useSDK>["client"], id: string) => { const clone = async (client: DirectorySDK["client"], id: string) => {
const index = store.all.findIndex((x) => x.id === id) const index = store.all.findIndex((x) => x.id === id)
const pty = store.all[index] const pty = store.all[index]
if (!pty) return if (!pty) return
@@ -412,7 +412,7 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
} }
const entry = createRoot((dispose) => ({ const entry = createRoot((dispose) => ({
value: createWorkspaceTerminalSession(sdk, dir, serverScope, legacySessionID), value: createWorkspaceTerminalSession(sdk(), dir, serverScope, legacySessionID),
dispose, dispose,
})) }))
+2 -2
View File
@@ -22,10 +22,10 @@ export function useProviders() {
const dir = createMemo(() => decode64(params.dir) ?? "") const dir = createMemo(() => decode64(params.dir) ?? "")
const providers = () => { const providers = () => {
if (dir()) { if (dir()) {
const [projectStore] = serverSync.child(dir()) const [projectStore] = serverSync().child(dir())
if (projectStore.provider_ready) return projectStore.provider if (projectStore.provider_ready) return projectStore.provider
} }
return serverSync.data.provider return serverSync().data.provider
} }
return { return {
all: () => providers().all, all: () => providers().all,
+3 -3
View File
@@ -20,7 +20,7 @@ export function DirectoryDataProvider(props: ParentProps<{ directory: string; dr
createEffect(() => { createEffect(() => {
// A draft lives at /new-session?draftId=… and has no directory segment to normalize. // A draft lives at /new-session?draftId=… and has no directory segment to normalize.
if (props.draftID) return if (props.draftID) return
const next = sync.data.path.directory const next = sync().data.path.directory
if (!next || next === props.directory) return if (!next || next === props.directory) return
const path = location.pathname.slice(slug().length + 1) const path = location.pathname.slice(slug().length + 1)
navigate(`/${base64Encode(next)}${path}${location.search}${location.hash}`, { replace: true }) navigate(`/${base64Encode(next)}${path}${location.search}${location.hash}`, { replace: true })
@@ -28,12 +28,12 @@ export function DirectoryDataProvider(props: ParentProps<{ directory: string; dr
createResource( createResource(
() => params.id, () => params.id,
(id) => sync.session.sync(id).catch(() => {}), (id) => sync().session.sync(id).catch(() => {}),
) )
return ( return (
<DataProvider <DataProvider
data={sync.data} data={sync().data}
directory={props.directory} directory={props.directory}
onNavigateToSession={(sessionID: string) => navigate(`/${slug()}/session/${sessionID}`)} onNavigateToSession={(sessionID: string) => navigate(`/${slug()}/session/${sessionID}`)}
onSessionHref={(sessionID: string) => `/${slug()}/session/${sessionID}`} onSessionHref={(sessionID: string) => `/${slug()}/session/${sessionID}`}
+7 -7
View File
@@ -24,7 +24,7 @@ import { DialogSelectServer, useServerManagementController } from "@/components/
import { DialogServerV2 } from "@/components/settings-v2/dialog-server-v2" import { DialogServerV2 } from "@/components/settings-v2/dialog-server-v2"
import { ServerConnection, useServer } from "@/context/server" import { ServerConnection, useServer } from "@/context/server"
import { sessionHasOpenTab, useTabs } from "@/context/tabs" import { sessionHasOpenTab, useTabs } from "@/context/tabs"
import { useServerSync } from "@/context/server-sync" import { useServerSync, type ServerSync } from "@/context/server-sync"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { useNotification } from "@/context/notification" import { useNotification } from "@/context/notification"
import { import {
@@ -80,7 +80,7 @@ const HOME_SEARCH_RESULT_META =
let pendingHomeNavigation: { server: ServerConnection.Key; href: string } | undefined let pendingHomeNavigation: { server: ServerConnection.Key; href: string } | undefined
function buildHomeSessionRecords(input: { function buildHomeSessionRecords(input: {
sync: Pick<ReturnType<typeof useServerSync>, "child"> sync: Pick<ServerSync, "child">
projectDirectories: () => string[] projectDirectories: () => string[]
projects: () => LocalProject[] projects: () => LocalProject[]
projectByID: () => Map<string, LocalProject> projectByID: () => Map<string, LocalProject>
@@ -149,7 +149,7 @@ function HomeDesign() {
if (!conn) return if (!conn) return
return global.createServerCtx(conn) return global.createServerCtx(conn)
}) })
const focusedSync = () => focusedServerCtx()?.sync ?? sync const focusedSync = () => focusedServerCtx()?.sync ?? sync()
const projects = createMemo(() => focusedServerCtx()?.projects.list() ?? layout.projects.list()) const projects = createMemo(() => focusedServerCtx()?.projects.list() ?? layout.projects.list())
const selectedProject = createMemo(() => projects().find((project) => project.worktree === state.selection.directory)) const selectedProject = createMemo(() => projects().find((project) => project.worktree === state.selection.directory))
const newSessionProject = createMemo( const newSessionProject = createMemo(
@@ -1102,9 +1102,9 @@ function LegacyHome() {
const global = useGlobal() const global = useGlobal()
const server = useServer() const server = useServer()
const language = useLanguage() const language = useLanguage()
const homedir = createMemo(() => sync.data.path.home) const homedir = createMemo(() => sync().data.path.home)
const recent = createMemo(() => { const recent = createMemo(() => {
return sync.data.project return sync().data.project
.slice() .slice()
.sort((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created)) .sort((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created))
.slice(0, 5) .slice(0, 5)
@@ -1164,7 +1164,7 @@ function LegacyHome() {
{server.name} {server.name}
</Button> </Button>
<Switch> <Switch>
<Match when={sync.data.project.length > 0}> <Match when={sync().data.project.length > 0}>
<div class="mt-20 w-full flex flex-col gap-4"> <div class="mt-20 w-full flex flex-col gap-4">
<div class="flex gap-2 items-center justify-between pl-3"> <div class="flex gap-2 items-center justify-between pl-3">
<div class="text-14-medium text-text-strong">{language.t("home.recentProjects")}</div> <div class="text-14-medium text-text-strong">{language.t("home.recentProjects")}</div>
@@ -1191,7 +1191,7 @@ function LegacyHome() {
</ul> </ul>
</div> </div>
</Match> </Match>
<Match when={!sync.ready}> <Match when={!sync().ready}>
<div class="mt-30 mx-auto flex flex-col items-center gap-3"> <div class="mt-30 mx-auto flex flex-col items-center gap-3">
<div class="text-12-regular text-text-weak">{language.t("common.loading")}</div> <div class="text-12-regular text-text-weak">{language.t("common.loading")}</div>
<Button class="px-3" onClick={chooseProject}> <Button class="px-3" onClick={chooseProject}>
+52 -52
View File
@@ -95,7 +95,7 @@ import { SidebarContent } from "./layout/sidebar-shell"
export default function Layout(props: ParentProps) { export default function Layout(props: ParentProps) {
const serverSDK = useServerSDK() const serverSDK = useServerSDK()
const [store, setStore, , ready] = persisted( const [store, setStore, , ready] = persisted(
Persist.serverGlobal(serverSDK.scope, "layout.page", ["layout.page.v1"]), Persist.serverGlobal(serverSDK().scope, "layout.page", ["layout.page.v1"]),
createStore({ createStore({
lastProjectSession: {} as { [directory: string]: { directory: string; id: string; at: number } }, lastProjectSession: {} as { [directory: string]: { directory: string; id: string; at: number } },
activeProject: undefined as string | undefined, activeProject: undefined as string | undefined,
@@ -140,7 +140,7 @@ export default function Layout(props: ParentProps) {
if (!slug) return { slug, dir: "" } if (!slug) return { slug, dir: "" }
const dir = decode64(slug) const dir = decode64(slug)
if (!dir) return { slug, dir: "" } if (!dir) return { slug, dir: "" }
const store = serverSync.peek(dir, { bootstrap: false }) const store = serverSync().peek(dir, { bootstrap: false })
return { return {
slug, slug,
store, store,
@@ -213,7 +213,7 @@ export default function Layout(props: ParentProps) {
active: () => state.hoverProject, active: () => state.hoverProject,
el: () => state.nav?.querySelector<HTMLElement>("[data-component='sidebar-rail']") ?? state.nav, el: () => state.nav?.querySelector<HTMLElement>("[data-component='sidebar-rail']") ?? state.nav,
onActivate: (directory) => { onActivate: (directory) => {
serverSync.child(directory) serverSync().child(directory)
setState("hoverProject", directory) setState("hoverProject", directory)
}, },
}) })
@@ -397,17 +397,17 @@ export default function Layout(props: ParentProps) {
alertedAtBySession.delete(sessionKey) alertedAtBySession.delete(sessionKey)
} }
const unsub = serverSDK.event.listen((e) => { const unsub = serverSDK().event.listen((e) => {
if (e.details?.type === "worktree.ready") { if (e.details?.type === "worktree.ready") {
setBusy(e.name, false) setBusy(e.name, false)
WorktreeState.ready(serverSDK.scope, e.name) WorktreeState.ready(serverSDK().scope, e.name)
return return
} }
if (e.details?.type === "worktree.failed") { if (e.details?.type === "worktree.failed") {
setBusy(e.name, false) setBusy(e.name, false)
WorktreeState.failed( WorktreeState.failed(
serverSDK.scope, serverSDK().scope,
e.name, e.name,
e.details.properties?.message ?? language.t("common.requestFailed"), e.details.properties?.message ?? language.t("common.requestFailed"),
) )
@@ -435,7 +435,7 @@ export default function Layout(props: ParentProps) {
const props = e.details.properties const props = e.details.properties
if (e.details.type === "permission.asked" && permission.autoResponds(e.details.properties, directory)) return if (e.details.type === "permission.asked" && permission.autoResponds(e.details.properties, directory)) return
const [store] = serverSync.child(directory, { bootstrap: false }) const [store] = serverSync().child(directory, { bootstrap: false })
const session = store.session.find((s) => s.id === props.sessionID) const session = store.session.find((s) => s.id === props.sessionID)
const sessionKey = `${directory}:${props.sessionID}` const sessionKey = `${directory}:${props.sessionID}`
@@ -498,7 +498,7 @@ export default function Layout(props: ParentProps) {
if (!currentDir() || !currentSession) return if (!currentDir() || !currentSession) return
const sessionKey = `${currentDir()}:${currentSession}` const sessionKey = `${currentDir()}:${currentSession}`
dismissSessionAlert(sessionKey) dismissSessionAlert(sessionKey)
const [store] = serverSync.child(currentDir(), { bootstrap: false }) const [store] = serverSync().child(currentDir(), { bootstrap: false })
const childSessions = store.session.filter((s) => s.parentID === currentSession) const childSessions = store.session.filter((s) => s.parentID === currentSession)
for (const child of childSessions) { for (const child of childSessions) {
dismissSessionAlert(`${currentDir()}:${child.id}`) dismissSessionAlert(`${currentDir()}:${child.id}`)
@@ -536,11 +536,11 @@ export default function Layout(props: ParentProps) {
const direct = projects.find((p) => pathKey(p.worktree) === key) const direct = projects.find((p) => pathKey(p.worktree) === key)
if (direct) return direct if (direct) return direct
const [child] = serverSync.child(directory, { bootstrap: false }) const [child] = serverSync().child(directory, { bootstrap: false })
const id = child.project const id = child.project
if (!id) return if (!id) return
const meta = serverSync.data.project.find((p) => p.id === id) const meta = serverSync().data.project.find((p) => p.id === id)
const root = meta?.worktree const root = meta?.worktree
if (!root) return if (!root) return
@@ -631,7 +631,7 @@ export default function Layout(props: ParentProps) {
const result: Session[] = [] const result: Session[] = []
for (const dir of dirs) { for (const dir of dirs) {
const [dirStore] = serverSync.child(dir, { bootstrap: true }) const [dirStore] = serverSync().child(dir, { bootstrap: true })
const dirSessions = sortedRootSessions(dirStore, now) const dirSessions = sortedRootSessions(dirStore, now)
result.push(...dirSessions) result.push(...dirSessions)
} }
@@ -683,10 +683,10 @@ export default function Layout(props: ParentProps) {
createEffect(() => { createEffect(() => {
route() route()
serverSDK.url serverSDK().url
prefetchToken.value += 1 prefetchToken.value += 1
clearSessionPrefetchInflight(serverSDK.scope) clearSessionPrefetchInflight(serverSDK().scope)
prefetchQueues.clear() prefetchQueues.clear()
}) })
@@ -730,17 +730,17 @@ export default function Layout(props: ParentProps) {
} }
async function prefetchMessages(directory: string, sessionID: string, token: number) { async function prefetchMessages(directory: string, sessionID: string, token: number) {
const [store, setStore] = serverSync.child(directory, { bootstrap: false }) const [store, setStore] = serverSync().child(directory, { bootstrap: false })
return runSessionPrefetch({ return runSessionPrefetch({
scope: serverSDK.scope, scope: serverSDK().scope,
directory, directory,
sessionID, sessionID,
task: (rev) => task: (rev) =>
retry(() => serverSDK.client.session.messages({ directory, sessionID, limit: prefetchChunk })) retry(() => serverSDK().client.session.messages({ directory, sessionID, limit: prefetchChunk }))
.then((messages) => { .then((messages) => {
if (prefetchToken.value !== token) return if (prefetchToken.value !== token) return
if (!isSessionPrefetchCurrent(serverSDK.scope, directory, sessionID, rev)) return if (!isSessionPrefetchCurrent(serverSDK().scope, directory, sessionID, rev)) return
const items = (messages.data ?? []).filter((x) => !!x?.info?.id) const items = (messages.data ?? []).filter((x) => !!x?.info?.id)
const next = items.map((x) => x.info).filter((m): m is Message => !!m?.id) const next = items.map((x) => x.info).filter((m): m is Message => !!m?.id)
@@ -755,9 +755,9 @@ export default function Layout(props: ParentProps) {
} }
if (stale.length > 0) { if (stale.length > 0) {
clearSessionPrefetch(serverSDK.scope, directory, stale) clearSessionPrefetch(serverSDK().scope, directory, stale)
for (const id of stale) { for (const id of stale) {
serverSync.todo.set(id, undefined) serverSync().todo.set(id, undefined)
} }
} }
@@ -767,7 +767,7 @@ export default function Layout(props: ParentProps) {
sorted, sorted,
) )
if (!isSessionPrefetchCurrent(serverSDK.scope, directory, sessionID, rev)) return if (!isSessionPrefetchCurrent(serverSDK().scope, directory, sessionID, rev)) return
batch(() => { batch(() => {
if (stale.length > 0) { if (stale.length > 0) {
@@ -779,7 +779,7 @@ export default function Layout(props: ParentProps) {
} }
setStore("message", sessionID, reconcile(merged, { key: "id" })) setStore("message", sessionID, reconcile(merged, { key: "id" }))
setSessionPrefetch({ scope: serverSDK.scope, directory, sessionID, ...meta }) setSessionPrefetch({ scope: serverSDK().scope, directory, sessionID, ...meta })
for (const message of items) { for (const message of items) {
const currentParts = store.part[message.info.id] ?? [] const currentParts = store.part[message.info.id] ?? []
@@ -822,9 +822,9 @@ export default function Layout(props: ParentProps) {
const directory = session.directory const directory = session.directory
if (!directory) return if (!directory) return
const [store] = serverSync.child(directory, { bootstrap: false }) const [store] = serverSync().child(directory, { bootstrap: false })
const cached = untrack(() => { const cached = untrack(() => {
const info = getSessionPrefetch(serverSDK.scope, directory, session.id) const info = getSessionPrefetch(serverSDK().scope, directory, session.id)
return shouldSkipSessionPrefetch({ return shouldSkipSessionPrefetch({
message: store.message[session.id] !== undefined, message: store.message[session.id] !== undefined,
info, info,
@@ -927,7 +927,7 @@ export default function Layout(props: ParentProps) {
if (!target) return if (!target) return
// warm up child store to prevent flicker // warm up child store to prevent flicker
serverSync.child(target.worktree) serverSync().child(target.worktree)
void openProject(target.worktree) void openProject(target.worktree)
} }
@@ -936,7 +936,7 @@ export default function Layout(props: ParentProps) {
const target = projects[index] const target = projects[index]
if (!target) return if (!target) return
serverSync.child(target.worktree) serverSync().child(target.worktree)
void openProject(target.worktree) void openProject(target.worktree)
} }
@@ -965,12 +965,12 @@ export default function Layout(props: ParentProps) {
} }
async function archiveSession(session: Session) { async function archiveSession(session: Session) {
const [store, setStore] = serverSync.child(session.directory) const [store, setStore] = serverSync().child(session.directory)
const sessions = store.session ?? [] const sessions = store.session ?? []
const index = sessions.findIndex((s) => s.id === session.id) const index = sessions.findIndex((s) => s.id === session.id)
const nextSession = sessions[index + 1] ?? sessions[index - 1] const nextSession = sessions[index + 1] ?? sessions[index - 1]
await serverSDK.client.session.update({ await serverSDK().client.session.update({
directory: session.directory, directory: session.directory,
sessionID: session.id, sessionID: session.id,
time: { archived: Date.now() }, time: { archived: Date.now() },
@@ -1241,11 +1241,11 @@ export default function Layout(props: ParentProps) {
) )
if (known) return known[0] if (known) return known[0]
const [child] = serverSync.child(directory, { bootstrap: false }) const [child] = serverSync().child(directory, { bootstrap: false })
const id = child.project const id = child.project
if (!id) return directory if (!id) return directory
const meta = serverSync.data.project.find((item) => item.id === id) const meta = serverSync().data.project.find((item) => item.id === id)
return meta?.worktree ?? directory return meta?.worktree ?? directory
} }
@@ -1293,7 +1293,7 @@ export default function Layout(props: ParentProps) {
} }
const refreshDirs = async (target?: string) => { const refreshDirs = async (target?: string) => {
if (!target || target === root || canOpen(target)) return canOpen(target) if (!target || target === root || canOpen(target)) return canOpen(target)
const listed = await serverSDK.client.worktree const listed = await serverSDK().client.worktree
.list({ directory: root }) .list({ directory: root })
.then((x) => x.data ?? []) .then((x) => x.data ?? [])
.catch(() => [] as string[]) .catch(() => [] as string[])
@@ -1302,13 +1302,13 @@ export default function Layout(props: ParentProps) {
} }
const openSession = async (target: { directory: string; id: string }) => { const openSession = async (target: { directory: string; id: string }) => {
if (!canOpen(target.directory)) return false if (!canOpen(target.directory)) return false
const [data] = serverSync.child(target.directory, { bootstrap: false }) const [data] = serverSync().child(target.directory, { bootstrap: false })
if (data.session.some((item) => item.id === target.id)) { if (data.session.some((item) => item.id === target.id)) {
setStore("lastProjectSession", root, { directory: target.directory, id: target.id, at: Date.now() }) setStore("lastProjectSession", root, { directory: target.directory, id: target.id, at: Date.now() })
navigateWithSidebarReset(`/${base64Encode(target.directory)}/session/${target.id}`) navigateWithSidebarReset(`/${base64Encode(target.directory)}/session/${target.id}`)
return true return true
} }
const resolved = await serverSDK.client.session const resolved = await serverSDK().client.session
.get({ sessionID: target.id }) .get({ sessionID: target.id })
.then((x) => x.data) .then((x) => x.data)
.catch(() => undefined) .catch(() => undefined)
@@ -1328,7 +1328,7 @@ export default function Layout(props: ParentProps) {
} }
const latest = latestRootSession( const latest = latestRootSession(
dirs.map((item) => serverSync.child(item, { bootstrap: false })[0]), dirs.map((item) => serverSync().child(item, { bootstrap: false })[0]),
Date.now(), Date.now(),
) )
if (latest && (await openSession(latest))) { if (latest && (await openSession(latest))) {
@@ -1339,7 +1339,7 @@ export default function Layout(props: ParentProps) {
await Promise.all( await Promise.all(
dirs.map(async (item) => ({ dirs.map(async (item) => ({
path: { directory: item }, path: { directory: item },
session: await serverSDK.client.session session: await serverSDK().client.session
.list({ directory: item }) .list({ directory: item })
.then((x) => x.data ?? []) .then((x) => x.data ?? [])
.catch(() => []), .catch(() => []),
@@ -1402,11 +1402,11 @@ export default function Layout(props: ParentProps) {
const name = next === getFilename(project.worktree) ? "" : next const name = next === getFilename(project.worktree) ? "" : next
if (project.id && project.id !== "global") { if (project.id && project.id !== "global") {
await serverSDK.client.project.update({ projectID: project.id, directory: project.worktree, name }) await serverSDK().client.project.update({ projectID: project.id, directory: project.worktree, name })
return return
} }
serverSync.project.meta(project.worktree, { name }) serverSync().project.meta(project.worktree, { name })
} }
const renameWorkspace = (directory: string, next: string, projectId?: string, branch?: string) => { const renameWorkspace = (directory: string, next: string, projectId?: string, branch?: string) => {
@@ -1495,7 +1495,7 @@ export default function Layout(props: ParentProps) {
setBusy(directory, true) setBusy(directory, true)
const result = await serverSDK.client.worktree const result = await serverSDK().client.worktree
.remove({ directory: root, worktreeRemoveInput: { directory } }) .remove({ directory: root, worktreeRemoveInput: { directory } })
.then((x) => x.data) .then((x) => x.data)
.catch((err) => { .catch((err) => {
@@ -1514,7 +1514,7 @@ export default function Layout(props: ParentProps) {
clearLastProjectSession(root) clearLastProjectSession(root)
} }
serverSync.set( serverSync().set(
"project", "project",
produce((draft) => { produce((draft) => {
const project = draft.find((item) => item.worktree === root) const project = draft.find((item) => item.worktree === root)
@@ -1553,7 +1553,7 @@ export default function Layout(props: ParentProps) {
}) })
const dismiss = () => toaster.dismiss(progress) const dismiss = () => toaster.dismiss(progress)
const sessions: Session[] = await serverSDK.client.session const sessions: Session[] = await serverSDK().client.session
.list({ directory }) .list({ directory })
.then((x) => x.data ?? []) .then((x) => x.data ?? [])
.catch(() => []) .catch(() => [])
@@ -1562,11 +1562,11 @@ export default function Layout(props: ParentProps) {
directory, directory,
sessions.map((s) => s.id), sessions.map((s) => s.id),
platform, platform,
serverSDK.scope, serverSDK().scope,
) )
await serverSDK.client.instance.dispose({ directory }).catch(() => undefined) await serverSDK().client.instance.dispose({ directory }).catch(() => undefined)
const result = await serverSDK.client.worktree const result = await serverSDK().client.worktree
.reset({ directory: root, worktreeResetInput: { directory } }) .reset({ directory: root, worktreeResetInput: { directory } })
.then((x) => x.data) .then((x) => x.data)
.catch((err) => { .catch((err) => {
@@ -1588,7 +1588,7 @@ export default function Layout(props: ParentProps) {
sessions sessions
.filter((session) => session.time.archived === undefined) .filter((session) => session.time.archived === undefined)
.map((session) => .map((session) =>
serverSDK.client.session serverSDK().client.session
.update({ .update({
sessionID: session.id, sessionID: session.id,
directory: session.directory, directory: session.directory,
@@ -1629,7 +1629,7 @@ export default function Layout(props: ParentProps) {
}) })
onMount(() => { onMount(() => {
serverSDK.client.vcs serverSDK().client.vcs
.status({ directory: props.directory }) .status({ directory: props.directory })
.then((x) => { .then((x) => {
const files = x.data ?? [] const files = x.data ?? []
@@ -1688,7 +1688,7 @@ export default function Layout(props: ParentProps) {
}) })
const refresh = async () => { const refresh = async () => {
const sessions = await serverSDK.client.session const sessions = await serverSDK().client.session
.list({ directory: props.directory }) .list({ directory: props.directory })
.then((x) => x.data ?? []) .then((x) => x.data ?? [])
.catch(() => []) .catch(() => [])
@@ -1697,7 +1697,7 @@ export default function Layout(props: ParentProps) {
} }
onMount(() => { onMount(() => {
serverSDK.client.vcs serverSDK().client.vcs
.status({ directory: props.directory }) .status({ directory: props.directory })
.then((x) => { .then((x) => {
const files = x.data ?? [] const files = x.data ?? []
@@ -1831,7 +1831,7 @@ export default function Layout(props: ParentProps) {
const next = new Set(dirs) const next = new Set(dirs)
for (const directory of next) { for (const directory of next) {
if (loadedSessionDirs.has(directory)) continue if (loadedSessionDirs.has(directory)) continue
void serverSync.project.loadSessions(directory) void serverSync().project.loadSessions(directory)
} }
loadedSessionDirs.clear() loadedSessionDirs.clear()
@@ -1876,7 +1876,7 @@ export default function Layout(props: ParentProps) {
directory && pathKey(directory) !== pathKey(local) && !dirs.some((item) => pathKey(item) === pathKey(directory)) directory && pathKey(directory) !== pathKey(local) && !dirs.some((item) => pathKey(item) === pathKey(directory))
? directory ? directory
: undefined : undefined
const pending = extra ? WorktreeState.get(serverSDK.scope, extra)?.status === "pending" : false const pending = extra ? WorktreeState.get(serverSDK().scope, extra)?.status === "pending" : false
const ordered = effectiveWorkspaceOrder(local, dirs, store.workspaceOrder[project.worktree]) const ordered = effectiveWorkspaceOrder(local, dirs, store.workspaceOrder[project.worktree])
if (pending && extra) return [local, extra, ...ordered.filter((item) => item !== local)] if (pending && extra) return [local, extra, ...ordered.filter((item) => item !== local)]
@@ -1928,7 +1928,7 @@ export default function Layout(props: ParentProps) {
const createWorkspace = async (project: LocalProject) => { const createWorkspace = async (project: LocalProject) => {
clearSidebarHoverState() clearSidebarHoverState()
const created = await serverSDK.client.worktree const created = await serverSDK().client.worktree
.create({ directory: project.worktree }) .create({ directory: project.worktree })
.then((x) => x.data) .then((x) => x.data)
.catch((err) => { .catch((err) => {
@@ -1948,7 +1948,7 @@ export default function Layout(props: ParentProps) {
const root = pathKey(local) const root = pathKey(local)
setBusy(created.directory, true) setBusy(created.directory, true)
WorktreeState.pending(serverSDK.scope, created.directory) WorktreeState.pending(serverSDK().scope, created.directory)
setStore("workspaceExpanded", key, true) setStore("workspaceExpanded", key, true)
if (key !== created.directory) { if (key !== created.directory) {
setStore("workspaceExpanded", created.directory, true) setStore("workspaceExpanded", created.directory, true)
@@ -1962,7 +1962,7 @@ export default function Layout(props: ParentProps) {
return [created.directory, ...next] return [created.directory, ...next]
}) })
serverSync.child(created.directory) serverSync().child(created.directory)
navigateWithSidebarReset(`/${base64Encode(created.directory)}/session`) navigateWithSidebarReset(`/${base64Encode(created.directory)}/session`)
} }
@@ -2067,7 +2067,7 @@ export default function Layout(props: ParentProps) {
if (!item) return false if (!item) return false
return item.vcs === "git" || layout.sidebar.workspaces(item.worktree)() return item.vcs === "git" || layout.sidebar.workspaces(item.worktree)()
}) })
const homedir = createMemo(() => serverSync.data.path.home) const homedir = createMemo(() => serverSync().data.path.home)
return ( return (
<div <div
@@ -14,7 +14,7 @@ export function useSessionTabAvatarState(
const permission = usePermission() const permission = usePermission()
const hasPermissions = createMemo(() => { const hasPermissions = createMemo(() => {
if (!active()) return false if (!active()) return false
const [store] = globalSync.child(directory(), { bootstrap: false }) const [store] = globalSync().child(directory(), { bootstrap: false })
return !!sessionPermissionRequest(store.session, store.permission, sessionId(), (item) => { return !!sessionPermissionRequest(store.session, store.permission, sessionId(), (item) => {
return !permission.autoResponds(item, directory()) return !permission.autoResponds(item, directory())
}) })
@@ -23,7 +23,7 @@ export function useSessionTabAvatarState(
const loading = createMemo(() => { const loading = createMemo(() => {
if (!active()) return false if (!active()) return false
if (hasPermissions()) return false if (hasPermissions()) return false
const [store] = globalSync.child(directory(), { bootstrap: false }) const [store] = globalSync().child(directory(), { bootstrap: false })
return store.session_working(sessionId()) return store.session_working(sessionId())
}) })
return { unread, loading } return { unread, loading }
@@ -33,7 +33,7 @@ export const ProjectIcon = (props: {
const hasError = createMemo(() => dirs().some((directory) => notification.project.unseenHasError(directory))) const hasError = createMemo(() => dirs().some((directory) => notification.project.unseenHasError(directory)))
const hasPermissions = createMemo(() => const hasPermissions = createMemo(() =>
dirs().some((directory) => { dirs().some((directory) => {
const [store] = serverSync.child(directory, { bootstrap: false }) const [store] = serverSync().child(directory, { bootstrap: false })
return hasProjectPermissions(store.permission, (item) => !permission.autoResponds(item, directory)) return hasProjectPermissions(store.permission, (item) => !permission.autoResponds(item, directory))
}), }),
) )
@@ -149,7 +149,7 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
const serverSync = useServerSync() const serverSync = useServerSync()
const unseenCount = createMemo(() => notification.session.unseenCount(props.session.id)) const unseenCount = createMemo(() => notification.session.unseenCount(props.session.id))
const hasError = createMemo(() => notification.session.unseenHasError(props.session.id)) const hasError = createMemo(() => notification.session.unseenHasError(props.session.id))
const [sessionStore] = serverSync.child(props.session.directory) const [sessionStore] = serverSync().child(props.session.directory)
const hasPermissions = createMemo(() => { const hasPermissions = createMemo(() => {
return !!sessionPermissionRequest(sessionStore.session, sessionStore.permission, props.session.id, (item) => { return !!sessionPermissionRequest(sessionStore.session, sessionStore.permission, props.session.id, (item) => {
return !permission.autoResponds(item, props.session.directory) return !permission.autoResponds(item, props.session.directory)
@@ -294,23 +294,23 @@ export const SortableProject = (props: {
const hoverOpen = () => isHoverProject() && preview() && !selected() && !state.menu const hoverOpen = () => isHoverProject() && preview() && !selected() && !state.menu
const label = (directory: string) => { const label = (directory: string) => {
const [data] = serverSync.child(directory, { bootstrap: false }) const [data] = serverSync().child(directory, { bootstrap: false })
const kind = const kind =
directory === props.project.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox") directory === props.project.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox")
const name = props.ctx.workspaceLabel(directory, data.vcs?.branch, props.project.id) const name = props.ctx.workspaceLabel(directory, data.vcs?.branch, props.project.id)
return `${kind} : ${name}` return `${kind} : ${name}`
} }
const projectStore = createMemo(() => serverSync.child(props.project.worktree, { bootstrap: false })[0]) const projectStore = createMemo(() => serverSync().child(props.project.worktree, { bootstrap: false })[0])
const isWorking = createMemo(() => const isWorking = createMemo(() =>
dirs().some((directory) => { dirs().some((directory) => {
const [store] = serverSync.child(directory, { bootstrap: false }) const [store] = serverSync().child(directory, { bootstrap: false })
return Object.keys(store.session_status).some((id) => store.session_working(id)) return Object.keys(store.session_status).some((id) => store.session_working(id))
}), }),
) )
const projectSessions = createMemo(() => sortedRootSessions(projectStore(), props.sortNow())) const projectSessions = createMemo(() => sortedRootSessions(projectStore(), props.sortNow()))
const workspaceSessions = (directory: string) => { const workspaceSessions = (directory: string) => {
const [data] = serverSync.child(directory, { bootstrap: false }) const [data] = serverSync().child(directory, { bootstrap: false })
return sortedRootSessions(data, props.sortNow()) return sortedRootSessions(data, props.sortNow())
} }
const tile = () => ( const tile = () => (
@@ -68,7 +68,7 @@ export const WorkspaceDragOverlay = (props: {
const directory = props.activeWorkspace() const directory = props.activeWorkspace()
if (!directory) return if (!directory) return
const [workspaceStore] = serverSync.child(directory, { bootstrap: false }) const [workspaceStore] = serverSync().child(directory, { bootstrap: false })
const kind = const kind =
directory === project.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox") directory === project.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox")
const name = props.workspaceLabel(directory, workspaceStore.vcs?.branch, project.id) const name = props.workspaceLabel(directory, workspaceStore.vcs?.branch, project.id)
@@ -303,7 +303,7 @@ export const SortableWorkspace = (props: {
const queryOptions = useQueryOptions() const queryOptions = useQueryOptions()
const language = useLanguage() const language = useLanguage()
const sortable = createSortable(props.directory) const sortable = createSortable(props.directory)
const [workspaceStore, setWorkspaceStore] = serverSync.child(props.directory, { bootstrap: false }) const [workspaceStore, setWorkspaceStore] = serverSync().child(props.directory, { bootstrap: false })
const [menu, setMenu] = createStore({ const [menu, setMenu] = createStore({
open: false, open: false,
pendingRename: false, pendingRename: false,
@@ -321,14 +321,14 @@ export const SortableWorkspace = (props: {
const boot = createMemo(() => open() || active()) const boot = createMemo(() => open() || active())
const count = createMemo(() => sessions()?.length ?? 0) const count = createMemo(() => sessions()?.length ?? 0)
const hasMore = createMemo(() => workspaceStore.sessionTotal > count()) const hasMore = createMemo(() => workspaceStore.sessionTotal > count())
const fetching = useIsFetching(() => queryOptions.sessions(pathKey(props.directory))) const fetching = useIsFetching(() => queryOptions().sessions(pathKey(props.directory)))
const busy = createMemo(() => props.ctx.isBusy(props.directory)) const busy = createMemo(() => props.ctx.isBusy(props.directory))
const loading = () => fetching() > 0 && count() === 0 const loading = () => fetching() > 0 && count() === 0
const touch = createMediaQuery("(hover: none)") const touch = createMediaQuery("(hover: none)")
const showNew = createMemo(() => !loading() && (touch() || count() === 0 || (active() && !params.id))) const showNew = createMemo(() => !loading() && (touch() || count() === 0 || (active() && !params.id)))
const loadMore = async () => { const loadMore = async () => {
setWorkspaceStore("limit", (limit) => (limit ?? 0) + 5) setWorkspaceStore("limit", (limit) => (limit ?? 0) + 5)
await serverSync.project.loadSessions(props.directory) await serverSync().project.loadSessions(props.directory)
} }
const workspaceEditActive = createMemo(() => props.ctx.editorOpen(`workspace:${props.directory}`)) const workspaceEditActive = createMemo(() => props.ctx.editorOpen(`workspace:${props.directory}`))
@@ -357,7 +357,7 @@ export const SortableWorkspace = (props: {
createEffect(() => { createEffect(() => {
if (!boot()) return if (!boot()) return
serverSync.child(props.directory, { bootstrap: true }) serverSync().child(props.directory, { bootstrap: true })
}) })
return ( return (
@@ -450,18 +450,18 @@ export const LocalWorkspace = (props: {
const queryOptions = useQueryOptions() const queryOptions = useQueryOptions()
const language = useLanguage() const language = useLanguage()
const workspace = createMemo(() => { const workspace = createMemo(() => {
const [store, setStore] = serverSync.child(props.project.worktree) const [store, setStore] = serverSync().child(props.project.worktree)
return { store, setStore } return { store, setStore }
}) })
const slug = createMemo(() => base64Encode(props.project.worktree)) const slug = createMemo(() => base64Encode(props.project.worktree))
const sessions = createMemo(() => sortedRootSessions(workspace().store, props.sortNow())) const sessions = createMemo(() => sortedRootSessions(workspace().store, props.sortNow()))
const count = createMemo(() => sessions()?.length ?? 0) const count = createMemo(() => sessions()?.length ?? 0)
const fetching = useIsFetching(() => queryOptions.sessions(pathKey(props.project.worktree))) const fetching = useIsFetching(() => queryOptions().sessions(pathKey(props.project.worktree)))
const hasMore = createMemo(() => workspace().store.sessionTotal > count()) const hasMore = createMemo(() => workspace().store.sessionTotal > count())
const loading = () => fetching() > 0 && count() === 0 const loading = () => fetching() > 0 && count() === 0
const loadMore = async () => { const loadMore = async () => {
workspace().setStore("limit", (limit) => (limit ?? 0) + 5) workspace().setStore("limit", (limit) => (limit ?? 0) + 5)
await serverSync.project.loadSessions(props.project.worktree) await serverSync().project.loadSessions(props.project.worktree)
} }
return ( return (
+2 -2
View File
@@ -30,8 +30,8 @@ export default function NewSessionPage() {
const newSessionWorktree = createMemo(() => { const newSessionWorktree = createMemo(() => {
if (store.worktree === "create") return "create" if (store.worktree === "create") return "create"
const project = sync.project const project = sync().project
if (project && sdk.directory !== project.worktree) return sdk.directory if (project && sdk().directory !== project.worktree) return sdk().directory
return "main" return "main"
}) })
+63 -63
View File
@@ -308,10 +308,10 @@ export default function Page() {
if (!view().reviewPanel.opened()) view().reviewPanel.open() if (!view().reviewPanel.opened()) view().reviewPanel.open()
} }
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined)) const info = createMemo(() => (params.id ? sync().session.get(params.id) : undefined))
const isChildSession = createMemo(() => !!info()?.parentID) const isChildSession = createMemo(() => !!info()?.parentID)
const diffs = createMemo(() => (params.id ? list(sync.data.session_diff[params.id]) : [])) const diffs = createMemo(() => (params.id ? list(sync().data.session_diff[params.id]) : []))
const canReview = createMemo(() => !!sync.project) const canReview = createMemo(() => !!sync().project)
const reviewTab = createMemo(() => isDesktop()) const reviewTab = createMemo(() => isDesktop())
const tabState = createSessionTabs({ const tabState = createSessionTabs({
tabs, tabs,
@@ -323,21 +323,21 @@ export default function Page() {
const activeTab = tabState.activeTab const activeTab = tabState.activeTab
const activeFileTab = tabState.activeFileTab const activeFileTab = tabState.activeFileTab
const revertMessageID = createMemo(() => info()?.revert?.messageID) const revertMessageID = createMemo(() => info()?.revert?.messageID)
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : [])) const messages = createMemo(() => (params.id ? (sync().data.message[params.id] ?? []) : []))
const messagesReady = createMemo(() => { const messagesReady = createMemo(() => {
const id = params.id const id = params.id
if (!id) return true if (!id) return true
return sync.data.message[id] !== undefined return sync().data.message[id] !== undefined
}) })
const historyMore = createMemo(() => { const historyMore = createMemo(() => {
const id = params.id const id = params.id
if (!id) return false if (!id) return false
return sync.session.history.more(id) return sync().session.history.more(id)
}) })
const historyLoading = createMemo(() => { const historyLoading = createMemo(() => {
const id = params.id const id = params.id
if (!id) return false if (!id) return false
return sync.session.history.loading(id) return sync().session.history.loading(id)
}) })
const userMessages = createMemo( const userMessages = createMemo(
() => messages().filter((m) => m.role === "user") as UserMessage[], () => messages().filter((m) => m.role === "user") as UserMessage[],
@@ -397,7 +397,7 @@ export default function Page() {
}) })
const [followup, setFollowup] = persisted( const [followup, setFollowup] = persisted(
Persist.serverWorkspace(serverSDK.scope, sdk.directory, "followup", ["followup.v1"]), Persist.serverWorkspace(serverSDK().scope, sdk().directory, "followup", ["followup.v1"]),
createStore<{ createStore<{
items: Record<string, FollowupItem[] | undefined> items: Record<string, FollowupItem[] | undefined>
failed: Record<string, string | undefined> failed: Record<string, string | undefined>
@@ -444,16 +444,16 @@ export default function Page() {
}, desktopReviewOpen()) }, desktopReviewOpen())
const turnDiffs = createMemo(() => list(lastUserMessage()?.summary?.diffs)) const turnDiffs = createMemo(() => list(lastUserMessage()?.summary?.diffs))
const nogit = createMemo(() => !!sync.project && sync.project.vcs !== "git") const nogit = createMemo(() => {
const project = sync().project
return !!project && project.vcs !== "git"
})
const changesOptions = createMemo<ChangeMode[]>(() => { const changesOptions = createMemo<ChangeMode[]>(() => {
const list: ChangeMode[] = [] const list: ChangeMode[] = []
if (sync.project?.vcs === "git") list.push("git") const project = sync().project
if ( const vcs = sync().data.vcs
sync.project?.vcs === "git" && if (project?.vcs === "git") list.push("git")
sync.data.vcs?.branch && if (project?.vcs === "git" && vcs?.branch && vcs?.default_branch && vcs.branch !== vcs.default_branch) {
sync.data.vcs?.default_branch &&
sync.data.vcs.branch !== sync.data.vcs.default_branch
) {
list.push("branch") list.push("branch")
} }
list.push("turn") list.push("turn")
@@ -469,18 +469,18 @@ export default function Page() {
if (store.changes === "git" || store.changes === "branch") return store.changes if (store.changes === "git" || store.changes === "branch") return store.changes
}) })
const vcsKey = createMemo( const vcsKey = createMemo(
() => ["session-vcs", sdk.directory, sync.data.vcs?.branch ?? "", sync.data.vcs?.default_branch ?? ""] as const, () => ["session-vcs", sdk().directory, sync().data.vcs?.branch ?? "", sync().data.vcs?.default_branch ?? ""] as const,
) )
const vcsQuery = createQuery(() => { const vcsQuery = createQuery(() => {
const mode = vcsMode() const mode = vcsMode()
const enabled = wantsReview() && sync.project?.vcs === "git" const enabled = wantsReview() && sync().project?.vcs === "git"
return { return {
queryKey: [...vcsKey(), mode] as const, queryKey: [...vcsKey(), mode] as const,
enabled, enabled,
queryFn: mode queryFn: mode
? () => ? () =>
sdk.client.vcs sdk().client.vcs
.diff({ mode }) .diff({ mode })
.then((result) => list(result.data)) .then((result) => list(result.data))
.catch((error) => { .catch((error) => {
@@ -506,8 +506,8 @@ export default function Page() {
const newSessionWorktree = createMemo(() => { const newSessionWorktree = createMemo(() => {
if (store.newSessionWorktree === "create") return "create" if (store.newSessionWorktree === "create") return "create"
const project = sync.project const project = sync().project
if (project && sdk.directory !== project.worktree) return sdk.directory if (project && sdk().directory !== project.worktree) return sdk().directory
return "main" return "main"
}) })
@@ -569,11 +569,11 @@ export default function Page() {
} }
function upsert(next: Project) { function upsert(next: Project) {
const list = serverSync.data.project const list = serverSync().data.project
sync.set("project", next.id) sync().set("project", next.id)
const idx = list.findIndex((item) => item.id === next.id) const idx = list.findIndex((item) => item.id === next.id)
if (idx >= 0) { if (idx >= 0) {
serverSync.set( serverSync().set(
"project", "project",
list.map((item, i) => (i === idx ? { ...item, ...next } : item)), list.map((item, i) => (i === idx ? { ...item, ...next } : item)),
) )
@@ -581,14 +581,14 @@ export default function Page() {
} }
const at = list.findIndex((item) => item.id > next.id) const at = list.findIndex((item) => item.id > next.id)
if (at >= 0) { if (at >= 0) {
serverSync.set("project", [...list.slice(0, at), next, ...list.slice(at)]) serverSync().set("project", [...list.slice(0, at), next, ...list.slice(at)])
return return
} }
serverSync.set("project", [...list, next]) serverSync().set("project", [...list, next])
} }
const gitMutation = useMutation(() => ({ const gitMutation = useMutation(() => ({
mutationFn: () => sdk.client.project.initGit(), mutationFn: () => sdk().client.project.initGit(),
onSuccess: (x) => { onSuccess: (x) => {
if (!x.data) return if (!x.data) return
upsert(x.data) upsert(x.data)
@@ -632,7 +632,7 @@ export default function Page() {
const hasScrollGesture = () => Date.now() - ui.scrollGesture < scrollGestureWindowMs const hasScrollGesture = () => Date.now() - ui.scrollGesture < scrollGestureWindowMs
const [sessionSync] = createResource( const [sessionSync] = createResource(
() => [sdk.directory, params.id] as const, () => [sdk().directory, params.id] as const,
([directory, id]) => { ([directory, id]) => {
if (refreshFrame !== undefined) cancelAnimationFrame(refreshFrame) if (refreshFrame !== undefined) cancelAnimationFrame(refreshFrame)
if (refreshTimer !== undefined) window.clearTimeout(refreshTimer) if (refreshTimer !== undefined) window.clearTimeout(refreshTimer)
@@ -640,11 +640,11 @@ export default function Page() {
refreshTimer = undefined refreshTimer = undefined
if (!id) return if (!id) return
const cached = untrack(() => sync.data.message[id] !== undefined) const cached = untrack(() => sync().data.message[id] !== undefined)
const stale = !cached const stale = !cached
? false ? false
: (() => { : (() => {
const info = getSessionPrefetch(serverSDK.scope, directory, id) const info = getSessionPrefetch(serverSDK().scope, directory, id)
if (!info) return true if (!info) return true
return Date.now() - info.at > SESSION_PREFETCH_TTL return Date.now() - info.at > SESSION_PREFETCH_TTL
})() })()
@@ -655,12 +655,12 @@ export default function Page() {
refreshTimer = undefined refreshTimer = undefined
if (params.id !== id) return if (params.id !== id) return
untrack(() => { untrack(() => {
if (stale) void sync.session.sync(id, { force: true }) if (stale) void sync().session.sync(id, { force: true })
}) })
}, 0) }, 0)
}) })
return sync.session.sync(id) return sync().session.sync(id)
}, },
) )
@@ -669,9 +669,9 @@ export default function Page() {
() => { () => {
const id = params.id const id = params.id
return [ return [
sdk.directory, sdk().directory,
id, id,
id ? (sync.data.session_status[id]?.type ?? "idle") : "idle", id ? (sync().data.session_status[id]?.type ?? "idle") : "idle",
id ? composer.blocked() : false, id ? composer.blocked() : false,
] as const ] as const
}, },
@@ -682,15 +682,15 @@ export default function Page() {
todoTimer = undefined todoTimer = undefined
if (!id) return if (!id) return
if (status === "idle" && !blocked) return if (status === "idle" && !blocked) return
const cached = untrack(() => sync.data.todo[id] !== undefined || serverSync.data.session_todo[id] !== undefined) const cached = untrack(() => sync().data.todo[id] !== undefined || serverSync().data.session_todo[id] !== undefined)
todoFrame = requestAnimationFrame(() => { todoFrame = requestAnimationFrame(() => {
todoFrame = undefined todoFrame = undefined
todoTimer = window.setTimeout(() => { todoTimer = window.setTimeout(() => {
todoTimer = undefined todoTimer = undefined
if (sdk.directory !== dir || params.id !== id) return if (sdk().directory !== dir || params.id !== id) return
untrack(() => { untrack(() => {
void sync.session.todo(id, cached ? { force: true } : undefined) void sync().session.todo(id, cached ? { force: true } : undefined)
}) })
}, 0) }, 0)
}) })
@@ -723,7 +723,7 @@ export default function Page() {
), ),
) )
const stopVcs = sdk.event.listen((evt) => { const stopVcs = sdk().event.listen((evt) => {
if (evt.details.type !== "file.watcher.updated") return if (evt.details.type !== "file.watcher.updated") return
const props = const props =
typeof evt.details.properties === "object" && evt.details.properties typeof evt.details.properties === "object" && evt.details.properties
@@ -866,7 +866,7 @@ export default function Page() {
createEffect( createEffect(
on( on(
() => sync.data.session_status[params.id ?? ""]?.type, () => sync().data.session_status[params.id ?? ""]?.type,
(next, prev) => { (next, prev) => {
if (next !== "idle" || prev === undefined || prev === "idle") return if (next !== "idle" || prev === undefined || prev === "idle") return
refreshVcs() refreshVcs()
@@ -1137,10 +1137,10 @@ export default function Page() {
if (!id) return if (!id) return
if (!wantsReview()) return if (!wantsReview()) return
if (sync.data.session_diff[id] !== undefined) return if (sync().data.session_diff[id] !== undefined) return
if (sync.status === "loading") return if (sync().status === "loading") return
void sync.session.diff(id) void sync().session.diff(id)
}) })
createEffect( createEffect(
@@ -1155,14 +1155,14 @@ export default function Page() {
const id = params.id const id = params.id
if (!id) return if (!id) return
if (!untrack(() => sync.data.session_diff[id] !== undefined)) return if (!untrack(() => sync().data.session_diff[id] !== undefined)) return
diffFrame = requestAnimationFrame(() => { diffFrame = requestAnimationFrame(() => {
diffFrame = undefined diffFrame = undefined
diffTimer = window.setTimeout(() => { diffTimer = window.setTimeout(() => {
diffTimer = undefined diffTimer = undefined
if (sessionKey() !== key) return if (sessionKey() !== key) return
void sync.session.diff(id, { force: true }) void sync().session.diff(id, { force: true })
}, 0) }, 0)
}) })
}, },
@@ -1172,10 +1172,10 @@ export default function Page() {
let treeDir: string | undefined let treeDir: string | undefined
createEffect(() => { createEffect(() => {
const dir = sdk.directory const dir = sdk().directory
if (!isDesktop()) return if (!isDesktop()) return
if (!layout.fileTree.opened()) return if (!layout.fileTree.opened()) return
if (sync.status === "loading") return if (sync().status === "loading") return
fileTreeTab() fileTreeTab()
const refresh = treeDir !== dir const refresh = treeDir !== dir
@@ -1185,7 +1185,7 @@ export default function Page() {
createEffect( createEffect(
on( on(
() => sdk.directory, () => sdk().directory,
() => { () => {
const tab = activeFileTab() const tab = activeFileTab()
if (!tab) return if (!tab) return
@@ -1285,7 +1285,7 @@ export default function Page() {
visibleUserMessages, visibleUserMessages,
historyMore, historyMore,
historyLoading, historyLoading,
loadMore: (sessionID) => sync.session.history.loadMore(sessionID), loadMore: (sessionID) => sync().session.history.loadMore(sessionID),
userScrolled: autoScroll.userScrolled, userScrolled: autoScroll.userScrolled,
scroller: () => scroller, scroller: () => scroller,
}) })
@@ -1329,8 +1329,8 @@ export default function Page() {
) )
const draft = (id: string) => const draft = (id: string) =>
extractPromptFromParts(sync.data.part[id] ?? [], { extractPromptFromParts(sync().data.part[id] ?? [], {
directory: sdk.directory, directory: sdk().directory,
attachmentName: language.t("common.attachment"), attachmentName: language.t("common.attachment"),
}) })
@@ -1353,7 +1353,7 @@ export default function Page() {
} }
const merge = (next: NonNullable<ReturnType<typeof info>>) => const merge = (next: NonNullable<ReturnType<typeof info>>) =>
sync.set("session", (list) => { sync().set("session", (list) => {
const idx = list.findIndex((item) => item.id === next.id) const idx = list.findIndex((item) => item.id === next.id)
if (idx < 0) return list if (idx < 0) return list
const out = list.slice() const out = list.slice()
@@ -1362,7 +1362,7 @@ export default function Page() {
}) })
const roll = (sessionID: string, next: NonNullable<ReturnType<typeof info>>["revert"]) => const roll = (sessionID: string, next: NonNullable<ReturnType<typeof info>>["revert"]) =>
sync.set("session", (list) => { sync().set("session", (list) => {
const idx = list.findIndex((item) => item.id === sessionID) const idx = list.findIndex((item) => item.id === sessionID)
if (idx < 0) return list if (idx < 0) return list
const out = list.slice() const out = list.slice()
@@ -1370,7 +1370,7 @@ export default function Page() {
return out return out
}) })
const busy = (sessionID: string) => sync.data.session_working(sessionID) const busy = (sessionID: string) => sync().data.session_working(sessionID)
const queuedFollowups = createMemo(() => { const queuedFollowups = createMemo(() => {
const id = params.id const id = params.id
@@ -1393,11 +1393,11 @@ export default function Page() {
setFollowup("failed", input.sessionID, undefined) setFollowup("failed", input.sessionID, undefined)
const ok = await sendFollowupDraft({ const ok = await sendFollowupDraft({
client: sdk.client, client: sdk().client,
sync, sync: sync(),
serverSync, serverSync: serverSync(),
draft: item, draft: item,
optimisticBusy: item.sessionDirectory === sdk.directory, optimisticBusy: item.sessionDirectory === sdk().directory,
}).catch((err) => { }).catch((err) => {
setFollowup("failed", input.sessionID, input.id) setFollowup("failed", input.sessionID, input.id)
fail(err) fail(err)
@@ -1455,7 +1455,7 @@ export default function Page() {
const followupDock = createMemo(() => queuedFollowups().map((item) => ({ id: item.id, text: followupText(item) }))) const followupDock = createMemo(() => queuedFollowups().map((item) => ({ id: item.id, text: followupText(item) })))
const sendFollowup = (sessionID: string, id: string, opts?: { manual?: boolean }) => { const sendFollowup = (sessionID: string, id: string, opts?: { manual?: boolean }) => {
if (sync.session.get(sessionID)?.parentID) return Promise.resolve() if (sync().session.get(sessionID)?.parentID) return Promise.resolve()
const item = (followup.items[sessionID] ?? []).find((entry) => entry.id === id) const item = (followup.items[sessionID] ?? []).find((entry) => entry.id === id)
if (!item) return Promise.resolve() if (!item) return Promise.resolve()
if (followupBusy(sessionID)) return Promise.resolve() if (followupBusy(sessionID)) return Promise.resolve()
@@ -1487,7 +1487,7 @@ export default function Page() {
} }
const halt = (sessionID: string) => const halt = (sessionID: string) =>
busy(sessionID) ? sdk.client.session.abort({ sessionID }).catch(() => {}) : Promise.resolve() busy(sessionID) ? sdk().client.session.abort({ sessionID }).catch(() => {}) : Promise.resolve()
const revertMutation = useMutation(() => ({ const revertMutation = useMutation(() => ({
mutationFn: async (input: { sessionID: string; messageID: string }) => { mutationFn: async (input: { sessionID: string; messageID: string }) => {
@@ -1499,7 +1499,7 @@ export default function Page() {
prompt.set(value) prompt.set(value)
}) })
await halt(input.sessionID) await halt(input.sessionID)
.then(() => sdk.client.session.revert(input)) .then(() => sdk().client.session.revert(input))
.then((result) => { .then((result) => {
if (result.data) merge(result.data) if (result.data) merge(result.data)
}) })
@@ -1532,9 +1532,9 @@ export default function Page() {
}) })
const task = !next const task = !next
? halt(sessionID).then(() => sdk.client.session.unrevert({ sessionID })) ? halt(sessionID).then(() => sdk().client.session.unrevert({ sessionID }))
: halt(sessionID).then(() => : halt(sessionID).then(() =>
sdk.client.session.revert({ sdk().client.session.revert({
sessionID, sessionID,
messageID: next.id, messageID: next.id,
}), }),
@@ -1622,7 +1622,7 @@ export default function Page() {
visibleUserMessages, visibleUserMessages,
historyMore, historyMore,
historyLoading, historyLoading,
loadMore: (sessionID) => sync.session.history.loadMore(sessionID), loadMore: (sessionID) => sync().session.history.loadMore(sessionID),
currentMessageId: () => store.messageId, currentMessageId: () => store.messageId,
pendingMessage: () => ui.pendingMessage, pendingMessage: () => ui.pendingMessage,
setPendingMessage: (value) => setUi("pendingMessage", value), setPendingMessage: (value) => setUi("pendingMessage", value),
@@ -57,7 +57,7 @@ export function SessionComposerRegion(props: {
const view = layout.view(route.sessionKey) const view = layout.view(route.sessionKey)
const handoffPrompt = createMemo(() => getSessionHandoff(route.sessionKey())?.prompt) const handoffPrompt = createMemo(() => getSessionHandoff(route.sessionKey())?.prompt)
const info = createMemo(() => (route.params.id ? sync.session.get(route.params.id) : undefined)) const info = createMemo(() => (route.params.id ? sync().session.get(route.params.id) : undefined))
const parentID = createMemo(() => info()?.parentID) const parentID = createMemo(() => info()?.parentID)
const child = createMemo(() => !!parentID()) const child = createMemo(() => !!parentID())
const showComposer = createMemo(() => !props.state.blocked() || child()) const showComposer = createMemo(() => !props.state.blocked() || child())
@@ -32,12 +32,12 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
const permission = usePermission() const permission = usePermission()
const questionRequest = createMemo((): QuestionRequest | undefined => { const questionRequest = createMemo((): QuestionRequest | undefined => {
return sessionQuestionRequest(sync.data.session, sync.data.question, params.id) return sessionQuestionRequest(sync().data.session, sync().data.question, params.id)
}) })
const permissionRequest = createMemo((): PermissionRequest | undefined => { const permissionRequest = createMemo((): PermissionRequest | undefined => {
return sessionPermissionRequest(sync.data.session, sync.data.permission, params.id, (item) => { return sessionPermissionRequest(sync().data.session, sync().data.permission, params.id, (item) => {
return !permission.autoResponds(item, sdk.directory) return !permission.autoResponds(item, sdk().directory)
}) })
}) })
@@ -50,14 +50,14 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
const todos = createMemo((): Todo[] => { const todos = createMemo((): Todo[] => {
const id = params.id const id = params.id
if (!id) return [] if (!id) return []
return serverSync.data.session_todo[id] ?? [] return serverSync().data.session_todo[id] ?? []
}) })
const done = createMemo( const done = createMemo(
() => todos().length > 0 && todos().every((todo) => todo.status === "completed" || todo.status === "cancelled"), () => todos().length > 0 && todos().every((todo) => todo.status === "completed" || todo.status === "cancelled"),
) )
const live = createMemo(() => sync.data.session_working(params.id ?? "") || blocked()) const live = createMemo(() => sync().data.session_working(params.id ?? "") || blocked())
const [store, setStore] = createStore({ const [store, setStore] = createStore({
responding: undefined as string | undefined, responding: undefined as string | undefined,
@@ -78,7 +78,7 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
if (store.responding === perm.id) return if (store.responding === perm.id) return
setStore("responding", perm.id) setStore("responding", perm.id)
sdk.client.permission sdk().client.permission
.respond({ sessionID: perm.sessionID, permissionID: perm.id, response }) .respond({ sessionID: perm.sessionID, permissionID: perm.id, response })
.catch((err: unknown) => { .catch((err: unknown) => {
const description = err instanceof Error ? err.message : String(err) const description = err instanceof Error ? err.message : String(err)
@@ -111,8 +111,8 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
const clear = () => { const clear = () => {
const id = params.id const id = params.id
if (!id) return if (!id) return
serverSync.todo.set(id, []) serverSync().todo.set(id, [])
sync.set("todo", id, []) sync().set("todo", id, [])
} }
createEffect( createEffect(
@@ -64,7 +64,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
const sdk = useSDK() const sdk = useSDK()
const serverSDK = useServerSDK() const serverSDK = useServerSDK()
const language = useLanguage() const language = useLanguage()
const cacheKey = ScopedKey.from(serverSDK.scope, props.request.id) const cacheKey = ScopedKey.from(serverSDK().scope, props.request.id)
const questions = createMemo(() => props.request.questions) const questions = createMemo(() => props.request.questions)
const total = createMemo(() => questions().length) const total = createMemo(() => questions().length)
@@ -209,7 +209,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
} }
const replyMutation = useMutation(() => ({ const replyMutation = useMutation(() => ({
mutationFn: (answers: QuestionAnswer[]) => sdk.client.question.reply({ requestID: props.request.id, answers }), mutationFn: (answers: QuestionAnswer[]) => sdk().client.question.reply({ requestID: props.request.id, answers }),
onMutate: () => { onMutate: () => {
props.onSubmit() props.onSubmit()
}, },
@@ -221,7 +221,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
})) }))
const rejectMutation = useMutation(() => ({ const rejectMutation = useMutation(() => ({
mutationFn: () => sdk.client.question.reject({ requestID: props.request.id }), mutationFn: () => sdk().client.question.reject({ requestID: props.request.id }),
onMutate: () => { onMutate: () => {
props.onSubmit() props.onSubmit()
}, },
@@ -300,7 +300,7 @@ export function MessageTimeline(props: {
const sessionMessages = createMemo(() => { const sessionMessages = createMemo(() => {
const id = sessionID() const id = sessionID()
if (!id) return emptyMessages if (!id) return emptyMessages
return sync.data.message[id] ?? emptyMessages return sync().data.message[id] ?? emptyMessages
}) })
const messageByID = createMemo(() => new Map(sessionMessages().map((message) => [message.id, message] as const))) const messageByID = createMemo(() => new Map(sessionMessages().map((message) => [message.id, message] as const)))
const assistantMessagesByParent = createMemo(() => { const assistantMessagesByParent = createMemo(() => {
@@ -324,10 +324,10 @@ export function MessageTimeline(props: {
const sessionStatus = createMemo(() => { const sessionStatus = createMemo(() => {
const id = sessionID() const id = sessionID()
if (!id) return idle if (!id) return idle
return sync.data.session_status[id] ?? idle return sync().data.session_status[id] ?? idle
}) })
const working = createMemo(() => sessionStatus().type !== "idle") const working = createMemo(() => sessionStatus().type !== "idle")
const tint = createMemo(() => messageAgentColor(sessionMessages(), sync.data.agent)) const tint = createMemo(() => messageAgentColor(sessionMessages(), sync().data.agent))
const [timeoutDone, setTimeoutDone] = createSignal(true) const [timeoutDone, setTimeoutDone] = createSignal(true)
@@ -366,25 +366,25 @@ export function MessageTimeline(props: {
const info = createMemo(() => { const info = createMemo(() => {
const id = sessionID() const id = sessionID()
if (!id) return if (!id) return
return sync.session.get(id) return sync().session.get(id)
}) })
const titleValue = createMemo(() => info()?.title) const titleValue = createMemo(() => info()?.title)
const titleLabel = createMemo(() => sessionTitle(titleValue())) const titleLabel = createMemo(() => sessionTitle(titleValue()))
const shareUrl = createMemo(() => info()?.share?.url) const shareUrl = createMemo(() => info()?.share?.url)
const shareEnabled = createMemo(() => sync.data.config.share !== "disabled") const shareEnabled = createMemo(() => sync().data.config.share !== "disabled")
const parentID = createMemo(() => info()?.parentID) const parentID = createMemo(() => info()?.parentID)
const parent = createMemo(() => { const parent = createMemo(() => {
const id = parentID() const id = parentID()
if (!id) return if (!id) return
return sync.session.get(id) return sync().session.get(id)
}) })
const parentMessages = createMemo(() => { const parentMessages = createMemo(() => {
const id = parentID() const id = parentID()
if (!id) return emptyMessages if (!id) return emptyMessages
return sync.data.message[id] ?? emptyMessages return sync().data.message[id] ?? emptyMessages
}) })
const parentTitle = createMemo(() => sessionTitle(parent()?.title) ?? language.t("command.session.new")) const parentTitle = createMemo(() => sessionTitle(parent()?.title) ?? language.t("command.session.new"))
const getMsgParts = (msgId: string) => sync.data.part[msgId] ?? emptyParts const getMsgParts = (msgId: string) => sync().data.part[msgId] ?? emptyParts
const childTaskDescription = createMemo(() => { const childTaskDescription = createMemo(() => {
const id = sessionID() const id = sessionID()
if (!id) return if (!id) return
@@ -730,14 +730,14 @@ export function MessageTimeline(props: {
} }
const shareMutation = useMutation(() => ({ const shareMutation = useMutation(() => ({
mutationFn: (id: string) => serverSDK.client.session.share({ sessionID: id, directory: sdk.directory }), mutationFn: (id: string) => serverSDK().client.session.share({ sessionID: id, directory: sdk().directory }),
onError: (err) => { onError: (err) => {
console.error("Failed to share session", err) console.error("Failed to share session", err)
}, },
})) }))
const unshareMutation = useMutation(() => ({ const unshareMutation = useMutation(() => ({
mutationFn: (id: string) => serverSDK.client.session.unshare({ sessionID: id, directory: sdk.directory }), mutationFn: (id: string) => serverSDK().client.session.unshare({ sessionID: id, directory: sdk().directory }),
onError: (err) => { onError: (err) => {
console.error("Failed to unshare session", err) console.error("Failed to unshare session", err)
}, },
@@ -745,9 +745,9 @@ export function MessageTimeline(props: {
const titleMutation = useMutation(() => ({ const titleMutation = useMutation(() => ({
mutationFn: (input: { id: string; title: string }) => mutationFn: (input: { id: string; title: string }) =>
sdk.client.session.update({ sessionID: input.id, title: input.title }), sdk().client.session.update({ sessionID: input.id, title: input.title }),
onSuccess: (_, input) => { onSuccess: (_, input) => {
sync.set( sync().set(
produce((draft) => { produce((draft) => {
const index = draft.session.findIndex((s) => s.id === input.id) const index = draft.session.findIndex((s) => s.id === input.id)
if (index !== -1) draft.session[index].title = input.title if (index !== -1) draft.session[index].title = input.title
@@ -797,8 +797,8 @@ export function MessageTimeline(props: {
() => [parentID(), childTaskDescription()] as const, () => [parentID(), childTaskDescription()] as const,
([id, description]) => { ([id, description]) => {
if (!id || description) return if (!id || description) return
if (sync.data.message[id] !== undefined) return if (sync().data.message[id] !== undefined) return
void sync.session.sync(id) void sync().session.sync(id)
}, },
{ defer: true }, { defer: true },
), ),
@@ -846,25 +846,25 @@ export function MessageTimeline(props: {
} }
const archiveSession = async (sessionID: string) => { const archiveSession = async (sessionID: string) => {
const session = sync.session.get(sessionID) const session = sync().session.get(sessionID)
if (!session) return if (!session) return
const sessions = sync.data.session ?? [] const sessions = sync().data.session ?? []
const index = sessions.findIndex((s) => s.id === sessionID) const index = sessions.findIndex((s) => s.id === sessionID)
const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1]) const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1])
await sdk.client.session await sdk().client.session
.update({ sessionID, time: { archived: Date.now() } }) .update({ sessionID, time: { archived: Date.now() } })
.then(() => { .then(() => {
sync.set( sync().set(
produce((draft) => { produce((draft) => {
const index = draft.session.findIndex((s) => s.id === sessionID) const index = draft.session.findIndex((s) => s.id === sessionID)
if (index !== -1) draft.session.splice(index, 1) if (index !== -1) draft.session.splice(index, 1)
}), }),
) )
sync.session.evict(sessionID) sync().session.evict(sessionID)
navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id) navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id)
notifySessionTabsRemoved({ directory: sdk.directory, sessionIDs: [sessionID] }) notifySessionTabsRemoved({ directory: sdk().directory, sessionIDs: [sessionID] })
}) })
.catch((err) => { .catch((err) => {
showToast({ showToast({
@@ -875,14 +875,14 @@ export function MessageTimeline(props: {
} }
const deleteSession = async (sessionID: string) => { const deleteSession = async (sessionID: string) => {
const session = sync.session.get(sessionID) const session = sync().session.get(sessionID)
if (!session) return false if (!session) return false
const sessions = (sync.data.session ?? []).filter((s) => !s.parentID && !s.time?.archived) const sessions = (sync().data.session ?? []).filter((s) => !s.parentID && !s.time?.archived)
const index = sessions.findIndex((s) => s.id === sessionID) const index = sessions.findIndex((s) => s.id === sessionID)
const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1]) const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1])
const result = await sdk.client.session const result = await sdk().client.session
.delete({ sessionID }) .delete({ sessionID })
.then((x) => x.data) .then((x) => x.data)
.catch((err) => { .catch((err) => {
@@ -897,7 +897,7 @@ export function MessageTimeline(props: {
const removed = new Set<string>([sessionID]) const removed = new Set<string>([sessionID])
const byParent = new Map<string, string[]>() const byParent = new Map<string, string[]>()
for (const item of sync.data.session) { for (const item of sync().data.session) {
const parentID = item.parentID const parentID = item.parentID
if (!parentID) continue if (!parentID) continue
const existing = byParent.get(parentID) const existing = byParent.get(parentID)
@@ -925,16 +925,16 @@ export function MessageTimeline(props: {
navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id) navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id)
sync.set( sync().set(
produce((draft) => { produce((draft) => {
draft.session = draft.session.filter((s) => !removed.has(s.id)) draft.session = draft.session.filter((s) => !removed.has(s.id))
}), }),
) )
for (const id of removed) { for (const id of removed) {
sync.session.evict(id) sync().session.evict(id)
} }
notifySessionTabsRemoved({ directory: sdk.directory, sessionIDs: [...removed] }) notifySessionTabsRemoved({ directory: sdk().directory, sessionIDs: [...removed] })
return true return true
} }
@@ -946,7 +946,7 @@ export function MessageTimeline(props: {
function DialogDeleteSession(props: { sessionID: string }) { function DialogDeleteSession(props: { sessionID: string }) {
const name = createMemo( const name = createMemo(
() => sessionTitle(sync.session.get(props.sessionID)?.title) ?? language.t("command.session.new"), () => sessionTitle(sync().session.get(props.sessionID)?.title) ?? language.t("command.session.new"),
) )
const handleDelete = async () => { const handleDelete = async () => {
await deleteSession(props.sessionID) await deleteSession(props.sessionID)
@@ -53,7 +53,7 @@ export function SessionReviewTab(props: SessionReviewTabProps) {
const layout = useLayout() const layout = useLayout()
const readFile = async (path: string) => { const readFile = async (path: string) => {
return sdk.client.file return sdk().client.file
.read({ path }) .read({ path })
.then((x) => x.data) .then((x) => x.data)
.catch((error) => { .catch((error) => {
@@ -51,7 +51,7 @@ export function useUsageExceededDialogs() {
) )
onCleanup( onCleanup(
sdk.event.on("session.status", (evt) => { sdk().event.on("session.status", (evt) => {
if (evt.properties.sessionID !== params.id) return if (evt.properties.sessionID !== params.id) return
if (evt.properties.status.type !== "retry") return if (evt.properties.status.type !== "retry") return
const { action } = evt.properties.status const { action } = evt.properties.status
@@ -52,7 +52,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
const info = () => { const info = () => {
const id = params.id const id = params.id
if (!id) return if (!id) return
return sync.session.get(id) return sync().session.get(id)
} }
const hasReview = () => !!params.id const hasReview = () => !!params.id
const normalizeTab = (tab: string) => { const normalizeTab = (tab: string) => {
@@ -73,7 +73,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
const messages = () => { const messages = () => {
const id = params.id const id = params.id
if (!id) return [] if (!id) return []
return sync.data.message[id] ?? [] return sync().data.message[id] ?? []
} }
const userMessages = () => messages().filter((m) => m.role === "user") as UserMessage[] const userMessages = () => messages().filter((m) => m.role === "user") as UserMessage[]
const visibleUserMessages = () => { const visibleUserMessages = () => {
@@ -122,8 +122,8 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
const isAutoAcceptActive = () => { const isAutoAcceptActive = () => {
const sessionID = params.id const sessionID = params.id
if (sessionID) return permission.isAutoAccepting(sessionID, sdk.directory) if (sessionID) return permission.isAutoAccepting(sessionID, sdk().directory)
return permission.isAutoAcceptingDirectory(sdk.directory) return permission.isAutoAcceptingDirectory(sdk().directory)
} }
const write = async (value: string) => { const write = async (value: string) => {
const body = typeof document === "undefined" ? undefined : document.body const body = typeof document === "undefined" ? undefined : document.body
@@ -175,7 +175,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
return return
} }
const url = await sdk.client.session const url = await sdk().client.session
.share({ sessionID }) .share({ sessionID })
.then((res) => res.data?.share?.url) .then((res) => res.data?.share?.url)
.catch(() => undefined) .catch(() => undefined)
@@ -195,7 +195,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
const sessionID = params.id const sessionID = params.id
if (!sessionID) return if (!sessionID) return
await sdk.client.session await sdk().client.session
.unshare({ sessionID }) .unshare({ sessionID })
.then(() => .then(() =>
showToast({ showToast({
@@ -263,12 +263,12 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
const toggleAutoAccept = () => { const toggleAutoAccept = () => {
const sessionID = params.id const sessionID = params.id
if (sessionID) permission.toggleAutoAccept(sessionID, sdk.directory) if (sessionID) permission.toggleAutoAccept(sessionID, sdk().directory)
else permission.toggleAutoAcceptDirectory(sdk.directory) else permission.toggleAutoAcceptDirectory(sdk().directory)
const active = sessionID const active = sessionID
? permission.isAutoAccepting(sessionID, sdk.directory) ? permission.isAutoAccepting(sessionID, sdk().directory)
: permission.isAutoAcceptingDirectory(sdk.directory) : permission.isAutoAcceptingDirectory(sdk().directory)
showToast({ showToast({
title: active title: active
? language.t("toast.permissions.autoaccept.on.title") ? language.t("toast.permissions.autoaccept.on.title")
@@ -283,18 +283,18 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
const sessionID = params.id const sessionID = params.id
if (!sessionID) return if (!sessionID) return
if (sync.data.session_working(params.id ?? "")) { if (sync().data.session_working(params.id ?? "")) {
await sdk.client.session.abort({ sessionID }).catch(() => {}) await sdk().client.session.abort({ sessionID }).catch(() => {})
} }
const revert = info()?.revert?.messageID const revert = info()?.revert?.messageID
const message = findLast(userMessages(), (x) => !revert || x.id < revert) const message = findLast(userMessages(), (x) => !revert || x.id < revert)
if (!message) return if (!message) return
await sdk.client.session.revert({ sessionID, messageID: message.id }) await sdk().client.session.revert({ sessionID, messageID: message.id })
const parts = sync.data.part[message.id] const parts = sync().data.part[message.id]
if (parts) { if (parts) {
const restored = extractPromptFromParts(parts, { directory: sdk.directory }) const restored = extractPromptFromParts(parts, { directory: sdk().directory })
prompt.set(restored) prompt.set(restored)
} }
@@ -311,14 +311,14 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
const next = userMessages().find((x) => x.id > revertMessageID) const next = userMessages().find((x) => x.id > revertMessageID)
if (!next) { if (!next) {
await sdk.client.session.unrevert({ sessionID }) await sdk().client.session.unrevert({ sessionID })
prompt.reset() prompt.reset()
const last = findLast(userMessages(), (x) => x.id >= revertMessageID) const last = findLast(userMessages(), (x) => x.id >= revertMessageID)
setActiveMessage(last) setActiveMessage(last)
return return
} }
await sdk.client.session.revert({ sessionID, messageID: next.id }) await sdk().client.session.revert({ sessionID, messageID: next.id })
const prev = findLast(userMessages(), (x) => x.id < next.id) const prev = findLast(userMessages(), (x) => x.id < next.id)
setActiveMessage(prev) setActiveMessage(prev)
} }
@@ -336,7 +336,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
return return
} }
await sdk.client.session.summarize({ await sdk().client.session.summarize({
sessionID, sessionID,
modelID: model.id, modelID: model.id,
providerID: model.provider.id, providerID: model.provider.id,
@@ -350,7 +350,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
} }
const shareCmds = () => { const shareCmds = () => {
if (sync.data.config.share === "disabled") return [] if (sync().data.config.share === "disabled") return []
return [ return [
sessionCommand({ sessionCommand({
id: "session.share", id: "session.share",