Răsfoiți Sursa

feat(vcs): expose branch info (#40368)

Aiden Cline 1 săptămână în urmă
părinte
comite
42e8d11552

+ 11 - 4
packages/client/src/effect/api/api.ts

@@ -1502,18 +1502,25 @@ export interface ProjectCopyApi<E = never> {
 export type Endpoint25_0Input = {
   readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
 }
-export type Endpoint25_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Vcs.FileStatus> }
-export type VcsStatusOperation<E = never> = (input?: Endpoint25_0Input) => Effect.Effect<Endpoint25_0Output, E>
+export type Endpoint25_0Output = { readonly location: Location.Info; readonly data: Vcs.Info }
+export type VcsGetOperation<E = never> = (input?: Endpoint25_0Input) => Effect.Effect<Endpoint25_0Output, E>
 
 export type Endpoint25_1Input = {
   readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
+}
+export type Endpoint25_1Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Vcs.FileStatus> }
+export type VcsStatusOperation<E = never> = (input?: Endpoint25_1Input) => Effect.Effect<Endpoint25_1Output, E>
+
+export type Endpoint25_2Input = {
+  readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
   readonly mode: Vcs.Mode
   readonly context?: number | undefined
 }
-export type Endpoint25_1Output = { readonly location: Location.Info; readonly data: ReadonlyArray<FileDiff.Info> }
-export type VcsDiffOperation<E = never> = (input: Endpoint25_1Input) => Effect.Effect<Endpoint25_1Output, E>
+export type Endpoint25_2Output = { readonly location: Location.Info; readonly data: ReadonlyArray<FileDiff.Info> }
+export type VcsDiffOperation<E = never> = (input: Endpoint25_2Input) => Effect.Effect<Endpoint25_2Output, E>
 
 export interface VcsApi<E = never> {
+  readonly get: VcsGetOperation<E>
   readonly status: VcsStatusOperation<E>
   readonly diff: VcsDiffOperation<E>
 }

+ 14 - 3
packages/client/src/effect/generated/client.ts

@@ -210,6 +210,8 @@ import type {
   Endpoint25_0Output,
   Endpoint25_1Input,
   Endpoint25_1Output,
+  Endpoint25_2Input,
+  Endpoint25_2Output,
   Endpoint26_0Output,
   Endpoint26_1Input,
   Endpoint26_1Output,
@@ -1182,17 +1184,26 @@ const adaptGroup24 = (raw: RawClient["server.projectCopy"]) => ({
 
 const Endpoint25_0 = (raw: RawClient["server.vcs"]) => (input?: Endpoint25_0Input) =>
   preserveEffect<Endpoint25_0Output>()(
-    raw["vcs.status"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)),
+    raw["vcs.get"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)),
   )
 
-const Endpoint25_1 = (raw: RawClient["server.vcs"]) => (input: Endpoint25_1Input) =>
+const Endpoint25_1 = (raw: RawClient["server.vcs"]) => (input?: Endpoint25_1Input) =>
   preserveEffect<Endpoint25_1Output>()(
+    raw["vcs.status"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)),
+  )
+
+const Endpoint25_2 = (raw: RawClient["server.vcs"]) => (input: Endpoint25_2Input) =>
+  preserveEffect<Endpoint25_2Output>()(
     raw["vcs.diff"]({ query: { location: input["location"], mode: input["mode"], context: input["context"] } }).pipe(
       Effect.mapError(mapClientError),
     ),
   )
 
-const adaptGroup25 = (raw: RawClient["server.vcs"]) => ({ status: Endpoint25_0(raw), diff: Endpoint25_1(raw) })
+const adaptGroup25 = (raw: RawClient["server.vcs"]) => ({
+  get: Endpoint25_0(raw),
+  status: Endpoint25_1(raw),
+  diff: Endpoint25_2(raw),
+})
 
 const Endpoint26_0 = (raw: RawClient["server.debug"]) => () =>
   preserveEffect<Endpoint26_0Output>()(raw["debug.location"]({}).pipe(Effect.mapError(mapClientError)))

+ 14 - 0
packages/client/src/promise/generated/client.ts

