Procházet zdrojové kódy

refactor(app): gate unsupported v2 capabilities (#40381)

Brendan Allan před 1 týdnem
rodič
revize
76b318e990
35 změnil soubory, kde provedl 402 přidání a 339 odebrání
  1. 4 4
      packages/app/e2e/regression/session-timeline-context-resize.spec.ts
  2. 1 1
      packages/app/src/components/dialog-custom-provider.tsx
  3. 2 5
      packages/app/src/components/dialog-select-directory-v2.tsx
  4. 2 5
      packages/app/src/components/dialog-select-directory.tsx
  5. 1 7
      packages/app/src/components/edit-project.ts
  6. 11 14
      packages/app/src/components/settings-general.tsx
  7. 1 3
      packages/app/src/components/settings-providers.tsx
  8. 11 14
      packages/app/src/components/settings-v2/general.tsx
  9. 1 3
      packages/app/src/components/settings-v2/providers.tsx
  10. 10 6
      packages/app/src/components/status-popover-body.tsx
  11. 1 2
      packages/app/src/context/directory-sync.ts
  12. 4 34
      packages/app/src/context/global-sync/bootstrap.test.ts
  13. 29 67
      packages/app/src/context/global-sync/bootstrap.ts
  14. 4 1
      packages/app/src/context/global-sync/child-store.ts
  15. 0 11
      packages/app/src/context/global-sync/session-load.ts
  16. 1 1
      packages/app/src/context/layout.tsx
  17. 0 3
      packages/app/src/context/permission.tsx
  18. 16 1
      packages/app/src/context/server-sdk.tsx
  19. 11 5
      packages/app/src/context/server-session.ts
  20. 24 32
      packages/app/src/context/server-sync.tsx
  21. 1 0
      packages/app/src/pages/home/home-projects-controller.tsx
  22. 6 3
      packages/app/src/pages/home/home-projects-view.tsx
  23. 1 0
      packages/app/src/pages/home/home-projects.tsx
  24. 7 9
      packages/app/src/pages/home/home-sessions-controller.tsx
  25. 5 2
      packages/app/src/pages/home/home-sessions-view.tsx
  26. 1 0
      packages/app/src/pages/home/home-sessions.tsx
  27. 27 37
      packages/app/src/pages/layout.tsx
  28. 2 1
      packages/app/src/pages/layout/sidebar-items.tsx
  29. 8 3
      packages/app/src/pages/layout/sidebar-project.tsx
  30. 13 6
      packages/app/src/pages/layout/sidebar-workspace.tsx
  31. 15 9
      packages/app/src/pages/session.tsx
  32. 16 13
      packages/app/src/pages/session/timeline/message-timeline.tsx
  33. 5 2
      packages/app/src/pages/session/use-session-commands.tsx
  34. 21 1
      packages/app/src/utils/server-compat.test.ts
  35. 140 34
      packages/app/src/utils/server-compat.ts

+ 4 - 4
packages/app/e2e/regression/session-timeline-context-resize.spec.ts

@@ -14,8 +14,8 @@ const projectID = "proj_context_resize_regression"
 const sessionID = "ses_context_resize_regression"
 const title = "Context resize regression"
 const model = { providerID: "opencode", modelID: "claude-opus-4-6", variant: "max" }
-const contextIDs = ["ctx_0100_read", "ctx_0101_glob", "ctx_0102_grep", "ctx_0103_list"]
-const followingTextID = `${id("msg_assistant", 10)}:text:0`
+const contextIDs = ["prt_0100_read", "prt_0101_glob", "prt_0102_grep", "prt_0103_list"]
+const followingTextID = "prt_0104_text"
 
 type Message = {
   info: Record<string, unknown> & { id: string; role: "user" | "assistant" }
@@ -263,7 +263,7 @@ function turn(index: number, target: boolean, status: "running" | "completed" =
             ),
             contextTool(contextIDs[3]!, assistantID, "list", { path: "src" }, status),
             {
-              id: "prt_0104_text",
+              id: followingTextID,
               sessionID,
               messageID: assistantID,
               type: "text",
@@ -295,7 +295,7 @@ function contextTool(
     sessionID,
     messageID,
     type: "tool",
-    callID: partID,
+    callID: `call_${partID}`,
     tool,
     state: {
       status,

+ 1 - 1
packages/app/src/components/dialog-custom-provider.tsx

@@ -136,7 +136,7 @@ export function CustomProviderForm(props: { autofocus?: boolean } = {}) {
       const nextDisabled = disabledProviders.filter((id) => id !== result.providerID)
 
       if (result.key) {
-        await serverSDK().client.auth.set({
+        await serverSDK().legacy.auth.set({
           providerID: result.providerID,
           auth: {
             type: "api",

+ 2 - 5
packages/app/src/components/dialog-select-directory-v2.tsx

@@ -71,11 +71,8 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) {
     () => (missingBase() ? true : undefined),
     async (): Promise<Path | undefined> => {
       if ((await sdk.protocol) === "v1")
-        return sdk.client.path
-          .get()
-          .then((result) => result.data)
-          .catch(() => undefined)
-      return sdk.currentApi.location
+        return sdk.legacy.path.get().catch(() => undefined)
+      return sdk.api.location
         .get()
         .then((location) => ({
           state: "",

+ 2 - 5
packages/app/src/components/dialog-select-directory.tsx

@@ -62,11 +62,8 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
     () => (missingBase() ? true : undefined),
     async (): Promise<Path | undefined> => {
       if ((await sdk.protocol) === "v1")
-        return sdk.client.path
-          .get()
-          .then((result) => result.data)
-          .catch(() => undefined)
-      return sdk.currentApi.location
+        return sdk.legacy.path.get().catch(() => undefined)
+      return sdk.api.location
         .get()
         .then((location) => ({
           state: "",

+ 1 - 7
packages/app/src/components/edit-project.ts

@@ -73,7 +73,7 @@ export function createEditProjectModel(props: { project: LocalProject; server: S
       if (props.project.id && props.project.id !== "global") {
         if ((await serverCtx().sdk.protocol) !== "v1") return
         const project = await serverCtx()
-          .sdk.client.project.update({
+          .sdk.legacy.project.update({
             projectID: props.project.id,
             directory: props.project.worktree,
             name,
@@ -82,12 +82,6 @@ export function createEditProjectModel(props: { project: LocalProject; server: S
           })
           .then((result) => result.data)
         if (!project) return
-        // const project = await serverCtx().sdk.api.project.update({
-        //   projectID: props.project.id,
-        //   name,
-        //   icon: { color: store.color || "", override: store.iconOverride || "" },
-        //   commands: { start },
-        // })
         serverCtx().sync.set("project", (items) =>
           items.map((item) => (item.id === project.id ? normalizeProjectInfo(project) : item)),
         )

+ 11 - 14
packages/app/src/components/settings-general.tsx

@@ -13,7 +13,7 @@ import { useLanguage } from "@/context/language"
 import { usePermission } from "@/context/permission"
 import { usePlatform, type DisplayBackend } from "@/context/platform"
 import { useServerSync } from "@/context/server-sync"
-import { useServerSDK } from "@/context/server-sdk"
+import { useServerProtocol, useServerSDK } from "@/context/server-sdk"
 import { useUpdaterAction } from "./updater-action"
 import {
   monoDefault,
@@ -125,16 +125,11 @@ export const SettingsGeneral: Component = () => {
 
   const serverSync = useServerSync()
   const serverSdk = useServerSDK()
+  const protocol = useServerProtocol()
 
   const [shells] = createResource(
-    async () => {
-      const sdk = serverSdk()
-      if ((await sdk.protocol) === "v1") {
-        return (await sdk.client.pty.shells()).data ?? []
-      }
-      // return (await sdk.api.pty.shells()).data
-      return [] as ShellOption[]
-    },
+    () => (protocol() === "v1" ? serverSdk() : undefined),
+    (sdk) => sdk.legacy.pty.shells().catch(() => [] as ShellOption[]),
     { initialValue: [] as ShellOption[] },
   )
 
@@ -325,10 +320,11 @@ export const SettingsGeneral: Component = () => {
           </div>
         </SettingsRow>
 
-        <SettingsRow
-          title={language.t("settings.general.row.shell.title")}
-          description={language.t("settings.general.row.shell.description")}
-        >
+        <Show when={protocol() === "v1"}>
+          <SettingsRow
+            title={language.t("settings.general.row.shell.title")}
+            description={language.t("settings.general.row.shell.description")}
+          >
           <Select
             data-action="settings-shell"
             options={shellOptions()}
@@ -345,7 +341,8 @@ export const SettingsGeneral: Component = () => {
             triggerVariant="settings"
             triggerStyle={{ "min-width": "180px" }}
           />
-        </SettingsRow>
+          </SettingsRow>
+        </Show>
 
         <SettingsRow
           title={language.t("settings.general.row.reasoningSummaries.title")}

+ 1 - 3
packages/app/src/components/settings-providers.tsx

@@ -122,9 +122,7 @@ const SettingsProvidersContent: Component<{ onBack?: () => void }> = (props) =>
 
   const disconnect = async (providerID: string, name: string) => {
     if (isConfigCustom(providerID)) {
-      await serverSDK()
-        .client.auth.remove({ providerID })
-        .catch(() => undefined)
+      await serverSDK().legacy.auth.remove({ providerID }).catch(() => undefined)
       await disableProvider(providerID, name)
       return
     }

+ 11 - 14
packages/app/src/components/settings-v2/general.tsx

@@ -10,7 +10,7 @@ import { useLanguage } from "@/context/language"
 import { usePermission } from "@/context/permission"
 import { usePlatform } from "@/context/platform"
 import { useServerSync } from "@/context/server-sync"
-import { useServerSDK } from "@/context/server-sdk"
+import { useServerProtocol, useServerSDK } from "@/context/server-sdk"
 import { useUpdaterAction } from "../updater-action"
 import {
   monoDefault,
@@ -92,6 +92,7 @@ export const SettingsGeneralV2: Component<{
   const settings = useSettings()
   const serverSync = useServerSync()
   const serverSdk = useServerSDK()
+  const protocol = useServerProtocol()
   const mobile = createMediaQuery("(max-width: 767px)")
 
   const updater = useUpdaterAction()
@@ -122,14 +123,8 @@ export const SettingsGeneralV2: Component<{
   const themeOptions = createMemo<ThemeOption[]>(() => theme.ids().map((id) => ({ id, name: theme.name(id) })))
 
   const [shells] = createResource(
-    async () => {
-      const sdk = serverSdk()
-      if ((await sdk.protocol) === "v1") {
-        return (await sdk.client.pty.shells()).data ?? []
-      }
-      // return (await sdk.api.pty.shells()).data
-      return [] as ShellOption[]
-    },
+    () => (protocol() === "v1" ? serverSdk() : undefined),
+    (sdk) => sdk.legacy.pty.shells().catch(() => [] as ShellOption[]),
     { initialValue: [] as ShellOption[] },
   )
 
@@ -284,10 +279,11 @@ export const SettingsGeneralV2: Component<{
           </div>
         </SettingsRowV2>
 
-        <SettingsRowV2
-          title={language.t("settings.general.row.shell.title")}
-          description={language.t("settings.general.row.shell.description")}
-        >
+        <Show when={protocol() === "v1"}>
+          <SettingsRowV2
+            title={language.t("settings.general.row.shell.title")}
+            description={language.t("settings.general.row.shell.description")}
+          >
           <SelectV2
             appearance="inline"
             data-action="settings-shell"
@@ -303,7 +299,8 @@ export const SettingsGeneralV2: Component<{
               serverSync().updateConfig({ shell: option.value })
             }}
           />
-        </SettingsRowV2>
+          </SettingsRowV2>
+        </Show>
 
         <SettingsRowV2
           title={language.t("settings.general.row.reasoningSummaries.title")}

+ 1 - 3
packages/app/src/components/settings-v2/providers.tsx

@@ -119,9 +119,7 @@ export const SettingsProvidersV2: Component<{
 
   const disconnect = async (providerID: string, name: string) => {
     if (isConfigCustom(providerID)) {
-      await serverSdk()
-        .client.auth.remove({ providerID })
-        .catch(() => undefined)
+      await serverSdk().legacy.auth.remove({ providerID }).catch(() => undefined)
       await disableProvider(providerID, name)
       return
     }

+ 10 - 6
packages/app/src/components/status-popover-body.tsx

@@ -318,10 +318,12 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
             {mcpConnected() > 0 ? `${mcpConnected()} ` : ""}
             {language.t("status.popover.tab.mcp")}
           </Tabs.Trigger>
-          <Tabs.Trigger value="lsp" data-slot="tab" class="text-12-regular">
-            {lspCount() > 0 ? `${lspCount()} ` : ""}
-            {language.t("status.popover.tab.lsp")}
-          </Tabs.Trigger>
+          <Show when={protocol() === "v1"}>
+            <Tabs.Trigger value="lsp" data-slot="tab" class="text-12-regular">
+              {lspCount() > 0 ? `${lspCount()} ` : ""}
+              {language.t("status.popover.tab.lsp")}
+            </Tabs.Trigger>
+          </Show>
           <Show when={protocol() === "v1"}>
             <Tabs.Trigger value="plugins" data-slot="tab" class="text-12-regular">
               {pluginCount() > 0 ? `${pluginCount()} ` : ""}
@@ -459,7 +461,8 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
           </div>
         </Tabs.Content>
 
-        <Tabs.Content value="lsp">
+        <Show when={protocol() === "v1"}>
+          <Tabs.Content value="lsp">
           <div class="flex flex-col px-2 pb-2">
             <div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14">
               <Show
@@ -485,7 +488,8 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
               </Show>
             </div>
           </div>
-        </Tabs.Content>
+          </Tabs.Content>
+        </Show>
 
         <Show when={protocol() === "v1"}>
           <Tabs.Content value="plugins">

+ 1 - 2
packages/app/src/context/directory-sync.ts

@@ -134,8 +134,7 @@ export const createDirSyncContext = (
       },
       more: createMemo(() => current()[0].session.length >= current()[0].limit),
       archive: async (sessionID: string) => {
-        if ((await serverSDK.protocol) !== "v1") return
-        await serverSDK.client.session.update({ sessionID, directory, time: { archived: Date.now() } })
+        await serverSDK.legacy.session.archive(sessionID, directory)
         current()[1](
           "session",
           produce((draft) => {

+ 4 - 34
packages/app/src/context/global-sync/bootstrap.test.ts

@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"
 import { createStore } from "solid-js/store"
 import { QueryClient } from "@tanstack/solid-query"
 import type { Config, Project } from "@/types"
-import type { OpencodeClient } from "@opencode-ai/sdk/v2/client"
+import type { LegacyCapabilities } from "@/utils/server-compat"
 import type { AgentApi, CatalogApi, CommandApi, ReferenceApi } from "@opencode-ai/client/promise"
 import type { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
 import {
@@ -111,36 +111,7 @@ describe("bootstrapDirectory", () => {
         project: [{ id: "project", worktree: "/project" } as Project],
         provider,
       },
-      sdk: {
-        app: { agents: async () => ({ data: [{ name: "build", mode: "primary" }] }) },
-        config: { get: async () => ({ data: {} }) },
-        session: { status: async () => ({ data: {} }) },
-        vcs: { get: async () => ({ data: undefined }) },
-        command: {
-          list: async () => {
-            mcpReads.push("command")
-            return { data: [] }
-          },
-        },
-        permission: { list: async () => ({ data: [] }) },
-        question: { list: async () => ({ data: [] }) },
-        v2: { reference: { list: async () => ({ data: { data: [] } }) } },
-        mcp: {
-          status: async () => {
-            mcpReads.push("status")
-            return { data: {} }
-          },
-        },
-        experimental: {
-          resource: {
-            list: async () => {
-              mcpReads.push("resource")
-              return { data: {} }
-            },
-          },
-        },
-        provider: { list: async () => ({ data: { all: [], connected: [], default: {} } }) },
-      } as unknown as OpencodeClient,
+      legacy: { config: { directory: async () => ({}) } } as unknown as LegacyCapabilities,
       api: currentApi,
       store,
       setStore,
@@ -163,16 +134,15 @@ describe("bootstrapDirectory", () => {
 describe("query keys", () => {
   test("partitions identical directories by server scope", () => {
     const location = {} as Parameters<typeof loadPathQuery>[2]
-    const client = {} as Parameters<typeof loadPathQuery>[3]
     const api = {} as CatalogApi
     const remote = "https://debian.example" as typeof ServerScope.local
 
-    expect([...loadPathQuery(ServerScope.local, "/repo", location, client).queryKey]).toEqual([
+    expect([...loadPathQuery(ServerScope.local, "/repo", location).queryKey]).toEqual([
       "local",
       "/repo",
       "path",
     ])
-    expect([...loadPathQuery(remote, "/repo", location, client).queryKey]).toEqual([
+    expect([...loadPathQuery(remote, "/repo", location).queryKey]).toEqual([
       "https://debian.example",
       "/repo",
       "path",

+ 29 - 67
packages/app/src/context/global-sync/bootstrap.ts

@@ -8,7 +8,7 @@ import type {
   ReferenceInfo,
   Session,
 } from "@/types"
-import type { OpencodeClient } from "@opencode-ai/sdk/v2/client"
+import type { LegacyCapabilities } from "@/utils/server-compat"
 import type {
   AgentListInput,
   AgentListOutput,
@@ -107,10 +107,11 @@ function showErrors(input: {
   })
 }
 
-export const loadGlobalConfigQuery = (scope: ServerScope, sdk: OpencodeClient) =>
+export const loadGlobalConfigQuery = (scope: ServerScope, legacy: LegacyCapabilities, enabled = true) =>
   queryOptions({
     queryKey: [scope, "config"],
-    queryFn: () => retry(() => sdk.global.config.get().then((x) => x.data!)),
+    queryFn: () => retry(() => legacy.config.global()),
+    enabled,
   })
 
 type ProjectApi = {
@@ -141,7 +142,7 @@ export const loadProjectsQuery = (scope: ServerScope, api: ProjectApi) =>
   })
 
 export async function bootstrapGlobal(input: {
-  serverSDK: OpencodeClient
+  legacy: LegacyCapabilities
   serverAPI: CatalogApi & { readonly location: LocationApi; readonly project: ProjectApi }
   protocol?: Promise<ServerProtocol>
   scope: ServerScope
@@ -151,21 +152,22 @@ export async function bootstrapGlobal(input: {
   setGlobalStore: SetStoreFunction<GlobalStore>
   queryClient: QueryClient
 }) {
+  const protocol = await input.protocol
   const slow = [
-    () => input.queryClient.fetchQuery(loadGlobalConfigQuery(input.scope, input.serverSDK)),
+    protocol === "v1" && (() => input.queryClient.fetchQuery(loadGlobalConfigQuery(input.scope, input.legacy))),
     () =>
       input.queryClient.fetchQuery(
         loadProvidersQuery(input.scope, null, input.serverAPI),
       ),
     () =>
       input.queryClient.fetchQuery(
-        loadPathQuery(input.scope, null, input.serverAPI.location, input.serverSDK, input.protocol),
+        loadPathQuery(input.scope, null, input.serverAPI.location),
       ),
     () =>
       input.queryClient
         .fetchQuery(loadProjectsQuery(input.scope, input.serverAPI.project))
         .then((data) => input.setGlobalStore("project", data)),
-  ]
+  ].filter(Boolean) as Array<() => Promise<unknown>>
   await runAll(slow)
   // showErrors({
   //   errors: errors(),
@@ -273,22 +275,17 @@ export const loadPathQuery = (
   scope: ServerScope,
   directory: string | null,
   api: LocationApi,
-  sdk: OpencodeClient,
-  protocol?: Promise<ServerProtocol>,
 ) =>
   queryOptions<Path>({
     queryKey: [scope, directory, "path"],
-    queryFn: async () => {
-      if ((await protocol) === "v1")
-        return retry(() => sdk.path.get({ directory: directory ?? undefined }).then((result) => result.data!))
-      return retry(() => api.get(directory ? { location: { directory } } : undefined)).then((location) => ({
+    queryFn: () =>
+      retry(() => api.get(directory ? { location: { directory } } : undefined)).then((location) => ({
         state: "",
         config: "",
         worktree: location.project.directory,
         directory: location.directory,
         home: "",
-      }))
-    },
+      })),
   })
 
 export const loadReferencesQuery = (
@@ -307,7 +304,7 @@ export async function bootstrapDirectory(input: {
   directory: string
   scope: ServerScope
   mcp: boolean
-  sdk: OpencodeClient
+  legacy: LegacyCapabilities
   api: CatalogApi & {
     readonly agent: AgentListApi
     readonly command: CommandListApi
@@ -355,35 +352,13 @@ export async function bootstrapDirectory(input: {
         input.queryClient
           .ensureQueryData(loadAgentsQuery(input.scope, input.directory, input.api.agent))
           .then((data) => input.setStore("agent", data)),
-      () =>
-        retry(() => input.sdk.config.get().then((x) => input.setStore("config", reconcile(x.data!, { merge: false })))),
-      () =>
-        retry(() =>
-          (async () => {
-            if ((await input.protocol) !== "v1") return
-            const x = await input.sdk.session.status()
-            if (!input.session) {
-              input.setStore("session_status", x.data!)
-              return
-            }
-            const statuses = x.data ?? {}
-            input.session.set(
-              "session_status",
-              produce((draft) => {
-                for (const sessionID of Object.keys(draft)) {
-                  if (statuses[sessionID]) continue
-                  if (input.session?.get(sessionID)?.directory === input.directory) delete draft[sessionID]
-                }
-              }),
-            )
-            for (const [sessionID, status] of Object.entries(statuses)) {
-              input.session.set("session_status", sessionID, reconcile(status))
-            }
-            await Promise.all(
-              Object.keys(statuses).map((sessionID) => input.session!.resolve(sessionID).catch(() => undefined)),
-            )
-          })(),
-        ),
+      (await input.protocol) === "v1" &&
+        (() =>
+          retry(() =>
+            input.legacy.config
+              .directory(input.directory)
+              .then((config) => input.setStore("config", reconcile(config, { merge: false }))),
+          )),
       !seededProject &&
         (() =>
           retry(() => input.api.project.current({ location: { directory: input.directory } })).then((project) =>
@@ -393,21 +368,12 @@ export async function bootstrapDirectory(input: {
         (() =>
           input.queryClient
             .ensureQueryData(
-              loadPathQuery(input.scope, input.directory, input.api.location, input.sdk, input.protocol),
+              loadPathQuery(input.scope, input.directory, input.api.location),
             )
             .then((data) => {
               const next = projectID(data.directory ?? input.directory, input.global.project)
               if (next) input.setStore("project", next)
             })),
-      () =>
-        retry(async () => {
-          if ((await input.protocol) !== "v1") return
-          return input.sdk.vcs.get().then((result) => {
-            const next = { branch: result.data?.branch, default_branch: result.data?.default_branch }
-            input.setStore("vcs", next)
-            if (next) input.vcsCache.setStore("value", next)
-          })
-        }),
       input.mcp &&
         (() =>
           loadCommands(input.directory, input.api.command).then((commands) =>
@@ -419,12 +385,10 @@ export async function bootstrapDirectory(input: {
         ),
       () =>
         retry(() =>
-          (async () => {
-            if ((await input.protocol) === "v1") return (await input.sdk.permission.list()).data ?? []
-            return input.api.permission.request
-              .list({ location: { directory: input.directory } })
-              .then((result) => result.data.map(normalizePermissionRequest))
-          })().then((permissions) => {
+          input.api.permission.request
+            .list({ location: { directory: input.directory } })
+            .then((result) => result.data.map(normalizePermissionRequest))
+            .then((permissions) => {
             const ids = permissions.map((permission) => permission.sessionID)
             const grouped = groupBySession(
               permissions.filter((permission) => !!permission.id && !!permission.sessionID),
@@ -455,12 +419,10 @@ export async function bootstrapDirectory(input: {
         ),
       () =>
         retry(() =>
-          (async () => {
-            if ((await input.protocol) === "v1") return (await input.sdk.question.list()).data ?? []
-            return input.api.question.request
-              .list({ location: { directory: input.directory } })
-              .then((result) => result.data)
-          })().then((questions) => {
+          input.api.question.request
+            .list({ location: { directory: input.directory } })
+            .then((result) => result.data)
+            .then((questions) => {
             const ids = questions.map((question) => question.sessionID)
             const grouped = groupBySession(
               questions.filter((question) => !!question.id && !!question.sessionID) as QuestionRequest[],

+ 4 - 1
packages/app/src/context/global-sync/child-store.ts

@@ -191,7 +191,10 @@ export function createChildStoreManager(input: {
           const pathQuery = useQuery(() => ({ ...input.queryOptions.path(key), enabled: instanceQueriesEnabled() }))
           const mcpQuery = useQuery(() => ({ ...input.queryOptions.mcp(key), enabled: mcpEnabled() }))
           const mcpResourceQuery = useQuery(() => ({ ...input.queryOptions.mcpResources(key), enabled: mcpEnabled() }))
-          const lspQuery = useQuery(() => ({ ...input.queryOptions.lsp(key), enabled: instanceQueriesEnabled() }))
+          const lspQuery = useQuery(() => {
+            const options = input.queryOptions.lsp(key)
+            return { ...options, enabled: options.enabled !== false && instanceQueriesEnabled() }
+          })
           const providerQuery = useQuery(() => ({
             ...input.queryOptions.providers(key),
             enabled: instanceQueriesEnabled(),

+ 0 - 11
packages/app/src/context/global-sync/session-load.ts

@@ -1,6 +1,5 @@
 import type { SessionApi } from "@opencode-ai/client/promise"
 import { normalizeSessionInfo } from "@/utils/session"
-import type { OpencodeClient } from "@opencode-ai/sdk/v2/client"
 
 export async function loadRootSessions(input: { api: Pick<SessionApi, "list">; directory: string; limit: number }) {
   const result = await input.api.list({
@@ -16,16 +15,6 @@ export async function loadRootSessions(input: { api: Pick<SessionApi, "list">; d
   } as const
 }
 
-export async function loadRootSessionsV1(input: { client: OpencodeClient; directory: string; limit: number }) {
-  try {
-    const result = await input.client.session.list({ directory: input.directory, roots: true, limit: input.limit })
-    return { data: result.data, limit: input.limit, limited: true } as const
-  } catch {
-    const result = await input.client.session.list({ directory: input.directory, roots: true })
-    return { data: result.data, limit: input.limit, limited: false } as const
-  }
-}
-
 export function estimateRootSessionTotal(input: { count: number; limit: number; limited: boolean }) {
   if (!input.limited) return input.count
   if (input.count < input.limit) return input.count

+ 1 - 1
packages/app/src/context/layout.tsx

@@ -574,7 +574,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
         void (async () => {
           const sdk = serverSdk()
           if ((await sdk.protocol) !== "v1") return
-          return sdk.client.project
+          return sdk.legacy.project
             .update({ projectID, directory: worktree, icon: { color } })
             .then((response) => response.data)
             .then((result) => {

+ 0 - 3
packages/app/src/context/permission.tsx

@@ -258,9 +258,6 @@ function createServerPermissionState(input: { sdk: ServerSDK; sync: ServerSync }
   }
 
   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))

+ 16 - 1
packages/app/src/context/server-sdk.tsx

@@ -12,7 +12,12 @@ import { createRefCountMap } from "@/utils/refcount"
 import { useGlobal } from "./global"
 import { ServerScope } from "@/utils/server-scope"
 import { detectServerProtocol, type ServerProtocol } from "@/utils/server-protocol"
-import { createCompatibleApi, type CompatibleApi } from "@/utils/server-compat"
+import {
+  createCompatibleApi,
+  createLegacyCapabilities,
+  type CompatibleApi,
+  type LegacyCapabilities,
+} from "@/utils/server-compat"
 import type { OpencodeClient } from "@opencode-ai/sdk/v2/client"
 
 const isAbortError = (error: unknown) =>
@@ -166,6 +171,7 @@ type ServerSDKBase = {
   url: string
   client: ReturnType<typeof createSdkForServer>
   api: CompatibleApi
+  legacy: LegacyCapabilities
   currentApi: ServerApi
   event: {
     on: ServerEventEmitter["on"]
@@ -329,6 +335,7 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS
       directory,
     })
   const api = createCompatibleApi({ protocol, current: currentApi, legacy })
+  const capabilities = createLegacyCapabilities({ protocol, current: currentApi, legacy })
 
   return {
     server,
@@ -338,6 +345,7 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS
     url: server.http.url,
     client: sdk,
     api,
+    legacy: capabilities,
     currentApi,
     event: {
       on: emitter.on.bind(emitter),
@@ -365,6 +373,7 @@ export type DirectorySDK = {
   client: OpencodeClient
   currentApi: ServerApi
   api: CompatibleApi
+  legacy: LegacyCapabilities
   event: ReturnType<typeof createGlobalEmitter<SDKEventMap>>
   readonly url: string
   createClient: ServerSDKBase["createClient"]
@@ -428,6 +437,12 @@ function createDirSdkContext(directory: string, serverSDK: ServerSDKBase): Direc
       legacy: (next) => serverSDK.createClient({ directory: next ?? directory, throwOnError: true }),
       directory,
     }),
+    legacy: createLegacyCapabilities({
+      protocol: serverSDK.protocol,
+      current: serverSDK.currentApi,
+      legacy: (next) => serverSDK.createClient({ directory: next ?? directory, throwOnError: true }),
+      directory,
+    }),
     event: emitter,
     get url() {
       return serverSDK.url

+ 11 - 5
packages/app/src/context/server-session.ts

@@ -10,7 +10,7 @@ import type {
   SessionStatus,
   Todo,
 } from "@/types"
-import type { OpencodeClient } from "@opencode-ai/sdk/v2/client"
+import type { LegacyCapabilities } from "@/utils/server-compat"
 import type { FileDiffInfo } from "@opencode-ai/client/promise"
 import { batch } from "solid-js"
 import { createStore, produce, reconcile } from "solid-js/store"
@@ -183,10 +183,14 @@ function reconcileFetched<T extends { id: string }>(
   return [...result.values()].sort((a, b) => cmp(a.id, b.id))
 }
 
-type ServerSessionOptions = { retry?: typeof retry; protocol?: Promise<"v1" | "v2"> }
+type ServerSessionOptions = {
+  retry?: typeof retry
+  protocol?: Promise<"v1" | "v2">
+  legacy?: LegacyCapabilities
+}
 
 export function createServerSession(
-  client: OpencodeClient,
+  client: { session: Pick<LegacyCapabilities["session"], "get" | "messages" | "message"> },
   sessionApiOrOptions?: SessionApi | ServerSessionOptions,
   messageApi?: MessageApi,
   currentOptions?: ServerSessionOptions,
@@ -1389,14 +1393,16 @@ export function createServerSession(
       touch(sessionID)
       if (data.todo[sessionID] !== undefined && !request?.force) return
       if ((await options?.protocol) === "v2") {
+        // TODO: Restore todos when the V2 API exposes a session todo snapshot.
         setData("todo", sessionID, [])
         return
       }
       return runInflight(inflightTodo, sessionID, () => {
         const active = generation(sessionID)
-        return (options?.retry ?? retry)(() => client.session.todo({ sessionID })).then((result) => {
+        if (!options?.legacy) return Promise.resolve()
+        return (options.retry ?? retry)(() => options.legacy!.session.todo(sessionID)).then((result) => {
           if (generations.get(sessionID) !== active) return
-          setData("todo", sessionID, reconcile(result.data ?? [], { key: "id" }))
+          setData("todo", sessionID, reconcile(result, { key: "id" }))
         })
       })
     },

+ 24 - 32
packages/app/src/context/server-sync.tsx

@@ -5,7 +5,6 @@ import type {
   ProviderAuthResponse,
   SessionStatus,
 } from "@/types"
-import type { OpencodeClient } from "@opencode-ai/sdk/v2/client"
 import { showToast } from "@/utils/toast"
 import { getFilename } from "@opencode-ai/core/util/path"
 import { type Accessor, batch, createMemo, getOwner, onCleanup, onMount, untrack } from "solid-js"
@@ -60,6 +59,7 @@ import type {
 import { toggleMcp } from "./global-sync/mcp"
 import { createServerSession, type ServerSession } from "./server-session"
 import { usePlatform } from "./platform"
+import type { LegacyCapabilities } from "@/utils/server-compat"
 
 type GlobalStore = {
   ready: boolean
@@ -132,10 +132,11 @@ export const loadMcpResourcesQuery = (
     placeholderData: {},
   })
 
-export const loadLspQuery = (scope: ServerScope, directory: string, sdk: OpencodeClient) =>
+export const loadLspQuery = (scope: ServerScope, directory: string, legacy: LegacyCapabilities, enabled = true) =>
   queryOptions({
     queryKey: [scope, directory, "lsp"] as const,
-    queryFn: () => sdk.lsp.status().then((r) => r.data ?? []),
+    queryFn: () => legacy.lsp.status(directory),
+    enabled,
   })
 
 export const loadActiveSessionsQuery = (
@@ -166,22 +167,20 @@ export function seedActiveSessionStatuses(
 
 function makeQueryOptionsApi(
   scope: ServerScope,
-  serverSDK: () => OpencodeClient,
   serverAPI: ServerApi,
-  sdkFor: (dir: PathKey) => OpencodeClient,
-  protocol: Promise<"v1" | "v2">,
+  protocolKind: Accessor<"v1" | "v2" | undefined>,
+  legacy: LegacyCapabilities,
 ) {
   return {
-    globalConfig: () => loadGlobalConfigQuery(scope, serverSDK()),
+    globalConfig: () => loadGlobalConfigQuery(scope, legacy, protocolKind() === "v1"),
     projects: () => loadProjectsQuery(scope, serverAPI.project),
     providers: (directory: PathKey | null) => loadProvidersQuery(scope, directory, serverAPI),
-    path: (directory: PathKey | null) =>
-      loadPathQuery(scope, directory, serverAPI.location, directory ? sdkFor(directory) : serverSDK(), protocol),
+    path: (directory: PathKey | null) => loadPathQuery(scope, directory, serverAPI.location),
     agents: (directory: PathKey) => loadAgentsQuery(scope, directory, serverAPI.agent),
     references: (directory: PathKey) => loadReferencesQuery(scope, directory, serverAPI.reference),
     mcp: (directory: PathKey) => loadMcpQuery(scope, directory, serverAPI.mcp),
     mcpResources: (directory: PathKey) => loadMcpResourcesQuery(scope, directory, serverAPI.mcp),
-    lsp: (directory: PathKey) => loadLspQuery(scope, directory, sdkFor(directory)),
+    lsp: (directory: PathKey) => loadLspQuery(scope, directory, legacy, protocolKind() === "v1"),
     sessions: (directory: PathKey) => ({ queryKey: [scope, directory, "loadSessions"] as const }),
   }
 }
@@ -193,30 +192,24 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
   const owner = getOwner()
   if (!owner) throw new Error("ServerSync must be created within owner")
 
-  const sdkCache = new Map<string, OpencodeClient>()
   const booting = new Map<string, Promise<void>>()
   const sessionLoads = new Map<string, Promise<void>>()
   const sessionMeta = new Map<string, { limit: number }>()
 
-  const sdkFor = (directory: string) => {
-    const key = directoryKey(directory)
-    const cached = sdkCache.get(key)
-    if (cached) return cached
-    const sdk = serverSDK.createClient({
-      directory,
-      throwOnError: true,
-    })
-    sdkCache.set(key, sdk)
-    return sdk
-  }
-
-  const session = createServerSession(serverSDK.client, serverSDK.currentApi.session, serverSDK.currentApi.message)
+  const session = createServerSession(
+    { session: serverSDK.legacy.session },
+    serverSDK.currentApi.session,
+    serverSDK.currentApi.message,
+    {
+      protocol: serverSDK.protocol,
+      legacy: serverSDK.legacy,
+    },
+  )
   const queryOptionsApi = makeQueryOptionsApi(
     serverSDK.scope,
-    () => serverSDK.client,
     serverSDK.currentApi,
-    sdkFor,
-    serverSDK.protocol,
+    serverSDK.protocolKind,
+    serverSDK.legacy,
   )
 
   const [configQuery, providerQuery, pathQuery] = useQueries(() => ({
@@ -293,7 +286,7 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
     queryKey: [serverSDK.scope, "bootstrap"],
     queryFn: async () => {
       await bootstrapGlobal({
-        serverSDK: serverSDK.client,
+        legacy: serverSDK.legacy,
         serverAPI: serverSDK.currentApi,
         protocol: serverSDK.protocol,
         scope: serverSDK.scope,
@@ -349,7 +342,6 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
       const key = directoryKey(directory)
       queue.clear(key)
       sessionMeta.delete(key)
-      sdkCache.delete(key)
       clearProviderRev(serverSDK.scope, key)
     },
     translate: language.t,
@@ -446,7 +438,6 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
       const child = children.ensureChild(directory)
       const cache = children.vcsCache.get(key)
       if (!cache) return
-      const sdk = sdkFor(directory)
       await bootstrapDirectory({
         directory,
         scope: serverSDK.scope,
@@ -457,7 +448,7 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
           project: globalStore.project,
           provider: globalStore.provider,
         },
-        sdk,
+        legacy: serverSDK.legacy,
         api: serverSDK.currentApi,
         store: child[0],
         setStore: child[1],
@@ -577,6 +568,7 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
       permission: session.data.permission,
       vcsCache: children.vcsCache.get(key),
       loadLsp: () => {
+        if (serverSDK.protocolKind() !== "v1") return
         if (!children.active(key)) return
         void queryClient.fetchQuery(queryOptionsApi.lsp(key))
       },
@@ -625,7 +617,7 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
   }
 
   const updateConfigMutation = useMutation(() => ({
-    mutationFn: (config: Config) => serverSDK.client.global.config.update({ config }),
+    mutationFn: (config: Config) => serverSDK.legacy.config.update(config),
     onSuccess: () => {
       bootstrap.refetch()
       // Invalidate all provider queries so newly configured custom providers

+ 1 - 0
packages/app/src/pages/home/home-projects-controller.tsx

@@ -109,6 +109,7 @@ export function createHomeProjectsController(home: HomeController) {
         home.server.context(conn).projects.move(worktree, index)
       },
       canReveal: canRevealProject,
+      canEdit: (conn: ServerConnection.Any) => home.server.context(conn).sdk.protocolKind() === "v1",
       reveal: (conn: ServerConnection.Any, project: LocalProject) => {
         if (!platform.openPath || !canRevealProject(conn)) return
         platform.openPath(project.worktree).catch((cause: unknown) =>

+ 6 - 3
packages/app/src/pages/home/home-projects-view.tsx

@@ -40,6 +40,7 @@ export type HomeProjectsViewProps = {
   canDefaultServer: Accessor<boolean>
   defaultServerKey: Accessor<ServerConnection.Key | null | undefined>
   canRevealProject: (server: ServerConnection.Any) => boolean
+  canEditProject: (server: ServerConnection.Any) => boolean
   unseenCount: (server: ServerConnection.Any, project: LocalProject) => number
   onWheel: (event: WheelEvent) => void
   onChooseProject: (server: ServerConnection.Any) => void
@@ -548,9 +549,11 @@ function HomeProjectRow(
               <MenuV2.Item onSelect={() => props.onOpenProjectNewSession(props.server, props.project.worktree)}>
                 {props.language.t("command.session.new")}
               </MenuV2.Item>
-              <MenuV2.Item onSelect={() => props.onEditProject(props.server, props.project)}>
-                {props.language.t("dialog.project.edit.title")}
-              </MenuV2.Item>
+              <Show when={props.canEditProject(props.server)}>
+                <MenuV2.Item onSelect={() => props.onEditProject(props.server, props.project)}>
+                  {props.language.t("dialog.project.edit.title")}
+                </MenuV2.Item>
+              </Show>
               <Show when={props.canRevealProject(props.server)}>
                 <MenuV2.Item onSelect={() => props.onRevealProject(props.server, props.project)}>
                   {props.language.t(

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

@@ -17,6 +17,7 @@ export function HomeProjects(props: { projects: HomeProjectsController; scroll:
       canDefaultServer={props.projects.server.canDefault}
       defaultServerKey={props.projects.server.defaultKey}
       canRevealProject={props.projects.project.canReveal}
+      canEditProject={props.projects.project.canEdit}
       unseenCount={props.projects.project.unseenCount}
       onWheel={props.scroll.viewport.containWheel}
       onChooseProject={props.projects.project.choose}

+ 7 - 9
packages/app/src/pages/home/home-sessions-controller.tsx

@@ -1,4 +1,4 @@
-import type { Session } from "@/types"
+import type { Session, V2SessionListResponse } from "@/types"
 import { preloadMarkdown } from "@opencode-ai/session-ui/markdown-cache"
 import { useDialog } from "@opencode-ai/ui/context/dialog"
 import { useMarked } from "@opencode-ai/ui/context/marked"
@@ -69,7 +69,10 @@ export function createHomeSessionsController(home: HomeController) {
       const cache = homeSessions()
       const eventSequence = cache.eventSequence()
       const index = await loadHomeSessionIndex(
-        (input, options) => ctx.sdk.client.v2.session.list(input, options),
+        (input, options) =>
+          ctx.sdk.currentApi.session.list(input, options).then((data) => ({
+            data: data as unknown as V2SessionListResponse,
+          })),
         eventSequence,
         signal,
       )
@@ -179,6 +182,7 @@ export function createHomeSessionsController(home: HomeController) {
       showProjectName: () => !home.project.selected(),
       server: () => home.selection.value().server,
       canCreate: () => !!home.project.newSession(),
+      canArchive: () => home.server.focusedContext()?.sdk.protocolKind() === "v1",
       create: home.project.openNewSession,
       open: (session: Session, options?: OpenSessionOptions) => {
         const directoryKey = pathKey(session.directory)
@@ -211,16 +215,10 @@ export function createHomeSessionsController(home: HomeController) {
         const ctx = home.server.focusedContext()
         if (!conn || !ctx) return
         const [, setStore] = ctx.sync.child(session.directory)
-        if ((await ctx.sdk.protocol) !== "v1") return
         await archiveHomeSession({
           server: ServerConnection.key(conn),
           session,
-          archive: (sessionID) =>
-            ctx.sdk.client.session.update({
-              sessionID,
-              directory: session.directory,
-              time: { archived: Date.now() },
-            }),
+          archive: (sessionID) => ctx.sdk.legacy.session.archive(sessionID, session.directory),
           remove: () =>
             setStore(
               produce((draft) => {

+ 5 - 2
packages/app/src/pages/home/home-sessions-view.tsx

@@ -43,6 +43,7 @@ export type HomeSessionsViewProps = {
   showProjectName: Accessor<boolean>
   server: Accessor<ServerConnection.Key>
   canCreateSession: Accessor<boolean>
+  canArchiveSession: Accessor<boolean>
   searchValue: Accessor<string>
   searchPlaceholder: Accessor<string>
   searchOpen: Accessor<boolean>
@@ -460,7 +461,8 @@ function HomeSessionRow(props: HomeSessionsViewProps & { record: HomeSessionReco
             group-hover/session:opacity-100 focus-within:opacity-100
           `}
         >
-          <TooltipV2 class="flex shrink-0 items-center" placement="bottom" value={props.language.t("common.archive")}>
+          <Show when={props.canArchiveSession()}>
+            <TooltipV2 class="flex shrink-0 items-center" placement="bottom" value={props.language.t("common.archive")}>
             <IconButtonV2
               data-action="home-session-archive"
               variant="ghost-muted"
@@ -473,7 +475,8 @@ function HomeSessionRow(props: HomeSessionsViewProps & { record: HomeSessionReco
                 void props.onArchiveSession(props.record.session)
               }}
             />
-          </TooltipV2>
+            </TooltipV2>
+          </Show>
         </div>
       </Show>
     </div>

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

@@ -16,6 +16,7 @@ export function HomeSessions(props: {
       showProjectName={props.sessions.session.showProjectName}
       server={props.sessions.session.server}
       canCreateSession={props.sessions.session.canCreate}
+      canArchiveSession={props.sessions.session.canArchive}
       searchValue={props.search.query.value}
       searchPlaceholder={props.search.query.placeholder}
       searchOpen={props.search.query.open}

+ 27 - 37
packages/app/src/pages/layout.tsx

@@ -872,17 +872,12 @@ export default function LegacyLayout(props: ParentProps) {
   }
 
   async function archiveSession(session: Session) {
-    if ((await serverSDK().protocol) !== "v1") return
     const [store, setStore] = serverSync().child(session.directory)
     const sessions = store.session ?? []
     const index = sessions.findIndex((s) => s.id === session.id)
     const nextSession = sessions[index + 1] ?? sessions[index - 1]
 
-    await serverSDK().client.session.update({
-      sessionID: session.id,
-      directory: session.directory,
-      time: { archived: Date.now() },
-    })
+    await serverSDK().legacy.session.archive(session.id, session.directory)
     setStore(
       produce((draft) => {
         const match = Binary.search(draft.session, session.id, (s) => s.id)
@@ -980,6 +975,7 @@ export default function LegacyLayout(props: ParentProps) {
         title: language.t("command.session.archive"),
         category: language.t("command.category.session"),
         keybind: "mod+shift+backspace",
+        hidden: serverSDK().protocolKind() !== "v1",
         disabled: !params.dir || !params.id,
         onSelect: () => {
           const session = currentSessions().find((s) => s.id === params.id)
@@ -1304,13 +1300,10 @@ export default function LegacyLayout(props: ParentProps) {
     const name = next === getFilename(project.worktree) ? "" : next
 
     if (project.id && project.id !== "global") {
-      const sdk = serverSDK()
-      if ((await sdk.protocol) !== "v1") return
-      const result = await sdk.client.project
+      const result = await serverSDK().legacy.project
         .update({ projectID: project.id, directory: project.worktree, name })
         .then((response) => response.data)
       if (!result) return
-      // const result = await serverSDK().api.project.update({ projectID: project.id, name })
       serverSync().set("project", (items) =>
         items.map((item) => (item.id === result.id ? normalizeProjectInfo(result) : item)),
       )
@@ -1477,12 +1470,8 @@ export default function LegacyLayout(props: ParentProps) {
       platform,
       serverSDK().scope,
     )
-    await serverSDK()
-      .client.instance.dispose({ directory })
-      .catch(() => undefined)
-
     const result = await serverSDK()
-      .client.worktree.reset({ directory: root, worktreeResetInput: { directory } })
+      .legacy.workspace.reset(root, directory)
       .then((x) => x.data)
       .catch((err) => {
         showToast({
@@ -1504,11 +1493,7 @@ export default function LegacyLayout(props: ParentProps) {
           .filter((session) => session.time.archived === undefined)
           .map((session) =>
             serverSDK()
-              .client.session.update({
-                sessionID: session.id,
-                directory: session.directory,
-                time: { archived: Date.now() },
-              })
+              .legacy.session.archive(session.id, session.directory)
               .catch(() => undefined),
           ),
       )
@@ -1895,6 +1880,8 @@ export default function LegacyLayout(props: ParentProps) {
     clearHoverProjectSoon,
     prefetchSession,
     archiveSession,
+    canArchive: () => serverSDK().protocolKind() === "v1",
+    canResetWorkspace: () => serverSDK().protocolKind() === "v1",
     workspaceName,
     renameWorkspace,
     editorOpen,
@@ -1931,6 +1918,7 @@ export default function LegacyLayout(props: ParentProps) {
     openSidebar: () => layout.sidebar.open(),
     closeProject,
     showEditProjectDialog: (proj) => showEditProjectDialog(server.current!, proj),
+    canEditProject: () => serverSDK().protocolKind() === "v1",
     toggleProjectWorkspaces,
     workspacesEnabled: (project) => project.vcs === "git" && layout.sidebar.workspaces(project.worktree)(),
     workspaceIds,
@@ -1941,6 +1929,7 @@ export default function LegacyLayout(props: ParentProps) {
       clearHoverProjectSoon,
       prefetchSession,
       archiveSession,
+      canArchive: () => serverSDK().protocolKind() === "v1",
     },
   }
 
@@ -2031,16 +2020,19 @@ export default function LegacyLayout(props: ParentProps) {
               <div class="shrink-0 pl-1 py-1">
                 <div class="group/project flex items-start justify-between gap-2 py-2 pl-2 pr-0">
                   <div class="flex flex-col min-w-0">
-                    <InlineEditor
-                      id={`project:${projectId()}`}
-                      value={projectName}
-                      onSave={(next) => {
-                        void renameProject(project, next)
-                      }}
-                      class="text-14-medium text-text-strong truncate"
-                      displayClass="text-14-medium text-text-strong truncate"
-                      stopPropagation
-                    />
+                    <Show
+                      when={serverSDK().protocolKind() === "v1" || !project.id || project.id === "global"}
+                      fallback={<span class="text-14-medium text-text-strong truncate">{projectName()}</span>}
+                    >
+                      <InlineEditor
+                        id={`project:${projectId()}`}
+                        value={projectName}
+                        onSave={(next) => void renameProject(project, next)}
+                        class="text-14-medium text-text-strong truncate"
+                        displayClass="text-14-medium text-text-strong truncate"
+                        stopPropagation
+                      />
+                    </Show>
 
                     <Tooltip
                       placement="bottom"
@@ -2075,13 +2067,11 @@ export default function LegacyLayout(props: ParentProps) {
                     />
                     <DropdownMenu.Portal>
                       <DropdownMenu.Content class="mt-1">
-                        <DropdownMenu.Item
-                          onSelect={() => {
-                            showEditProjectDialog(server.current!, project)
-                          }}
-                        >
-                          <DropdownMenu.ItemLabel>{language.t("common.edit")}</DropdownMenu.ItemLabel>
-                        </DropdownMenu.Item>
+                        <Show when={serverSDK().protocolKind() === "v1"}>
+                          <DropdownMenu.Item onSelect={() => showEditProjectDialog(server.current!, project)}>
+                            <DropdownMenu.ItemLabel>{language.t("common.edit")}</DropdownMenu.ItemLabel>
+                          </DropdownMenu.Item>
+                        </Show>
                         <DropdownMenu.Item
                           data-action="project-workspaces-toggle"
                           data-project={slug()}

+ 2 - 1
packages/app/src/pages/layout/sidebar-items.tsx

@@ -87,6 +87,7 @@ export type SessionItemProps = {
   clearHoverProjectSoon: () => void
   prefetchSession: (session: Session, priority?: "high" | "low") => void
   archiveSession: (session: Session) => Promise<void>
+  canArchive: Accessor<boolean>
 }
 
 const SessionRow = (props: {
@@ -241,7 +242,7 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
             </Show>
           </div>
 
-          <Show when={!props.level}>
+          <Show when={!props.level && props.canArchive()}>
             <div
               class="shrink-0 overflow-hidden transition-[width,opacity]"
               classList={{

+ 8 - 3
packages/app/src/pages/layout/sidebar-project.tsx

@@ -27,6 +27,7 @@ export type ProjectSidebarContext = {
   openSidebar: () => void
   closeProject: (directory: string) => void
   showEditProjectDialog: (project: LocalProject) => void
+  canEditProject: Accessor<boolean>
   toggleProjectWorkspaces: (project: LocalProject) => void
   workspacesEnabled: (project: LocalProject) => boolean
   workspaceIds: (project: LocalProject) => string[]
@@ -65,6 +66,7 @@ const ProjectTile = (props: {
   onProjectFocus: (worktree: string) => void
   navigateToProject: (directory: string) => void
   showEditProjectDialog: (project: LocalProject) => void
+  canEditProject: Accessor<boolean>
   toggleProjectWorkspaces: (project: LocalProject) => void
   workspacesEnabled: (project: LocalProject) => boolean
   closeProject: (directory: string) => void
@@ -148,9 +150,11 @@ const ProjectTile = (props: {
       </ContextMenu.Trigger>
       <ContextMenu.Portal>
         <ContextMenu.Content>
-          <ContextMenu.Item onSelect={() => props.showEditProjectDialog(props.project)}>
-            <ContextMenu.ItemLabel>{props.language.t("common.edit")}</ContextMenu.ItemLabel>
-          </ContextMenu.Item>
+          <Show when={props.canEditProject()}>
+            <ContextMenu.Item onSelect={() => props.showEditProjectDialog(props.project)}>
+              <ContextMenu.ItemLabel>{props.language.t("common.edit")}</ContextMenu.ItemLabel>
+            </ContextMenu.Item>
+          </Show>
           <ContextMenu.Item
             data-action="project-workspaces-toggle"
             data-project={base64Encode(props.project.worktree)}
@@ -331,6 +335,7 @@ export const SortableProject = (props: {
       onProjectFocus={props.ctx.onProjectFocus}
       navigateToProject={props.ctx.navigateToProject}
       showEditProjectDialog={props.ctx.showEditProjectDialog}
+      canEditProject={props.ctx.canEditProject}
       toggleProjectWorkspaces={props.ctx.toggleProjectWorkspaces}
       workspacesEnabled={props.ctx.workspacesEnabled}
       closeProject={props.ctx.closeProject}

+ 13 - 6
packages/app/src/pages/layout/sidebar-workspace.tsx

@@ -42,6 +42,8 @@ export type WorkspaceSidebarContext = {
   clearHoverProjectSoon: () => void
   prefetchSession: (session: Session, priority?: "high" | "low") => void
   archiveSession: (session: Session) => Promise<void>
+  canArchive: Accessor<boolean>
+  canResetWorkspace: Accessor<boolean>
   workspaceName: (directory: string, projectId?: string, branch?: string) => string | undefined
   renameWorkspace: (directory: string, next: string, projectId?: string, branch?: string) => void
   editorOpen: (id: string) => boolean
@@ -151,6 +153,7 @@ const WorkspaceActions = (props: {
   workspaceValue: Accessor<string>
   openEditor: WorkspaceSidebarContext["openEditor"]
   showResetWorkspaceDialog: WorkspaceSidebarContext["showResetWorkspaceDialog"]
+  canResetWorkspace: WorkspaceSidebarContext["canResetWorkspace"]
   showDeleteWorkspaceDialog: WorkspaceSidebarContext["showDeleteWorkspaceDialog"]
   root: string
   clearHoverProjectSoon: WorkspaceSidebarContext["clearHoverProjectSoon"]
@@ -199,12 +202,14 @@ const WorkspaceActions = (props: {
           >
             <DropdownMenu.ItemLabel>{props.language.t("common.rename")}</DropdownMenu.ItemLabel>
           </DropdownMenu.Item>
-          <DropdownMenu.Item
-            disabled={props.local() || props.busy()}
-            onSelect={() => props.showResetWorkspaceDialog(props.root, props.directory)}
-          >
-            <DropdownMenu.ItemLabel>{props.language.t("common.reset")}</DropdownMenu.ItemLabel>
-          </DropdownMenu.Item>
+          <Show when={props.canResetWorkspace()}>
+            <DropdownMenu.Item
+              disabled={props.local() || props.busy()}
+              onSelect={() => props.showResetWorkspaceDialog(props.root, props.directory)}
+            >
+              <DropdownMenu.ItemLabel>{props.language.t("common.reset")}</DropdownMenu.ItemLabel>
+            </DropdownMenu.Item>
+          </Show>
           <DropdownMenu.Item
             disabled={props.local() || props.busy()}
             onSelect={() => props.showDeleteWorkspaceDialog(props.root, props.directory)}
@@ -272,6 +277,7 @@ const WorkspaceSessionList = (props: {
           clearHoverProjectSoon={props.ctx.clearHoverProjectSoon}
           prefetchSession={props.ctx.prefetchSession}
           archiveSession={props.ctx.archiveSession}
+          canArchive={props.ctx.canArchive}
         />
       )}
     </For>
@@ -416,6 +422,7 @@ export const SortableWorkspace = (props: {
                 workspaceValue={workspaceValue}
                 openEditor={props.ctx.openEditor}
                 showResetWorkspaceDialog={props.ctx.showResetWorkspaceDialog}
+                canResetWorkspace={props.ctx.canResetWorkspace}
                 showDeleteWorkspaceDialog={props.ctx.showDeleteWorkspaceDialog}
                 root={props.project.worktree}
                 clearHoverProjectSoon={props.ctx.clearHoverProjectSoon}

+ 15 - 9
packages/app/src/pages/session.tsx

@@ -52,7 +52,7 @@ import { useNotification } from "@/context/notification"
 import { PromptProvider, usePrompt } from "@/context/prompt"
 import { usePlatform } from "@/context/platform"
 import { SDKProvider, useSDK } from "@/context/sdk"
-import { useServerSDK } from "@/context/server-sdk"
+import { useServerProtocol, useServerSDK } from "@/context/server-sdk"
 import { ServerConnection, serverName, useServer } from "@/context/server"
 import { useSettings } from "@/context/settings"
 import { useSync } from "@/context/sync"
@@ -361,6 +361,7 @@ export default function Page() {
   const language = useLanguage()
   const sdk = useSDK()
   const serverSDK = useServerSDK()
+  const protocol = useServerProtocol()
   const settings = useSettings()
   const platform = usePlatform()
   const prompt = usePrompt()
@@ -847,7 +848,7 @@ export default function Page() {
   }
 
   const gitMutation = useMutation(() => ({
-    mutationFn: () => sdk().client.project.initGit(),
+    mutationFn: () => sdk().legacy.project.initGit(sdk().directory),
     onSuccess: (x) => {
       if (!x.data) return
       upsert(x.data)
@@ -896,17 +897,19 @@ export default function Page() {
       () => {
         const id = params.id
         return [
+          protocol(),
           sdk().directory,
           id,
           id ? (sync().data.session_status[id]?.type ?? "idle") : "idle",
           id ? composer.blocked() : false,
         ] as const
       },
-      ([dir, id, status, blocked]) => {
+      ([serverProtocol, dir, id, status, blocked]) => {
         if (todoFrame !== undefined) cancelAnimationFrame(todoFrame)
         if (todoTimer !== undefined) window.clearTimeout(todoTimer)
         todoFrame = undefined
         todoTimer = undefined
+        if (serverProtocol !== "v1") return
         if (!id) return
         if (status === "idle" && !blocked) return
         const cached = untrack(() => sync().data.todo[id] !== undefined)
@@ -1217,11 +1220,13 @@ export default function Page() {
           {language.t("session.review.noVcs.createGit.description")}
         </div>
       </div>
-      <Button size="large" disabled={gitMutation.isPending} onClick={initGit}>
-        {gitMutation.isPending
-          ? language.t("session.review.noVcs.createGit.actionLoading")
-          : language.t("session.review.noVcs.createGit.action")}
-      </Button>
+      <Show when={protocol() === "v1"}>
+        <Button size="large" disabled={gitMutation.isPending} onClick={initGit}>
+          {gitMutation.isPending
+            ? language.t("session.review.noVcs.createGit.actionLoading")
+            : language.t("session.review.noVcs.createGit.action")}
+        </Button>
+      </Show>
     </div>
   )
 
@@ -1254,7 +1259,8 @@ export default function Page() {
       return <div class="px-6 py-4 text-text-weak">{language.t("session.review.loadingChanges")}</div>
     }
     if (reviewMode() === "turn" && nogit()) {
-      return <SessionReviewEmptyNoGitV2 pending={gitMutation.isPending} onInitGit={initGit} />
+      if (protocol() === "v1") return <SessionReviewEmptyNoGitV2 pending={gitMutation.isPending} onInitGit={initGit} />
+      return empty(language.t("session.review.noVcs.createGit.description"))
     }
     return <SessionReviewEmptyChangesV2 />
   }

+ 16 - 13
packages/app/src/pages/session/timeline/message-timeline.tsx

@@ -62,7 +62,7 @@ import { SessionContextUsage } from "@/components/session-context-usage"
 import { useDialog } from "@opencode-ai/ui/context/dialog"
 import { useLanguage } from "@/context/language"
 import { useSessionKey } from "@/pages/session/session-layout"
-import { useServerSDK } from "@/context/server-sdk"
+import { useServerProtocol, useServerSDK } from "@/context/server-sdk"
 import { usePlatform } from "@/context/platform"
 import { useSettings } from "@/context/settings"
 import { useTabs } from "@/context/tabs"
@@ -302,7 +302,8 @@ export function MessageTimeline(props: {
     return displayLabel(session)
   })
   const shareUrl = createMemo(() => info()?.share?.url)
-  const shareEnabled = createMemo(() => sync().data.config.share !== "disabled")
+  const protocol = useServerProtocol()
+  const shareEnabled = createMemo(() => protocol() === "v1" && sync().data.config.share !== "disabled")
   const parentID = createMemo(() => info()?.parentID)
   const parent = createMemo(() => {
     const id = parentID()
@@ -665,14 +666,14 @@ export function MessageTimeline(props: {
   }
 
   const shareMutation = useMutation(() => ({
-    mutationFn: (id: string) => serverSDK().client.session.share({ sessionID: id }),
+    mutationFn: (id: string) => serverSDK().legacy.session.share(id),
     onError: (err) => {
       console.error("Failed to share session", err)
     },
   }))
 
   const unshareMutation = useMutation(() => ({
-    mutationFn: (id: string) => serverSDK().client.session.unshare({ sessionID: id }),
+    mutationFn: (id: string) => serverSDK().legacy.session.unshare(id),
     onError: (err) => {
       console.error("Failed to unshare session", err)
     },
@@ -818,14 +819,12 @@ export function MessageTimeline(props: {
   const archiveSession = async (sessionID: string) => {
     const session = sync().session.get(sessionID)
     if (!session) return
-    if ((await sdk().protocol) !== "v1") return
-
     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.update({ sessionID, directory: sdk().directory, time: { archived: Date.now() } })
+      .legacy.session.archive(sessionID, sdk().directory)
       .then(() => {
         sync().set(
           produce((draft) => {
@@ -1574,9 +1573,11 @@ export function MessageTimeline(props: {
                                     </DropdownMenu.ItemLabel>
                                   </DropdownMenu.Item>
                                 </Show>
-                                <DropdownMenu.Item onSelect={() => void archiveSession(id)}>
-                                  <DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
-                                </DropdownMenu.Item>
+                                <Show when={protocol() === "v1"}>
+                                  <DropdownMenu.Item onSelect={() => void archiveSession(id)}>
+                                    <DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
+                                  </DropdownMenu.Item>
+                                </Show>
                                 <DropdownMenu.Separator />
                                 <DropdownMenu.Item
                                   onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id} />)}
@@ -1645,9 +1646,11 @@ export function MessageTimeline(props: {
                                   {language.t("session.share.action.share")}...
                                 </MenuV2.Item>
                               </Show>
-                              <MenuV2.Item onSelect={() => void archiveSession(id)}>
-                                {language.t("common.archive")}
-                              </MenuV2.Item>
+                              <Show when={protocol() === "v1"}>
+                                <MenuV2.Item onSelect={() => void archiveSession(id)}>
+                                  {language.t("common.archive")}
+                                </MenuV2.Item>
+                              </Show>
                               <MenuV2.Separator />
                               <MenuV2.Item onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id} />)}>
                                 {language.t("common.delete")}...

+ 5 - 2
packages/app/src/pages/session/use-session-commands.tsx

@@ -19,6 +19,7 @@ import type { UserMessage } from "@/types"
 import { useSessionLayout } from "@/pages/session/session-layout"
 import { createSessionOwnership } from "./session-ownership"
 import { useLocal } from "@/context/local"
+import { useServerProtocol } from "@/context/server-sdk"
 
 export type SessionCommandContext = {
   navigateMessageByOffset: (offset: number) => void
@@ -43,6 +44,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
   const permission = usePermission()
   const prompt = usePrompt()
   const sdk = useSDK()
+  const protocol = useServerProtocol()
   const settings = useSettings()
   const sync = useSync()
   const terminal = useTerminal()
@@ -194,7 +196,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
     }
 
     const url = await sdk()
-      .client.session.share({ sessionID })
+      .legacy.session.share(sessionID)
       .then((res) => res.data?.share?.url)
       .catch(() => undefined)
     if (!url) {
@@ -214,7 +216,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
     if (!sessionID) return
 
     await sdk()
-      .client.session.unshare({ sessionID })
+      .legacy.session.unshare(sessionID)
       .then(() =>
         showToast({
           title: language.t("toast.session.unshare.success.title"),
@@ -377,6 +379,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
   }
 
   const shareCmds = () => {
+    if (protocol() !== "v1") return []
     if (sync().data.config.share === "disabled") return []
     return [
       sessionCommand({

+ 21 - 1
packages/app/src/utils/server-compat.test.ts

@@ -4,7 +4,10 @@ import { createCompatibleApi } from "./server-compat"
 
 function setup(
   protocol: "v1" | "v2" | Promise<"v1" | "v2">,
-  responses?: { vcs?: { branch: string; default_branch: string } },
+  responses?: {
+    vcs?: { branch: string; default_branch: string }
+    question?: { id: string; sessionID: string; questions: never[]; tool?: { messageID: string; callID: string } }[]
+  },
 ) {
   const requests: Request[] = []
   const fetcher = Object.assign(
@@ -36,6 +39,8 @@ function setup(
       }
       if (request.method === "GET" && new URL(request.url).pathname === "/vcs")
         return Response.json(responses?.vcs ?? {})
+      if (request.method === "GET" && new URL(request.url).pathname === "/question")
+        return Response.json(responses?.question ?? [])
       if (request.method === "GET") return Response.json([])
       return new Response(undefined, { status: 204 })
     },
@@ -163,6 +168,21 @@ describe("createCompatibleApi", () => {
     expect(new URL(requests[0]!.url).pathname).toBe("/experimental/session")
   })
 
+  test("translates V1 question tool call IDs", async () => {
+    const { api } = setup("v1", {
+      question: [
+        {
+          id: "que_1",
+          sessionID: "ses_1",
+          questions: [],
+          tool: { messageID: "msg_1", callID: "call_1" },
+        },
+      ],
+    })
+
+    expect((await api.question.request.list()).data[0]?.tool).toEqual({ messageID: "msg_1", id: "call_1" })
+  })
+
   /*
   test("projects the V1 default branch", async () => {
     const { api } = setup("v1", { vcs: { branch: "feature", default_branch: "dev" } })

+ 140 - 34
packages/app/src/utils/server-compat.ts

@@ -3,7 +3,6 @@ import type { ServerProtocol } from "./server-protocol"
 import type { AgentPartInput, FilePartInput, Session, TextPartInput } from "@/types"
 import type { OpencodeClient } from "@opencode-ai/sdk/v2/client"
 import type {
-  Project,
   ProjectCurrent,
   SessionApi,
   SessionCommandInput,
@@ -28,7 +27,6 @@ type CompatibleSessionApi = Omit<
   shell: (input: SessionShellInput & LegacyPrompt) => Promise<SessionShellOutput>
   compact: (input: SessionCompactInput & { model?: LegacyPrompt["model"] }) => Promise<SessionCompactOutput>
   rename: (input: Parameters<SessionApi["rename"]>[0] & LegacyLocation) => ReturnType<SessionApi["rename"]>
-  // archive: (input: Parameters<SessionApi["archive"]>[0] & LegacyLocation) => ReturnType<SessionApi["archive"]>
   remove: (input: Parameters<SessionApi["remove"]>[0] & LegacyLocation) => ReturnType<SessionApi["remove"]>
 }
 type CompatiblePermissionApi = Omit<ServerApi["permission"], "reply"> & {
@@ -54,6 +52,99 @@ type CompatibleInput = {
   directory?: string
 }
 
+export function createLegacyCapabilities(input: CompatibleInput) {
+  const directory = (value?: string) => value ?? input.directory
+  const client = (value?: string) => input.legacy(directory(value))
+  const requireV1 = async () => {
+    if ((await input.protocol) !== "v1") throw new Error("This capability is unavailable on V2 servers")
+  }
+
+  return {
+    config: {
+      global: async () => {
+        await requireV1()
+        return (await client().global.config.get()).data ?? {}
+      },
+      directory: async (value?: string) => {
+        await requireV1()
+        return (await client(value).config.get()).data ?? {}
+      },
+      update: async (config: NonNullable<Parameters<LegacyClient["global"]["config"]["update"]>[0]>["config"]) => {
+        await requireV1()
+        return client().global.config.update({ config })
+      },
+    },
+    auth: {
+      set: async (value: Parameters<LegacyClient["auth"]["set"]>[0]) => {
+        await requireV1()
+        return client().auth.set(value)
+      },
+      remove: async (value: Parameters<LegacyClient["auth"]["remove"]>[0]) => {
+        await requireV1()
+        return client().auth.remove(value)
+      },
+    },
+    session: {
+      get: (value: Parameters<LegacyClient["session"]["get"]>[0]) => client().session.get(value),
+      messages: (value: Parameters<LegacyClient["session"]["messages"]>[0]) => client().session.messages(value),
+      message: (value: Parameters<LegacyClient["session"]["message"]>[0]) => client().session.message(value),
+      share: async (sessionID: string) => {
+        await requireV1()
+        return client().session.share({ sessionID })
+      },
+      unshare: async (sessionID: string) => {
+        await requireV1()
+        return client().session.unshare({ sessionID })
+      },
+      archive: async (sessionID: string, value?: string) => {
+        await requireV1()
+        return client(value).session.update({ sessionID, time: { archived: Date.now() } })
+      },
+      todo: async (sessionID: string, value?: string) => {
+        await requireV1()
+        return (await client(value).session.todo({ sessionID })).data ?? []
+      },
+    },
+    project: {
+      update: async (value: Parameters<LegacyClient["project"]["update"]>[0]) => {
+        await requireV1()
+        return client(value.directory).project.update(value)
+      },
+      initGit: async (value?: string) => {
+        await requireV1()
+        return client(value).project.initGit()
+      },
+    },
+    workspace: {
+      reset: async (root: string, value: string) => {
+        await requireV1()
+        await client(value).instance.dispose().catch(() => undefined)
+        return client(root).worktree.reset({ worktreeResetInput: { directory: value } })
+      },
+    },
+    pty: {
+      shells: async () => {
+        await requireV1()
+        return (await client().pty.shells()).data ?? []
+      },
+    },
+    path: {
+      get: async (value?: string) => {
+        await requireV1()
+        return (await client(value).path.get()).data
+      },
+    },
+    lsp: {
+      status: async (value: string) => {
+        await requireV1()
+        return (await client(value).lsp.status()).data ?? []
+      },
+    },
+  }
+}
+
+export type LegacyCapabilities = ReturnType<typeof createLegacyCapabilities>
+
 function mime(uri: string) {
   const match = /^data:([^;,]+)/.exec(uri)
   return match?.[1] ?? "application/octet-stream"
@@ -184,9 +275,6 @@ function createV1Api(input: CompatibleInput): CompatibleApi {
       async rename(value: Parameters<ServerApi["session"]["rename"]>[0] & LegacyLocation) {
         await legacy(value).session.update({ sessionID: value.sessionID, title: value.title })
       },
-      // async archive(value: Parameters<ServerApi["session"]["archive"]>[0] & LegacyLocation) {
-      //   await legacy(value).session.update({ sessionID: value.sessionID, time: { archived: Date.now() } })
-      // },
       async remove(value: Parameters<ServerApi["session"]["remove"]>[0] & LegacyLocation) {
         await legacy(value).session.delete(value)
       },
@@ -313,34 +401,28 @@ function createV1Api(input: CompatibleInput): CompatibleApi {
           canonical: result.data.worktree,
         } satisfies ProjectCurrent
       },
-      // async update(value: Parameters<ServerApi["project"]["update"]>[0]) {
-      //   const project = (await legacy().project.list()).data?.find((item) => item.id === value.projectID)
-      //   const result = await legacy({ directory: project?.worktree }).project.update({
-      //     ...value,
-      //     directory: project?.worktree,
-      //   })
-      //   if (!result.data) throw new Error(`Project not found: ${value.projectID}`)
-      //   return result.data as Project
-      // },
       async directories(value: Parameters<ServerApi["project"]["directories"]>[0]) {
         const result = await legacy(value.location).worktree.list()
         return (result.data ?? []).map((item) => ({ directory: item }))
       },
     },
-    // path: {
-    //   ...input.current.path,
-    //   async get(value?: Parameters<ServerApi["path"]["get"]>[0]) {
-    //     const result = await legacy(value?.location).path.get()
-    //     if (!result.data) throw new Error("Path unavailable")
-    //     return result.data
-    //   },
-    // },
+    location: {
+      ...input.current.location,
+      async get(value?: Parameters<ServerApi["location"]["get"]>[0]) {
+        const result = await legacy(value?.location).path.get()
+        if (!result.data) throw new Error("Location unavailable")
+        return {
+          directory: result.data.directory,
+          project: {
+            id: "",
+            directory: result.data.worktree,
+            canonical: result.data.worktree,
+          },
+        }
+      },
+    },
     vcs: {
       ...input.current.vcs,
-      // async get(value?: Parameters<ServerApi["vcs"]["get"]>[0]) {
-      //   const result = await legacy(value?.location).vcs.get()
-      //   return located({ branch: result.data?.branch, defaultBranch: result.data?.default_branch }, value?.location)
-      // },
       async status(value?: Parameters<ServerApi["vcs"]["status"]>[0]) {
         const result = await legacy(value?.location).vcs.status()
         return located(result.data ?? [], value?.location)
@@ -456,9 +538,6 @@ function createV1Api(input: CompatibleInput): CompatibleApi {
     },
     pty: {
       ...input.current.pty,
-      // async shells(value?: Parameters<ServerApi["pty"]["shells"]>[0]) {
-      //   return located((await legacy(value?.location).pty.shells()).data ?? [], value?.location)
-      // },
       async list(value?: Parameters<ServerApi["pty"]["list"]>[0]) {
         return located((await legacy(value?.location).pty.list()).data ?? [], value?.location)
       },
@@ -490,14 +569,29 @@ function createV1Api(input: CompatibleInput): CompatibleApi {
       async remove(value: Parameters<ServerApi["pty"]["remove"]>[0]) {
         await legacy(value.location).pty.remove({ ptyID: value.ptyID })
       },
-      // async connectToken(value: Parameters<ServerApi["pty"]["connectToken"]>[0]) {
-      //   const result = await legacy(value.location).pty.connectToken({ ptyID: value.ptyID })
-      //   if (!result.data) throw new Error(`Failed to connect terminal: ${value.ptyID}`)
-      //   return located(result.data, value.location)
-      // },
     },
     permission: {
       ...input.current.permission,
+      request: {
+        ...input.current.permission.request,
+        async list(value?: Parameters<ServerApi["permission"]["request"]["list"]>[0]) {
+          const result = await legacy(value?.location).permission.list()
+          return located(
+            (result.data ?? []).map((item) => ({
+              id: item.id,
+              sessionID: item.sessionID,
+              action: item.permission,
+              resources: item.patterns,
+              metadata: item.metadata,
+              save: item.always,
+              source: item.tool
+                ? { type: "tool" as const, messageID: item.tool.messageID, callID: item.tool.callID }
+                : undefined,
+            })),
+            value?.location,
+          ) as Awaited<ReturnType<ServerApi["permission"]["request"]["list"]>>
+        },
+      },
       async reply(value: Parameters<ServerApi["permission"]["reply"]>[0] & { location?: { directory?: string } }) {
         await legacy(value.location).permission.respond({
           sessionID: value.sessionID,
@@ -509,6 +603,18 @@ function createV1Api(input: CompatibleInput): CompatibleApi {
     },
     question: {
       ...input.current.question,
+      request: {
+        ...input.current.question.request,
+        async list(value?: Parameters<ServerApi["question"]["request"]["list"]>[0]) {
+          return located(
+            ((await legacy(value?.location).question.list()).data ?? []).map((request) => ({
+              ...request,
+              tool: request.tool && { messageID: request.tool.messageID, id: request.tool.callID },
+            })),
+            value?.location,
+          )
+        },
+      },
       async reply(value: Parameters<ServerApi["question"]["reply"]>[0]) {
         await legacy().question.reply({
           requestID: value.requestID,