Fix OpenAPI workspace query drift (#26609)

This commit is contained in:
Kit Langton
2026-05-09 21:12:34 -04:00
committed by GitHub
parent 7a79f3a5ea
commit e30d8173c1
26 changed files with 752 additions and 212 deletions

View File

@@ -5,6 +5,7 @@ import * as Stream from "effect/Stream"
import { HttpServerResponse } from "effect/unstable/http"
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
import * as Sse from "effect/unstable/encoding/Sse"
import { WorkspaceRoutingQuery } from "./middleware/workspace-routing"
const log = Log.create({ service: "server" })
@@ -16,6 +17,7 @@ export const EventApi = HttpApi.make("event").add(
HttpApiGroup.make("event")
.add(
HttpApiEndpoint.get("subscribe", EventPaths.event, {
query: WorkspaceRoutingQuery,
success: Schema.String.pipe(HttpApiSchema.asText({ contentType: "text/event-stream" })),
}).annotateMerge(
OpenApi.annotations({

View File

@@ -3,7 +3,7 @@ import { Provider } from "@/provider/provider"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../middleware/authorization"
import { InstanceContextMiddleware } from "../middleware/instance-context"
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
import { described } from "./metadata"
const root = "/config"
@@ -13,6 +13,7 @@ export const ConfigApi = HttpApi.make("config")
HttpApiGroup.make("config")
.add(
HttpApiEndpoint.get("get", root, {
query: WorkspaceRoutingQuery,
success: described(Config.Info, "Get config info"),
}).annotateMerge(
OpenApi.annotations({
@@ -22,6 +23,7 @@ export const ConfigApi = HttpApi.make("config")
}),
),
HttpApiEndpoint.patch("update", root, {
query: WorkspaceRoutingQuery,
payload: Config.Info,
success: described(Config.Info, "Successfully updated config"),
error: HttpApiError.BadRequest,
@@ -33,6 +35,7 @@ export const ConfigApi = HttpApi.make("config")
}),
),
HttpApiEndpoint.get("providers", `${root}/providers`, {
query: WorkspaceRoutingQuery,
success: described(Provider.ConfigProvidersResult, "List of providers"),
}).annotateMerge(
OpenApi.annotations({

View File

@@ -8,7 +8,11 @@ import { Schema, SchemaGetter } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../middleware/authorization"
import { InstanceContextMiddleware } from "../middleware/instance-context"
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQueryFields } from "../middleware/workspace-routing"
import {
WorkspaceRoutingMiddleware,
WorkspaceRoutingQuery,
WorkspaceRoutingQueryFields,
} from "../middleware/workspace-routing"
import { described } from "./metadata"
const ConsoleStateResponse = Schema.Struct({
@@ -82,6 +86,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
HttpApiGroup.make("experimental")
.add(
HttpApiEndpoint.get("console", ExperimentalPaths.console, {
query: WorkspaceRoutingQuery,
success: described(ConsoleStateResponse, "Active Console provider metadata"),
error: HttpApiError.InternalServerError,
}).annotateMerge(
@@ -92,6 +97,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
}),
),
HttpApiEndpoint.get("consoleOrgs", ExperimentalPaths.consoleOrgs, {
query: WorkspaceRoutingQuery,
success: described(ConsoleOrgList, "Switchable Console orgs"),
error: HttpApiError.InternalServerError,
}).annotateMerge(
@@ -102,6 +108,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
}),
),
HttpApiEndpoint.post("consoleSwitch", ExperimentalPaths.consoleSwitch, {
query: WorkspaceRoutingQuery,
payload: ConsoleSwitchPayload,
success: described(Schema.Boolean, "Switch success"),
error: HttpApiError.BadRequest,
@@ -125,6 +132,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
}),
),
HttpApiEndpoint.get("toolIDs", ExperimentalPaths.toolIDs, {
query: WorkspaceRoutingQuery,
success: described(ToolIDs, "Tool IDs"),
error: HttpApiError.BadRequest,
}).annotateMerge(
@@ -136,6 +144,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
}),
),
HttpApiEndpoint.get("worktree", ExperimentalPaths.worktree, {
query: WorkspaceRoutingQuery,
success: described(WorktreeList, "List of worktree directories"),
}).annotateMerge(
OpenApi.annotations({
@@ -145,6 +154,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
}),
),
HttpApiEndpoint.post("worktreeCreate", ExperimentalPaths.worktree, {
query: WorkspaceRoutingQuery,
payload: Schema.optional(Worktree.CreateInput),
success: described(Worktree.Info, "Worktree created"),
error: HttpApiError.BadRequest,
@@ -156,6 +166,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
}),
),
HttpApiEndpoint.delete("worktreeRemove", ExperimentalPaths.worktree, {
query: WorkspaceRoutingQuery,
payload: Worktree.RemoveInput,
success: described(Schema.Boolean, "Worktree removed"),
error: HttpApiError.BadRequest,
@@ -167,6 +178,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
}),
),
HttpApiEndpoint.post("worktreeReset", ExperimentalPaths.worktreeReset, {
query: WorkspaceRoutingQuery,
payload: Worktree.ResetInput,
success: described(Schema.Boolean, "Worktree reset"),
error: HttpApiError.BadRequest,
@@ -189,6 +201,7 @@ export const ExperimentalApi = HttpApi.make("experimental")
}),
),
HttpApiEndpoint.get("resource", ExperimentalPaths.resource, {
query: WorkspaceRoutingQuery,
success: described(Schema.Record(Schema.String, MCP.Resource), "MCP resources"),
}).annotateMerge(
OpenApi.annotations({

View File

@@ -5,7 +5,11 @@ import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../middleware/authorization"
import { InstanceContextMiddleware } from "../middleware/instance-context"
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQueryFields } from "../middleware/workspace-routing"
import {
WorkspaceRoutingMiddleware,
WorkspaceRoutingQuery,
WorkspaceRoutingQueryFields,
} from "../middleware/workspace-routing"
import { described } from "./metadata"
export const FileQuery = Schema.Struct({
@@ -97,6 +101,7 @@ export const FileApi = HttpApi.make("file")
}),
),
HttpApiEndpoint.get("status", FilePaths.status, {
query: WorkspaceRoutingQuery,
success: described(Schema.Array(File.Info), "File status"),
}).annotateMerge(
OpenApi.annotations({

View File

@@ -8,7 +8,11 @@ import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../middleware/authorization"
import { InstanceContextMiddleware } from "../middleware/instance-context"
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQueryFields } from "../middleware/workspace-routing"
import {
WorkspaceRoutingMiddleware,
WorkspaceRoutingQuery,
WorkspaceRoutingQueryFields,
} from "../middleware/workspace-routing"
import { described } from "./metadata"
const PathInfo = Schema.Struct({
@@ -55,6 +59,7 @@ export const InstanceApi = HttpApi.make("instance")
HttpApiGroup.make("instance")
.add(
HttpApiEndpoint.post("dispose", InstancePaths.dispose, {
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "Instance disposed"),
}).annotateMerge(
OpenApi.annotations({
@@ -64,6 +69,7 @@ export const InstanceApi = HttpApi.make("instance")
}),
),
HttpApiEndpoint.get("path", InstancePaths.path, {
query: WorkspaceRoutingQuery,
success: PathInfo,
}).annotateMerge(
OpenApi.annotations({
@@ -74,6 +80,7 @@ export const InstanceApi = HttpApi.make("instance")
}),
),
HttpApiEndpoint.get("vcs", InstancePaths.vcs, {
query: WorkspaceRoutingQuery,
success: described(Vcs.Info, "VCS info"),
}).annotateMerge(
OpenApi.annotations({
@@ -84,6 +91,7 @@ export const InstanceApi = HttpApi.make("instance")
}),
),
HttpApiEndpoint.get("vcsStatus", InstancePaths.vcsStatus, {
query: WorkspaceRoutingQuery,
success: described(Schema.Array(Vcs.FileStatus), "VCS status"),
}).annotateMerge(
OpenApi.annotations({
@@ -103,6 +111,7 @@ export const InstanceApi = HttpApi.make("instance")
}),
),
HttpApiEndpoint.get("vcsDiffRaw", InstancePaths.vcsDiffRaw, {
query: WorkspaceRoutingQuery,
success: described(
Schema.String.pipe(HttpApiSchema.asText({ contentType: "text/x-diff; charset=utf-8" })),
"Raw VCS diff",
@@ -115,6 +124,7 @@ export const InstanceApi = HttpApi.make("instance")
}),
),
HttpApiEndpoint.post("vcsApply", InstancePaths.vcsApply, {
query: WorkspaceRoutingQuery,
payload: Vcs.ApplyInput,
success: described(Vcs.ApplyResult, "VCS patch applied"),
error: ApiVcsApplyError,
@@ -126,6 +136,7 @@ export const InstanceApi = HttpApi.make("instance")
}),
),
HttpApiEndpoint.get("command", InstancePaths.command, {
query: WorkspaceRoutingQuery,
success: described(Schema.Array(Command.Info), "List of commands"),
}).annotateMerge(
OpenApi.annotations({
@@ -135,6 +146,7 @@ export const InstanceApi = HttpApi.make("instance")
}),
),
HttpApiEndpoint.get("agent", InstancePaths.agent, {
query: WorkspaceRoutingQuery,
success: described(Schema.Array(Agent.Info), "List of agents"),
}).annotateMerge(
OpenApi.annotations({
@@ -144,6 +156,7 @@ export const InstanceApi = HttpApi.make("instance")
}),
),
HttpApiEndpoint.get("skill", InstancePaths.skill, {
query: WorkspaceRoutingQuery,
success: described(Schema.Array(Skill.Info), "List of skills"),
}).annotateMerge(
OpenApi.annotations({
@@ -153,6 +166,7 @@ export const InstanceApi = HttpApi.make("instance")
}),
),
HttpApiEndpoint.get("lsp", InstancePaths.lsp, {
query: WorkspaceRoutingQuery,
success: described(Schema.Array(LSP.Status), "LSP server status"),
}).annotateMerge(
OpenApi.annotations({
@@ -162,6 +176,7 @@ export const InstanceApi = HttpApi.make("instance")
}),
),
HttpApiEndpoint.get("formatter", InstancePaths.formatter, {
query: WorkspaceRoutingQuery,
success: described(Schema.Array(Format.Status), "Formatter status"),
}).annotateMerge(
OpenApi.annotations({

View File

@@ -4,7 +4,7 @@ import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../middleware/authorization"
import { InstanceContextMiddleware } from "../middleware/instance-context"
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
import { described } from "./metadata"
export const AddPayload = Schema.Struct({
@@ -42,6 +42,7 @@ export const McpApi = HttpApi.make("mcp")
HttpApiGroup.make("mcp")
.add(
HttpApiEndpoint.get("status", McpPaths.status, {
query: WorkspaceRoutingQuery,
success: described(Schema.Record(Schema.String, MCP.Status), "MCP server status"),
}).annotateMerge(
OpenApi.annotations({
@@ -51,6 +52,7 @@ export const McpApi = HttpApi.make("mcp")
}),
),
HttpApiEndpoint.post("add", McpPaths.status, {
query: WorkspaceRoutingQuery,
payload: AddPayload,
success: described(StatusMap, "MCP server added successfully"),
error: HttpApiError.BadRequest,
@@ -63,6 +65,7 @@ export const McpApi = HttpApi.make("mcp")
),
HttpApiEndpoint.post("authStart", McpPaths.auth, {
params: { name: Schema.String },
query: WorkspaceRoutingQuery,
success: described(AuthStartResponse, "OAuth flow started"),
error: [UnsupportedOAuthError, HttpApiError.NotFound],
}).annotateMerge(
@@ -74,6 +77,7 @@ export const McpApi = HttpApi.make("mcp")
),
HttpApiEndpoint.post("authCallback", McpPaths.authCallback, {
params: { name: Schema.String },
query: WorkspaceRoutingQuery,
payload: AuthCallbackPayload,
success: described(MCP.Status, "OAuth authentication completed"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
@@ -87,6 +91,7 @@ export const McpApi = HttpApi.make("mcp")
),
HttpApiEndpoint.post("authAuthenticate", McpPaths.authAuthenticate, {
params: { name: Schema.String },
query: WorkspaceRoutingQuery,
success: described(MCP.Status, "OAuth authentication completed"),
error: [UnsupportedOAuthError, HttpApiError.NotFound],
}).annotateMerge(
@@ -98,6 +103,7 @@ export const McpApi = HttpApi.make("mcp")
),
HttpApiEndpoint.delete("authRemove", McpPaths.auth, {
params: { name: Schema.String },
query: WorkspaceRoutingQuery,
success: described(AuthRemoveResponse, "OAuth credentials removed"),
error: HttpApiError.NotFound,
}).annotateMerge(
@@ -109,6 +115,7 @@ export const McpApi = HttpApi.make("mcp")
),
HttpApiEndpoint.post("connect", McpPaths.connect, {
params: { name: Schema.String },
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "MCP server connected successfully"),
}).annotateMerge(
OpenApi.annotations({
@@ -118,6 +125,7 @@ export const McpApi = HttpApi.make("mcp")
),
HttpApiEndpoint.post("disconnect", McpPaths.disconnect, {
params: { name: Schema.String },
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "MCP server disconnected successfully"),
}).annotateMerge(
OpenApi.annotations({

View File

@@ -4,7 +4,7 @@ import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../middleware/authorization"
import { InstanceContextMiddleware } from "../middleware/instance-context"
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
import { described } from "./metadata"
const root = "/permission"
@@ -18,6 +18,7 @@ export const PermissionApi = HttpApi.make("permission")
HttpApiGroup.make("permission")
.add(
HttpApiEndpoint.get("list", root, {
query: WorkspaceRoutingQuery,
success: described(Schema.Array(Permission.Request), "List of pending permissions"),
}).annotateMerge(
OpenApi.annotations({
@@ -28,6 +29,7 @@ export const PermissionApi = HttpApi.make("permission")
),
HttpApiEndpoint.post("reply", `${root}/:requestID/reply`, {
params: { requestID: PermissionID },
query: WorkspaceRoutingQuery,
payload: ReplyPayload,
success: described(Schema.Boolean, "Permission processed successfully"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound],

View File

@@ -4,7 +4,7 @@ import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../middleware/authorization"
import { InstanceContextMiddleware } from "../middleware/instance-context"
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
import { described } from "./metadata"
const root = "/project"
@@ -19,6 +19,7 @@ export const ProjectApi = HttpApi.make("project")
HttpApiGroup.make("project")
.add(
HttpApiEndpoint.get("list", root, {
query: WorkspaceRoutingQuery,
success: described(Schema.Array(Project.Info), "List of projects"),
}).annotateMerge(
OpenApi.annotations({
@@ -28,6 +29,7 @@ export const ProjectApi = HttpApi.make("project")
}),
),
HttpApiEndpoint.get("current", `${root}/current`, {
query: WorkspaceRoutingQuery,
success: described(Project.Info, "Current project information"),
}).annotateMerge(
OpenApi.annotations({
@@ -37,6 +39,7 @@ export const ProjectApi = HttpApi.make("project")
}),
),
HttpApiEndpoint.post("initGit", `${root}/git/init`, {
query: WorkspaceRoutingQuery,
success: described(Project.Info, "Project information after git initialization"),
}).annotateMerge(
OpenApi.annotations({
@@ -47,6 +50,7 @@ export const ProjectApi = HttpApi.make("project")
),
HttpApiEndpoint.patch("update", `${root}/:projectID`, {
params: { projectID: ProjectID },
query: WorkspaceRoutingQuery,
payload: UpdatePayload,
success: described(Project.Info, "Updated project information"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound],

View File

@@ -5,7 +5,7 @@ import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../middleware/authorization"
import { InstanceContextMiddleware } from "../middleware/instance-context"
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
import { described } from "./metadata"
const root = "/provider"
@@ -15,6 +15,7 @@ export const ProviderApi = HttpApi.make("provider")
HttpApiGroup.make("provider")
.add(
HttpApiEndpoint.get("list", root, {
query: WorkspaceRoutingQuery,
success: described(Provider.ListResult, "List of providers"),
}).annotateMerge(
OpenApi.annotations({
@@ -24,6 +25,7 @@ export const ProviderApi = HttpApi.make("provider")
}),
),
HttpApiEndpoint.get("auth", `${root}/auth`, {
query: WorkspaceRoutingQuery,
success: described(ProviderAuth.Methods, "Provider auth methods"),
}).annotateMerge(
OpenApi.annotations({
@@ -34,6 +36,7 @@ export const ProviderApi = HttpApi.make("provider")
),
HttpApiEndpoint.post("authorize", `${root}/:providerID/oauth/authorize`, {
params: { providerID: ProviderID },
query: WorkspaceRoutingQuery,
payload: ProviderAuth.AuthorizeInput,
success: described(Schema.UndefinedOr(ProviderAuth.Authorization), "Authorization URL and method"),
error: HttpApiError.BadRequest,
@@ -46,6 +49,7 @@ export const ProviderApi = HttpApi.make("provider")
),
HttpApiEndpoint.post("callback", `${root}/:providerID/oauth/callback`, {
params: { providerID: ProviderID },
query: WorkspaceRoutingQuery,
payload: ProviderAuth.CallbackInput,
success: described(Schema.Boolean, "OAuth callback processed successfully"),
error: HttpApiError.BadRequest,

View File

@@ -5,7 +5,11 @@ import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../middleware/authorization"
import { InstanceContextMiddleware } from "../middleware/instance-context"
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQueryFields } from "../middleware/workspace-routing"
import {
WorkspaceRoutingMiddleware,
WorkspaceRoutingQuery,
WorkspaceRoutingQueryFields,
} from "../middleware/workspace-routing"
import { ApiNotFoundError } from "../errors"
import { described } from "./metadata"
@@ -37,6 +41,7 @@ export const PtyApi = HttpApi.make("pty")
HttpApiGroup.make("pty")
.add(
HttpApiEndpoint.get("shells", PtyPaths.shells, {
query: WorkspaceRoutingQuery,
success: described(Schema.Array(ShellItem), "List of shells"),
}).annotateMerge(
OpenApi.annotations({
@@ -46,6 +51,7 @@ export const PtyApi = HttpApi.make("pty")
}),
),
HttpApiEndpoint.get("list", PtyPaths.list, {
query: WorkspaceRoutingQuery,
success: described(Schema.Array(Pty.Info), "List of sessions"),
}).annotateMerge(
OpenApi.annotations({
@@ -55,6 +61,7 @@ export const PtyApi = HttpApi.make("pty")
}),
),
HttpApiEndpoint.post("create", PtyPaths.create, {
query: WorkspaceRoutingQuery,
payload: Pty.CreateInput,
success: described(Pty.Info, "Created session"),
error: HttpApiError.BadRequest,
@@ -67,6 +74,7 @@ export const PtyApi = HttpApi.make("pty")
),
HttpApiEndpoint.get("get", PtyPaths.get, {
params: { ptyID: PtyID },
query: WorkspaceRoutingQuery,
success: described(Pty.Info, "Session info"),
error: ApiNotFoundError,
}).annotateMerge(
@@ -78,6 +86,7 @@ export const PtyApi = HttpApi.make("pty")
),
HttpApiEndpoint.put("update", PtyPaths.update, {
params: { ptyID: PtyID },
query: WorkspaceRoutingQuery,
payload: Pty.UpdateInput,
success: described(Pty.Info, "Updated session"),
error: [HttpApiError.BadRequest, ApiNotFoundError],
@@ -90,6 +99,7 @@ export const PtyApi = HttpApi.make("pty")
),
HttpApiEndpoint.delete("remove", PtyPaths.remove, {
params: { ptyID: PtyID },
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "Session removed"),
error: ApiNotFoundError,
}).annotateMerge(
@@ -101,6 +111,7 @@ export const PtyApi = HttpApi.make("pty")
),
HttpApiEndpoint.post("connectToken", PtyPaths.connectToken, {
params: { ptyID: PtyID },
query: WorkspaceRoutingQuery,
success: described(PtyTicket.ConnectToken, "WebSocket connect token"),
error: [HttpApiError.Forbidden, ApiNotFoundError],
}).annotateMerge(
@@ -129,6 +140,7 @@ export const PtyConnectApi = HttpApi.make("pty-connect").add(
.add(
HttpApiEndpoint.get("connect", PtyPaths.connect, {
params: Params,
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "Connected session"),
error: [HttpApiError.Forbidden, HttpApiError.NotFound],
}).annotateMerge(

View File

@@ -4,7 +4,7 @@ import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../middleware/authorization"
import { InstanceContextMiddleware } from "../middleware/instance-context"
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
import { described } from "./metadata"
const root = "/question"
@@ -19,6 +19,7 @@ export const QuestionApi = HttpApi.make("question")
HttpApiGroup.make("question")
.add(
HttpApiEndpoint.get("list", root, {
query: WorkspaceRoutingQuery,
success: described(Schema.Array(Question.Request), "List of pending questions"),
}).annotateMerge(
OpenApi.annotations({
@@ -29,6 +30,7 @@ export const QuestionApi = HttpApi.make("question")
),
HttpApiEndpoint.post("reply", `${root}/:requestID/reply`, {
params: { requestID: QuestionID },
query: WorkspaceRoutingQuery,
payload: ReplyPayload,
success: described(Schema.Boolean, "Question answered successfully"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
@@ -41,6 +43,7 @@ export const QuestionApi = HttpApi.make("question")
),
HttpApiEndpoint.post("reject", `${root}/:requestID/reject`, {
params: { requestID: QuestionID },
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "Question rejected successfully"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
}).annotateMerge(

View File

@@ -14,7 +14,11 @@ import { Schema, SchemaGetter, Struct } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../middleware/authorization"
import { InstanceContextMiddleware } from "../middleware/instance-context"
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQueryFields } from "../middleware/workspace-routing"
import {
WorkspaceRoutingMiddleware,
WorkspaceRoutingQuery,
WorkspaceRoutingQueryFields,
} from "../middleware/workspace-routing"
import { ApiNotFoundError } from "../errors"
import { described } from "./metadata"
@@ -116,6 +120,7 @@ export const SessionApi = HttpApi.make("session")
}),
),
HttpApiEndpoint.get("status", SessionPaths.status, {
query: WorkspaceRoutingQuery,
success: described(StatusMap, "Get session status"),
error: HttpApiError.BadRequest,
}).annotateMerge(
@@ -127,6 +132,7 @@ export const SessionApi = HttpApi.make("session")
),
HttpApiEndpoint.get("get", SessionPaths.get, {
params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
success: described(Session.Info, "Get session"),
error: [HttpApiError.BadRequest, ApiNotFoundError],
}).annotateMerge(
@@ -138,6 +144,7 @@ export const SessionApi = HttpApi.make("session")
),
HttpApiEndpoint.get("children", SessionPaths.children, {
params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
success: described(Schema.Array(Session.Info), "List of children"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
}).annotateMerge(
@@ -149,6 +156,7 @@ export const SessionApi = HttpApi.make("session")
),
HttpApiEndpoint.get("todo", SessionPaths.todo, {
params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
success: described(Schema.Array(Todo.Info), "Todo list"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
}).annotateMerge(
@@ -183,6 +191,7 @@ export const SessionApi = HttpApi.make("session")
),
HttpApiEndpoint.get("message", SessionPaths.message, {
params: { sessionID: SessionID, messageID: MessageID },
query: WorkspaceRoutingQuery,
success: described(MessageV2.WithParts, "Message"),
error: [HttpApiError.BadRequest, ApiNotFoundError],
}).annotateMerge(
@@ -193,6 +202,7 @@ export const SessionApi = HttpApi.make("session")
}),
),
HttpApiEndpoint.post("create", SessionPaths.create, {
query: WorkspaceRoutingQuery,
payload: [HttpApiSchema.NoContent, Session.CreateInput],
success: described(Session.Info, "Successfully created session"),
error: HttpApiError.BadRequest,
@@ -205,6 +215,7 @@ export const SessionApi = HttpApi.make("session")
),
HttpApiEndpoint.delete("remove", SessionPaths.remove, {
params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "Successfully deleted session"),
error: [HttpApiError.BadRequest, ApiNotFoundError],
}).annotateMerge(
@@ -216,6 +227,7 @@ export const SessionApi = HttpApi.make("session")
),
HttpApiEndpoint.patch("update", SessionPaths.update, {
params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
payload: UpdatePayload,
success: described(Session.Info, "Successfully updated session"),
error: [HttpApiError.BadRequest, ApiNotFoundError],
@@ -228,6 +240,7 @@ export const SessionApi = HttpApi.make("session")
),
HttpApiEndpoint.post("fork", SessionPaths.fork, {
params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
payload: ForkPayload,
success: described(Session.Info, "200"),
error: ApiNotFoundError,
@@ -240,6 +253,7 @@ export const SessionApi = HttpApi.make("session")
),
HttpApiEndpoint.post("abort", SessionPaths.abort, {
params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "Aborted session"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
}).annotateMerge(
@@ -251,6 +265,7 @@ export const SessionApi = HttpApi.make("session")
),
HttpApiEndpoint.post("init", SessionPaths.init, {
params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
payload: InitPayload,
success: described(Schema.Boolean, "200"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
@@ -264,6 +279,7 @@ export const SessionApi = HttpApi.make("session")
),
HttpApiEndpoint.post("share", SessionPaths.share, {
params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
success: described(Session.Info, "Successfully shared session"),
error: [HttpApiError.InternalServerError, ApiNotFoundError],
}).annotateMerge(
@@ -275,6 +291,7 @@ export const SessionApi = HttpApi.make("session")
),
HttpApiEndpoint.delete("unshare", SessionPaths.share, {
params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
success: described(Session.Info, "Successfully unshared session"),
error: [HttpApiError.InternalServerError, ApiNotFoundError],
}).annotateMerge(
@@ -286,6 +303,7 @@ export const SessionApi = HttpApi.make("session")
),
HttpApiEndpoint.post("summarize", SessionPaths.summarize, {
params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
payload: SummarizePayload,
success: described(Schema.Boolean, "Summarized session"),
error: [HttpApiError.BadRequest, ApiNotFoundError],
@@ -298,6 +316,7 @@ export const SessionApi = HttpApi.make("session")
),
HttpApiEndpoint.post("prompt", SessionPaths.prompt, {
params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
payload: PromptPayload,
success: described(MessageV2.WithParts, "Created message"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
@@ -310,6 +329,7 @@ export const SessionApi = HttpApi.make("session")
),
HttpApiEndpoint.post("promptAsync", SessionPaths.promptAsync, {
params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
payload: PromptPayload,
success: described(HttpApiSchema.NoContent, "Prompt accepted"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
@@ -323,6 +343,7 @@ export const SessionApi = HttpApi.make("session")
),
HttpApiEndpoint.post("command", SessionPaths.command, {
params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
payload: CommandPayload,
success: described(MessageV2.WithParts, "Created message"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
@@ -335,6 +356,7 @@ export const SessionApi = HttpApi.make("session")
),
HttpApiEndpoint.post("shell", SessionPaths.shell, {
params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
payload: ShellPayload,
success: described(MessageV2.WithParts, "Created message"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
@@ -347,6 +369,7 @@ export const SessionApi = HttpApi.make("session")
),
HttpApiEndpoint.post("revert", SessionPaths.revert, {
params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
payload: RevertPayload,
success: described(Session.Info, "Updated session"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
@@ -360,6 +383,7 @@ export const SessionApi = HttpApi.make("session")
),
HttpApiEndpoint.post("unrevert", SessionPaths.unrevert, {
params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
success: described(Session.Info, "Updated session"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
}).annotateMerge(
@@ -371,6 +395,7 @@ export const SessionApi = HttpApi.make("session")
),
HttpApiEndpoint.post("permissionRespond", SessionPaths.permissions, {
params: { sessionID: SessionID, permissionID: PermissionID },
query: WorkspaceRoutingQuery,
payload: PermissionResponsePayload,
success: described(Schema.Boolean, "Permission processed successfully"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
@@ -384,6 +409,7 @@ export const SessionApi = HttpApi.make("session")
),
HttpApiEndpoint.delete("deleteMessage", SessionPaths.deleteMessage, {
params: { sessionID: SessionID, messageID: MessageID },
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "Successfully deleted message"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
}).annotateMerge(
@@ -396,6 +422,7 @@ export const SessionApi = HttpApi.make("session")
),
HttpApiEndpoint.delete("deletePart", SessionPaths.deletePart, {
params: { sessionID: SessionID, messageID: MessageID, partID: PartID },
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "Successfully deleted part"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
}).annotateMerge(
@@ -406,6 +433,7 @@ export const SessionApi = HttpApi.make("session")
),
HttpApiEndpoint.patch("updatePart", SessionPaths.updatePart, {
params: { sessionID: SessionID, messageID: MessageID, partID: PartID },
query: WorkspaceRoutingQuery,
payload: MessageV2.Part,
success: described(MessageV2.Part, "Successfully updated part"),
error: [HttpApiError.BadRequest, HttpApiError.NotFound],

View File

@@ -4,7 +4,7 @@ import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../middleware/authorization"
import { InstanceContextMiddleware } from "../middleware/instance-context"
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
import { described } from "./metadata"
const root = "/sync"
@@ -46,6 +46,7 @@ export const SyncApi = HttpApi.make("sync")
HttpApiGroup.make("sync")
.add(
HttpApiEndpoint.post("start", SyncPaths.start, {
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "Workspace sync started"),
}).annotateMerge(
OpenApi.annotations({
@@ -55,6 +56,7 @@ export const SyncApi = HttpApi.make("sync")
}),
),
HttpApiEndpoint.post("replay", SyncPaths.replay, {
query: WorkspaceRoutingQuery,
payload: ReplayPayload,
success: described(ReplayResponse, "Replayed sync events"),
error: HttpApiError.BadRequest,
@@ -66,6 +68,7 @@ export const SyncApi = HttpApi.make("sync")
}),
),
HttpApiEndpoint.post("steal", SyncPaths.steal, {
query: WorkspaceRoutingQuery,
payload: SessionPayload,
success: described(SessionPayload, "Session stolen into workspace"),
error: HttpApiError.BadRequest,
@@ -77,6 +80,7 @@ export const SyncApi = HttpApi.make("sync")
}),
),
HttpApiEndpoint.post("history", SyncPaths.history, {
query: WorkspaceRoutingQuery,
payload: HistoryPayload,
success: described(Schema.Array(HistoryEvent), "Sync events"),
error: HttpApiError.BadRequest,

View File

@@ -4,7 +4,7 @@ import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../middleware/authorization"
import { InstanceContextMiddleware } from "../middleware/instance-context"
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
import { ApiNotFoundError } from "../errors"
import { described } from "./metadata"
@@ -54,6 +54,7 @@ export const TuiApi = HttpApi.make("tui")
HttpApiGroup.make("tui")
.add(
HttpApiEndpoint.post("appendPrompt", TuiPaths.appendPrompt, {
query: WorkspaceRoutingQuery,
payload: TuiEvent.PromptAppend.properties,
success: described(Schema.Boolean, "Prompt processed successfully"),
error: HttpApiError.BadRequest,
@@ -65,6 +66,7 @@ export const TuiApi = HttpApi.make("tui")
}),
),
HttpApiEndpoint.post("openHelp", TuiPaths.openHelp, {
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "Help dialog opened successfully"),
}).annotateMerge(
OpenApi.annotations({
@@ -74,6 +76,7 @@ export const TuiApi = HttpApi.make("tui")
}),
),
HttpApiEndpoint.post("openSessions", TuiPaths.openSessions, {
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "Session dialog opened successfully"),
}).annotateMerge(
OpenApi.annotations({
@@ -83,6 +86,7 @@ export const TuiApi = HttpApi.make("tui")
}),
),
HttpApiEndpoint.post("openThemes", TuiPaths.openThemes, {
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "Theme dialog opened successfully"),
}).annotateMerge(
OpenApi.annotations({
@@ -92,6 +96,7 @@ export const TuiApi = HttpApi.make("tui")
}),
),
HttpApiEndpoint.post("openModels", TuiPaths.openModels, {
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "Model dialog opened successfully"),
}).annotateMerge(
OpenApi.annotations({
@@ -101,6 +106,7 @@ export const TuiApi = HttpApi.make("tui")
}),
),
HttpApiEndpoint.post("submitPrompt", TuiPaths.submitPrompt, {
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "Prompt submitted successfully"),
}).annotateMerge(
OpenApi.annotations({
@@ -110,6 +116,7 @@ export const TuiApi = HttpApi.make("tui")
}),
),
HttpApiEndpoint.post("clearPrompt", TuiPaths.clearPrompt, {
query: WorkspaceRoutingQuery,
success: described(Schema.Boolean, "Prompt cleared successfully"),
}).annotateMerge(
OpenApi.annotations({
@@ -119,6 +126,7 @@ export const TuiApi = HttpApi.make("tui")
}),
),
HttpApiEndpoint.post("executeCommand", TuiPaths.executeCommand, {
query: WorkspaceRoutingQuery,
payload: CommandPayload,
success: described(Schema.Boolean, "Command executed successfully"),
error: HttpApiError.BadRequest,
@@ -130,6 +138,7 @@ export const TuiApi = HttpApi.make("tui")
}),
),
HttpApiEndpoint.post("showToast", TuiPaths.showToast, {
query: WorkspaceRoutingQuery,
payload: TuiEvent.ToastShow.properties,
success: described(Schema.Boolean, "Toast notification shown successfully"),
}).annotateMerge(
@@ -140,6 +149,7 @@ export const TuiApi = HttpApi.make("tui")
}),
),
HttpApiEndpoint.post("publish", TuiPaths.publish, {
query: WorkspaceRoutingQuery,
payload: TuiPublishPayload,
success: described(Schema.Boolean, "Event published successfully"),
error: HttpApiError.BadRequest,
@@ -151,6 +161,7 @@ export const TuiApi = HttpApi.make("tui")
}),
),
HttpApiEndpoint.post("selectSession", TuiPaths.selectSession, {
query: WorkspaceRoutingQuery,
payload: TuiEvent.SessionSelect.properties,
success: described(Schema.Boolean, "Session selected successfully"),
error: [HttpApiError.BadRequest, ApiNotFoundError],
@@ -162,6 +173,7 @@ export const TuiApi = HttpApi.make("tui")
}),
),
HttpApiEndpoint.get("controlNext", TuiPaths.controlNext, {
query: WorkspaceRoutingQuery,
success: described(TuiRequestPayload, "Next TUI request"),
}).annotateMerge(
OpenApi.annotations({
@@ -171,6 +183,7 @@ export const TuiApi = HttpApi.make("tui")
}),
),
HttpApiEndpoint.post("controlResponse", TuiPaths.controlResponse, {
query: WorkspaceRoutingQuery,
payload: Schema.Unknown,
success: described(Schema.Boolean, "Response submitted successfully"),
}).annotateMerge(

View File

@@ -3,46 +3,31 @@ import { SessionMessage } from "@/v2/session-message"
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../../middleware/authorization"
import { WorkspaceRoutingQueryFields } from "../../middleware/workspace-routing"
export const MessagesQuery = Schema.Struct({
...WorkspaceRoutingQueryFields,
limit: Schema.optional(
Schema.NumberFromString.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(200)),
).annotate({
description: "Maximum number of messages to return. When omitted, the endpoint returns its default page size.",
}),
order: Schema.optional(Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")])).annotate({
description: "Message order for the first page. Use desc for newest first or asc for oldest first.",
}),
cursor: Schema.optional(
Schema.String.annotate({
description:
"Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. Do not combine with order.",
}),
),
}).annotate({ identifier: "V2SessionMessagesQuery" })
export const MessageGroup = HttpApiGroup.make("v2.message")
.add(
HttpApiEndpoint.get("messages", "/api/session/:sessionID/message", {
params: { sessionID: SessionID },
query: Schema.Union([
Schema.Struct({
limit: Schema.optional(
Schema.NumberFromString.check(
Schema.isInt(),
Schema.isGreaterThanOrEqualTo(1),
Schema.isLessThanOrEqualTo(200),
),
).annotate({
description:
"Maximum number of messages to return. When omitted, the endpoint returns its default page size.",
}),
order: Schema.optional(Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")])).annotate({
description: "Message order for the first page. Use desc for newest first or asc for oldest first.",
}),
cursor: Schema.optional(Schema.Never),
}),
Schema.Struct({
limit: Schema.optional(
Schema.NumberFromString.check(
Schema.isInt(),
Schema.isGreaterThanOrEqualTo(1),
Schema.isLessThanOrEqualTo(200),
),
).annotate({
description:
"Maximum number of messages to return. When omitted, the endpoint returns its default page size.",
}),
cursor: Schema.String.annotate({
description:
"Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. Do not combine with order.",
}),
order: Schema.optional(Schema.Never),
}),
]).annotate({ identifier: "V2SessionMessagesQuery" }),
query: MessagesQuery,
success: Schema.Struct({
items: Schema.Array(SessionMessage.Message),
cursor: Schema.Struct({

View File

@@ -1,4 +1,3 @@
import { WorkspaceID } from "@/control-plane/schema"
import { SessionID } from "@/session/schema"
import { SessionMessage } from "@/v2/session-message"
import { Prompt } from "@/v2/session-prompt"
@@ -6,62 +5,41 @@ import { SessionV2 } from "@/v2/session"
import { Schema, SchemaGetter } from "effect"
import { HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
import { Authorization } from "../../middleware/authorization"
import { WorkspaceRoutingQuery, WorkspaceRoutingQueryFields } from "../../middleware/workspace-routing"
const QueryBoolean = Schema.Literals(["true", "false"]).pipe(
Schema.decodeTo(Schema.Boolean, {
decode: SchemaGetter.transform((value) => value === "true"),
encode: SchemaGetter.transform((value) => (value ? "true" : "false")),
}),
)
export const SessionsQuery = Schema.Struct({
...WorkspaceRoutingQueryFields,
limit: Schema.optional(
Schema.NumberFromString.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(200)),
).annotate({
description: "Maximum number of sessions to return. Defaults to the newest 50 sessions.",
}),
order: Schema.optional(Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")])).annotate({
description: "Session order for the first page. Use desc for newest first or asc for oldest first.",
}),
path: Schema.optional(Schema.String),
roots: Schema.optional(QueryBoolean),
start: Schema.optional(Schema.NumberFromString),
search: Schema.optional(Schema.String),
cursor: Schema.optional(
Schema.String.annotate({
description:
"Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. Do not combine with order or filters.",
}),
),
}).annotate({ identifier: "V2SessionsQuery" })
export const SessionGroup = HttpApiGroup.make("v2.session")
.add(
HttpApiEndpoint.get("sessions", "/api/session", {
query: Schema.Union([
Schema.Struct({
limit: Schema.optional(
Schema.NumberFromString.check(
Schema.isInt(),
Schema.isGreaterThanOrEqualTo(1),
Schema.isLessThanOrEqualTo(200),
),
).annotate({
description: "Maximum number of sessions to return. Defaults to the newest 50 sessions.",
}),
order: Schema.optional(Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")])).annotate({
description: "Session order for the first page. Use desc for newest first or asc for oldest first.",
}),
directory: Schema.String.pipe(Schema.optional),
path: Schema.String.pipe(Schema.optional),
workspace: WorkspaceID.pipe(Schema.optional),
roots: Schema.Literals(["true", "false"])
.pipe(
Schema.decodeTo(Schema.Boolean, {
decode: SchemaGetter.transform((value) => value === "true"),
encode: SchemaGetter.transform((value) => (value ? "true" : "false")),
}),
)
.pipe(Schema.optional),
start: Schema.NumberFromString.pipe(Schema.optional),
search: Schema.String.pipe(Schema.optional),
cursor: Schema.optional(Schema.Never),
}),
Schema.Struct({
limit: Schema.optional(
Schema.NumberFromString.check(
Schema.isInt(),
Schema.isGreaterThanOrEqualTo(1),
Schema.isLessThanOrEqualTo(200),
),
).annotate({
description: "Maximum number of sessions to return. Defaults to the newest 50 sessions.",
}),
cursor: Schema.String.annotate({
description:
"Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. Do not combine with order.",
}),
order: Schema.optional(Schema.Never),
directory: Schema.optional(Schema.Never),
path: Schema.optional(Schema.Never),
workspace: Schema.optional(Schema.Never),
roots: Schema.optional(Schema.Never),
start: Schema.optional(Schema.Never),
search: Schema.optional(Schema.Never),
}),
]).annotate({ identifier: "V2SessionsQuery" }),
query: SessionsQuery,
success: Schema.Struct({
items: Schema.Array(SessionV2.Info),
cursor: Schema.Struct({
@@ -82,6 +60,7 @@ export const SessionGroup = HttpApiGroup.make("v2.session")
.add(
HttpApiEndpoint.post("prompt", "/api/session/:sessionID/prompt", {
params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
payload: Schema.Struct({
prompt: Prompt,
delivery: SessionV2.Delivery.pipe(Schema.optional),
@@ -98,6 +77,7 @@ export const SessionGroup = HttpApiGroup.make("v2.session")
.add(
HttpApiEndpoint.post("compact", "/api/session/:sessionID/compact", {
params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
success: HttpApiSchema.NoContent,
}).annotateMerge(
OpenApi.annotations({
@@ -110,6 +90,7 @@ export const SessionGroup = HttpApiGroup.make("v2.session")
.add(
HttpApiEndpoint.post("wait", "/api/session/:sessionID/wait", {
params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
success: HttpApiSchema.NoContent,
}).annotateMerge(
OpenApi.annotations({
@@ -122,6 +103,7 @@ export const SessionGroup = HttpApiGroup.make("v2.session")
.add(
HttpApiEndpoint.get("context", "/api/session/:sessionID/context", {
params: { sessionID: SessionID },
query: WorkspaceRoutingQuery,
success: Schema.Array(SessionMessage.Message),
}).annotateMerge(
OpenApi.annotations({

View File

@@ -5,7 +5,7 @@ import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, Op
import { ApiVcsApplyError } from "./instance"
import { Authorization } from "../middleware/authorization"
import { InstanceContextMiddleware } from "../middleware/instance-context"
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware/workspace-routing"
import { described } from "./metadata"
const root = "/experimental/workspace"
@@ -40,6 +40,7 @@ export const WorkspaceApi = HttpApi.make("workspace")
HttpApiGroup.make("workspace")
.add(
HttpApiEndpoint.get("adapters", WorkspacePaths.adapters, {
query: WorkspaceRoutingQuery,
success: described(Schema.Array(WorkspaceAdapterEntry), "Workspace adapters"),
}).annotateMerge(
OpenApi.annotations({
@@ -49,6 +50,7 @@ export const WorkspaceApi = HttpApi.make("workspace")
}),
),
HttpApiEndpoint.get("list", WorkspacePaths.list, {
query: WorkspaceRoutingQuery,
success: described(Schema.Array(Workspace.Info), "Workspaces"),
}).annotateMerge(
OpenApi.annotations({
@@ -58,6 +60,7 @@ export const WorkspaceApi = HttpApi.make("workspace")
}),
),
HttpApiEndpoint.post("create", WorkspacePaths.list, {
query: WorkspaceRoutingQuery,
payload: CreatePayload,
success: described(Workspace.Info, "Workspace created"),
error: HttpApiError.BadRequest,
@@ -69,6 +72,7 @@ export const WorkspaceApi = HttpApi.make("workspace")
}),
),
HttpApiEndpoint.post("syncList", WorkspacePaths.syncList, {
query: WorkspaceRoutingQuery,
success: described(HttpApiSchema.NoContent, "Workspace list synced"),
}).annotateMerge(
OpenApi.annotations({
@@ -78,6 +82,7 @@ export const WorkspaceApi = HttpApi.make("workspace")
}),
),
HttpApiEndpoint.get("status", WorkspacePaths.status, {
query: WorkspaceRoutingQuery,
success: described(Schema.Array(Workspace.ConnectionStatus), "Workspace status"),
}).annotateMerge(
OpenApi.annotations({
@@ -88,6 +93,7 @@ export const WorkspaceApi = HttpApi.make("workspace")
),
HttpApiEndpoint.delete("remove", WorkspacePaths.remove, {
params: { id: Workspace.Info.fields.id },
query: WorkspaceRoutingQuery,
success: described(Schema.UndefinedOr(Workspace.Info), "Workspace removed"),
error: HttpApiError.BadRequest,
}).annotateMerge(
@@ -98,6 +104,7 @@ export const WorkspaceApi = HttpApi.make("workspace")
}),
),
HttpApiEndpoint.post("warp", WorkspacePaths.warp, {
query: WorkspaceRoutingQuery,
payload: WarpPayload,
success: described(HttpApiSchema.NoContent, "Session warped"),
error: [ApiWorkspaceWarpError, ApiVcsApplyError],

View File

@@ -34,6 +34,7 @@ export const messageHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.message
return handlers.handle(
"messages",
Effect.fn(function* (ctx) {
if (ctx.query.cursor && ctx.query.order !== undefined) return yield* new HttpApiError.BadRequest({})
const decoded = yield* Effect.try({
try: () => (ctx.query.cursor ? cursor.decode(ctx.query.cursor) : undefined),
catch: () => new HttpApiError.BadRequest({}),

View File

@@ -22,6 +22,31 @@ type SessionCursor = typeof SessionCursor.Type
const decodeCursor = Schema.decodeUnknownSync(SessionCursor)
function hasCursorFilter(query: {
readonly order?: unknown
readonly path?: unknown
readonly roots?: unknown
readonly start?: unknown
readonly search?: unknown
}) {
return (
query.order !== undefined ||
query.path !== undefined ||
query.roots !== undefined ||
query.start !== undefined ||
query.search !== undefined
)
}
function hasCursorRoutingMismatch(
query: { readonly directory?: string; readonly workspace?: string },
decoded: SessionCursor | undefined,
) {
if (!decoded) return false
if (query.directory !== undefined && query.directory !== decoded.directory) return true
return query.workspace !== undefined && query.workspace !== decoded.workspaceID
}
const sessionCursor = {
encode(
session: SessionV2.Info,
@@ -46,10 +71,12 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.session
.handle(
"sessions",
Effect.fn(function* (ctx) {
if (ctx.query.cursor && hasCursorFilter(ctx.query)) return yield* new HttpApiError.BadRequest({})
const decoded = yield* Effect.try({
try: () => (ctx.query.cursor ? sessionCursor.decode(ctx.query.cursor) : undefined),
catch: () => new HttpApiError.BadRequest({}),
})
if (hasCursorRoutingMismatch(ctx.query, decoded)) return yield* new HttpApiError.BadRequest({})
const order = decoded?.order ?? ctx.query.order ?? "desc"
const filters = decoded ?? {
directory: ctx.query.directory,

View File

@@ -24,6 +24,8 @@ export const WorkspaceRoutingQueryFields = {
workspace: Schema.optional(Schema.String),
}
export const WorkspaceRoutingQuery = Schema.Struct(WorkspaceRoutingQueryFields)
type RemoteTarget = Extract<Target, { type: "remote" }>
type RequestPlan = Data.TaggedEnum<{

View File

@@ -51,30 +51,14 @@ type OpenApiResponse = {
content?: Record<string, { schema?: OpenApiSchema }>
}
// Instance routes use middleware for directory/workspace resolution, but HttpApi
// doesn't surface middleware query params in the spec. Inject them explicitly.
const InstanceQueryParameters = [
{
name: "directory",
in: "query",
required: false,
schema: { type: "string" },
},
{
name: "workspace",
in: "query",
required: false,
schema: { type: "string" },
},
] satisfies OpenApiParameter[]
// Query schemas describe decoded Effect values, but the generated SDK needs the
// public call shape. These keep SDK callers passing numbers/booleans while the
// server still decodes string query params at runtime.
const QueryNumberParameters = new Set(["start", "cursor", "limit", "method"])
const QueryNumberParameters = new Set(["start", "limit", "method"])
const QueryBooleanParameters = new Set(["roots", "archived"])
const QueryParameterSchemas = {
"GET /find/file limit": { type: "integer", minimum: 1, maximum: 200 },
"GET /experimental/session cursor": { type: "number" },
"GET /session/{sessionID}/diff messageID": { type: "string", pattern: "^msg.*" },
"GET /session/{sessionID}/message limit": { type: "integer", minimum: 0, maximum: Number.MAX_SAFE_INTEGER },
} satisfies Record<string, OpenApiSchema>
@@ -122,7 +106,6 @@ function matchLegacyOpenApi(input: Record<string, unknown>) {
delete spec.components?.securitySchemes
for (const [path, item] of Object.entries(spec.paths ?? {})) {
const isInstanceRoute = !path.startsWith("/global/") && !path.startsWith("/auth/")
for (const method of ["get", "post", "put", "delete", "patch"] as const) {
const operation = item[method]
if (!operation) continue
@@ -183,14 +166,8 @@ function matchLegacyOpenApi(input: Record<string, unknown>) {
},
}
}
if (!isInstanceRoute) continue
operation.parameters = [
...InstanceQueryParameters,
...(operation.parameters ?? []).filter(
(param) => param.in !== "query" || (param.name !== "directory" && param.name !== "workspace"),
),
]
for (const param of operation.parameters) normalizeParameter(param, `${method.toUpperCase()} ${path}`)
const route = `${method.toUpperCase()} ${path}`
for (const param of operation.parameters ?? []) normalizeParameter(param, route)
}
}
return input