@@ -202,6 +202,8 @@ import type {
   ProjectCopyRemoveOutput,
   ProjectCopyRefreshInput,
   ProjectCopyRefreshOutput,
+  VcsGetInput,
+  VcsGetOutput,
   VcsStatusInput,
   VcsStatusOutput,
   VcsDiffInput,
@@ -1702,6 +1704,18 @@ export function make(options: ClientOptions) {
         ),
     },
     vcs: {
+      get: (input?: VcsGetInput, requestOptions?: RequestOptions) =>
+        request<VcsGetOutput>(
+          {
+            method: "GET",
+            path: `/api/vcs`,
+            query: { location: input?.["location"] },
+            successStatus: 200,
+            declaredStatuses: [401, 400],
+            empty: false,
+          },
+          requestOptions,
+        ),
       status: (input?: VcsStatusInput, requestOptions?: RequestOptions) =>
         request<VcsStatusOutput>(
           {

+ 15 - 0
packages/client/src/promise/generated/types.ts

@@ -530,6 +530,8 @@ export type ReferenceGitSource = {
 
 export type ProjectCopyCopy = { directory: string }
 
+export type VcsBranch = { current?: string; default?: string }
+
 export type VcsFileStatus = {
   file: string
   additions: number
@@ -1745,6 +1747,8 @@ export type SessionStatus2 = {
 
 export type ReferenceSource = ReferenceLocalSource | ReferenceGitSource
 
+export type VcsInfo = { branch: VcsBranch }
+
 export type PermissionRuleset = Array<PermissionRule>
 
 export type SessionInfo = {
@@ -4902,6 +4906,17 @@ export type ProjectCopyRefreshInput = {
 
 export type ProjectCopyRefreshOutput = void
 
+export type VcsGetInput = {
+  readonly location?: {
+    readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
+  }["location"]
+}
+
+export type VcsGetOutput = {
+  location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } }
+  data: VcsInfo
+}
+
 export type VcsStatusInput = {
   readonly location?: {
     readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined

+ 1 - 1
packages/client/test/promise.test.ts

@@ -44,7 +44,7 @@ test("exposes every standard HTTP API group", () => {
   expect(Object.keys(client.integration.command)).toEqual(["connect", "status", "cancel"])
   expect(Object.keys(client.websearch)).toEqual(["providers", "query"])
   expect(Object.keys(client.file)).toEqual(["read", "list", "find"])
-  expect(Object.keys(client.vcs)).toEqual(["status", "diff"])
+  expect(Object.keys(client.vcs)).toEqual(["get", "status", "diff"])
   expect(Object.keys(client.pty)).toEqual(["list", "create", "get", "update", "remove"])
   expect(Object.keys(client.shell)).toEqual(["list", "create", "get", "timeout", "output", "remove"])
   expect(Object.keys(client.project)).toEqual(["list", "current", "directories"])

+ 7 - 2
packages/core/src/vcs.ts

@@ -2,7 +2,7 @@ export * as Vcs from "./vcs"
 
 import { Context, Effect, Layer } from "effect"
 import { FileDiff } from "@opencode-ai/schema/file-diff"
-import { FileStatus, Mode } from "@opencode-ai/schema/vcs"
+import { FileStatus, Info, Mode } from "@opencode-ai/schema/vcs"
 import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
 import { FSUtil } from "@opencode-ai/util/fs-util"
 import { Location } from "./location"
@@ -10,13 +10,14 @@ import { AppProcess } from "@opencode-ai/util/process"
 import { VcsGit } from "./vcs/git"
 import { VcsHg } from "./vcs/hg"
 
-export { FileStatus, Mode }
+export { FileStatus, Info, Mode }
 
 export interface DiffOptions {
   readonly context?: number
 }
 
 export interface Interface {
+  readonly info: () => Effect.Effect<Info>
   readonly status: () => Effect.Effect<FileStatus[]>
   readonly diff: (mode: Mode, options?: DiffOptions) => Effect.Effect<FileDiff.Info[]>
 }
@@ -40,6 +41,10 @@ const layer = Layer.effect(
     const location = yield* Location.Service
     const impl = adapter(proc, fs, location)
     return Service.of({
+      info: Effect.fn("Vcs.info")(function* () {
+        if (!impl) return { branch: {} }
+        return yield* impl.info()
+      }),
       status: Effect.fn("Vcs.status")(function* () {
         if (!impl) return []
         return yield* impl.status()

+ 7 - 1
packages/core/src/vcs/git.ts

@@ -3,7 +3,7 @@ export * as VcsGit from "./git"
 import { Effect } from "effect"
 import { ChildProcess } from "effect/unstable/process"
 import { FileDiff } from "@opencode-ai/schema/file-diff"
-import { FileStatus, Mode } from "@opencode-ai/schema/vcs"
+import { FileStatus, Info, Mode } from "@opencode-ai/schema/vcs"
 import { AppProcess } from "@opencode-ai/util/process"
 import type { DiffOptions, Interface } from "../vcs"
 import { chunksByFile, emptyPatch, MAX_PATCH_BYTES, MAX_TOTAL_PATCH_BYTES, PATCH_CONTEXT_LINES } from "./patch"
@@ -20,6 +20,12 @@ export function make(proc: AppProcess.Interface, input: { directory: string; wor
   const ctx: Ctx = { git: makeGit(proc), directory: input.directory, worktree: input.worktree }
 
   return {
+    info: Effect.fn("VcsGit.info")(function* () {
+      const [current, root] = yield* Effect.all([ctx.git.branch(ctx.directory), ctx.git.defaultBranch(ctx.directory)], {
+        concurrency: 2,
+      })
+      return { branch: { current, default: root?.name } } satisfies Info
+    }),
     status: Effect.fn("VcsGit.status")(function* () {
       const git = ctx.git
       const ref = (yield* git.hasHead(ctx.directory)) ? "HEAD" : undefined

+ 4 - 1
packages/core/src/vcs/hg.ts

@@ -4,7 +4,7 @@ import path from "path"
 import { Effect } from "effect"
 import { ChildProcess } from "effect/unstable/process"
 import { FileDiff } from "@opencode-ai/schema/file-diff"
-import { FileStatus, Mode } from "@opencode-ai/schema/vcs"
+import { FileStatus, Info, Mode } from "@opencode-ai/schema/vcs"
 import { FSUtil } from "@opencode-ai/util/fs-util"
 import { AppProcess } from "@opencode-ai/util/process"
 import type { DiffOptions, Interface } from "../vcs"
@@ -73,6 +73,9 @@ export function make(
   })
 
   return {
+    info: Effect.fn("VcsHg.info")(function* () {
+      return { branch: { current: yield* hg.branch(), default: "default" } } satisfies Info
+    }),
     status: Effect.fn("VcsHg.status")(function* () {
       const [items, batch] = yield* Effect.all(
         // Zero-context patches are enough to count changed lines.

+ 1 - 0
packages/core/test/vcs-hg.test.ts

@@ -160,6 +160,7 @@ describeHg("Vcs mercurial", () => {
           await commitAll(directory, "feature change")
         })
         const diff = yield* vcs.diff("branch")
+        expect(yield* vcs.info()).toEqual({ branch: { current: "feature", default: "default" } })
         expect(diff.map((item) => ({ file: item.file, status: item.status }))).toEqual([
           { file: "file.txt", status: "modified" },
         ])

+ 2 - 0
packages/core/test/vcs.test.ts

@@ -53,6 +53,7 @@ describe("Vcs", () => {
     withTmp((directory) =>
       Effect.gen(function* () {
         const vcs = yield* Vcs.Service
+        expect(yield* vcs.info()).toEqual({ branch: {} })
         expect(yield* vcs.status()).toEqual([])
         expect(yield* vcs.diff("working")).toEqual([])
         expect(yield* vcs.diff("branch")).toEqual([])
@@ -163,6 +164,7 @@ describe("Vcs", () => {
           await commitAll(directory, "feature change")
         })
         const diff = yield* vcs.diff("branch")
+        expect(yield* vcs.info()).toEqual({ branch: { current: "feature", default: "main" } })
         expect(diff.map((item) => ({ file: item.file, status: item.status }))).toEqual([
           { file: "file.txt", status: "modified" },
         ])

+ 153 - 6
packages/protocol/openapi.json

@@ -851,6 +851,16 @@
                       }
                     ]
                   },
+                  "title": {
+                    "anyOf": [
+                      {
+                        "type": "string"
+                      },
+                      {
+                        "type": "null"
+                      }
+                    ]
+                  },
                   "agent": {
                     "anyOf": [
                       {
@@ -11301,6 +11311,104 @@
         }
       }
     },
+    "/api/vcs": {
+      "get": {
+        "tags": [
+          "vcs"
+        ],
+        "operationId": "v2.vcs.get",
+        "parameters": [
+          {
+            "name": "location",
+            "in": "query",
+            "schema": {
+              "anyOf": [
+                {
+                  "type": "object",
+                  "properties": {
+                    "directory": {
+                      "anyOf": [
+                        {
+                          "type": "string"
+                        },
+                        {
+                          "type": "null"
+                        }
+                      ]
+                    },
+                    "workspace": {
+                      "anyOf": [
+                        {
+                          "type": "string"
+                        },
+                        {
+                          "type": "null"
+                        }
+                      ]
+                    }
+                  },
+                  "additionalProperties": false
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
+            "required": false,
+            "style": "deepObject",
+            "explode": true
+          }
+        ],
+        "security": [],
+        "responses": {
+          "200": {
+            "description": "Success",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "object",
+                  "properties": {
+                    "location": {
+                      "$ref": "#/components/schemas/Location.Info"
+                    },
+                    "data": {
+                      "$ref": "#/components/schemas/Vcs.Info"
+                    }
+                  },
+                  "required": [
+                    "location",
+                    "data"
+                  ],
+                  "additionalProperties": false
+                }
+              }
+            }
+          },
+          "400": {
+            "description": "InvalidRequestError",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/InvalidRequestError"
+                }
+              }
+            }
+          },
+          "401": {
+            "description": "UnauthorizedError",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/UnauthorizedError"
+                }
+              }
+            }
+          }
+        },
+        "description": "Get current and default branch information for the requested location.",
+        "summary": "VCS info"
+      }
+    },
     "/api/vcs/status": {
       "get": {
         "tags": [
@@ -12032,11 +12140,15 @@
               },
               "directory": {
                 "type": "string"
+              },
+              "canonical": {
+                "type": "string"
               }
             },
             "required": [
               "id",
-              "directory"
+              "directory",
+              "canonical"
             ],
             "additionalProperties": false
           }
@@ -12498,7 +12610,6 @@
           "cost",
           "tokens",
           "time",
-          "title",
           "location"
         ],
         "additionalProperties": false
@@ -13837,6 +13948,15 @@
           },
           "message": {
             "type": "string"
+          },
+          "status": {
+            "type": "integer",
+            "allOf": [
+              {
+                "minimum": 100,
+                "maximum": 599
+              }
+            ]
           }
         },
         "required": [
@@ -20765,7 +20885,7 @@
           "id": {
             "type": "string"
           },
-          "worktree": {
+          "canonical": {
             "type": "string"
           },
           "vcs": {
@@ -20792,7 +20912,7 @@
         },
         "required": [
           "id",
-          "worktree",
+          "canonical",
           "time",
           "sandboxes"
         ],
@@ -20806,11 +20926,15 @@
           },
           "directory": {
             "type": "string"
+          },
+          "canonical": {
+            "type": "string"
           }
         },
         "required": [
           "id",
-          "directory"
+          "directory",
+          "canonical"
         ],
         "additionalProperties": false
       },
@@ -22445,7 +22569,6 @@
           "slug",
           "projectID",
           "directory",
-          "title",
           "version",
           "time"
         ],
@@ -29127,6 +29250,30 @@
         ],
         "additionalProperties": false
       },
+      "Vcs.Branch": {
+        "type": "object",
+        "properties": {
+          "current": {
+            "type": "string"
+          },
+          "default": {
+            "type": "string"
+          }
+        },
+        "additionalProperties": false
+      },
+      "Vcs.Info": {
+        "type": "object",
+        "properties": {
+          "branch": {
+            "$ref": "#/components/schemas/Vcs.Branch"
+          }
+        },
+        "required": [
+          "branch"
+        ],
+        "additionalProperties": false
+      },
       "Vcs.FileStatus": {
         "type": "object",
         "properties": {

+ 14 - 0
packages/protocol/src/groups/vcs.ts

@@ -13,6 +13,20 @@ const DiffQuery = Schema.Struct({
 })
 
 export const VcsGroup = HttpApiGroup.make("server.vcs")
+  .add(
+    HttpApiEndpoint.get("vcs.get", "/api/vcs", {
+      query: LocationQuery,
+      success: Location.response(Vcs.Info),
+    })
+      .annotateMerge(locationQueryOpenApi)
+      .annotateMerge(
+        OpenApi.annotations({
+          identifier: "v2.vcs.get",
+          summary: "VCS info",
+          description: "Get current and default branch information for the requested location.",
+        }),
+      ),
+  )
   .add(
     HttpApiEndpoint.get("vcs.status", "/api/vcs/status", {
       query: LocationQuery,

+ 12 - 1
packages/schema/src/vcs.ts

@@ -1,7 +1,18 @@
 export * as Vcs from "./vcs.js"
 
 import { Schema } from "effect"
-import { NonNegativeInt } from "./schema.js"
+import { NonNegativeInt, optional } from "./schema.js"
+
+export const Branch = Schema.Struct({
+  current: optional(Schema.String),
+  default: optional(Schema.String),
+}).annotate({ identifier: "Vcs.Branch" })
+export interface Branch extends Schema.Schema.Type<typeof Branch> {}
+
+export const Info = Schema.Struct({
+  branch: Branch,
+}).annotate({ identifier: "Vcs.Info" })
+export interface Info extends Schema.Schema.Type<typeof Info> {}
 
 export const Mode = Schema.Literals(["working", "branch"]).annotate({ identifier: "Vcs.Mode" })
 export type Mode = typeof Mode.Type

+ 7 - 0
packages/schema/test/contract-hygiene.test.ts

@@ -16,6 +16,7 @@ import { FileDiff } from "../src/file-diff.js"
 import { Money } from "../src/money.js"
 import { Skill } from "../src/skill.js"
 import { Shell } from "../src/shell.js"
+import { Vcs } from "../src/vcs.js"
 import { PersistedRevert } from "../src/session-revert.js"
 import { AbsolutePath, optional } from "../src/schema.js"
 
@@ -134,6 +135,10 @@ describe("contract hygiene", () => {
     expect(Pty.ID.create()).toStartWith("pty_")
   })
 
+  test("VCS info omits unavailable branch names", () => {
+    expect(Schema.encodeSync(Vcs.Info)({ branch: { current: undefined, default: undefined } })).toEqual({ branch: {} })
+  })
+
   test("reusable public identifiers are stable and unique", () => {
     const identifiers = [
       Agent.Color,
@@ -166,6 +171,8 @@ describe("contract hygiene", () => {
       SessionPending.SyntheticData,
       SessionPending.User,
       SessionPending.Synthetic,
+      Vcs.Branch,
+      Vcs.Info,
     ].map((schema) => schema.ast.annotations?.identifier)
 
     expect(identifiers.every((identifier) => typeof identifier === "string")).toBe(true)

+ 8 - 0
packages/server/src/handlers/vcs.ts

@@ -7,6 +7,14 @@ import { response } from "../location"
 export const VcsHandler = HttpApiBuilder.group(Api, "server.vcs", (handlers) =>
   Effect.gen(function* () {
     return handlers
+      .handle("vcs.get", () =>
+        response(
+          Effect.gen(function* () {
+            const vcs = yield* Vcs.Service
+            return yield* vcs.info()
+          }),
+        ),
+      )
       .handle("vcs.status", () =>
         response(
           Effect.gen(function* () {

+ 153 - 6
packages/www/openapi.json

@@ -851,6 +851,16 @@
                       }
                     ]
                   },
+                  "title": {
+                    "anyOf": [
+                      {
+                        "type": "string"
+                      },
+                      {
+                        "type": "null"
+                      }
+                    ]
+                  },
                   "agent": {
                     "anyOf": [
                       {
@@ -11301,6 +11311,104 @@
         }
       }
     },
+    "/api/vcs": {
+      "get": {
+        "tags": [
+          "vcs"
+        ],
+        "operationId": "v2.vcs.get",
+        "parameters": [
+          {
+            "name": "location",
+            "in": "query",
+            "schema": {
+              "anyOf": [
+                {
+                  "type": "object",
+                  "properties": {
+                    "directory": {
+                      "anyOf": [
+                        {
+                          "type": "string"
+                        },
+                        {
+                          "type": "null"
+                        }
+                      ]
+                    },
+                    "workspace": {
+                      "anyOf": [
+                        {
+                          "type": "string"
+                        },
+                        {
+                          "type": "null"
+                        }
+                      ]
+                    }
+                  },
+                  "additionalProperties": false
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
+            "required": false,
+            "style": "deepObject",
+            "explode": true
+          }
+        ],
+        "security": [],
+        "responses": {
+          "200": {
+            "description": "Success",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "object",
+                  "properties": {
+                    "location": {
+                      "$ref": "#/components/schemas/Location.Info"
+                    },
+                    "data": {
+                      "$ref": "#/components/schemas/Vcs.Info"
+                    }
+                  },
+                  "required": [
+                    "location",
+                    "data"
+                  ],
+                  "additionalProperties": false
+                }
+              }
+            }
+          },
+          "400": {
+            "description": "InvalidRequestError",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/InvalidRequestError"
+                }
+              }
+            }
+          },
+          "401": {
+            "description": "UnauthorizedError",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/UnauthorizedError"
+                }
+              }
+            }
+          }
+        },
+        "description": "Get current and default branch information for the requested location.",
+        "summary": "VCS info"
+      }
+    },
     "/api/vcs/status": {
       "get": {
         "tags": [
@@ -12032,11 +12140,15 @@
               },
               "directory": {
                 "type": "string"
+              },
+              "canonical": {
+                "type": "string"
               }
             },
             "required": [
               "id",
-              "directory"
+              "directory",
+              "canonical"
             ],
             "additionalProperties": false
           }
@@ -12498,7 +12610,6 @@
           "cost",
           "tokens",
           "time",
-          "title",
           "location"
         ],
         "additionalProperties": false
@@ -13837,6 +13948,15 @@
           },
           "message": {
             "type": "string"
+          },
+          "status": {
+            "type": "integer",
+            "allOf": [
+              {
+                "minimum": 100,
+                "maximum": 599
+              }
+            ]
           }
         },
         "required": [
@@ -20765,7 +20885,7 @@
           "id": {
             "type": "string"
           },
-          "worktree": {
+          "canonical": {
             "type": "string"
           },
           "vcs": {
@@ -20792,7 +20912,7 @@
         },
         "required": [
           "id",
-          "worktree",
+          "canonical",
           "time",
           "sandboxes"
         ],
@@ -20806,11 +20926,15 @@
           },
           "directory": {
             "type": "string"
+          },
+          "canonical": {
+            "type": "string"
           }
         },
         "required": [
           "id",
-          "directory"
+          "directory",
+          "canonical"
         ],
         "additionalProperties": false
       },
@@ -22445,7 +22569,6 @@
           "slug",
           "projectID",
           "directory",
-          "title",
           "version",
           "time"
         ],
@@ -29127,6 +29250,30 @@
         ],
         "additionalProperties": false
       },
+      "Vcs.Branch": {
+        "type": "object",
+        "properties": {
+          "current": {
+            "type": "string"
+          },
+          "default": {
+            "type": "string"
+          }
+        },
+        "additionalProperties": false
+      },
+      "Vcs.Info": {
+        "type": "object",
+        "properties": {
+          "branch": {
+            "$ref": "#/components/schemas/Vcs.Branch"
+          }
+        },
+        "required": [
+          "branch"
+        ],
+        "additionalProperties": false
+      },
       "Vcs.FileStatus": {
         "type": "object",
         "properties": {

+ 153 - 6
packages/www/public/openapi.json

@@ -851,6 +851,16 @@
                       }
                     ]
                   },
+                  "title": {
+                    "anyOf": [
+                      {
+                        "type": "string"
+                      },
+                      {
+                        "type": "null"
+                      }
+                    ]
+                  },
                   "agent": {
                     "anyOf": [
                       {
@@ -11301,6 +11311,104 @@
         }
       }
     },
+    "/api/vcs": {
+      "get": {
+        "tags": [
+          "vcs"
+        ],
+        "operationId": "v2.vcs.get",
+        "parameters": [
+          {
+            "name": "location",
+            "in": "query",
+            "schema": {
+              "anyOf": [
+                {
+                  "type": "object",
+                  "properties": {
+                    "directory": {
+                      "anyOf": [
+                        {
+                          "type": "string"
+                        },
+                        {
+                          "type": "null"
+                        }
+                      ]
+                    },
+                    "workspace": {
+                      "anyOf": [
+                        {
+                          "type": "string"
+                        },
+                        {
+                          "type": "null"
+                        }
+                      ]
+                    }
+                  },
+                  "additionalProperties": false
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
+            "required": false,
+            "style": "deepObject",
+            "explode": true
+          }
+        ],
+        "security": [],
+        "responses": {
+          "200": {
+            "description": "Success",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "object",
+                  "properties": {
+                    "location": {
+                      "$ref": "#/components/schemas/Location.Info"
+                    },
+                    "data": {
+                      "$ref": "#/components/schemas/Vcs.Info"
+                    }
+                  },
+                  "required": [
+                    "location",
+                    "data"
+                  ],
+                  "additionalProperties": false
+                }
+              }
+            }
+          },
+          "400": {
+            "description": "InvalidRequestError",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/InvalidRequestError"
+                }
+              }
+            }
+          },
+          "401": {
+            "description": "UnauthorizedError",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/UnauthorizedError"
+                }
+              }
+            }
+          }
+        },
+        "description": "Get current and default branch information for the requested location.",
+        "summary": "VCS info"
+      }
+    },
     "/api/vcs/status": {
       "get": {
         "tags": [
@@ -12032,11 +12140,15 @@
               },
               "directory": {
                 "type": "string"
+              },
+              "canonical": {
+                "type": "string"
               }
             },
             "required": [
               "id",
-              "directory"
+              "directory",
+              "canonical"
             ],
             "additionalProperties": false
           }
@@ -12498,7 +12610,6 @@
           "cost",
           "tokens",
           "time",
-          "title",
           "location"
         ],
         "additionalProperties": false
@@ -13837,6 +13948,15 @@
           },
           "message": {
             "type": "string"
+          },
+          "status": {
+            "type": "integer",
+            "allOf": [
+              {
+                "minimum": 100,
+                "maximum": 599
+              }
+            ]
           }
         },
         "required": [
@@ -20765,7 +20885,7 @@
           "id": {
             "type": "string"
           },
-          "worktree": {
+          "canonical": {
             "type": "string"
           },
           "vcs": {
@@ -20792,7 +20912,7 @@
         },
         "required": [
           "id",
-          "worktree",
+          "canonical",
           "time",
           "sandboxes"
         ],
@@ -20806,11 +20926,15 @@
           },
           "directory": {
             "type": "string"
+          },
+          "canonical": {
+            "type": "string"
           }
         },
         "required": [
           "id",
-          "directory"
+          "directory",
+          "canonical"
         ],
         "additionalProperties": false
       },
@@ -22445,7 +22569,6 @@
           "slug",
           "projectID",
           "directory",
-          "title",
           "version",
           "time"
         ],
@@ -29127,6 +29250,30 @@
         ],
         "additionalProperties": false
       },
+      "Vcs.Branch": {
+        "type": "object",
+        "properties": {
+          "current": {
+            "type": "string"
+          },
+          "default": {
+            "type": "string"
+          }
+        },
+        "additionalProperties": false
+      },
+      "Vcs.Info": {
+        "type": "object",
+        "properties": {
+          "branch": {
+            "$ref": "#/components/schemas/Vcs.Branch"
+          }
+        },
+        "required": [
+          "branch"
+        ],
+        "additionalProperties": false
+      },
       "Vcs.FileStatus": {
         "type": "object",
         "properties": {