Browse Source

feat(app): migrate session interactions (#38461)

Brendan Allan 2 weeks ago
parent
commit
29af2e39ff

+ 3 - 3
packages/app/e2e/regression/remote-session-settings.spec.ts

@@ -98,7 +98,7 @@ test("auto-accept responds for an unfocused server session", async ({ page }) =>
     .toEqual([
       {
         origin: serverA,
-        directory: directoryA,
+        directory: undefined,
         sessionID: sessionA.id,
         permissionID: "permission-background-a",
         body: { response: "once" },
@@ -126,14 +126,14 @@ test("auto-accept responds for an unfocused server session", async ({ page }) =>
     .toEqual([
       {
         origin: serverA,
-        directory: directoryA,
+        directory: undefined,
         sessionID: sessionA.id,
         permissionID: "permission-background-a",
         body: { response: "once" },
       },
       {
         origin: serverA,
-        directory: directoryA,
+        directory: undefined,
         sessionID: childSessionA.id,
         permissionID: "permission-background-a-child",
         body: { response: "once" },

+ 5 - 2
packages/app/e2e/regression/session-request-docks.spec.ts

@@ -42,7 +42,8 @@ test("shows a pending question dock", async ({ page }) => {
   const rejectRequests: string[] = []
   page.on("request", (request) => {
     if (request.method() !== "POST") return
-    if (new URL(request.url()).pathname === "/question/question-request/reject") rejectRequests.push(request.url())
+    if (new URL(request.url()).pathname === "/question/question-request/reject")
+      rejectRequests.push(request.url())
   })
 
   await question.locator('[data-component="icon-button"][data-icon="chevron-down"]').click()
@@ -64,7 +65,9 @@ test("shows a pending question dock", async ({ page }) => {
 
   await question.getByRole("radio", { name: /Minimal/ }).click()
   const reply = page.waitForRequest(
-    (request) => request.method() === "POST" && new URL(request.url()).pathname === "/question/question-request/reply",
+    (request) =>
+      request.method() === "POST" &&
+      new URL(request.url()).pathname === "/question/question-request/reply",
   )
   await question.getByRole("button", { name: "Submit" }).click()
   expect((await reply).postDataJSON()).toEqual({ answers: [["Minimal"]] })

+ 7 - 4
packages/app/e2e/regression/subagent-child-navigation.spec.ts

@@ -1,6 +1,6 @@
 import { base64Encode } from "@opencode-ai/core/util/encode"
 import { expect, test, type Page } from "@playwright/test"
-import { mockOpenCodeServer } from "../utils/mock-server"
+import { currentSession, mockOpenCodeServer } from "../utils/mock-server"
 import { expectSessionTitle } from "../utils/waits"
 
 const directory = "C:/OpenCode/SubagentNavigation"
@@ -72,16 +72,19 @@ async function setup(page: Page, events?: () => EventPayload[]) {
     events,
     eventRetry: events ? 16 : undefined,
   })
-  // The child session resolves via /session/:id but is absent from the /session list,
+  // The child session resolves by ID but is absent from the session list,
   // matching a subagent session that has not been loaded into the list cache yet.
   await page.route(
-    (url) => url.pathname === "/session" && url.port === (process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"),
+    (url) => url.pathname === "/api/session" && url.port === (process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"),
     (route) =>
       route.fulfill({
         status: 200,
         contentType: "application/json",
         headers: { "access-control-allow-origin": "*" },
-        body: JSON.stringify([session(parentID, parentTitle, 1700000000000)]),
+        body: JSON.stringify({
+          data: [currentSession(session(parentID, parentTitle, 1700000000000))],
+          cursor: {},
+        }),
       }),
   )
   await configurePage(page)

+ 6 - 0
packages/app/e2e/utils/mock-server.ts

@@ -205,6 +205,12 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
     if (/^\/api\/session\/[^/]+\/permission\/[^/]+\/reply$/.test(path) && route.request().method() === "POST") {
       return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } })
     }
+    if (/^\/question\/[^/]+\/(reply|reject)$/.test(path) && route.request().method() === "POST") {
+      return json(route, true)
+    }
+    if (/^\/session\/[^/]+\/permissions\/[^/]+$/.test(path) && route.request().method() === "POST") {
+      return json(route, true)
+    }
     if (
       /^\/api\/session\/[^/]+\/(archive|rename|interrupt|revert\/clear|revert\/commit)$/.test(path) &&
       route.request().method() === "POST"

+ 3 - 7
packages/app/src/components/dialog-fork.tsx

@@ -69,15 +69,11 @@ export const DialogFork: Component = () => {
     const dir = base64Encode(sdk().directory)
 
     sdk()
-      .client.session.fork({ sessionID, messageID: item.id })
+      .api.session.fork({ sessionID, messageID: item.id })
       .then((forked) => {
-        if (!forked.data) {
-          showToast({ title: language.t("common.requestFailed") })
-          return
-        }
         dialog.close()
-        prompt.set(restored, undefined, { dir, id: forked.data.id })
-        navigate(`/${dir}/session/${forked.data.id}`)
+        prompt.set(restored, undefined, { dir, id: forked.id })
+        navigate(`/${dir}/session/${forked.id}`)
       })
       .catch((err: unknown) => {
         const message = err instanceof Error ? err.message : String(err)

+ 121 - 43
packages/app/src/components/prompt-input/submit.test.ts

@@ -7,6 +7,11 @@ let createPromptSubmit: typeof import("./submit").createPromptSubmit
 
 const createdClients: string[] = []
 const createdSessions: string[] = []
+const sessionCreateInputs: Array<{
+  agent?: string
+  model?: { id: string; providerID: string; variant?: string }
+  location?: { directory: string }
+}> = []
 const enabledAutoAccept: Array<{ server: string; sessionID: string; directory: string }> = []
 const optimistic: Array<{
   directory?: string
@@ -19,11 +24,15 @@ const optimistic: Array<{
 }> = []
 const optimisticSeeded: boolean[] = []
 const storedSessions: Record<string, Array<{ id: string; title?: string }>> = {}
-const sessionDirectories: Record<string, string> = {}
 const promoted: Array<{ directory: string; sessionID: string }> = []
-const sentShell: string[] = []
+const sentShell: Array<{ sessionID: string; id?: string; command: string }> = []
 const syncedDirectories: string[] = []
 const promotedDrafts: Array<{ draftID: string; server: string; sessionId: string }> = []
+const sentPrompts: string[] = []
+const promptInputs: unknown[] = []
+const sentCommands: unknown[] = []
+const commands: Array<{ name: string }> = []
+let serverSessionSyncs = 0
 
 let params: { id?: string } = {}
 let search: { draftId?: string } = {}
@@ -32,7 +41,7 @@ let variant: string | undefined
 let permissionServer = "server-a"
 let createSessionGate: Promise<void> | undefined
 
-const promptValue: Prompt = [{ type: "text", content: "ls", start: 0, end: 2 }]
+let promptValue: Prompt = [{ type: "text", content: "ls", start: 0, end: 2 }]
 const [promptStore, setPromptStore] = createStore<PromptStore>({
   prompt: promptValue,
   cursor: 0,
@@ -64,23 +73,39 @@ const prompt = {
 const clientFor = (directory: string) => {
   createdClients.push(directory)
   return {
-    session: {
-      create: async () => {
-        await createSessionGate
-        createdSessions.push(directory)
-        return {
-          data: {
+    api: {
+      session: {
+        create: async (input: (typeof sessionCreateInputs)[number]) => {
+          await createSessionGate
+          const location = input.location?.directory ?? directory
+          createdSessions.push(location)
+          sessionCreateInputs.push(input)
+          return {
             id: `session-${createdSessions.length}`,
+            projectID: "project",
+            agent: input.agent,
+            model: input.model,
+            cost: 0,
+            tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
+            time: { created: 1, updated: 1 },
             title: `New session ${createdSessions.length}`,
-          },
-        }
-      },
-      shell: async () => {
-        sentShell.push(directory)
-        return { data: undefined }
+            location: { directory: location },
+          }
+        },
+        prompt: async (input: unknown) => {
+          sentPrompts.push(directory)
+          promptInputs.push(input)
+          return { data: undefined }
+        },
+        command: async (input: unknown) => {
+          sentCommands.push(input)
+        },
+        shell: async (input: { sessionID: string; id?: string; command: string }) => {
+          sentShell.push(input)
+        },
       },
-      prompt: async () => ({ data: undefined }),
-      promptAsync: async () => ({ data: undefined }),
+    },
+    session: {
       command: async () => ({ data: undefined }),
       abort: async () => ({ data: undefined }),
     },
@@ -90,27 +115,6 @@ const clientFor = (directory: string) => {
   }
 }
 
-const api = {
-  session: {
-    async create(input: { location: { directory: string } }) {
-      await createSessionGate
-      createdSessions.push(input.location.directory)
-      const session = {
-        id: `session-${createdSessions.length}`,
-        title: `New session ${createdSessions.length}`,
-      }
-      sessionDirectories[session.id] = input.location.directory
-      return session
-    },
-    async shell(input: { sessionID: string }) {
-      sentShell.push(sessionDirectories[input.sessionID] ?? "/repo/main")
-    },
-    async prompt() {},
-    async command() {},
-    async interrupt() {},
-  },
-}
-
 beforeAll(async () => {
   const rootClient = clientFor("/repo/main")
 
@@ -193,8 +197,8 @@ beforeAll(async () => {
       const sdk = {
         scope: "local",
         directory: "/repo/main",
-        api,
         client: rootClient,
+        api: rootClient.api,
         url: "http://localhost:4096",
         createClient(opts: any) {
           return clientFor(opts.directory)
@@ -206,7 +210,7 @@ beforeAll(async () => {
 
   mock.module("@/context/sync", () => ({
     useSync: () => () => ({
-      data: { command: [] },
+      data: { command: commands },
       session: {
         optimistic: {
           add: (value: {
@@ -233,6 +237,9 @@ beforeAll(async () => {
       session: {
         remember: () => undefined,
         set: () => undefined,
+        sync: async () => {
+          serverSessionSyncs++
+        },
       },
       child: (directory: string) => {
         syncedDirectories.push(directory)
@@ -274,11 +281,17 @@ beforeAll(async () => {
 beforeEach(() => {
   createdClients.length = 0
   createdSessions.length = 0
+  sessionCreateInputs.length = 0
   enabledAutoAccept.length = 0
   optimistic.length = 0
   optimisticSeeded.length = 0
   promoted.length = 0
   promotedDrafts.length = 0
+  sentPrompts.length = 0
+  promptInputs.length = 0
+  sentCommands.length = 0
+  commands.length = 0
+  promptValue = [{ type: "text", content: "ls", start: 0, end: 2 }]
   params = {}
   search = {}
   sentShell.length = 0
@@ -287,8 +300,8 @@ beforeEach(() => {
   variant = undefined
   permissionServer = "server-a"
   createSessionGate = undefined
+  serverSessionSyncs = 0
   for (const key of Object.keys(storedSessions)) delete storedSessions[key]
-  for (const key of Object.keys(sessionDirectories)) delete sessionDirectories[key]
 })
 
 describe("prompt submit worktree selection", () => {
@@ -321,8 +334,24 @@ describe("prompt submit worktree selection", () => {
 
     expect(createdClients).toEqual(["/repo/worktree-a", "/repo/worktree-b"])
     expect(createdSessions).toEqual(["/repo/worktree-a", "/repo/worktree-b"])
-    expect(sentShell).toEqual(["/repo/worktree-a", "/repo/worktree-b"])
+    expect(sessionCreateInputs).toEqual([
+      {
+        agent: "agent",
+        model: { id: "model", providerID: "provider", variant: undefined },
+        location: { directory: "/repo/worktree-a" },
+      },
+      {
+        agent: "agent",
+        model: { id: "model", providerID: "provider", variant: undefined },
+        location: { directory: "/repo/worktree-b" },
+      },
+    ])
+    expect(sentShell).toEqual([
+      expect.objectContaining({ sessionID: "session-1", id: expect.stringMatching(/^evt_/), command: "ls" }),
+      expect.objectContaining({ sessionID: "session-2", id: expect.stringMatching(/^evt_/), command: "ls" }),
+    ])
     expect(syncedDirectories).toEqual(["/repo/worktree-a", "/repo/worktree-a", "/repo/worktree-b", "/repo/worktree-b"])
+    expect(serverSessionSyncs).toBe(0)
     expect(promoted).toEqual([
       { directory: "/repo/worktree-a", sessionID: "session-1" },
       { directory: "/repo/worktree-b", sessionID: "session-2" },
@@ -443,6 +472,7 @@ describe("prompt submit worktree selection", () => {
     const event = { preventDefault: () => undefined } as unknown as Event
 
     await submit.handleSubmit(event)
+    await Bun.sleep(0)
 
     expect(optimistic).toHaveLength(1)
     expect(optimistic[0]).toMatchObject({
@@ -451,6 +481,53 @@ describe("prompt submit worktree selection", () => {
         model: { providerID: "provider", modelID: "model", variant: "high" },
       },
     })
+    expect(sentPrompts).toEqual(["/repo/main"])
+    expect(promptInputs[0]).toMatchObject({
+      sessionID: "session-1",
+      text: "ls",
+      files: [],
+      agents: [],
+    })
+    expect((promptInputs[0] as { id?: string }).id).toStartWith("msg_")
+  })
+
+  test("submits slash commands through the current session API", async () => {
+    params = { id: "session-1" }
+    variant = "high"
+    commands.push({ name: "review" })
+    promptValue = [{ type: "text", content: "/review staged changes", start: 0, end: 22 }]
+
+    const submit = createPromptSubmit({
+      prompt,
+      info: () => ({ id: "session-1" }),
+      imageAttachments: () => [],
+      commentCount: () => 0,
+      autoAccept: () => false,
+      mode: () => "normal",
+      working: () => false,
+      editor: () => undefined,
+      queueScroll: () => undefined,
+      promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0),
+      addToHistory: () => undefined,
+      resetHistoryNavigation: () => undefined,
+      setMode: () => undefined,
+      setPopover: () => undefined,
+    })
+
+    await submit.handleSubmit({ preventDefault: () => undefined } as unknown as Event)
+
+    expect(sentCommands).toEqual([
+      {
+        sessionID: "session-1",
+        id: expect.stringMatching(/^msg_/),
+        command: "review",
+        arguments: "staged changes",
+        agent: "agent",
+        model: { id: "model", providerID: "provider", variant: "high" },
+        files: [],
+      },
+    ])
+    expect(serverSessionSyncs).toBe(0)
   })
 
   test("uses an injected model selection", async () => {
@@ -511,7 +588,8 @@ describe("prompt submit worktree selection", () => {
 
     await submit.handleSubmit(event)
 
-    expect(storedSessions["/repo/worktree-a"]).toEqual([{ id: "session-1", title: "New session 1" }])
+    expect(storedSessions["/repo/worktree-a"]).toHaveLength(1)
+    expect(storedSessions["/repo/worktree-a"]?.[0]).toMatchObject({ id: "session-1", title: "New session 1" })
     expect(optimisticSeeded).toEqual([true])
   })
 })

+ 23 - 15
packages/app/src/context/permission.tsx

@@ -13,6 +13,7 @@ import { type DraftTab, useTabs } from "./tabs"
 import { useSettings } from "./settings"
 import { requireServerKey } from "@/utils/session-route"
 import type { ServerScope } from "@/utils/server-scope"
+import { normalizePermissionRequest } from "./global-sync/utils"
 import {
   acceptKey,
   directoryAcceptKey,
@@ -243,9 +244,20 @@ function createServerPermissionState(input: { sdk: ServerSDK; sync: ServerSync }
 
   const respond: PermissionRespondFn = (request) => {
     if (meta.disposed) return
-    input.sdk.client.permission.respond(request).catch(() => {
-      responded.delete(request.permissionID)
-    })
+    input.sdk.api.permission
+      .reply({ sessionID: request.sessionID, requestID: request.permissionID, reply: request.response })
+      .catch(() => {
+        responded.delete(request.permissionID)
+      })
+  }
+
+  const list = async (directory: string) => {
+    if ((await input.sdk.protocol) === "v1") {
+      return (await input.sdk.client.permission.list({ directory })).data ?? []
+    }
+    return input.sdk.api.permission.request
+      .list({ location: { directory } })
+      .then((result) => result.data.map(normalizePermissionRequest))
   }
 
   function respondOnce(permission: PermissionRequest, directory?: string) {
@@ -343,14 +355,12 @@ function createServerPermissionState(input: { sdk: ServerSDK; sync: ServerSync }
       }),
     )
 
-    input.sdk.client.permission
-      .list({ directory })
-      .then((x) => {
+    list(directory)
+      .then((permissions) => {
         if (meta.disposed) return
         if (!isAutoAcceptingDirectory(directory)) return
-        for (const perm of x.data ?? []) {
-          if (!perm?.id) continue
-          void respondPending(perm, directory, () => isAutoAcceptingDirectory(directory))
+        for (const permission of permissions) {
+          void respondPending(permission, directory, () => isAutoAcceptingDirectory(directory))
         }
       })
       .catch(() => undefined)
@@ -377,16 +387,14 @@ function createServerPermissionState(input: { sdk: ServerSDK; sync: ServerSync }
       }),
     )
 
-    input.sdk.client.permission
-      .list({ directory })
-      .then((x) => {
+    list(directory)
+      .then((permissions) => {
         if (meta.disposed) return
         if (enableVersion.get(key) !== version) return
         if (!isAutoAccepting(sessionID, directory)) return
-        for (const perm of x.data ?? []) {
-          if (!perm?.id) continue
+        for (const permission of permissions) {
           void respondPending(
-            perm,
+            permission,
             directory,
             () => enableVersion.get(key) === version && isAutoAccepting(sessionID, directory),
           )

+ 2 - 2
packages/app/src/pages/home-session-archive.test.ts

@@ -19,7 +19,7 @@ test("archiving a Home session removes its open titlebar tab", async () => {
   await archiveHomeSession({
     server: remote,
     session: { id: "ses_1", directory: "/workspace" },
-    update: async () => undefined,
+    archive: async () => undefined,
     remove: () => {
       removed = true
     },
@@ -37,7 +37,7 @@ test("reports archive failures without removing the session", async () => {
   await archiveHomeSession({
     server: remote,
     session: { id: "ses_1", directory: "/workspace" },
-    update: async () => Promise.reject(failure),
+    archive: async () => Promise.reject(failure),
     remove: () => {
       removed = true
     },

+ 2 - 12
packages/app/src/pages/home-session-archive.ts

@@ -6,25 +6,15 @@ type HomeSession = {
   directory: string
 }
 
-type SessionUpdate = {
-  directory: string
-  sessionID: string
-  time: { archived: number }
-}
-
 export async function archiveHomeSession(input: {
   server: ServerConnection.Key
   session: HomeSession
-  update: (value: SessionUpdate) => Promise<unknown>
+  archive: (sessionID: string) => Promise<unknown>
   remove: () => void
   onError?: (error: unknown) => void
 }) {
   await input
-    .update({
-      directory: input.session.directory,
-      sessionID: input.session.id,
-      time: { archived: Date.now() },
-    })
+    .archive(input.session.id)
     .then(() => {
       input.remove()
       notifySessionTabsRemoved({

+ 1 - 1
packages/app/src/pages/home.tsx

@@ -606,7 +606,7 @@ export function NewHome() {
     await archiveHomeSession({
       server: ServerConnection.key(conn),
       session,
-      update: (value) => ctx.sdk.client.session.update(value),
+      archive: (sessionID) => ctx.sdk.api.session.archive({ sessionID, directory: session.directory }),
       remove: () =>
         setStore(
           produce((draft) => {

+ 2 - 1
packages/app/src/pages/session/composer/session-composer-controls.ts

@@ -45,7 +45,8 @@ export function createPromptInputController(input: {
     model: {
       selection: input.model ?? local.model,
       paid: providers.paid().length > 0,
-      loading: agentsQuery.isLoading || providersQuery.isLoading || globalProvidersQuery.isLoading,
+      loading:
+        (local.agent.visible() && agentsQuery.isLoading) || providersQuery.isLoading || globalProvidersQuery.isLoading,
     },
     session: {
       id: input.sessionID(),

+ 1 - 1
packages/app/src/pages/session/composer/session-composer-state.ts

@@ -82,7 +82,7 @@ export function createSessionComposerController(options?: { closeMs?: number | (
 
     setStore("responding", perm.id)
     sdk()
-      .client.permission.respond({ sessionID: perm.sessionID, permissionID: perm.id, response })
+      .api.permission.reply({ sessionID: perm.sessionID, requestID: perm.id, reply: response })
       .catch((err: unknown) => {
         const description = err instanceof Error ? err.message : String(err)
         showToast({ title: language.t("common.requestFailed"), description })

+ 3 - 2
packages/app/src/pages/session/composer/session-question-dock.tsx

@@ -223,7 +223,8 @@ 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().api.question.reply({ sessionID: props.request.sessionID, requestID: props.request.id, answers }),
     onMutate: () => {
       props.onSubmit()
     },
@@ -235,7 +236,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
   }))
 
   const rejectMutation = useMutation(() => ({
-    mutationFn: () => sdk().client.question.reject({ requestID: props.request.id }),
+    mutationFn: () => sdk().api.question.reject({ sessionID: props.request.sessionID, requestID: props.request.id }),
     onMutate: () => {
       props.onSubmit()
     },

+ 10 - 11
packages/app/src/pages/session/use-session-commands.tsx

@@ -5,7 +5,6 @@ import { previewSelectedLines } from "@opencode-ai/session-ui/pierre/selection-b
 import { useFile, selectionFromLines, type FileSelection, type SelectedLineRange } from "@/context/file"
 import { useLanguage } from "@/context/language"
 import { useLayout } from "@/context/layout"
-import { useLocal } from "@/context/local"
 import { usePermission } from "@/context/permission"
 import { usePrompt } from "@/context/prompt"
 import { useSDK } from "@/context/sdk"
@@ -19,6 +18,7 @@ import { extractPromptFromParts } from "@/utils/prompt"
 import { UserMessage } from "@opencode-ai/sdk/v2"
 import { useSessionLayout } from "@/pages/session/session-layout"
 import { createSessionOwnership } from "./session-ownership"
+import { useLocal } from "@/context/local"
 
 export type SessionCommandContext = {
   navigateMessageByOffset: (offset: number) => void
@@ -40,7 +40,6 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
   const dialog = useDialog()
   const file = useFile()
   const language = useLanguage()
-  const local = useLocal()
   const permission = usePermission()
   const prompt = usePrompt()
   const sdk = useSDK()
@@ -48,6 +47,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
   const sync = useSync()
   const terminal = useTerminal()
   const layout = useLayout()
+  const local = useLocal()
   const navigate = useNavigate()
   const { params, sessionKey, tabs, view } = useSessionLayout()
   const sessionOwnership = createSessionOwnership(sessionKey)
@@ -306,7 +306,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
     const sessionID = params.id
     if (!sessionID) return
     const owner = sessionOwnership.capture()
-    const client = sdk().client
+    const session = sdk().api.session
     const directory = sdk().directory
     const promptSession = prompt.capture()
     const revert = info()?.revert?.messageID
@@ -316,13 +316,13 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
     const parts = sync().data.part[message.id]
 
     if (sync().data.session_working(sessionID)) {
-      await client.session.abort({ sessionID }).catch(() => {})
+      await session.interrupt({ sessionID }).catch(() => {})
     }
 
     await runCommand({
       owner,
       prompt: promptSession,
-      request: () => client.session.revert({ sessionID, messageID: message.id }),
+      request: () => session.revert.stage({ sessionID, messageID: message.id }),
       updatePrompt: (promptSession) => {
         if (parts) promptSession.set(extractPromptFromParts(parts, { directory }))
       },
@@ -334,7 +334,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
     const sessionID = params.id
     if (!sessionID) return
     const owner = sessionOwnership.capture()
-    const client = sdk().client
+    const session = sdk().api.session
     const messages = userMessages()
     const promptSession = prompt.capture()
 
@@ -346,7 +346,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
       await runCommand({
         owner,
         prompt: promptSession,
-        request: () => client.session.unrevert({ sessionID }),
+        request: () => session.revert.clear({ sessionID }),
         updatePrompt: (promptSession) => promptSession.reset(),
         updateViewport: () => setActiveMessage(findLast(messages, (x) => x.id >= revertMessageID)),
       })
@@ -356,7 +356,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
     await runCommand({
       owner,
       prompt: promptSession,
-      request: () => client.session.revert({ sessionID, messageID: next.id }),
+      request: () => session.revert.stage({ sessionID, messageID: next.id }),
       updatePrompt: () => undefined,
       updateViewport: () => setActiveMessage(findLast(messages, (x) => x.id < next.id)),
     })
@@ -375,10 +375,9 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
       return
     }
 
-    await sdk().client.session.summarize({
+    await sdk().api.session.compact({
       sessionID,
-      modelID: model.id,
-      providerID: model.provider.id,
+      model: { providerID: model.provider.id, modelID: model.id },
     })
   }