opencode-agent[bot] před 3 měsíci
rodič
revize
df147b65fd
25 změnil soubory, kde provedl 971 přidání a 708 odebrání
  1. 6 2
      packages/opencode/src/server/routes/instance/httpapi/groups/pty.ts
  2. 43 12
      packages/opencode/src/server/routes/instance/httpapi/groups/tui.ts
  3. 1 3
      packages/opencode/src/server/routes/instance/httpapi/groups/workspace.ts
  4. 20 20
      packages/opencode/src/server/routes/instance/httpapi/handlers/config.ts
  5. 20 20
      packages/opencode/src/server/routes/instance/httpapi/handlers/control.ts
  6. 123 123
      packages/opencode/src/server/routes/instance/httpapi/handlers/experimental.ts
  7. 44 44
      packages/opencode/src/server/routes/instance/httpapi/handlers/file.ts
  8. 77 77
      packages/opencode/src/server/routes/instance/httpapi/handlers/global.ts
  9. 54 54
      packages/opencode/src/server/routes/instance/httpapi/handlers/instance.ts
  10. 51 51
      packages/opencode/src/server/routes/instance/httpapi/handlers/mcp.ts
  11. 17 17
      packages/opencode/src/server/routes/instance/httpapi/handlers/permission.ts
  12. 27 27
      packages/opencode/src/server/routes/instance/httpapi/handlers/project.ts
  13. 70 70
      packages/opencode/src/server/routes/instance/httpapi/handlers/provider.ts
  14. 3 1
      packages/opencode/src/server/routes/instance/httpapi/handlers/pty.ts
  15. 20 20
      packages/opencode/src/server/routes/instance/httpapi/handlers/question.ts
  16. 49 34
      packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts
  17. 36 15
      packages/opencode/src/server/routes/instance/httpapi/instance-context.ts
  18. 4 1
      packages/opencode/src/server/routes/instance/httpapi/lifecycle.ts
  19. 21 13
      packages/opencode/src/server/routes/instance/httpapi/public.ts
  20. 7 2
      packages/opencode/src/server/server.ts
  21. 3 1
      packages/opencode/src/session/message-v2.ts
  22. 0 2
      packages/opencode/src/util/schema.ts
  23. 3 3
      packages/opencode/test/server/httpapi-bridge.test.ts
  24. 5 1
      packages/opencode/test/server/httpapi-workspace.test.ts
  25. 267 95
      packages/sdk/openapi.json

+ 6 - 2
packages/opencode/src/server/routes/instance/httpapi/groups/pty.ts

