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
commit 010b456dfd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 524 additions and 458 deletions

View file

@ -57,7 +57,7 @@ export function SessionComposerRegion(props: {
const view = layout.view(route.sessionKey)
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 child = createMemo(() => !!parentID())
const showComposer = createMemo(() => !props.state.blocked() || child())

View file

@ -32,12 +32,12 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
const permission = usePermission()
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 => {
return sessionPermissionRequest(sync.data.session, sync.data.permission, params.id, (item) => {
return !permission.autoResponds(item, sdk.directory)
return sessionPermissionRequest(sync().data.session, sync().data.permission, params.id, (item) => {
return !permission.autoResponds(item, sdk().directory)
})
})
@ -50,14 +50,14 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
const todos = createMemo((): Todo[] => {
const id = params.id
if (!id) return []
return serverSync.data.session_todo[id] ?? []
return serverSync().data.session_todo[id] ?? []
})
const done = createMemo(
() => 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({
responding: undefined as string | undefined,
@ -78,7 +78,7 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
if (store.responding === perm.id) return
setStore("responding", perm.id)
sdk.client.permission
sdk().client.permission
.respond({ sessionID: perm.sessionID, permissionID: perm.id, response })
.catch((err: unknown) => {
const description = err instanceof Error ? err.message : String(err)
@ -111,8 +111,8 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
const clear = () => {
const id = params.id
if (!id) return
serverSync.todo.set(id, [])
sync.set("todo", id, [])
serverSync().todo.set(id, [])
sync().set("todo", id, [])
}
createEffect(

View file

@ -64,7 +64,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
const sdk = useSDK()
const serverSDK = useServerSDK()
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 total = createMemo(() => questions().length)
@ -209,7 +209,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
}
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: () => {
props.onSubmit()
},
@ -221,7 +221,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
}))
const rejectMutation = useMutation(() => ({
mutationFn: () => sdk.client.question.reject({ requestID: props.request.id }),
mutationFn: () => sdk().client.question.reject({ requestID: props.request.id }),
onMutate: () => {
props.onSubmit()
},

View file

@ -300,7 +300,7 @@ export function MessageTimeline(props: {
const sessionMessages = createMemo(() => {
const id = sessionID()
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 assistantMessagesByParent = createMemo(() => {
@ -324,10 +324,10 @@ export function MessageTimeline(props: {
const sessionStatus = createMemo(() => {
const id = sessionID()
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 tint = createMemo(() => messageAgentColor(sessionMessages(), sync.data.agent))
const tint = createMemo(() => messageAgentColor(sessionMessages(), sync().data.agent))
const [timeoutDone, setTimeoutDone] = createSignal(true)
@ -366,25 +366,25 @@ export function MessageTimeline(props: {
const info = createMemo(() => {
const id = sessionID()
if (!id) return
return sync.session.get(id)
return sync().session.get(id)
})
const titleValue = createMemo(() => info()?.title)
const titleLabel = createMemo(() => sessionTitle(titleValue()))
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 parent = createMemo(() => {
const id = parentID()
if (!id) return
return sync.session.get(id)
return sync().session.get(id)
})
const parentMessages = createMemo(() => {
const id = parentID()
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 getMsgParts = (msgId: string) => sync.data.part[msgId] ?? emptyParts
const getMsgParts = (msgId: string) => sync().data.part[msgId] ?? emptyParts
const childTaskDescription = createMemo(() => {
const id = sessionID()
if (!id) return
@ -730,14 +730,14 @@ export function MessageTimeline(props: {
}
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) => {
console.error("Failed to share session", err)
},
}))
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) => {
console.error("Failed to unshare session", err)
},
@ -745,9 +745,9 @@ export function MessageTimeline(props: {
const titleMutation = useMutation(() => ({
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) => {
sync.set(
sync().set(
produce((draft) => {
const index = draft.session.findIndex((s) => s.id === input.id)
if (index !== -1) draft.session[index].title = input.title
@ -797,8 +797,8 @@ export function MessageTimeline(props: {
() => [parentID(), childTaskDescription()] as const,
([id, description]) => {
if (!id || description) return
if (sync.data.message[id] !== undefined) return
void sync.session.sync(id)
if (sync().data.message[id] !== undefined) return
void sync().session.sync(id)
},
{ defer: true },
),
@ -846,25 +846,25 @@ export function MessageTimeline(props: {
}
const archiveSession = async (sessionID: string) => {
const session = sync.session.get(sessionID)
const session = sync().session.get(sessionID)
if (!session) return
const sessions = sync.data.session ?? []
const sessions = sync().data.session ?? []
const index = sessions.findIndex((s) => s.id === sessionID)
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() } })
.then(() => {
sync.set(
sync().set(
produce((draft) => {
const index = draft.session.findIndex((s) => s.id === sessionID)
if (index !== -1) draft.session.splice(index, 1)
}),
)
sync.session.evict(sessionID)
sync().session.evict(sessionID)
navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id)
notifySessionTabsRemoved({ directory: sdk.directory, sessionIDs: [sessionID] })
notifySessionTabsRemoved({ directory: sdk().directory, sessionIDs: [sessionID] })
})
.catch((err) => {
showToast({
@ -875,14 +875,14 @@ export function MessageTimeline(props: {
}
const deleteSession = async (sessionID: string) => {
const session = sync.session.get(sessionID)
const session = sync().session.get(sessionID)
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 nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1])
const result = await sdk.client.session
const result = await sdk().client.session
.delete({ sessionID })
.then((x) => x.data)
.catch((err) => {
@ -897,7 +897,7 @@ export function MessageTimeline(props: {
const removed = new Set<string>([sessionID])
const byParent = new Map<string, string[]>()
for (const item of sync.data.session) {
for (const item of sync().data.session) {
const parentID = item.parentID
if (!parentID) continue
const existing = byParent.get(parentID)
@ -925,16 +925,16 @@ export function MessageTimeline(props: {
navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id)
sync.set(
sync().set(
produce((draft) => {
draft.session = draft.session.filter((s) => !removed.has(s.id))
}),
)
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
}
@ -946,7 +946,7 @@ export function MessageTimeline(props: {
function DialogDeleteSession(props: { sessionID: string }) {
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 () => {
await deleteSession(props.sessionID)

View file

@ -53,7 +53,7 @@ export function SessionReviewTab(props: SessionReviewTabProps) {
const layout = useLayout()
const readFile = async (path: string) => {
return sdk.client.file
return sdk().client.file
.read({ path })
.then((x) => x.data)
.catch((error) => {

View file

@ -51,7 +51,7 @@ export function useUsageExceededDialogs() {
)
onCleanup(
sdk.event.on("session.status", (evt) => {
sdk().event.on("session.status", (evt) => {
if (evt.properties.sessionID !== params.id) return
if (evt.properties.status.type !== "retry") return
const { action } = evt.properties.status

View file

@ -52,7 +52,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
const info = () => {
const id = params.id
if (!id) return
return sync.session.get(id)
return sync().session.get(id)
}
const hasReview = () => !!params.id
const normalizeTab = (tab: string) => {
@ -73,7 +73,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
const messages = () => {
const id = params.id
if (!id) return []
return sync.data.message[id] ?? []
return sync().data.message[id] ?? []
}
const userMessages = () => messages().filter((m) => m.role === "user") as UserMessage[]
const visibleUserMessages = () => {
@ -122,8 +122,8 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
const isAutoAcceptActive = () => {
const sessionID = params.id
if (sessionID) return permission.isAutoAccepting(sessionID, sdk.directory)
return permission.isAutoAcceptingDirectory(sdk.directory)
if (sessionID) return permission.isAutoAccepting(sessionID, sdk().directory)
return permission.isAutoAcceptingDirectory(sdk().directory)
}
const write = async (value: string) => {
const body = typeof document === "undefined" ? undefined : document.body
@ -175,7 +175,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
return
}
const url = await sdk.client.session
const url = await sdk().client.session
.share({ sessionID })
.then((res) => res.data?.share?.url)
.catch(() => undefined)
@ -195,7 +195,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
const sessionID = params.id
if (!sessionID) return
await sdk.client.session
await sdk().client.session
.unshare({ sessionID })
.then(() =>
showToast({
@ -263,12 +263,12 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
const toggleAutoAccept = () => {
const sessionID = params.id
if (sessionID) permission.toggleAutoAccept(sessionID, sdk.directory)
else permission.toggleAutoAcceptDirectory(sdk.directory)
if (sessionID) permission.toggleAutoAccept(sessionID, sdk().directory)
else permission.toggleAutoAcceptDirectory(sdk().directory)
const active = sessionID
? permission.isAutoAccepting(sessionID, sdk.directory)
: permission.isAutoAcceptingDirectory(sdk.directory)
? permission.isAutoAccepting(sessionID, sdk().directory)
: permission.isAutoAcceptingDirectory(sdk().directory)
showToast({
title: active
? language.t("toast.permissions.autoaccept.on.title")
@ -283,18 +283,18 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
const sessionID = params.id
if (!sessionID) return
if (sync.data.session_working(params.id ?? "")) {
await sdk.client.session.abort({ sessionID }).catch(() => {})
if (sync().data.session_working(params.id ?? "")) {
await sdk().client.session.abort({ sessionID }).catch(() => {})
}
const revert = info()?.revert?.messageID
const message = findLast(userMessages(), (x) => !revert || x.id < revert)
if (!message) return
await sdk.client.session.revert({ sessionID, messageID: message.id })
const parts = sync.data.part[message.id]
await sdk().client.session.revert({ sessionID, messageID: message.id })
const parts = sync().data.part[message.id]
if (parts) {
const restored = extractPromptFromParts(parts, { directory: sdk.directory })
const restored = extractPromptFromParts(parts, { directory: sdk().directory })
prompt.set(restored)
}
@ -311,14 +311,14 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
const next = userMessages().find((x) => x.id > revertMessageID)
if (!next) {
await sdk.client.session.unrevert({ sessionID })
await sdk().client.session.unrevert({ sessionID })
prompt.reset()
const last = findLast(userMessages(), (x) => x.id >= revertMessageID)
setActiveMessage(last)
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)
setActiveMessage(prev)
}
@ -336,7 +336,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
return
}
await sdk.client.session.summarize({
await sdk().client.session.summarize({
sessionID,
modelID: model.id,
providerID: model.provider.id,
@ -350,7 +350,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
}
const shareCmds = () => {
if (sync.data.config.share === "disabled") return []
if (sync().data.config.share === "disabled") return []
return [
sessionCommand({
id: "session.share",