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 13:24:10 +02:00
committed by GitHub
parent c81cd3202c
commit 010b456dfd
53 changed files with 524 additions and 458 deletions
+63 -63
View File
@@ -308,10 +308,10 @@ export default function Page() {
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 diffs = createMemo(() => (params.id ? list(sync.data.session_diff[params.id]) : []))
const canReview = createMemo(() => !!sync.project)
const diffs = createMemo(() => (params.id ? list(sync().data.session_diff[params.id]) : []))
const canReview = createMemo(() => !!sync().project)
const reviewTab = createMemo(() => isDesktop())
const tabState = createSessionTabs({
tabs,
@@ -323,21 +323,21 @@ export default function Page() {
const activeTab = tabState.activeTab
const activeFileTab = tabState.activeFileTab
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 id = params.id
if (!id) return true
return sync.data.message[id] !== undefined
return sync().data.message[id] !== undefined
})
const historyMore = createMemo(() => {
const id = params.id
if (!id) return false
return sync.session.history.more(id)
return sync().session.history.more(id)
})
const historyLoading = createMemo(() => {
const id = params.id
if (!id) return false
return sync.session.history.loading(id)
return sync().session.history.loading(id)
})
const userMessages = createMemo(
() => messages().filter((m) => m.role === "user") as UserMessage[],
@@ -397,7 +397,7 @@ export default function Page() {
})
const [followup, setFollowup] = persisted(
Persist.serverWorkspace(serverSDK.scope, sdk.directory, "followup", ["followup.v1"]),
Persist.serverWorkspace(serverSDK().scope, sdk().directory, "followup", ["followup.v1"]),
createStore<{
items: Record<string, FollowupItem[] | undefined>
failed: Record<string, string | undefined>
@@ -444,16 +444,16 @@ export default function Page() {
}, desktopReviewOpen())
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 list: ChangeMode[] = []
if (sync.project?.vcs === "git") list.push("git")
if (
sync.project?.vcs === "git" &&
sync.data.vcs?.branch &&
sync.data.vcs?.default_branch &&
sync.data.vcs.branch !== sync.data.vcs.default_branch
) {
const project = sync().project
const vcs = sync().data.vcs
if (project?.vcs === "git") list.push("git")
if (project?.vcs === "git" && vcs?.branch && vcs?.default_branch && vcs.branch !== vcs.default_branch) {
list.push("branch")
}
list.push("turn")
@@ -469,18 +469,18 @@ export default function Page() {
if (store.changes === "git" || store.changes === "branch") return store.changes
})
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 mode = vcsMode()
const enabled = wantsReview() && sync.project?.vcs === "git"
const enabled = wantsReview() && sync().project?.vcs === "git"
return {
queryKey: [...vcsKey(), mode] as const,
enabled,
queryFn: mode
? () =>
sdk.client.vcs
sdk().client.vcs
.diff({ mode })
.then((result) => list(result.data))
.catch((error) => {
@@ -506,8 +506,8 @@ export default function Page() {
const newSessionWorktree = createMemo(() => {
if (store.newSessionWorktree === "create") return "create"
const project = sync.project
if (project && sdk.directory !== project.worktree) return sdk.directory
const project = sync().project
if (project && sdk().directory !== project.worktree) return sdk().directory
return "main"
})
@@ -569,11 +569,11 @@ export default function Page() {
}
function upsert(next: Project) {
const list = serverSync.data.project
sync.set("project", next.id)
const list = serverSync().data.project
sync().set("project", next.id)
const idx = list.findIndex((item) => item.id === next.id)
if (idx >= 0) {
serverSync.set(
serverSync().set(
"project",
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)
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
}
serverSync.set("project", [...list, next])
serverSync().set("project", [...list, next])
}
const gitMutation = useMutation(() => ({
mutationFn: () => sdk.client.project.initGit(),
mutationFn: () => sdk().client.project.initGit(),
onSuccess: (x) => {
if (!x.data) return
upsert(x.data)
@@ -632,7 +632,7 @@ export default function Page() {
const hasScrollGesture = () => Date.now() - ui.scrollGesture < scrollGestureWindowMs
const [sessionSync] = createResource(
() => [sdk.directory, params.id] as const,
() => [sdk().directory, params.id] as const,
([directory, id]) => {
if (refreshFrame !== undefined) cancelAnimationFrame(refreshFrame)
if (refreshTimer !== undefined) window.clearTimeout(refreshTimer)
@@ -640,11 +640,11 @@ export default function Page() {
refreshTimer = undefined
if (!id) return
const cached = untrack(() => sync.data.message[id] !== undefined)
const cached = untrack(() => sync().data.message[id] !== undefined)
const stale = !cached
? false
: (() => {
const info = getSessionPrefetch(serverSDK.scope, directory, id)
const info = getSessionPrefetch(serverSDK().scope, directory, id)
if (!info) return true
return Date.now() - info.at > SESSION_PREFETCH_TTL
})()
@@ -655,12 +655,12 @@ export default function Page() {
refreshTimer = undefined
if (params.id !== id) return
untrack(() => {
if (stale) void sync.session.sync(id, { force: true })
if (stale) void sync().session.sync(id, { force: true })
})
}, 0)
})
return sync.session.sync(id)
return sync().session.sync(id)
},
)
@@ -669,9 +669,9 @@ export default function Page() {
() => {
const id = params.id
return [
sdk.directory,
sdk().directory,
id,
id ? (sync.data.session_status[id]?.type ?? "idle") : "idle",
id ? (sync().data.session_status[id]?.type ?? "idle") : "idle",
id ? composer.blocked() : false,
] as const
},
@@ -682,15 +682,15 @@ export default function Page() {
todoTimer = undefined
if (!id) 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 = undefined
todoTimer = window.setTimeout(() => {
todoTimer = undefined
if (sdk.directory !== dir || params.id !== id) return
if (sdk().directory !== dir || params.id !== id) return
untrack(() => {
void sync.session.todo(id, cached ? { force: true } : undefined)
void sync().session.todo(id, cached ? { force: true } : undefined)
})
}, 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
const props =
typeof evt.details.properties === "object" && evt.details.properties
@@ -866,7 +866,7 @@ export default function Page() {
createEffect(
on(
() => sync.data.session_status[params.id ?? ""]?.type,
() => sync().data.session_status[params.id ?? ""]?.type,
(next, prev) => {
if (next !== "idle" || prev === undefined || prev === "idle") return
refreshVcs()
@@ -1137,10 +1137,10 @@ export default function Page() {
if (!id) return
if (!wantsReview()) return
if (sync.data.session_diff[id] !== undefined) return
if (sync.status === "loading") return
if (sync().data.session_diff[id] !== undefined) return
if (sync().status === "loading") return
void sync.session.diff(id)
void sync().session.diff(id)
})
createEffect(
@@ -1155,14 +1155,14 @@ export default function Page() {
const id = params.id
if (!id) return
if (!untrack(() => sync.data.session_diff[id] !== undefined)) return
if (!untrack(() => sync().data.session_diff[id] !== undefined)) return
diffFrame = requestAnimationFrame(() => {
diffFrame = undefined
diffTimer = window.setTimeout(() => {
diffTimer = undefined
if (sessionKey() !== key) return
void sync.session.diff(id, { force: true })
void sync().session.diff(id, { force: true })
}, 0)
})
},
@@ -1172,10 +1172,10 @@ export default function Page() {
let treeDir: string | undefined
createEffect(() => {
const dir = sdk.directory
const dir = sdk().directory
if (!isDesktop()) return
if (!layout.fileTree.opened()) return
if (sync.status === "loading") return
if (sync().status === "loading") return
fileTreeTab()
const refresh = treeDir !== dir
@@ -1185,7 +1185,7 @@ export default function Page() {
createEffect(
on(
() => sdk.directory,
() => sdk().directory,
() => {
const tab = activeFileTab()
if (!tab) return
@@ -1285,7 +1285,7 @@ export default function Page() {
visibleUserMessages,
historyMore,
historyLoading,
loadMore: (sessionID) => sync.session.history.loadMore(sessionID),
loadMore: (sessionID) => sync().session.history.loadMore(sessionID),
userScrolled: autoScroll.userScrolled,
scroller: () => scroller,
})
@@ -1329,8 +1329,8 @@ export default function Page() {
)
const draft = (id: string) =>
extractPromptFromParts(sync.data.part[id] ?? [], {
directory: sdk.directory,
extractPromptFromParts(sync().data.part[id] ?? [], {
directory: sdk().directory,
attachmentName: language.t("common.attachment"),
})
@@ -1353,7 +1353,7 @@ export default function Page() {
}
const merge = (next: NonNullable<ReturnType<typeof info>>) =>
sync.set("session", (list) => {
sync().set("session", (list) => {
const idx = list.findIndex((item) => item.id === next.id)
if (idx < 0) return list
const out = list.slice()
@@ -1362,7 +1362,7 @@ export default function Page() {
})
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)
if (idx < 0) return list
const out = list.slice()
@@ -1370,7 +1370,7 @@ export default function Page() {
return out
})
const busy = (sessionID: string) => sync.data.session_working(sessionID)
const busy = (sessionID: string) => sync().data.session_working(sessionID)
const queuedFollowups = createMemo(() => {
const id = params.id
@@ -1393,11 +1393,11 @@ export default function Page() {
setFollowup("failed", input.sessionID, undefined)
const ok = await sendFollowupDraft({
client: sdk.client,
sync,
serverSync,
client: sdk().client,
sync: sync(),
serverSync: serverSync(),
draft: item,
optimisticBusy: item.sessionDirectory === sdk.directory,
optimisticBusy: item.sessionDirectory === sdk().directory,
}).catch((err) => {
setFollowup("failed", input.sessionID, input.id)
fail(err)
@@ -1455,7 +1455,7 @@ export default function Page() {
const followupDock = createMemo(() => queuedFollowups().map((item) => ({ id: item.id, text: followupText(item) })))
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)
if (!item) return Promise.resolve()
if (followupBusy(sessionID)) return Promise.resolve()
@@ -1487,7 +1487,7 @@ export default function Page() {
}
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(() => ({
mutationFn: async (input: { sessionID: string; messageID: string }) => {
@@ -1499,7 +1499,7 @@ export default function Page() {
prompt.set(value)
})
await halt(input.sessionID)
.then(() => sdk.client.session.revert(input))
.then(() => sdk().client.session.revert(input))
.then((result) => {
if (result.data) merge(result.data)
})
@@ -1532,9 +1532,9 @@ export default function Page() {
})
const task = !next
? halt(sessionID).then(() => sdk.client.session.unrevert({ sessionID }))
? halt(sessionID).then(() => sdk().client.session.unrevert({ sessionID }))
: halt(sessionID).then(() =>
sdk.client.session.revert({
sdk().client.session.revert({
sessionID,
messageID: next.id,
}),
@@ -1622,7 +1622,7 @@ export default function Page() {
visibleUserMessages,
historyMore,
historyLoading,
loadMore: (sessionID) => sync.session.history.loadMore(sessionID),
loadMore: (sessionID) => sync().session.history.loadMore(sessionID),
currentMessageId: () => store.messageId,
pendingMessage: () => ui.pendingMessage,
setPendingMessage: (value) => setUi("pendingMessage", value),