@@ -29,14 +29,18 @@ export const PtyApi = HttpApi.make("pty")
   .add(
     HttpApiGroup.make("pty")
       .add(
-        HttpApiEndpoint.get("shells", PtyPaths.shells, { success: described(Schema.Array(ShellItem), "List of shells") }).annotateMerge(
+        HttpApiEndpoint.get("shells", PtyPaths.shells, {
+          success: described(Schema.Array(ShellItem), "List of shells"),
+        }).annotateMerge(
           OpenApi.annotations({
             identifier: "pty.shells",
             summary: "List available shells",
             description: "Get a list of available shells on the system.",
           }),
         ),
-        HttpApiEndpoint.get("list", PtyPaths.list, { success: described(Schema.Array(Pty.Info), "List of sessions") }).annotateMerge(
+        HttpApiEndpoint.get("list", PtyPaths.list, {
+          success: described(Schema.Array(Pty.Info), "List of sessions"),
+        }).annotateMerge(
           OpenApi.annotations({
             identifier: "pty.list",
             summary: "List PTY sessions",

+ 43 - 12
packages/opencode/src/server/routes/instance/httpapi/groups/tui.ts

@@ -11,11 +11,28 @@ export const TuiRequestPayload = Schema.Struct({
   path: Schema.String,
   body: Schema.Unknown,
 })
-const EventTuiPromptAppend = Schema.Struct({ type: Schema.Literal(TuiEvent.PromptAppend.type), properties: TuiEvent.PromptAppend.properties }).annotate({ identifier: "EventTuiPromptAppend" })
-const EventTuiCommandExecute = Schema.Struct({ type: Schema.Literal(TuiEvent.CommandExecute.type), properties: TuiEvent.CommandExecute.properties }).annotate({ identifier: "EventTuiCommandExecute" })
-const EventTuiToastShow = Schema.Struct({ type: Schema.Literal(TuiEvent.ToastShow.type), properties: TuiEvent.ToastShow.properties }).annotate({ identifier: "EventTuiToastShow" })
-const EventTuiSessionSelect = Schema.Struct({ type: Schema.Literal(TuiEvent.SessionSelect.type), properties: TuiEvent.SessionSelect.properties }).annotate({ identifier: "EventTuiSessionSelect" })
-export const TuiPublishPayload = Schema.Union([EventTuiPromptAppend, EventTuiCommandExecute, EventTuiToastShow, EventTuiSessionSelect])
+const EventTuiPromptAppend = Schema.Struct({
+  type: Schema.Literal(TuiEvent.PromptAppend.type),
+  properties: TuiEvent.PromptAppend.properties,
+}).annotate({ identifier: "EventTuiPromptAppend" })
+const EventTuiCommandExecute = Schema.Struct({
+  type: Schema.Literal(TuiEvent.CommandExecute.type),
+  properties: TuiEvent.CommandExecute.properties,
+}).annotate({ identifier: "EventTuiCommandExecute" })
+const EventTuiToastShow = Schema.Struct({
+  type: Schema.Literal(TuiEvent.ToastShow.type),
+  properties: TuiEvent.ToastShow.properties,
+}).annotate({ identifier: "EventTuiToastShow" })
+const EventTuiSessionSelect = Schema.Struct({
+  type: Schema.Literal(TuiEvent.SessionSelect.type),
+  properties: TuiEvent.SessionSelect.properties,
+}).annotate({ identifier: "EventTuiSessionSelect" })
+export const TuiPublishPayload = Schema.Union([
+  EventTuiPromptAppend,
+  EventTuiCommandExecute,
+  EventTuiToastShow,
+  EventTuiSessionSelect,
+])
 
 export const TuiPaths = {
   appendPrompt: `${root}/append-prompt`,
@@ -48,42 +65,54 @@ export const TuiApi = HttpApi.make("tui")
             description: "Append prompt to the TUI.",
           }),
         ),
-        HttpApiEndpoint.post("openHelp", TuiPaths.openHelp, { success: described(Schema.Boolean, "Help dialog opened successfully") }).annotateMerge(
+        HttpApiEndpoint.post("openHelp", TuiPaths.openHelp, {
+          success: described(Schema.Boolean, "Help dialog opened successfully"),
+        }).annotateMerge(
           OpenApi.annotations({
             identifier: "tui.openHelp",
             summary: "Open help dialog",
             description: "Open the help dialog in the TUI to display user assistance information.",
           }),
         ),
-        HttpApiEndpoint.post("openSessions", TuiPaths.openSessions, { success: described(Schema.Boolean, "Session dialog opened successfully") }).annotateMerge(
+        HttpApiEndpoint.post("openSessions", TuiPaths.openSessions, {
+          success: described(Schema.Boolean, "Session dialog opened successfully"),
+        }).annotateMerge(
           OpenApi.annotations({
             identifier: "tui.openSessions",
             summary: "Open sessions dialog",
             description: "Open the session dialog.",
           }),
         ),
-        HttpApiEndpoint.post("openThemes", TuiPaths.openThemes, { success: described(Schema.Boolean, "Theme dialog opened successfully") }).annotateMerge(
+        HttpApiEndpoint.post("openThemes", TuiPaths.openThemes, {
+          success: described(Schema.Boolean, "Theme dialog opened successfully"),
+        }).annotateMerge(
           OpenApi.annotations({
             identifier: "tui.openThemes",
             summary: "Open themes dialog",
             description: "Open the theme dialog.",
           }),
         ),
-        HttpApiEndpoint.post("openModels", TuiPaths.openModels, { success: described(Schema.Boolean, "Model dialog opened successfully") }).annotateMerge(
+        HttpApiEndpoint.post("openModels", TuiPaths.openModels, {
+          success: described(Schema.Boolean, "Model dialog opened successfully"),
+        }).annotateMerge(
           OpenApi.annotations({
             identifier: "tui.openModels",
             summary: "Open models dialog",
             description: "Open the model dialog.",
           }),
         ),
-        HttpApiEndpoint.post("submitPrompt", TuiPaths.submitPrompt, { success: described(Schema.Boolean, "Prompt submitted successfully") }).annotateMerge(
+        HttpApiEndpoint.post("submitPrompt", TuiPaths.submitPrompt, {
+          success: described(Schema.Boolean, "Prompt submitted successfully"),
+        }).annotateMerge(
           OpenApi.annotations({
             identifier: "tui.submitPrompt",
             summary: "Submit TUI prompt",
             description: "Submit the prompt.",
           }),
         ),
-        HttpApiEndpoint.post("clearPrompt", TuiPaths.clearPrompt, { success: described(Schema.Boolean, "Prompt cleared successfully") }).annotateMerge(
+        HttpApiEndpoint.post("clearPrompt", TuiPaths.clearPrompt, {
+          success: described(Schema.Boolean, "Prompt cleared successfully"),
+        }).annotateMerge(
           OpenApi.annotations({
             identifier: "tui.clearPrompt",
             summary: "Clear TUI prompt",
@@ -133,7 +162,9 @@ export const TuiApi = HttpApi.make("tui")
             description: "Navigate the TUI to display the specified session.",
           }),
         ),
-        HttpApiEndpoint.get("controlNext", TuiPaths.controlNext, { success: described(TuiRequestPayload, "Next TUI request") }).annotateMerge(
+        HttpApiEndpoint.get("controlNext", TuiPaths.controlNext, {
+          success: described(TuiRequestPayload, "Next TUI request"),
+        }).annotateMerge(
           OpenApi.annotations({
             identifier: "tui.control.next",
             summary: "Get next TUI request",

+ 1 - 3
packages/opencode/src/server/routes/instance/httpapi/groups/workspace.ts

@@ -9,9 +9,7 @@ import { described } from "./metadata"
 
 const root = "/experimental/workspace"
 export const CreatePayload = Schema.Struct(Struct.omit(Workspace.CreateInput.fields, ["projectID"]))
-export const SessionRestorePayload = Schema.Struct(
-  Struct.omit(Workspace.SessionRestoreInput.fields, ["workspaceID"]),
-)
+export const SessionRestorePayload = Schema.Struct(Struct.omit(Workspace.SessionRestoreInput.fields, ["workspaceID"]))
 export const SessionRestoreResponse = Schema.Struct({
   total: NonNegativeInt,
 })

+ 20 - 20
packages/opencode/src/server/routes/instance/httpapi/handlers/config.ts

@@ -7,28 +7,28 @@ import { InstanceHttpApi } from "../api"
 import { markInstanceForDisposal } from "../lifecycle"
 
 export const configHandlers = HttpApiBuilder.group(InstanceHttpApi, "config", (handlers) =>
-    Effect.gen(function* () {
-      const providerSvc = yield* Provider.Service
-      const configSvc = yield* Config.Service
+  Effect.gen(function* () {
+    const providerSvc = yield* Provider.Service
+    const configSvc = yield* Config.Service
 
-      const get = Effect.fn("ConfigHttpApi.get")(function* () {
-        return yield* configSvc.get()
-      })
+    const get = Effect.fn("ConfigHttpApi.get")(function* () {
+      return yield* configSvc.get()
+    })
 
-      const update = Effect.fn("ConfigHttpApi.update")(function* (ctx) {
-        yield* configSvc.update(ctx.payload, { dispose: false })
-        yield* markInstanceForDisposal(yield* InstanceState.context)
-        return ctx.payload
-      })
+    const update = Effect.fn("ConfigHttpApi.update")(function* (ctx) {
+      yield* configSvc.update(ctx.payload, { dispose: false })
+      yield* markInstanceForDisposal(yield* InstanceState.context)
+      return ctx.payload
+    })
 
-      const providers = Effect.fn("ConfigHttpApi.providers")(function* () {
-        const providers = yield* providerSvc.list()
-        return {
-          providers: Object.values(providers),
-          default: Provider.defaultModelIDs(providers),
-        }
-      })
+    const providers = Effect.fn("ConfigHttpApi.providers")(function* () {
+      const providers = yield* providerSvc.list()
+      return {
+        providers: Object.values(providers),
+        default: Provider.defaultModelIDs(providers),
+      }
+    })
 
-      return handlers.handle("get", get).handle("update", update).handle("providers", providers)
-    }),
+    return handlers.handle("get", get).handle("update", update).handle("providers", providers)
+  }),
 )

+ 20 - 20
packages/opencode/src/server/routes/instance/httpapi/handlers/control.ts

@@ -7,28 +7,28 @@ import { RootHttpApi } from "../api"
 import { LogInput } from "../groups/control"
 
 export const controlHandlers = HttpApiBuilder.group(RootHttpApi, "control", (handlers) =>
-    Effect.gen(function* () {
-      const auth = yield* Auth.Service
+  Effect.gen(function* () {
+    const auth = yield* Auth.Service
 
-      const authSet = Effect.fn("ControlHttpApi.authSet")(function* (ctx: {
-        params: { providerID: ProviderID }
-        payload: Auth.Info
-      }) {
-        yield* auth.set(ctx.params.providerID, ctx.payload).pipe(Effect.orDie)
-        return true
-      })
+    const authSet = Effect.fn("ControlHttpApi.authSet")(function* (ctx: {
+      params: { providerID: ProviderID }
+      payload: Auth.Info
+    }) {
+      yield* auth.set(ctx.params.providerID, ctx.payload).pipe(Effect.orDie)
+      return true
+    })
 
-      const authRemove = Effect.fn("ControlHttpApi.authRemove")(function* (ctx: { params: { providerID: ProviderID } }) {
-        yield* auth.remove(ctx.params.providerID).pipe(Effect.orDie)
-        return true
-      })
+    const authRemove = Effect.fn("ControlHttpApi.authRemove")(function* (ctx: { params: { providerID: ProviderID } }) {
+      yield* auth.remove(ctx.params.providerID).pipe(Effect.orDie)
+      return true
+    })
 
-      const log = Effect.fn("ControlHttpApi.log")(function* (ctx: { payload: typeof LogInput.Type }) {
-        const logger = Log.create({ service: ctx.payload.service })
-        logger[ctx.payload.level](ctx.payload.message, ctx.payload.extra)
-        return true
-      })
+    const log = Effect.fn("ControlHttpApi.log")(function* (ctx: { payload: typeof LogInput.Type }) {
+      const logger = Log.create({ service: ctx.payload.service })
+      logger[ctx.payload.level](ctx.payload.message, ctx.payload.extra)
+      return true
+    })
 
-      return handlers.handle("authSet", authSet).handle("authRemove", authRemove).handle("log", log)
-    }),
+    return handlers.handle("authSet", authSet).handle("authRemove", authRemove).handle("log", log)
+  }),
 )

+ 123 - 123
packages/opencode/src/server/routes/instance/httpapi/handlers/experimental.ts

@@ -15,141 +15,141 @@ import { InstanceHttpApi } from "../api"
 import { ConsoleSwitchPayload, SessionListQuery, ToolListQuery } from "../groups/experimental"
 
 export const experimentalHandlers = HttpApiBuilder.group(InstanceHttpApi, "experimental", (handlers) =>
-    Effect.gen(function* () {
-      const account = yield* Account.Service
-      const agents = yield* Agent.Service
-      const config = yield* Config.Service
-      const mcp = yield* MCP.Service
-      const project = yield* Project.Service
-      const registry = yield* ToolRegistry.Service
-      const worktreeSvc = yield* Worktree.Service
+  Effect.gen(function* () {
+    const account = yield* Account.Service
+    const agents = yield* Agent.Service
+    const config = yield* Config.Service
+    const mcp = yield* MCP.Service
+    const project = yield* Project.Service
+    const registry = yield* ToolRegistry.Service
+    const worktreeSvc = yield* Worktree.Service
 
-      const getConsole = Effect.fn("ExperimentalHttpApi.console")(function* () {
-        const [state, groups] = yield* Effect.all(
-          [config.getConsoleState(), account.orgsByAccount().pipe(Effect.orDie)],
-          {
-            concurrency: "unbounded",
-          },
-        )
-        return {
-          consoleManagedProviders: state.consoleManagedProviders,
-          ...(state.activeOrgName ? { activeOrgName: state.activeOrgName } : {}),
-          switchableOrgCount: groups.reduce((count, group) => count + group.orgs.length, 0),
-        }
-      })
+    const getConsole = Effect.fn("ExperimentalHttpApi.console")(function* () {
+      const [state, groups] = yield* Effect.all(
+        [config.getConsoleState(), account.orgsByAccount().pipe(Effect.orDie)],
+        {
+          concurrency: "unbounded",
+        },
+      )
+      return {
+        consoleManagedProviders: state.consoleManagedProviders,
+        ...(state.activeOrgName ? { activeOrgName: state.activeOrgName } : {}),
+        switchableOrgCount: groups.reduce((count, group) => count + group.orgs.length, 0),
+      }
+    })
 
-      const listConsoleOrgs = Effect.fn("ExperimentalHttpApi.consoleOrgs")(function* () {
-        const [groups, active] = yield* Effect.all(
-          [account.orgsByAccount().pipe(Effect.orDie), account.active().pipe(Effect.orDie)],
-          {
-            concurrency: "unbounded",
-          },
-        )
-        const info = Option.getOrUndefined(active)
-        return {
-          orgs: groups.flatMap((group) =>
-            group.orgs.map((org) => ({
-              accountID: group.account.id,
-              accountEmail: group.account.email,
-              accountUrl: group.account.url,
-              orgID: org.id,
-              orgName: org.name,
-              active: !!info && info.id === group.account.id && info.active_org_id === org.id,
-            })),
-          ),
-        }
-      })
+    const listConsoleOrgs = Effect.fn("ExperimentalHttpApi.consoleOrgs")(function* () {
+      const [groups, active] = yield* Effect.all(
+        [account.orgsByAccount().pipe(Effect.orDie), account.active().pipe(Effect.orDie)],
+        {
+          concurrency: "unbounded",
+        },
+      )
+      const info = Option.getOrUndefined(active)
+      return {
+        orgs: groups.flatMap((group) =>
+          group.orgs.map((org) => ({
+            accountID: group.account.id,
+            accountEmail: group.account.email,
+            accountUrl: group.account.url,
+            orgID: org.id,
+            orgName: org.name,
+            active: !!info && info.id === group.account.id && info.active_org_id === org.id,
+          })),
+        ),
+      }
+    })
 
-      const switchConsole = Effect.fn("ExperimentalHttpApi.consoleSwitch")(function* (ctx: {
-        payload: typeof ConsoleSwitchPayload.Type
-      }) {
-        yield* account
-          .use(ctx.payload.accountID, Option.some(ctx.payload.orgID))
-          .pipe(Effect.catch(() => Effect.fail(new HttpApiError.BadRequest({}))))
-        return true
-      })
+    const switchConsole = Effect.fn("ExperimentalHttpApi.consoleSwitch")(function* (ctx: {
+      payload: typeof ConsoleSwitchPayload.Type
+    }) {
+      yield* account
+        .use(ctx.payload.accountID, Option.some(ctx.payload.orgID))
+        .pipe(Effect.catch(() => Effect.fail(new HttpApiError.BadRequest({}))))
+      return true
+    })
 
-      const tool = Effect.fn("ExperimentalHttpApi.tool")(function* (ctx: { query: typeof ToolListQuery.Type }) {
-        const list = yield* registry.tools({
-          providerID: ctx.query.provider,
-          modelID: ctx.query.model,
-          agent: yield* agents.get(yield* agents.defaultAgent()),
-        })
-        return list.map((item) => ({
-          id: item.id,
-          description: item.description,
-          parameters: EffectZod.toJsonSchema(item.parameters),
-        }))
+    const tool = Effect.fn("ExperimentalHttpApi.tool")(function* (ctx: { query: typeof ToolListQuery.Type }) {
+      const list = yield* registry.tools({
+        providerID: ctx.query.provider,
+        modelID: ctx.query.model,
+        agent: yield* agents.get(yield* agents.defaultAgent()),
       })
+      return list.map((item) => ({
+        id: item.id,
+        description: item.description,
+        parameters: EffectZod.toJsonSchema(item.parameters),
+      }))
+    })
 
-      const toolIDs = Effect.fn("ExperimentalHttpApi.toolIDs")(function* () {
-        return yield* registry.ids()
-      })
+    const toolIDs = Effect.fn("ExperimentalHttpApi.toolIDs")(function* () {
+      return yield* registry.ids()
+    })
 
-      const worktree = Effect.fn("ExperimentalHttpApi.worktree")(function* () {
-        const ctx = yield* InstanceState.context
-        return yield* project.sandboxes(ctx.project.id)
-      })
+    const worktree = Effect.fn("ExperimentalHttpApi.worktree")(function* () {
+      const ctx = yield* InstanceState.context
+      return yield* project.sandboxes(ctx.project.id)
+    })
 
-      const worktreeCreate = Effect.fn("ExperimentalHttpApi.worktreeCreate")(function* (ctx: {
-        payload: Worktree.CreateInput | undefined
-      }) {
-        return yield* worktreeSvc.create(ctx.payload)
-      })
+    const worktreeCreate = Effect.fn("ExperimentalHttpApi.worktreeCreate")(function* (ctx: {
+      payload: Worktree.CreateInput | undefined
+    }) {
+      return yield* worktreeSvc.create(ctx.payload)
+    })
 
-      const worktreeRemove = Effect.fn("ExperimentalHttpApi.worktreeRemove")(function* (input: {
-        payload: Worktree.RemoveInput
-      }) {
-        const ctx = yield* InstanceState.context
-        yield* worktreeSvc.remove(input.payload)
-        yield* project.removeSandbox(ctx.project.id, input.payload.directory)
-        return true
-      })
+    const worktreeRemove = Effect.fn("ExperimentalHttpApi.worktreeRemove")(function* (input: {
+      payload: Worktree.RemoveInput
+    }) {
+      const ctx = yield* InstanceState.context
+      yield* worktreeSvc.remove(input.payload)
+      yield* project.removeSandbox(ctx.project.id, input.payload.directory)
+      return true
+    })
 
-      const worktreeReset = Effect.fn("ExperimentalHttpApi.worktreeReset")(function* (ctx: {
-        payload: Worktree.ResetInput
-      }) {
-        yield* worktreeSvc.reset(ctx.payload)
-        return true
-      })
+    const worktreeReset = Effect.fn("ExperimentalHttpApi.worktreeReset")(function* (ctx: {
+      payload: Worktree.ResetInput
+    }) {
+      yield* worktreeSvc.reset(ctx.payload)
+      return true
+    })
 
-      const session = Effect.fn("ExperimentalHttpApi.session")(function* (ctx: { query: typeof SessionListQuery.Type }) {
-        const limit = ctx.query.limit ?? 100
-        const sessions = Array.from(
-          Session.listGlobal({
-            directory: ctx.query.directory,
-            roots: ctx.query.roots,
-            start: ctx.query.start,
-            cursor: ctx.query.cursor,
-            search: ctx.query.search,
-            limit: limit + 1,
-            archived: ctx.query.archived,
-          }),
-        )
-        const list = sessions.length > limit ? sessions.slice(0, limit) : sessions
-        return HttpServerResponse.jsonUnsafe(list, {
-          headers:
-            sessions.length > limit && list.length > 0
-              ? { "x-next-cursor": String(list[list.length - 1].time.updated) }
-              : undefined,
-        })
+    const session = Effect.fn("ExperimentalHttpApi.session")(function* (ctx: { query: typeof SessionListQuery.Type }) {
+      const limit = ctx.query.limit ?? 100
+      const sessions = Array.from(
+        Session.listGlobal({
+          directory: ctx.query.directory,
+          roots: ctx.query.roots,
+          start: ctx.query.start,
+          cursor: ctx.query.cursor,
+          search: ctx.query.search,
+          limit: limit + 1,
+          archived: ctx.query.archived,
+        }),
+      )
+      const list = sessions.length > limit ? sessions.slice(0, limit) : sessions
+      return HttpServerResponse.jsonUnsafe(list, {
+        headers:
+          sessions.length > limit && list.length > 0
+            ? { "x-next-cursor": String(list[list.length - 1].time.updated) }
+            : undefined,
       })
+    })
 
-      const resource = Effect.fn("ExperimentalHttpApi.resource")(function* () {
-        return yield* mcp.resources()
-      })
+    const resource = Effect.fn("ExperimentalHttpApi.resource")(function* () {
+      return yield* mcp.resources()
+    })
 
-      return handlers
-        .handle("console", getConsole)
-        .handle("consoleOrgs", listConsoleOrgs)
-        .handle("consoleSwitch", switchConsole)
-        .handle("tool", tool)
-        .handle("toolIDs", toolIDs)
-        .handle("worktree", worktree)
-        .handle("worktreeCreate", worktreeCreate)
-        .handle("worktreeRemove", worktreeRemove)
-        .handle("worktreeReset", worktreeReset)
-        .handle("session", session)
-        .handle("resource", resource)
-    }),
+    return handlers
+      .handle("console", getConsole)
+      .handle("consoleOrgs", listConsoleOrgs)
+      .handle("consoleSwitch", switchConsole)
+      .handle("tool", tool)
+      .handle("toolIDs", toolIDs)
+      .handle("worktree", worktree)
+      .handle("worktreeCreate", worktreeCreate)
+      .handle("worktreeRemove", worktreeRemove)
+      .handle("worktreeReset", worktreeReset)
+      .handle("session", session)
+      .handle("resource", resource)
+  }),
 )

+ 44 - 44
packages/opencode/src/server/routes/instance/httpapi/handlers/file.ts

@@ -6,49 +6,49 @@ import { HttpApiBuilder } from "effect/unstable/httpapi"
 import { InstanceHttpApi } from "../api"
 
 export const fileHandlers = HttpApiBuilder.group(InstanceHttpApi, "file", (handlers) =>
-    Effect.gen(function* () {
-      const svc = yield* File.Service
-      const ripgrep = yield* Ripgrep.Service
-
-      const findText = Effect.fn("FileHttpApi.findText")(function* (ctx: { query: { pattern: string } }) {
-        return (yield* ripgrep
-          .search({ cwd: (yield* InstanceState.context).directory, pattern: ctx.query.pattern, limit: 10 })
-          .pipe(Effect.orDie)).items
+  Effect.gen(function* () {
+    const svc = yield* File.Service
+    const ripgrep = yield* Ripgrep.Service
+
+    const findText = Effect.fn("FileHttpApi.findText")(function* (ctx: { query: { pattern: string } }) {
+      return (yield* ripgrep
+        .search({ cwd: (yield* InstanceState.context).directory, pattern: ctx.query.pattern, limit: 10 })
+        .pipe(Effect.orDie)).items
+    })
+
+    const findFile = Effect.fn("FileHttpApi.findFile")(function* (ctx: {
+      query: { query: string; dirs?: "true" | "false"; type?: "file" | "directory"; limit?: number }
+    }) {
+      return yield* svc.search({
+        query: ctx.query.query,
+        limit: ctx.query.limit ?? 10,
+        dirs: ctx.query.dirs !== "false",
+        type: ctx.query.type,
       })
-
-      const findFile = Effect.fn("FileHttpApi.findFile")(function* (ctx: {
-        query: { query: string; dirs?: "true" | "false"; type?: "file" | "directory"; limit?: number }
-      }) {
-        return yield* svc.search({
-          query: ctx.query.query,
-          limit: ctx.query.limit ?? 10,
-          dirs: ctx.query.dirs !== "false",
-          type: ctx.query.type,
-        })
-      })
-
-      const findSymbol = Effect.fn("FileHttpApi.findSymbol")(function* () {
-        return []
-      })
-
-      const list = Effect.fn("FileHttpApi.list")(function* (ctx: { query: { path: string } }) {
-        return yield* svc.list(ctx.query.path)
-      })
-
-      const content = Effect.fn("FileHttpApi.content")(function* (ctx: { query: { path: string } }) {
-        return yield* svc.read(ctx.query.path)
-      })
-
-      const status = Effect.fn("FileHttpApi.status")(function* () {
-        return yield* svc.status()
-      })
-
-      return handlers
-        .handle("findText", findText)
-        .handle("findFile", findFile)
-        .handle("findSymbol", findSymbol)
-        .handle("list", list)
-        .handle("content", content)
-        .handle("status", status)
-    }),
+    })
+
+    const findSymbol = Effect.fn("FileHttpApi.findSymbol")(function* () {
+      return []
+    })
+
+    const list = Effect.fn("FileHttpApi.list")(function* (ctx: { query: { path: string } }) {
+      return yield* svc.list(ctx.query.path)
+    })
+
+    const content = Effect.fn("FileHttpApi.content")(function* (ctx: { query: { path: string } }) {
+      return yield* svc.read(ctx.query.path)
+    })
+
+    const status = Effect.fn("FileHttpApi.status")(function* () {
+      return yield* svc.status()
+    })
+
+    return handlers
+      .handle("findText", findText)
+      .handle("findFile", findFile)
+      .handle("findSymbol", findSymbol)
+      .handle("list", list)
+      .handle("content", content)
+      .handle("status", status)
+  }),
 )

+ 77 - 77
packages/opencode/src/server/routes/instance/httpapi/handlers/global.ts

@@ -65,92 +65,92 @@ function eventResponse() {
 }
 
 export const globalHandlers = HttpApiBuilder.group(RootHttpApi, "global", (handlers) =>
-    Effect.gen(function* () {
-      const config = yield* Config.Service
-      const installation = yield* Installation.Service
+  Effect.gen(function* () {
+    const config = yield* Config.Service
+    const installation = yield* Installation.Service
 
-      const health = Effect.fn("GlobalHttpApi.health")(function* () {
-        return { healthy: true as const, version: InstallationVersion }
-      })
+    const health = Effect.fn("GlobalHttpApi.health")(function* () {
+      return { healthy: true as const, version: InstallationVersion }
+    })
 
-      const event = Effect.fn("GlobalHttpApi.event")(function* () {
-        return eventResponse()
-      })
+    const event = Effect.fn("GlobalHttpApi.event")(function* () {
+      return eventResponse()
+    })
 
-      const configGet = Effect.fn("GlobalHttpApi.configGet")(function* () {
-        return yield* config.getGlobal()
-      })
+    const configGet = Effect.fn("GlobalHttpApi.configGet")(function* () {
+      return yield* config.getGlobal()
+    })
 
-      const configUpdate = Effect.fn("GlobalHttpApi.configUpdate")(function* (ctx) {
-        return yield* config.updateGlobal(ctx.payload)
-      })
+    const configUpdate = Effect.fn("GlobalHttpApi.configUpdate")(function* (ctx) {
+      return yield* config.updateGlobal(ctx.payload)
+    })
 
-      const dispose = Effect.fn("GlobalHttpApi.dispose")(function* () {
-        yield* Effect.promise(() => Instance.disposeAll())
-        GlobalBus.emit("event", {
-          directory: "global",
-          payload: { type: "global.disposed", properties: {} },
-        })
-        return true
+    const dispose = Effect.fn("GlobalHttpApi.dispose")(function* () {
+      yield* Effect.promise(() => Instance.disposeAll())
+      GlobalBus.emit("event", {
+        directory: "global",
+        payload: { type: "global.disposed", properties: {} },
       })
+      return true
+    })
 
-      const upgrade = Effect.fn("GlobalHttpApi.upgrade")(function* (ctx: { payload: typeof GlobalUpgradeInput.Type }) {
-        const method = yield* installation.method()
-        if (method === "unknown") {
-          return {
-            status: 400,
-            body: { success: false as const, error: "Unknown installation method" },
-          }
+    const upgrade = Effect.fn("GlobalHttpApi.upgrade")(function* (ctx: { payload: typeof GlobalUpgradeInput.Type }) {
+      const method = yield* installation.method()
+      if (method === "unknown") {
+        return {
+          status: 400,
+          body: { success: false as const, error: "Unknown installation method" },
         }
-        const target = ctx.payload.target || (yield* installation.latest(method))
-        const result = yield* installation.upgrade(method, target).pipe(
-          Effect.as({ status: 200, body: { success: true as const, version: target } }),
-          Effect.catch((err) =>
-            Effect.succeed({
-              status: 500,
-              body: {
-                success: false as const,
-                error: err instanceof Error ? err.message : String(err),
-              },
-            }),
-          ),
-        )
-        if (!result.body.success) return result
-        GlobalBus.emit("event", {
-          directory: "global",
-          payload: {
-            type: Installation.Event.Updated.type,
-            properties: { version: target },
-          },
-        })
-        return result
+      }
+      const target = ctx.payload.target || (yield* installation.latest(method))
+      const result = yield* installation.upgrade(method, target).pipe(
+        Effect.as({ status: 200, body: { success: true as const, version: target } }),
+        Effect.catch((err) =>
+          Effect.succeed({
+            status: 500,
+            body: {
+              success: false as const,
+              error: err instanceof Error ? err.message : String(err),
+            },
+          }),
+        ),
+      )
+      if (!result.body.success) return result
+      GlobalBus.emit("event", {
+        directory: "global",
+        payload: {
+          type: Installation.Event.Updated.type,
+          properties: { version: target },
+        },
       })
+      return result
+    })
 
-      const upgradeRaw = Effect.fn("GlobalHttpApi.upgradeRaw")(function* (ctx: {
-        request: HttpServerRequest.HttpServerRequest
-      }) {
-        const body = yield* Effect.orDie(ctx.request.text)
-        const json = parseBody(body)
-        if (json === undefined) {
-          return HttpServerResponse.jsonUnsafe({ success: false, error: "Invalid request body" }, { status: 400 })
-        }
-        const payload = yield* Schema.decodeUnknownEffect(GlobalUpgradeInput)(json).pipe(
-          Effect.map((payload) => ({ valid: true as const, payload })),
-          Effect.catch(() => Effect.succeed({ valid: false as const })),
-        )
-        if (!payload.valid) {
-          return HttpServerResponse.jsonUnsafe({ success: false, error: "Invalid request body" }, { status: 400 })
-        }
-        const result = yield* upgrade({ payload: payload.payload })
-        return HttpServerResponse.jsonUnsafe(result.body, { status: result.status })
-      })
+    const upgradeRaw = Effect.fn("GlobalHttpApi.upgradeRaw")(function* (ctx: {
+      request: HttpServerRequest.HttpServerRequest
+    }) {
+      const body = yield* Effect.orDie(ctx.request.text)
+      const json = parseBody(body)
+      if (json === undefined) {
+        return HttpServerResponse.jsonUnsafe({ success: false, error: "Invalid request body" }, { status: 400 })
+      }
+      const payload = yield* Schema.decodeUnknownEffect(GlobalUpgradeInput)(json).pipe(
+        Effect.map((payload) => ({ valid: true as const, payload })),
+        Effect.catch(() => Effect.succeed({ valid: false as const })),
+      )
+      if (!payload.valid) {
+        return HttpServerResponse.jsonUnsafe({ success: false, error: "Invalid request body" }, { status: 400 })
+      }
+      const result = yield* upgrade({ payload: payload.payload })
+      return HttpServerResponse.jsonUnsafe(result.body, { status: result.status })
+    })
 
-      return handlers
-        .handle("health", health)
-        .handleRaw("event", event)
-        .handle("configGet", configGet)
-        .handle("configUpdate", configUpdate)
-        .handle("dispose", dispose)
-        .handleRaw("upgrade", upgradeRaw)
-    }),
+    return handlers
+      .handle("health", health)
+      .handleRaw("event", event)
+      .handle("configGet", configGet)
+      .handle("configUpdate", configUpdate)
+      .handle("dispose", dispose)
+      .handleRaw("upgrade", upgradeRaw)
+  }),
 )

+ 54 - 54
packages/opencode/src/server/routes/instance/httpapi/handlers/instance.ts

@@ -12,68 +12,68 @@ import { InstanceHttpApi } from "../api"
 import { markInstanceForDisposal } from "../lifecycle"
 
 export const instanceHandlers = HttpApiBuilder.group(InstanceHttpApi, "instance", (handlers) =>
-    Effect.gen(function* () {
-      const agent = yield* Agent.Service
-      const command = yield* Command.Service
-      const format = yield* Format.Service
-      const lsp = yield* LSP.Service
-      const skill = yield* Skill.Service
-      const vcs = yield* Vcs.Service
+  Effect.gen(function* () {
+    const agent = yield* Agent.Service
+    const command = yield* Command.Service
+    const format = yield* Format.Service
+    const lsp = yield* LSP.Service
+    const skill = yield* Skill.Service
+    const vcs = yield* Vcs.Service
 
-      const dispose = Effect.fn("InstanceHttpApi.dispose")(function* () {
-        yield* markInstanceForDisposal(yield* InstanceState.context)
-        return true
-      })
+    const dispose = Effect.fn("InstanceHttpApi.dispose")(function* () {
+      yield* markInstanceForDisposal(yield* InstanceState.context)
+      return true
+    })
 
-      const getPath = Effect.fn("InstanceHttpApi.path")(function* () {
-        const ctx = yield* InstanceState.context
-        return {
-          home: Global.Path.home,
-          state: Global.Path.state,
-          config: Global.Path.config,
-          worktree: ctx.worktree,
-          directory: ctx.directory,
-        }
-      })
+    const getPath = Effect.fn("InstanceHttpApi.path")(function* () {
+      const ctx = yield* InstanceState.context
+      return {
+        home: Global.Path.home,
+        state: Global.Path.state,
+        config: Global.Path.config,
+        worktree: ctx.worktree,
+        directory: ctx.directory,
+      }
+    })
 
-      const getVcs = Effect.fn("InstanceHttpApi.vcs")(function* () {
-        const [branch, default_branch] = yield* Effect.all([vcs.branch(), vcs.defaultBranch()], { concurrency: 2 })
-        return { branch, default_branch }
-      })
+    const getVcs = Effect.fn("InstanceHttpApi.vcs")(function* () {
+      const [branch, default_branch] = yield* Effect.all([vcs.branch(), vcs.defaultBranch()], { concurrency: 2 })
+      return { branch, default_branch }
+    })
 
-      const getVcsDiff = Effect.fn("InstanceHttpApi.vcsDiff")(function* (ctx: { query: { mode: Vcs.Mode } }) {
-        return yield* vcs.diff(ctx.query.mode)
-      })
+    const getVcsDiff = Effect.fn("InstanceHttpApi.vcsDiff")(function* (ctx: { query: { mode: Vcs.Mode } }) {
+      return yield* vcs.diff(ctx.query.mode)
+    })
 
-      const getCommand = Effect.fn("InstanceHttpApi.command")(function* () {
-        return yield* command.list()
-      })
+    const getCommand = Effect.fn("InstanceHttpApi.command")(function* () {
+      return yield* command.list()
+    })
 
-      const getAgent = Effect.fn("InstanceHttpApi.agent")(function* () {
-        return yield* agent.list()
-      })
+    const getAgent = Effect.fn("InstanceHttpApi.agent")(function* () {
+      return yield* agent.list()
+    })
 
-      const getSkill = Effect.fn("InstanceHttpApi.skill")(function* () {
-        return yield* skill.all()
-      })
+    const getSkill = Effect.fn("InstanceHttpApi.skill")(function* () {
+      return yield* skill.all()
+    })
 
-      const getLsp = Effect.fn("InstanceHttpApi.lsp")(function* () {
-        return yield* lsp.status()
-      })
+    const getLsp = Effect.fn("InstanceHttpApi.lsp")(function* () {
+      return yield* lsp.status()
+    })
 
-      const getFormatter = Effect.fn("InstanceHttpApi.formatter")(function* () {
-        return yield* format.status()
-      })
+    const getFormatter = Effect.fn("InstanceHttpApi.formatter")(function* () {
+      return yield* format.status()
+    })
 
-      return handlers
-        .handle("dispose", dispose)
-        .handle("path", getPath)
-        .handle("vcs", getVcs)
-        .handle("vcsDiff", getVcsDiff)
-        .handle("command", getCommand)
-        .handle("agent", getAgent)
-        .handle("skill", getSkill)
-        .handle("lsp", getLsp)
-        .handle("formatter", getFormatter)
-    }),
+    return handlers
+      .handle("dispose", dispose)
+      .handle("path", getPath)
+      .handle("vcs", getVcs)
+      .handle("vcsDiff", getVcsDiff)
+      .handle("command", getCommand)
+      .handle("agent", getAgent)
+      .handle("skill", getSkill)
+      .handle("lsp", getLsp)
+      .handle("formatter", getFormatter)
+  }),
 )

+ 51 - 51
packages/opencode/src/server/routes/instance/httpapi/handlers/mcp.ts

@@ -5,64 +5,64 @@ import { InstanceHttpApi } from "../api"
 import { AddPayload, AuthCallbackPayload, StatusMap, UnsupportedOAuthError } from "../groups/mcp"
 
 export const mcpHandlers = HttpApiBuilder.group(InstanceHttpApi, "mcp", (handlers) =>
-    Effect.gen(function* () {
-      const mcp = yield* MCP.Service
+  Effect.gen(function* () {
+    const mcp = yield* MCP.Service
 
-      const status = Effect.fn("McpHttpApi.status")(function* () {
-        return yield* mcp.status()
-      })
+    const status = Effect.fn("McpHttpApi.status")(function* () {
+      return yield* mcp.status()
+    })
 
-      const add = Effect.fn("McpHttpApi.add")(function* (ctx: { payload: typeof AddPayload.Type }) {
-        const result = (yield* mcp.add(ctx.payload.name, ctx.payload.config)).status
-        return yield* Schema.decodeUnknownEffect(StatusMap)(
-          "status" in result ? { [ctx.payload.name]: result } : result,
-        ).pipe(Effect.mapError(() => new HttpApiError.BadRequest({})))
-      })
+    const add = Effect.fn("McpHttpApi.add")(function* (ctx: { payload: typeof AddPayload.Type }) {
+      const result = (yield* mcp.add(ctx.payload.name, ctx.payload.config)).status
+      return yield* Schema.decodeUnknownEffect(StatusMap)(
+        "status" in result ? { [ctx.payload.name]: result } : result,
+      ).pipe(Effect.mapError(() => new HttpApiError.BadRequest({})))
+    })
 
-      const authStart = Effect.fn("McpHttpApi.authStart")(function* (ctx: { params: { name: string } }) {
-        if (!(yield* mcp.supportsOAuth(ctx.params.name))) {
-          return yield* new UnsupportedOAuthError({ error: `MCP server ${ctx.params.name} does not support OAuth` })
-        }
-        return yield* mcp.startAuth(ctx.params.name)
-      })
+    const authStart = Effect.fn("McpHttpApi.authStart")(function* (ctx: { params: { name: string } }) {
+      if (!(yield* mcp.supportsOAuth(ctx.params.name))) {
+        return yield* new UnsupportedOAuthError({ error: `MCP server ${ctx.params.name} does not support OAuth` })
+      }
+      return yield* mcp.startAuth(ctx.params.name)
+    })
 
-      const authCallback = Effect.fn("McpHttpApi.authCallback")(function* (ctx: {
-        params: { name: string }
-        payload: typeof AuthCallbackPayload.Type
-      }) {
-        return yield* mcp.finishAuth(ctx.params.name, ctx.payload.code)
-      })
+    const authCallback = Effect.fn("McpHttpApi.authCallback")(function* (ctx: {
+      params: { name: string }
+      payload: typeof AuthCallbackPayload.Type
+    }) {
+      return yield* mcp.finishAuth(ctx.params.name, ctx.payload.code)
+    })
 
-      const authAuthenticate = Effect.fn("McpHttpApi.authAuthenticate")(function* (ctx: { params: { name: string } }) {
-        if (!(yield* mcp.supportsOAuth(ctx.params.name))) {
-          return yield* new UnsupportedOAuthError({ error: `MCP server ${ctx.params.name} does not support OAuth` })
-        }
-        return yield* mcp.authenticate(ctx.params.name)
-      })
+    const authAuthenticate = Effect.fn("McpHttpApi.authAuthenticate")(function* (ctx: { params: { name: string } }) {
+      if (!(yield* mcp.supportsOAuth(ctx.params.name))) {
+        return yield* new UnsupportedOAuthError({ error: `MCP server ${ctx.params.name} does not support OAuth` })
+      }
+      return yield* mcp.authenticate(ctx.params.name)
+    })
 
-      const authRemove = Effect.fn("McpHttpApi.authRemove")(function* (ctx: { params: { name: string } }) {
-        yield* mcp.removeAuth(ctx.params.name)
-        return { success: true as const }
-      })
+    const authRemove = Effect.fn("McpHttpApi.authRemove")(function* (ctx: { params: { name: string } }) {
+      yield* mcp.removeAuth(ctx.params.name)
+      return { success: true as const }
+    })
 
-      const connect = Effect.fn("McpHttpApi.connect")(function* (ctx: { params: { name: string } }) {
-        yield* mcp.connect(ctx.params.name)
-        return true
-      })
+    const connect = Effect.fn("McpHttpApi.connect")(function* (ctx: { params: { name: string } }) {
+      yield* mcp.connect(ctx.params.name)
+      return true
+    })
 
-      const disconnect = Effect.fn("McpHttpApi.disconnect")(function* (ctx: { params: { name: string } }) {
-        yield* mcp.disconnect(ctx.params.name)
-        return true
-      })
+    const disconnect = Effect.fn("McpHttpApi.disconnect")(function* (ctx: { params: { name: string } }) {
+      yield* mcp.disconnect(ctx.params.name)
+      return true
+    })
 
-      return handlers
-        .handle("status", status)
-        .handle("add", add)
-        .handle("authStart", authStart)
-        .handle("authCallback", authCallback)
-        .handle("authAuthenticate", authAuthenticate)
-        .handle("authRemove", authRemove)
-        .handle("connect", connect)
-        .handle("disconnect", disconnect)
-    }),
+    return handlers
+      .handle("status", status)
+      .handle("add", add)
+      .handle("authStart", authStart)
+      .handle("authCallback", authCallback)
+      .handle("authAuthenticate", authAuthenticate)
+      .handle("authRemove", authRemove)
+      .handle("connect", connect)
+      .handle("disconnect", disconnect)
+  }),
 )

+ 17 - 17
packages/opencode/src/server/routes/instance/httpapi/handlers/permission.ts

@@ -5,25 +5,25 @@ import { HttpApiBuilder } from "effect/unstable/httpapi"
 import { InstanceHttpApi } from "../api"
 
 export const permissionHandlers = HttpApiBuilder.group(InstanceHttpApi, "permission", (handlers) =>
-    Effect.gen(function* () {
-      const svc = yield* Permission.Service
+  Effect.gen(function* () {
+    const svc = yield* Permission.Service
 
-      const list = Effect.fn("PermissionHttpApi.list")(function* () {
-        return yield* svc.list()
-      })
+    const list = Effect.fn("PermissionHttpApi.list")(function* () {
+      return yield* svc.list()
+    })
 
-      const reply = Effect.fn("PermissionHttpApi.reply")(function* (ctx: {
-        params: { requestID: PermissionID }
-        payload: Permission.ReplyBody
-      }) {
-        yield* svc.reply({
-          requestID: ctx.params.requestID,
-          reply: ctx.payload.reply,
-          message: ctx.payload.message,
-        })
-        return true
+    const reply = Effect.fn("PermissionHttpApi.reply")(function* (ctx: {
+      params: { requestID: PermissionID }
+      payload: Permission.ReplyBody
+    }) {
+      yield* svc.reply({
+        requestID: ctx.params.requestID,
+        reply: ctx.payload.reply,
+        message: ctx.payload.message,
       })
+      return true
+    })
 
-      return handlers.handle("list", list).handle("reply", reply)
-    }),
+    return handlers.handle("list", list).handle("reply", reply)
+  }),
 )

+ 27 - 27
packages/opencode/src/server/routes/instance/httpapi/handlers/project.ts

@@ -9,38 +9,38 @@ import { InstanceHttpApi } from "../api"
 import { markInstanceForReload } from "../lifecycle"
 
 export const projectHandlers = HttpApiBuilder.group(InstanceHttpApi, "project", (handlers) =>
-    Effect.gen(function* () {
-      const svc = yield* Project.Service
+  Effect.gen(function* () {
+    const svc = yield* Project.Service
 
-      const list = Effect.fn("ProjectHttpApi.list")(function* () {
-        return yield* svc.list()
-      })
+    const list = Effect.fn("ProjectHttpApi.list")(function* () {
+      return yield* svc.list()
+    })
 
-      const current = Effect.fn("ProjectHttpApi.current")(function* () {
-        return (yield* InstanceState.context).project
-      })
+    const current = Effect.fn("ProjectHttpApi.current")(function* () {
+      return (yield* InstanceState.context).project
+    })
 
-      const initGit = Effect.fn("ProjectHttpApi.initGit")(function* () {
-        const ctx = yield* InstanceState.context
-        const next = yield* svc.initGit({ directory: ctx.directory, project: ctx.project })
-        if (next.id === ctx.project.id && next.vcs === ctx.project.vcs && next.worktree === ctx.project.worktree)
-          return next
-        yield* markInstanceForReload(ctx, {
-          directory: ctx.directory,
-          worktree: ctx.directory,
-          project: next,
-          init: () => AppRuntime.runPromise(InstanceBootstrap),
-        })
+    const initGit = Effect.fn("ProjectHttpApi.initGit")(function* () {
+      const ctx = yield* InstanceState.context
+      const next = yield* svc.initGit({ directory: ctx.directory, project: ctx.project })
+      if (next.id === ctx.project.id && next.vcs === ctx.project.vcs && next.worktree === ctx.project.worktree)
         return next
+      yield* markInstanceForReload(ctx, {
+        directory: ctx.directory,
+        worktree: ctx.directory,
+        project: next,
+        init: () => AppRuntime.runPromise(InstanceBootstrap),
       })
+      return next
+    })
 
-      const update = Effect.fn("ProjectHttpApi.update")(function* (ctx: {
-        params: { projectID: ProjectID }
-        payload: Project.UpdatePayload
-      }) {
-        return yield* svc.update({ ...ctx.payload, projectID: ctx.params.projectID })
-      })
+    const update = Effect.fn("ProjectHttpApi.update")(function* (ctx: {
+      params: { projectID: ProjectID }
+      payload: Project.UpdatePayload
+    }) {
+      return yield* svc.update({ ...ctx.payload, projectID: ctx.params.projectID })
+    })
 
-      return handlers.handle("list", list).handle("current", current).handle("initGit", initGit).handle("update", update)
-    }),
+    return handlers.handle("list", list).handle("current", current).handle("initGit", initGit).handle("update", update)
+  }),
 )

+ 70 - 70
packages/opencode/src/server/routes/instance/httpapi/handlers/provider.ts

@@ -10,80 +10,80 @@ import { HttpApiBuilder, HttpApiError } from "effect/unstable/httpapi"
 import { InstanceHttpApi } from "../api"
 
 export const providerHandlers = HttpApiBuilder.group(InstanceHttpApi, "provider", (handlers) =>
-    Effect.gen(function* () {
-      const cfg = yield* Config.Service
-      const provider = yield* Provider.Service
-      const svc = yield* ProviderAuth.Service
+  Effect.gen(function* () {
+    const cfg = yield* Config.Service
+    const provider = yield* Provider.Service
+    const svc = yield* ProviderAuth.Service
 
-      const list = Effect.fn("ProviderHttpApi.list")(function* () {
-        const config = yield* cfg.get()
-        const all = yield* Effect.promise(() => ModelsDev.get())
-        const disabled = new Set(config.disabled_providers ?? [])
-        const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined
-        const filtered: Record<string, (typeof all)[string]> = {}
-        for (const [key, value] of Object.entries(all)) {
-          if ((enabled ? enabled.has(key) : true) && !disabled.has(key)) filtered[key] = value
-        }
-        const connected = yield* provider.list()
-        const providers = Object.assign(
-          mapValues(filtered, (item) => Provider.fromModelsDevProvider(item)),
-          connected,
-        )
-        return {
-          all: Object.values(providers),
-          default: Provider.defaultModelIDs(providers),
-          connected: Object.keys(connected),
-        }
-      })
+    const list = Effect.fn("ProviderHttpApi.list")(function* () {
+      const config = yield* cfg.get()
+      const all = yield* Effect.promise(() => ModelsDev.get())
+      const disabled = new Set(config.disabled_providers ?? [])
+      const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined
+      const filtered: Record<string, (typeof all)[string]> = {}
+      for (const [key, value] of Object.entries(all)) {
+        if ((enabled ? enabled.has(key) : true) && !disabled.has(key)) filtered[key] = value
+      }
+      const connected = yield* provider.list()
+      const providers = Object.assign(
+        mapValues(filtered, (item) => Provider.fromModelsDevProvider(item)),
+        connected,
+      )
+      return {
+        all: Object.values(providers),
+        default: Provider.defaultModelIDs(providers),
+        connected: Object.keys(connected),
+      }
+    })
 
-      const auth = Effect.fn("ProviderHttpApi.auth")(function* () {
-        return yield* svc.methods()
-      })
+    const auth = Effect.fn("ProviderHttpApi.auth")(function* () {
+      return yield* svc.methods()
+    })
 
-      const authorize = Effect.fn("ProviderHttpApi.authorize")(function* (ctx: {
-        params: { providerID: ProviderID }
-        payload: ProviderAuth.AuthorizeInput
-      }) {
-        return yield* svc
-          .authorize({
-            providerID: ctx.params.providerID,
-            method: ctx.payload.method,
-            inputs: ctx.payload.inputs,
-          })
-          .pipe(Effect.catch(() => Effect.fail(new HttpApiError.BadRequest({}))))
-      })
+    const authorize = Effect.fn("ProviderHttpApi.authorize")(function* (ctx: {
+      params: { providerID: ProviderID }
+      payload: ProviderAuth.AuthorizeInput
+    }) {
+      return yield* svc
+        .authorize({
+          providerID: ctx.params.providerID,
+          method: ctx.payload.method,
+          inputs: ctx.payload.inputs,
+        })
+        .pipe(Effect.catch(() => Effect.fail(new HttpApiError.BadRequest({}))))
+    })
 
-      const authorizeRaw = Effect.fn("ProviderHttpApi.authorizeRaw")(function* (ctx: {
-        params: { providerID: ProviderID }
-        request: HttpServerRequest.HttpServerRequest
-      }) {
-        const body = yield* Effect.orDie(ctx.request.text)
-        const payload = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(ProviderAuth.AuthorizeInput))(body).pipe(
-          Effect.mapError(() => new HttpApiError.BadRequest({})),
-        )
-        const result = yield* authorize({ params: ctx.params, payload })
-        if (result === undefined) return HttpServerResponse.empty({ status: 200 })
-        return HttpServerResponse.jsonUnsafe(result)
-      })
+    const authorizeRaw = Effect.fn("ProviderHttpApi.authorizeRaw")(function* (ctx: {
+      params: { providerID: ProviderID }
+      request: HttpServerRequest.HttpServerRequest
+    }) {
+      const body = yield* Effect.orDie(ctx.request.text)
+      const payload = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(ProviderAuth.AuthorizeInput))(body).pipe(
+        Effect.mapError(() => new HttpApiError.BadRequest({})),
+      )
+      const result = yield* authorize({ params: ctx.params, payload })
+      if (result === undefined) return HttpServerResponse.empty({ status: 200 })
+      return HttpServerResponse.jsonUnsafe(result)
+    })
 
-      const callback = Effect.fn("ProviderHttpApi.callback")(function* (ctx: {
-        params: { providerID: ProviderID }
-        payload: ProviderAuth.CallbackInput
-      }) {
-        yield* svc
-          .callback({
-            providerID: ctx.params.providerID,
-            method: ctx.payload.method,
-            code: ctx.payload.code,
-          })
-          .pipe(Effect.catch(() => Effect.fail(new HttpApiError.BadRequest({}))))
-        return true
-      })
+    const callback = Effect.fn("ProviderHttpApi.callback")(function* (ctx: {
+      params: { providerID: ProviderID }
+      payload: ProviderAuth.CallbackInput
+    }) {
+      yield* svc
+        .callback({
+          providerID: ctx.params.providerID,
+          method: ctx.payload.method,
+          code: ctx.payload.code,
+        })
+        .pipe(Effect.catch(() => Effect.fail(new HttpApiError.BadRequest({}))))
+      return true
+    })
 
-      return handlers
-        .handle("list", list)
-        .handle("auth", auth)
-        .handleRaw("authorize", authorizeRaw)
-        .handle("callback", callback)
-    }),
+    return handlers
+      .handle("list", list)
+      .handle("auth", auth)
+      .handleRaw("authorize", authorizeRaw)
+      .handle("callback", callback)
+  }),
 )

+ 3 - 1
packages/opencode/src/server/routes/instance/httpapi/handlers/pty.ts

@@ -88,7 +88,9 @@ export const ptyConnectRoute = HttpRouter.add(
       },
       send: (data: string | Uint8Array | ArrayBuffer) => {
         if (closed) return
-        Effect.runFork(write(data instanceof ArrayBuffer ? new Uint8Array(data) : data).pipe(Effect.catch(() => Effect.void)))
+        Effect.runFork(
+          write(data instanceof ArrayBuffer ? new Uint8Array(data) : data).pipe(Effect.catch(() => Effect.void)),
+        )
       },
       close: (code?: number, reason?: string) => {
         if (closed) return

+ 20 - 20
packages/opencode/src/server/routes/instance/httpapi/handlers/question.ts

@@ -5,29 +5,29 @@ import { HttpApiBuilder } from "effect/unstable/httpapi"
 import { InstanceHttpApi } from "../api"
 
 export const questionHandlers = HttpApiBuilder.group(InstanceHttpApi, "question", (handlers) =>
-    Effect.gen(function* () {
-      const svc = yield* Question.Service
+  Effect.gen(function* () {
+    const svc = yield* Question.Service
 
-      const list = Effect.fn("QuestionHttpApi.list")(function* () {
-        return yield* svc.list()
-      })
+    const list = Effect.fn("QuestionHttpApi.list")(function* () {
+      return yield* svc.list()
+    })
 
-      const reply = Effect.fn("QuestionHttpApi.reply")(function* (ctx: {
-        params: { requestID: QuestionID }
-        payload: Question.Reply
-      }) {
-        yield* svc.reply({
-          requestID: ctx.params.requestID,
-          answers: ctx.payload.answers,
-        })
-        return true
+    const reply = Effect.fn("QuestionHttpApi.reply")(function* (ctx: {
+      params: { requestID: QuestionID }
+      payload: Question.Reply
+    }) {
+      yield* svc.reply({
+        requestID: ctx.params.requestID,
+        answers: ctx.payload.answers,
       })
+      return true
+    })
 
-      const reject = Effect.fn("QuestionHttpApi.reject")(function* (ctx: { params: { requestID: QuestionID } }) {
-        yield* svc.reject(ctx.params.requestID)
-        return true
-      })
+    const reject = Effect.fn("QuestionHttpApi.reject")(function* (ctx: { params: { requestID: QuestionID } }) {
+      yield* svc.reject(ctx.params.requestID)
+      return true
+    })
 
-      return handlers.handle("list", list).handle("reply", reply).handle("reject", reject)
-    }),
+    return handlers.handle("list", list).handle("reply", reply).handle("reject", reject)
+  }),
 )

+ 49 - 34
packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts

@@ -25,7 +25,20 @@ import * as Stream from "effect/Stream"
 import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
 import { HttpApiBuilder, HttpApiError, HttpApiSchema } from "effect/unstable/httpapi"
 import { InstanceHttpApi } from "../api"
-import { CommandPayload, DiffQuery, ForkPayload, InitPayload, ListQuery, MessagesQuery, PermissionResponsePayload, PromptPayload, RevertPayload, ShellPayload, SummarizePayload, UpdatePayload } from "../groups/session"
+import {
+  CommandPayload,
+  DiffQuery,
+  ForkPayload,
+  InitPayload,
+  ListQuery,
+  MessagesQuery,
+  PermissionResponsePayload,
+  PromptPayload,
+  RevertPayload,
+  ShellPayload,
+  SummarizePayload,
+  UpdatePayload,
+} from "../groups/session"
 
 const log = Log.create({ service: "server" })
 
@@ -88,40 +101,42 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
       params: { sessionID: SessionID }
       query: typeof MessagesQuery.Type
     }) {
-      return yield* mapNotFound(Effect.gen(function* () {
-        if (ctx.query.before && ctx.query.limit === undefined) return yield* new HttpApiError.BadRequest({})
-        if (ctx.query.before) {
-          const before = ctx.query.before
-          yield* Effect.try({
-            try: () => MessageV2.cursor.decode(before),
-            catch: () => new HttpApiError.BadRequest({}),
-          })
-        }
-        if (ctx.query.limit === undefined || ctx.query.limit === 0) {
+      return yield* mapNotFound(
+        Effect.gen(function* () {
+          if (ctx.query.before && ctx.query.limit === undefined) return yield* new HttpApiError.BadRequest({})
+          if (ctx.query.before) {
+            const before = ctx.query.before
+            yield* Effect.try({
+              try: () => MessageV2.cursor.decode(before),
+              catch: () => new HttpApiError.BadRequest({}),
+            })
+          }
+          if (ctx.query.limit === undefined || ctx.query.limit === 0) {
+            yield* session.get(ctx.params.sessionID)
+            return yield* session.messages({ sessionID: ctx.params.sessionID })
+          }
+
           yield* session.get(ctx.params.sessionID)
-          return yield* session.messages({ sessionID: ctx.params.sessionID })
-        }
-
-        yield* session.get(ctx.params.sessionID)
-        const page = MessageV2.page({
-          sessionID: ctx.params.sessionID,
-          limit: ctx.query.limit,
-          before: ctx.query.before,
-        })
-        if (!page.cursor) return page.items
-
-        const request = yield* HttpServerRequest.HttpServerRequest
-        const url = new URL(request.url, "http://localhost")
-        url.searchParams.set("limit", ctx.query.limit.toString())
-        url.searchParams.set("before", page.cursor)
-        return HttpServerResponse.jsonUnsafe(page.items, {
-          headers: {
-            "Access-Control-Expose-Headers": "Link, X-Next-Cursor",
-            Link: `<${url.toString()}>; rel="next"`,
-            "X-Next-Cursor": page.cursor,
-          },
-        })
-      }))
+          const page = MessageV2.page({
+            sessionID: ctx.params.sessionID,
+            limit: ctx.query.limit,
+            before: ctx.query.before,
+          })
+          if (!page.cursor) return page.items
+
+          const request = yield* HttpServerRequest.HttpServerRequest
+          const url = new URL(request.url, "http://localhost")
+          url.searchParams.set("limit", ctx.query.limit.toString())
+          url.searchParams.set("before", page.cursor)
+          return HttpServerResponse.jsonUnsafe(page.items, {
+            headers: {
+              "Access-Control-Expose-Headers": "Link, X-Next-Cursor",
+              Link: `<${url.toString()}>; rel="next"`,
+              "X-Next-Cursor": page.cursor,
+            },
+          })
+        }),
+      )
     })
 
     const message = Effect.fn("SessionHttpApi.message")(function* (ctx: {

+ 36 - 15
packages/opencode/src/server/routes/instance/httpapi/instance-context.ts

@@ -19,11 +19,12 @@ import * as Socket from "effect/unstable/socket/Socket"
 
 type HandlerEffect = Effect.Effect<HttpServerResponse.HttpServerResponse, unhandled, never>
 
-export class InstanceContextMiddleware extends HttpApiMiddleware.Service<InstanceContextMiddleware, {
-  requires: Session.Service
-}>()(
-  "@opencode/ExperimentalHttpApiInstanceContext",
-) {}
+export class InstanceContextMiddleware extends HttpApiMiddleware.Service<
+  InstanceContextMiddleware,
+  {
+    requires: Session.Service
+  }
+>()("@opencode/ExperimentalHttpApiInstanceContext") {}
 
 function decode(input: string) {
   try {
@@ -53,9 +54,14 @@ function requestHeaders(request: HttpServerRequest.HttpServerRequest) {
   return sourceRequest(request).headers
 }
 
-function writeSocket(write: (data: string | Uint8Array | Socket.CloseEvent) => Effect.Effect<void, unknown>, data: unknown) {
+function writeSocket(
+  write: (data: string | Uint8Array | Socket.CloseEvent) => Effect.Effect<void, unknown>,
+  data: unknown,
+) {
   if (data instanceof Blob) {
-    void data.arrayBuffer().then((buffer) => Effect.runFork(write(new Uint8Array(buffer)).pipe(Effect.catch(() => Effect.void))))
+    void data
+      .arrayBuffer()
+      .then((buffer) => Effect.runFork(write(new Uint8Array(buffer)).pipe(Effect.catch(() => Effect.void))))
     return
   }
   if (typeof data === "string" || data instanceof Uint8Array) {
@@ -78,7 +84,8 @@ function proxyWebSocket(request: HttpServerRequest.HttpServerRequest, target: st
       queue.length = 0
     }
     remote.onmessage = (event) => writeSocket(write, event.data)
-    remote.onerror = () => Effect.runFork(write(new Socket.CloseEvent(1011, "proxy error")).pipe(Effect.catch(() => Effect.void)))
+    remote.onerror = () =>
+      Effect.runFork(write(new Socket.CloseEvent(1011, "proxy error")).pipe(Effect.catch(() => Effect.void)))
     remote.onclose = (event) =>
       Effect.runFork(write(new Socket.CloseEvent(event.code, event.reason)).pipe(Effect.catch(() => Effect.void)))
 
@@ -109,7 +116,9 @@ function proxyRemote(
   const url = workspaceProxyURL(target.url, requestURL)
   const source = sourceRequest(request)
   if (source.headers.get("upgrade")?.toLowerCase() === "websocket") return proxyWebSocket(request, url)
-  return Effect.promise(() => ServerProxy.http(url, target.headers, source, workspace.id)).pipe(Effect.map(HttpServerResponse.raw))
+  return Effect.promise(() => ServerProxy.http(url, target.headers, source, workspace.id)).pipe(
+    Effect.map(HttpServerResponse.raw),
+  )
 }
 
 function requestContext() {
@@ -118,14 +127,19 @@ function requestContext() {
   )
 }
 
-function provideRequestContext(effect: HandlerEffect, request: HttpServerRequest.HttpServerRequest, sessionWorkspaceID?: WorkspaceID) {
+function provideRequestContext(
+  effect: HandlerEffect,
+  request: HttpServerRequest.HttpServerRequest,
+  sessionWorkspaceID?: WorkspaceID,
+) {
   return Effect.gen(function* () {
     const url = new URL(request.url, "http://localhost")
     const headers = requestHeaders(request)
     const envWorkspaceID = Flag.OPENCODE_WORKSPACE_ID ? WorkspaceID.make(Flag.OPENCODE_WORKSPACE_ID) : undefined
     const workspaceParam = url.searchParams.get("workspace")
     const workspaceID = sessionWorkspaceID ?? (workspaceParam ? WorkspaceID.make(workspaceParam) : undefined)
-    const workspace = workspaceID && !envWorkspaceID ? yield* Effect.promise(() => Workspace.get(workspaceID)) : undefined
+    const workspace =
+      workspaceID && !envWorkspaceID ? yield* Effect.promise(() => Workspace.get(workspaceID)) : undefined
 
     if (workspaceID && !workspace && !envWorkspaceID) {
       return HttpServerResponse.text(`Workspace not found: ${workspaceID}`, {
@@ -134,7 +148,12 @@ function provideRequestContext(effect: HandlerEffect, request: HttpServerRequest
       })
     }
 
-    if (workspace && !isLocalWorkspaceRoute(request.method, url.pathname) && !url.pathname.startsWith("/console") && !envWorkspaceID) {
+    if (
+      workspace &&
+      !isLocalWorkspaceRoute(request.method, url.pathname) &&
+      !url.pathname.startsWith("/console") &&
+      !envWorkspaceID
+    ) {
       const adaptor = yield* Effect.promise(() => getAdaptor(workspace.projectID, workspace.type))
       const target = yield* Effect.promise(() => Promise.resolve(adaptor.target(workspace)))
       if (target.type === "remote") return yield* proxyRemote(request, workspace, target, url)
@@ -186,6 +205,8 @@ export const instanceContextLayer = Layer.succeed(
   InstanceContextMiddleware.of((effect) => provideInstanceContext(effect)),
 )
 
-export const instanceRouterLayer = HttpRouter.middleware()(Effect.succeed((effect) =>
-  requestContext().pipe(Effect.flatMap((request) => provideRequestContext(effect, request))),
-)).layer
+export const instanceRouterLayer = HttpRouter.middleware()(
+  Effect.succeed((effect) =>
+    requestContext().pipe(Effect.flatMap((request) => provideRequestContext(effect, request))),
+  ),
+).layer

+ 4 - 1
packages/opencode/src/server/routes/instance/httpapi/lifecycle.ts

@@ -14,7 +14,10 @@ export const markInstanceForDisposal = (ctx: InstanceContext) =>
 
 export const markInstanceForReload = (ctx: InstanceContext, next: Parameters<typeof Instance.reload>[0]) =>
   HttpEffect.appendPreResponseHandler((_request, response) =>
-    Effect.as(Effect.uninterruptible(Effect.promise(() => Instance.restore(ctx, () => Instance.reload(next)))), response),
+    Effect.as(
+      Effect.uninterruptible(Effect.promise(() => Instance.restore(ctx, () => Instance.reload(next)))),
+      response,
+    ),
   )
 
 export const disposeMiddleware: HttpMiddleware.HttpMiddleware = (effect) =>

+ 21 - 13
packages/opencode/src/server/routes/instance/httpapi/public.ts

@@ -126,8 +126,13 @@ function matchLegacyOpenApi(input: Record<string, unknown>) {
           // Workspace creation fields `branch` and `extra` are Schema.NullOr —
           // genuinely nullable, not just optional. Re-add the null that the
           // component-level strip above removed.
-          const ref = operation.requestBody.content?.["application/json"]?.schema?.$ref?.replace("#/components/schemas/", "")
-          const properties = ref ? spec.components?.schemas?.[ref]?.properties : operation.requestBody.content?.["application/json"]?.schema?.properties
+          const ref = operation.requestBody.content?.["application/json"]?.schema?.$ref?.replace(
+            "#/components/schemas/",
+            "",
+          )
+          const properties = ref
+            ? spec.components?.schemas?.[ref]?.properties
+            : operation.requestBody.content?.["application/json"]?.schema?.properties
           if (properties?.branch) properties.branch = { anyOf: [properties.branch, { type: "null" }] }
           if (properties?.extra) properties.extra = { anyOf: [properties.extra, { type: "null" }] }
         }
@@ -150,7 +155,10 @@ function matchLegacyOpenApi(input: Record<string, unknown>) {
           description: "Event stream",
           content: {
             "text/event-stream": {
-              schema: path === "/event" ? { $ref: "#/components/schemas/Event" } : { $ref: "#/components/schemas/GlobalEvent" },
+              schema:
+                path === "/event"
+                  ? { $ref: "#/components/schemas/Event" }
+                  : { $ref: "#/components/schemas/GlobalEvent" },
             },
           },
         }
@@ -251,7 +259,8 @@ function applyLegacySchemaOverrides(spec: OpenApiSpec) {
     schemas.Workspace.properties.directory = nullable(schemas.Workspace.properties.directory)
     schemas.Workspace.properties.extra = nullable(schemas.Workspace.properties.extra)
   }
-  if (schemas.GlobalSession?.properties?.project) schemas.GlobalSession.properties.project = nullable(schemas.GlobalSession.properties.project)
+  if (schemas.GlobalSession?.properties?.project)
+    schemas.GlobalSession.properties.project = nullable(schemas.GlobalSession.properties.project)
   const providerOptions = schemas.ProviderConfig?.properties?.options
   if (providerOptions) providerOptions.additionalProperties = {}
   const model = schemas.ProviderConfig?.properties?.models?.additionalProperties
@@ -486,12 +495,11 @@ function normalizeParameter(param: OpenApiParameter, route: string) {
   param.schema = stripOptionalNull(param.schema)
 }
 
-export const PublicApi = OpenCodeHttpApi
-  .annotateMerge(
-    OpenApi.annotations({
-      title: "opencode",
-      version: "1.0.0",
-      description: "opencode api",
-      transform: matchLegacyOpenApi,
-    }),
-  )
+export const PublicApi = OpenCodeHttpApi.annotateMerge(
+  OpenApi.annotations({
+    title: "opencode",
+    version: "1.0.0",
+    description: "opencode api",
+    transform: matchLegacyOpenApi,
+  }),
+)

+ 7 - 2
packages/opencode/src/server/server.ts

@@ -38,7 +38,9 @@ type ServerApp = {
   request(input: string | URL | Request, init?: RequestInit): Response | Promise<Response>
 }
 
-const DefaultHono = lazy(() => withBackend({ backend: "hono", reason: "stable" }, createHono({}, { backend: "hono", reason: "stable" })))
+const DefaultHono = lazy(() =>
+  withBackend({ backend: "hono", reason: "stable" }, createHono({}, { backend: "hono", reason: "stable" })),
+)
 const DefaultHttpApi = lazy(() => createDefaultHttpApi())
 
 function select() {
@@ -86,7 +88,10 @@ function createHttpApi() {
   }
 }
 
-function createHono(opts: { cors?: string[] }, selection: ServerBackend.Selection = ServerBackend.force(select(), "hono")) {
+function createHono(
+  opts: { cors?: string[] },
+  selection: ServerBackend.Selection = ServerBackend.force(select(), "hono"),
+) {
   const backendAttributes = ServerBackend.attributes(selection)
   const app = new Hono()
     .onError(ErrorMiddleware)

+ 3 - 1
packages/opencode/src/session/message-v2.ts

@@ -461,7 +461,9 @@ type AssistantError = z.infer<typeof AssistantErrorZod>
 // Effect Schema for the same union — used by HttpApi OpenAPI generation.
 const AssistantErrorSchema = Schema.Union([
   AuthError.EffectSchema,
-  Schema.Struct({ name: Schema.Literal("UnknownError"), data: Schema.Struct({ message: Schema.String }) }).annotate({ identifier: "UnknownError" }),
+  Schema.Struct({ name: Schema.Literal("UnknownError"), data: Schema.Struct({ message: Schema.String }) }).annotate({
+    identifier: "UnknownError",
+  }),
   OutputLengthError.EffectSchema,
   AbortedError.EffectSchema,
   StructuredOutputError.EffectSchema,

+ 0 - 2
packages/opencode/src/util/schema.ts

@@ -11,8 +11,6 @@ export const PositiveInt = Schema.Int.check(Schema.isGreaterThan(0))
  */
 export const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
 
-
-
 /**
  * Optional public JSON field that can hold explicit `undefined` on the type
  * side but encodes it as an omitted key, matching legacy `JSON.stringify`.

+ 3 - 3
packages/opencode/test/server/httpapi-bridge.test.ts

@@ -126,9 +126,9 @@ function requestBodyKey(spec: OpenApiSpec, body: unknown) {
 
 function requestBodySchemaKind(spec: OpenApiSpec, schema: OpenApiSchema | undefined) {
   if (!schema) return ""
-  const resolved = (schema.$ref ? spec.components?.schemas?.[schema.$ref.replace("#/components/schemas/", "")] : schema) as
-    | OpenApiSchema
-    | undefined
+  const resolved = (
+    schema.$ref ? spec.components?.schemas?.[schema.$ref.replace("#/components/schemas/", "")] : schema
+  ) as OpenApiSchema | undefined
   if (resolved?.properties) return "object"
   if (resolved?.anyOf ?? resolved?.oneOf ?? resolved?.allOf) return "object"
   return resolved?.type ?? schema.type ?? "inline"

+ 5 - 1
packages/opencode/test/server/httpapi-workspace.test.ts

@@ -214,7 +214,11 @@ describe("workspace HttpApi", () => {
     const workspace = await Instance.provide({
       directory: tmp.path,
       fn: async () => {
-        registerAdaptor(Instance.project.id, "remote-target", remoteAdaptor(path.join(tmp.path, ".remote"), "https://remote.test/base"))
+        registerAdaptor(
+          Instance.project.id,
+          "remote-target",
+          remoteAdaptor(path.join(tmp.path, ".remote"), "https://remote.test/base"),
+        )
         return Workspace.create({
           type: "remote-target",
           branch: null,

+ 267 - 95
packages/sdk/openapi.json

@@ -1339,10 +1339,14 @@
                     "type": "object",
                     "properties": {
                       "rows": {
-                        "type": "number"
+                        "type": "integer",
+                        "exclusiveMinimum": 0,
+                        "maximum": 9007199254740991
                       },
                       "cols": {
-                        "type": "number"
+                        "type": "integer",
+                        "exclusiveMinimum": 0,
+                        "maximum": 9007199254740991
                       }
                     },
                     "required": ["rows", "cols"]
@@ -5595,10 +5599,14 @@
                         "required": ["text"]
                       },
                       "line_number": {
-                        "type": "number"
+                        "type": "integer",
+                        "minimum": 0,
+                        "maximum": 9007199254740991
                       },
                       "absolute_offset": {
-                        "type": "number"
+                        "type": "integer",
+                        "minimum": 0,
+                        "maximum": 9007199254740991
                       },
                       "submatches": {
                         "type": "array",
@@ -5615,10 +5623,14 @@
                               "required": ["text"]
                             },
                             "start": {
-                              "type": "number"
+                              "type": "integer",
+                              "minimum": 0,
+                              "maximum": 9007199254740991
                             },
                             "end": {
-                              "type": "number"
+                              "type": "integer",
+                              "minimum": 0,
+                              "maximum": 9007199254740991
                             }
                           },
                           "required": ["match", "start", "end"]
@@ -6901,7 +6913,9 @@
                   },
                   "duration": {
                     "description": "Duration in milliseconds",
-                    "type": "number"
+                    "type": "integer",
+                    "exclusiveMinimum": 0,
+                    "maximum": 9007199254740991
                   }
                 },
                 "required": ["message", "variant"]
@@ -7621,13 +7635,19 @@
             "type": "object",
             "properties": {
               "created": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               },
               "updated": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               },
               "initialized": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               }
             },
             "required": ["created", "updated"]
@@ -7913,10 +7933,14 @@
             "type": "string"
           },
           "additions": {
-            "type": "number"
+            "type": "integer",
+            "minimum": 0,
+            "maximum": 9007199254740991
           },
           "deletions": {
-            "type": "number"
+            "type": "integer",
+            "minimum": 0,
+            "maximum": 9007199254740991
           },
           "status": {
             "type": "string",
@@ -8039,7 +8063,9 @@
                 "type": "string"
               },
               "retries": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               }
             },
             "required": ["message", "retries"]
@@ -8083,7 +8109,9 @@
                 "type": "string"
               },
               "statusCode": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               },
               "isRetryable": {
                 "type": "boolean"
@@ -8420,13 +8448,17 @@
                 "const": "retry"
               },
               "attempt": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               },
               "message": {
                 "type": "string"
               },
               "next": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               }
             },
             "required": ["type", "attempt", "message", "next"]
@@ -8591,7 +8623,9 @@
               },
               "duration": {
                 "description": "Duration in milliseconds",
-                "type": "number"
+                "type": "integer",
+                "exclusiveMinimum": 0,
+                "maximum": 9007199254740991
               }
             },
             "required": ["message", "variant"]
@@ -8777,7 +8811,9 @@
             "enum": ["running", "exited"]
           },
           "pid": {
-            "type": "number"
+            "type": "integer",
+            "exclusiveMinimum": 0,
+            "maximum": 9007199254740991
           }
         },
         "required": ["id", "title", "command", "args", "cwd", "status", "pid"]
@@ -8835,7 +8871,9 @@
                 "pattern": "^pty.*"
               },
               "exitCode": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               }
             },
             "required": ["id", "exitCode"]
@@ -9024,7 +9062,9 @@
             "type": "object",
             "properties": {
               "created": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               }
             },
             "required": ["created"]
@@ -9102,10 +9142,14 @@
             "type": "object",
             "properties": {
               "created": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               },
               "completed": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               }
             },
             "required": ["created"]
@@ -9173,25 +9217,37 @@
             "type": "object",
             "properties": {
               "total": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               },
               "input": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               },
               "output": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               },
               "reasoning": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               },
               "cache": {
                 "type": "object",
                 "properties": {
                   "read": {
-                    "type": "number"
+                    "type": "integer",
+                    "minimum": 0,
+                    "maximum": 9007199254740991
                   },
                   "write": {
-                    "type": "number"
+                    "type": "integer",
+                    "minimum": 0,
+                    "maximum": 9007199254740991
                   }
                 },
                 "required": ["read", "write"]
@@ -9311,10 +9367,14 @@
             "type": "object",
             "properties": {
               "start": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               },
               "end": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               }
             },
             "required": ["start"]
@@ -9408,10 +9468,14 @@
             "type": "object",
             "properties": {
               "start": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               },
               "end": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               }
             },
             "required": ["start"]
@@ -9427,12 +9491,12 @@
           },
           "start": {
             "type": "integer",
-            "minimum": -9007199254740991,
+            "minimum": 0,
             "maximum": 9007199254740991
           },
           "end": {
             "type": "integer",
-            "minimum": -9007199254740991,
+            "minimum": 0,
             "maximum": 9007199254740991
           }
         },
@@ -9461,10 +9525,14 @@
             "type": "object",
             "properties": {
               "line": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               },
               "character": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               }
             },
             "required": ["line", "character"]
@@ -9473,10 +9541,14 @@
             "type": "object",
             "properties": {
               "line": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               },
               "character": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               }
             },
             "required": ["line", "character"]
@@ -9505,7 +9577,7 @@
           },
           "kind": {
             "type": "integer",
-            "minimum": -9007199254740991,
+            "minimum": 0,
             "maximum": 9007199254740991
           }
         },
@@ -9625,7 +9697,9 @@
             "type": "object",
             "properties": {
               "start": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               }
             },
             "required": ["start"]
@@ -9664,13 +9738,19 @@
             "type": "object",
             "properties": {
               "start": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               },
               "end": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               },
               "compacted": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               }
             },
             "required": ["start", "end"]
@@ -9712,10 +9792,14 @@
             "type": "object",
             "properties": {
               "start": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               },
               "end": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               }
             },
             "required": ["start", "end"]
@@ -9834,25 +9918,37 @@
             "type": "object",
             "properties": {
               "total": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               },
               "input": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               },
               "output": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               },
               "reasoning": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               },
               "cache": {
                 "type": "object",
                 "properties": {
                   "read": {
-                    "type": "number"
+                    "type": "integer",
+                    "minimum": 0,
+                    "maximum": 9007199254740991
                   },
                   "write": {
-                    "type": "number"
+                    "type": "integer",
+                    "minimum": 0,
+                    "maximum": 9007199254740991
                   }
                 },
                 "required": ["read", "write"]
@@ -9949,12 +10045,12 @@
               },
               "start": {
                 "type": "integer",
-                "minimum": -9007199254740991,
+                "minimum": 0,
                 "maximum": 9007199254740991
               },
               "end": {
                 "type": "integer",
-                "minimum": -9007199254740991,
+                "minimum": 0,
                 "maximum": 9007199254740991
               }
             },
@@ -9983,7 +10079,9 @@
             "const": "retry"
           },
           "attempt": {
-            "type": "number"
+            "type": "integer",
+            "minimum": 0,
+            "maximum": 9007199254740991
           },
           "error": {
             "$ref": "#/components/schemas/APIError"
@@ -9992,7 +10090,9 @@
             "type": "object",
             "properties": {
               "created": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               }
             },
             "required": ["created"]
@@ -10090,7 +10190,9 @@
                 "$ref": "#/components/schemas/Part"
               },
               "time": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               }
             },
             "required": ["sessionID", "part", "time"]
@@ -10182,13 +10284,19 @@
             "type": "object",
             "properties": {
               "additions": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               },
               "deletions": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               },
               "files": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               },
               "diffs": {
                 "type": "array",
@@ -10218,16 +10326,24 @@
             "type": "object",
             "properties": {
               "created": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               },
               "updated": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               },
               "compacting": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               },
               "archived": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               }
             },
             "required": ["created", "updated"]
@@ -10434,7 +10550,9 @@
                 "$ref": "#/components/schemas/Part"
               },
               "time": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               }
             },
             "required": ["sessionID", "part", "time"]
@@ -10631,13 +10749,19 @@
                         "type": "object",
                         "properties": {
                           "additions": {
-                            "type": "number"
+                            "type": "integer",
+                            "minimum": 0,
+                            "maximum": 9007199254740991
                           },
                           "deletions": {
-                            "type": "number"
+                            "type": "integer",
+                            "minimum": 0,
+                            "maximum": 9007199254740991
                           },
                           "files": {
-                            "type": "number"
+                            "type": "integer",
+                            "minimum": 0,
+                            "maximum": 9007199254740991
                           },
                           "diffs": {
                             "type": "array",
@@ -10694,7 +10818,9 @@
                       "created": {
                         "anyOf": [
                           {
-                            "type": "number"
+                            "type": "integer",
+                            "minimum": 0,
+                            "maximum": 9007199254740991
                           },
                           {
                             "type": "null"
@@ -10704,7 +10830,9 @@
                       "updated": {
                         "anyOf": [
                           {
-                            "type": "number"
+                            "type": "integer",
+                            "minimum": 0,
+                            "maximum": 9007199254740991
                           },
                           {
                             "type": "null"
@@ -10714,7 +10842,9 @@
                       "compacting": {
                         "anyOf": [
                           {
-                            "type": "number"
+                            "type": "integer",
+                            "minimum": 0,
+                            "maximum": 9007199254740991
                           },
                           {
                             "type": "null"
@@ -10724,7 +10854,9 @@
                       "archived": {
                         "anyOf": [
                           {
-                            "type": "number"
+                            "type": "integer",
+                            "minimum": 0,
+                            "maximum": 9007199254740991
                           },
                           {
                             "type": "null"
@@ -11481,7 +11613,9 @@
           },
           "timeout": {
             "description": "Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified.",
-            "type": "number"
+            "type": "integer",
+            "exclusiveMinimum": 0,
+            "maximum": 9007199254740991
           }
         },
         "required": ["type", "command"]
@@ -11547,7 +11681,9 @@
           },
           "timeout": {
             "description": "Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified.",
-            "type": "number"
+            "type": "integer",
+            "exclusiveMinimum": 0,
+            "maximum": 9007199254740991
           }
         },
         "required": ["type", "url"]
@@ -12055,7 +12191,9 @@
             "type": "string"
           },
           "expires": {
-            "type": "number"
+            "type": "integer",
+            "minimum": 0,
+            "maximum": 9007199254740991
           },
           "accountId": {
             "type": "string"
@@ -12458,7 +12596,9 @@
             "type": "string"
           },
           "switchableOrgCount": {
-            "type": "number"
+            "type": "integer",
+            "minimum": 0,
+            "maximum": 9007199254740991
           }
         },
         "required": ["consoleManagedProviders", "switchableOrgCount"]
@@ -12579,13 +12719,19 @@
             "type": "object",
             "properties": {
               "additions": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               },
               "deletions": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               },
               "files": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               },
               "diffs": {
                 "type": "array",
@@ -12615,16 +12761,24 @@
             "type": "object",
             "properties": {
               "created": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               },
               "updated": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               },
               "compacting": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               },
               "archived": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               }
             },
             "required": ["created", "updated"]
@@ -12710,10 +12864,14 @@
             "type": "object",
             "properties": {
               "start": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               },
               "end": {
-                "type": "number"
+                "type": "integer",
+                "minimum": 0,
+                "maximum": 9007199254740991
               }
             },
             "required": ["start"]
@@ -12776,12 +12934,12 @@
               },
               "start": {
                 "type": "integer",
-                "minimum": -9007199254740991,
+                "minimum": 0,
                 "maximum": 9007199254740991
               },
               "end": {
                 "type": "integer",
-                "minimum": -9007199254740991,
+                "minimum": 0,
                 "maximum": 9007199254740991
               }
             },
@@ -12956,7 +13114,9 @@
             "type": "string"
           },
           "kind": {
-            "type": "number"
+            "type": "integer",
+            "minimum": 0,
+            "maximum": 9007199254740991
           },
           "location": {
             "type": "object",
@@ -13029,16 +13189,24 @@
                   "type": "object",
                   "properties": {
                     "oldStart": {
-                      "type": "number"
+                      "type": "integer",
+                      "minimum": 0,
+                      "maximum": 9007199254740991
                     },
                     "oldLines": {
-                      "type": "number"
+                      "type": "integer",
+                      "minimum": 0,
+                      "maximum": 9007199254740991
                     },
                     "newStart": {
-                      "type": "number"
+                      "type": "integer",
+                      "minimum": 0,
+                      "maximum": 9007199254740991
                     },
                     "newLines": {
-                      "type": "number"
+                      "type": "integer",
+                      "minimum": 0,
+                      "maximum": 9007199254740991
                     },
                     "lines": {
                       "type": "array",
@@ -13074,12 +13242,12 @@
           },
           "added": {
             "type": "integer",
-            "minimum": -9007199254740991,
+            "minimum": 0,
             "maximum": 9007199254740991
           },
           "removed": {
             "type": "integer",
-            "minimum": -9007199254740991,
+            "minimum": 0,
             "maximum": 9007199254740991
           },
           "status": {
@@ -13360,10 +13528,14 @@
             "type": "string"
           },
           "additions": {
-            "type": "number"
+            "type": "integer",
+            "minimum": 0,
+            "maximum": 9007199254740991
           },
           "deletions": {
-            "type": "number"
+            "type": "integer",
+            "minimum": 0,
+            "maximum": 9007199254740991
           },
           "status": {
             "type": "string",