Compare commits

...
Author SHA1 Message Date
Brendonovich dbe1fbbc40 fix(app): restore Git init after review extension migration 2026-10-02 05:18:33 +00:00
opencode d259ae7163 sync release versions for v2.0.22 2026-10-02 04:23:05 +00:00
Kit Langton bb381e8bdd refactor(core): drop unused SessionRevert error re-export (#52661) 2026-10-02 04:14:44 +00:00
81efb7f146 fix(app): keep Working during reasoning-only turns (#51090)
Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
Co-authored-by: Brendan Allan <git@brendonovich.dev>
2026-10-02 12:14:04 +08:00
Kit Langton 77c5f7a516 refactor(schema): remove unused SessionV1.WithParts (#52662) 2026-10-02 04:11:35 +00:00
Kit Langton 4a18a86653 refactor(schema): remove unused IDE event definition (#52660) 2026-10-02 04:11:15 +00:00
Jérôme BenoitandTest User c07905012f fix(nix): repair three packaging defects on the v2 branch (#51891)
Co-authored-by: Test User <test@test.com>
2026-10-01 23:09:54 -05:00
opencode-agent[bot]andrekram1-node e067948dd5 refactor(worktree): tag errors without changing the API (#52631)
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2026-10-01 23:09:27 -05:00
Aiden Cline 64744ce034 fix(core): explain why the provider blocked a compaction (#52664) 2026-10-01 23:07:32 -05:00
Kit Langton 840cd232a8 refactor(core): remove orphaned filesystem ignore patterns (#52658) 2026-10-02 04:05:58 +00:00
Kit Langton ce97dd2805 refactor(core): drop unused imports (#52659) 2026-10-02 04:05:40 +00:00
Kit Langton f00390665f refactor(core): remove unused aisdk text helper (#52657) 2026-10-02 04:05:05 +00:00
Brendonovich a241b41545 fix(vcs): report unsupported initialization as 501 2026-09-29 07:14:45 +00:00
Brendonovich 8c46c09f8e feat(vcs): initialize through provider definitions 2026-09-29 06:55:27 +00:00
Brendonovich cb7f6b7ba4 fix(vcs): accept explicit Git init provider 2026-09-29 06:28:15 +00:00
Brendonovich 81c967c4e1 fix(app): restore Git initialization for non-Git projects 2026-09-26 06:28:01 +00:00
88 changed files with 2735 additions and 328 deletions

No files matched your search

+36 -36
View File
@@ -33,7 +33,7 @@
},
"packages/ai": {
"name": "@opencode/ai",
"version": "2.0.21",
"version": "2.0.22",
"dependencies": {
"@aws-sdk/credential-providers": "3.1057.0",
"@opencode/schema": "workspace:*",
@@ -55,7 +55,7 @@
},
"packages/app": {
"name": "@opencode/app",
"version": "2.0.21",
"version": "2.0.22",
"dependencies": {
"@corvu/drawer": "catalog:",
"@dnd-kit/abstract": "0.5.0",
@@ -112,7 +112,7 @@
},
"packages/cli": {
"name": "@opencode/cli",
"version": "2.0.21",
"version": "2.0.22",
"bin": {
"opencode": "./bin/opencode.cjs",
"opencode2": "./bin/opencode2.cjs",
@@ -178,7 +178,7 @@
},
"packages/client": {
"name": "@opencode/client",
"version": "2.0.21",
"version": "2.0.22",
"dependencies": {
"@opencode/protocol": "workspace:*",
"@opencode/schema": "workspace:*",
@@ -204,7 +204,7 @@
},
"packages/codemode": {
"name": "@opencode/codemode",
"version": "2.0.21",
"version": "2.0.22",
"dependencies": {
"acorn": "8.15.0",
"effect": "catalog:",
@@ -217,7 +217,7 @@
},
"packages/console/app": {
"name": "@opencode/console-app",
"version": "2.0.21",
"version": "2.0.22",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -253,7 +253,7 @@
},
"packages/console/core": {
"name": "@opencode/console-core",
"version": "2.0.21",
"version": "2.0.22",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -280,7 +280,7 @@
},
"packages/console/function": {
"name": "@opencode/console-function",
"version": "2.0.21",
"version": "2.0.22",
"dependencies": {
"@openauthjs/openauth": "0.0.0-20250322224806",
"@opencode/console-core": "workspace:*",
@@ -297,7 +297,7 @@
},
"packages/console/mail": {
"name": "@opencode/console-mail",
"version": "2.0.21",
"version": "2.0.22",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -321,7 +321,7 @@
},
"packages/console/support": {
"name": "@opencode/console-support",
"version": "2.0.21",
"version": "2.0.22",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@opencode/console-core": "workspace:*",
@@ -341,7 +341,7 @@
},
"packages/core": {
"name": "@opencode/core",
"version": "2.0.21",
"version": "2.0.22",
"dependencies": {
"@ai-sdk/cohere": "3.0.27",
"@ai-sdk/gateway": "3.0.104",
@@ -410,7 +410,7 @@
},
"packages/desktop": {
"name": "@opencode/desktop",
"version": "2.0.21",
"version": "2.0.22",
"dependencies": {
"@zip.js/zip.js": "2.7.62",
"electron-context-menu": "5.0.0",
@@ -455,7 +455,7 @@
},
"packages/enterprise": {
"name": "@opencode/enterprise",
"version": "2.0.21",
"version": "2.0.22",
"dependencies": {
"@hono/standard-validator": "catalog:",
"@opencode-ai/sdk": "1.18.21",
@@ -492,7 +492,7 @@
},
"packages/function": {
"name": "@opencode/function",
"version": "2.0.21",
"version": "2.0.22",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -508,7 +508,7 @@
},
"packages/gui-extensions": {
"name": "@opencode/gui-extensions",
"version": "2.0.20",
"version": "2.0.22",
"dependencies": {
"@dnd-kit/abstract": "0.5.0",
"@dnd-kit/dom": "0.5.0",
@@ -553,7 +553,7 @@
},
"packages/http-recorder": {
"name": "@opencode/http-recorder",
"version": "2.0.21",
"version": "2.0.22",
"dependencies": {
"@effect/platform-node-shared": "4.0.0-rc.112",
},
@@ -572,7 +572,7 @@
},
"packages/httpapi-codegen": {
"name": "@opencode/httpapi-codegen",
"version": "2.0.21",
"version": "2.0.22",
"dependencies": {
"effect": "catalog:",
"prettier": "3.6.2",
@@ -585,7 +585,7 @@
},
"packages/latex": {
"name": "@opencode/latex",
"version": "2.0.21",
"version": "2.0.22",
"dependencies": {
"@opencode/plugin": "workspace:*",
"@opentui/core": "catalog:",
@@ -599,7 +599,7 @@
},
"packages/merman": {
"name": "@opencode/merman",
"version": "2.0.21",
"version": "2.0.22",
"dependencies": {
"@opencode/plugin": "workspace:*",
"@opentui/core": "catalog:",
@@ -614,7 +614,7 @@
},
"packages/plugin": {
"name": "@opencode/plugin",
"version": "2.0.21",
"version": "2.0.22",
"dependencies": {
"@ai-sdk/provider": "3.0.8",
"@opencode/ai": "workspace:*",
@@ -653,7 +653,7 @@
},
"packages/plugin-browser": {
"name": "@opencode/plugin-browser",
"version": "2.0.21",
"version": "2.0.22",
"dependencies": {
"@opencode/plugin": "workspace:*",
"@opencode/schema": "workspace:*",
@@ -683,7 +683,7 @@
},
"packages/protocol": {
"name": "@opencode/protocol",
"version": "2.0.21",
"version": "2.0.22",
"dependencies": {
"@opencode/schema": "workspace:*",
"effect": "catalog:",
@@ -698,7 +698,7 @@
},
"packages/schema": {
"name": "@opencode/schema",
"version": "2.0.21",
"version": "2.0.22",
"dependencies": {
"@standard-schema/spec": "catalog:",
"effect": "catalog:",
@@ -722,7 +722,7 @@
},
"packages/sdk": {
"name": "@opencode/sdk",
"version": "2.0.21",
"version": "2.0.22",
"dependencies": {
"@opencode/client": "workspace:*",
"@opencode/core": "workspace:*",
@@ -743,7 +743,7 @@
},
"packages/server": {
"name": "@opencode/server",
"version": "2.0.21",
"version": "2.0.22",
"dependencies": {
"@effect/platform-node": "catalog:",
"@effect/platform-node-shared": "catalog:",
@@ -765,7 +765,7 @@
},
"packages/session-ui": {
"name": "@opencode/session-ui",
"version": "2.0.21",
"version": "2.0.22",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode/client": "workspace:*",
@@ -800,7 +800,7 @@
},
"packages/simulation": {
"name": "@opencode/simulation",
"version": "2.0.21",
"version": "2.0.22",
"dependencies": {
"@opencode/ai": "workspace:*",
"@opencode/core": "workspace:*",
@@ -820,7 +820,7 @@
},
"packages/stats/app": {
"name": "@opencode/stats-app",
"version": "2.0.21",
"version": "2.0.22",
"dependencies": {
"@ibm/plex": "6.4.1",
"@kobalte/core": "catalog:",
@@ -854,7 +854,7 @@
},
"packages/stats/core": {
"name": "@opencode/stats-core",
"version": "2.0.21",
"version": "2.0.22",
"dependencies": {
"@aws-sdk/client-athena": "3.933.0",
"@planetscale/database": "1.19.0",
@@ -873,7 +873,7 @@
},
"packages/stats/server": {
"name": "@opencode/stats-server",
"version": "2.0.21",
"version": "2.0.22",
"dependencies": {
"@aws-sdk/client-firehose": "3.933.0",
"@effect/platform-node": "catalog:",
@@ -919,7 +919,7 @@
},
"packages/theme": {
"name": "@opencode/theme",
"version": "2.0.21",
"version": "2.0.22",
"dependencies": {
"@opentui/core": "catalog:",
"effect": "catalog:",
@@ -933,7 +933,7 @@
},
"packages/tui": {
"name": "@opencode/tui",
"version": "2.0.21",
"version": "2.0.22",
"dependencies": {
"@opencode/client": "workspace:*",
"@opencode/core": "workspace:*",
@@ -967,7 +967,7 @@
},
"packages/ui": {
"name": "@opencode/ui",
"version": "2.0.21",
"version": "2.0.22",
"dependencies": {
"@kobalte/core": "catalog:",
"@pierre/diffs": "catalog:",
@@ -1002,7 +1002,7 @@
},
"packages/util": {
"name": "@opencode/util",
"version": "2.0.21",
"version": "2.0.22",
"dependencies": {
"@effect/opentelemetry": "catalog:",
"@effect/platform-node": "catalog:",
@@ -1040,7 +1040,7 @@
},
"packages/web": {
"name": "@opencode/web",
"version": "2.0.21",
"version": "2.0.22",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -1081,7 +1081,7 @@
},
"services/update": {
"name": "@opencode/update",
"version": "2.0.21",
"version": "2.0.22",
"dependencies": {
"jose": "6.0.11",
"semver": "catalog:",
+22 -2
View File
@@ -13,7 +13,12 @@
opencode,
}:
let
electron = callPackage ./electron.nix { };
electronPin =
(lib.pipe ../packages/desktop/package.json [
builtins.readFile
builtins.fromJSON
]).devDependencies.electron;
electron = callPackage ./electron.nix { inherit electronPin; };
in
stdenv.mkDerivation (finalAttrs: {
pname = "opencode-desktop";
@@ -66,7 +71,7 @@ stdenv.mkDerivation (finalAttrs: {
''
# https://github.com/electron/electron/issues/31121
# mac builds use a .app bundle which doesnt have this issue
+ lib.optionalString stdenv.isLinux ''
+ lib.optionalString stdenv.hostPlatform.isLinux ''
substituteInPlace \
packages/desktop/src/main/windows/appearance.ts \
packages/desktop/src/main/service/desktop-cli.ts \
@@ -74,6 +79,7 @@ stdenv.mkDerivation (finalAttrs: {
'';
preBuild = ''
echo "electron ${electron.version} from nixpkgs ${lib.version}, package.json pins ${electronPin}"
cp -r "${electron.dist}" $HOME/.electron-dist
chmod -R u+w $HOME/.electron-dist
@@ -89,8 +95,15 @@ stdenv.mkDerivation (finalAttrs: {
export OPENCODE_CLI_DIST="$TMPDIR/desktop-cli"
cli_package=$(bun -e 'import { getCurrentCli } from "./scripts/utils.ts"; console.log(getCurrentCli().package.replace("@opencode/", ""))')
# copyBuiltCliToResources joins this dist with the npm package name getCurrentCli()
# reports, not the Nix build's name. It reads only .version from the manifest and
# writes it as opencode-cli.version beside the binary.
mkdir -p "$OPENCODE_CLI_DIST/$cli_package/bin"
cp ${lib.getExe opencode} "$OPENCODE_CLI_DIST/$cli_package/bin/opencode"
# OPENCODE_VERSION is what the bundled CLI prints for --version, so the manifest
# and the executable cannot drift.
bun -e 'await Bun.write(process.argv[1], JSON.stringify({ version: process.env.OPENCODE_VERSION }) + "\n")' \
"$OPENCODE_CLI_DIST/$cli_package/package.json"
bun run build
npx electron-builder --dir \
@@ -138,6 +151,13 @@ stdenv.mkDerivation (finalAttrs: {
"libc.musl-x86_64.so.1"
];
passthru = {
# electronVersion is what ships; electronPin is what packages/desktop/package.json
# asks for. They differ whenever nixpkgs carries no release of the pinned minor.
electronVersion = electron.version;
inherit electronPin;
};
meta = {
description = "OpenCode Desktop App";
mainProgram = "opencode-desktop";
+8 -10
View File
@@ -1,12 +1,10 @@
{ callPackage, path }:
{ lib, pkgs, electronPin }:
let
version = (builtins.fromJSON (builtins.readFile ../packages/desktop/package.json)).devDependencies.electron;
# Nixpkgs owns the release hashes, so bumping the pin no longer means editing this repo. Only the
# major is delegated, so what gets built trails the pin whenever nixpkgs has not shipped it yet.
# That is safe: the bundle ships one native addon, node-pty's Node-API prebuild, and
# only the win32 WSL runtime loads it, so nothing in the main process binds the
# Electron ABI.
major = lib.versions.major electronPin;
in
(callPackage (path + "/pkgs/development/tools/electron/binary/generic.nix") { }) version {
# Electron 42.10.1 SHASUMS256.txt; update with the desktop package version.
aarch64-linux = "20e68d6c4e47f3ebf59de7c6b1f8b8bec6a6ebda6a451132f9b465f3f13ce467";
x86_64-linux = "2452b27112d92387471fa2488aafac85d79ea3f2ee1216c0abd5150d6c12362b";
aarch64-darwin = "ac7194a3dfd81930ba35355c01620262c1254752859b42dcb8f4b9e4d174a871";
# fetchzip hashes the unpacked headers, not the release tarball.
headers = "sha256-4eUy3BZVvxTl7KUOsxio7769lL6ag/ecbeK+qLURWMI=";
}
pkgs."electron_${major}-bin" or (throw "nixpkgs ${lib.version} carries no prebuilt electron ${major}: run `nix flake update nixpkgs`, or pin a major nixpkgs still carries")
+22 -6
View File
@@ -12,7 +12,7 @@
installShellFiles,
versionCheckHook,
writableTmpDirAsHomeHook,
node_modules ? callPackage ./node-modules.nix { },
node_modules ? callPackage ./node_modules.nix { },
}:
stdenvNoCC.mkDerivation (finalAttrs: {
pname = "opencode";
@@ -85,14 +85,30 @@ stdenvNoCC.mkDerivation (finalAttrs: {
'';
postInstall = lib.optionalString (stdenvNoCC.buildPlatform.canExecute stdenvNoCC.hostPlatform) ''
# trick yargs into also generating zsh completions
# v2 dropped the `completion` subcommand; --completions is the global flag.
# --completions also accepts sh, which emits the same script as bash.
# staged to files, substitute below rejects anything that is not a regular file
$out/bin/opencode --completions bash > opencode.bash
$out/bin/opencode --completions zsh > _opencode
$out/bin/opencode --completions fish > opencode.fish
installShellCompletion --cmd opencode \
--bash <($out/bin/opencode completion) \
--zsh <(SHELL=/bin/zsh $out/bin/opencode completion)
--bash opencode.bash \
--fish opencode.fish \
--zsh _opencode
# OPENCODE_CLI_NAME is a build-time define, so the opencode2 copies are
# renamed rather than regenerated. --replace-fail is a global literal
# substitution, so any lowercase opencode that later appears in a
# description or help text ships as opencode2 in the opencode2 copy.
substitute opencode.bash opencode2.bash --replace-fail opencode opencode2
substitute _opencode _opencode2 --replace-fail opencode opencode2
substitute opencode.fish opencode2.fish --replace-fail opencode opencode2
installShellCompletion --cmd opencode2 \
--bash <($out/bin/opencode2 completion) \
--zsh <(SHELL=/bin/zsh $out/bin/opencode2 completion)
--bash opencode2.bash \
--fish opencode2.fish \
--zsh _opencode2
'';
nativeInstallCheckInputs = [
+1 -1
View File
@@ -2,7 +2,7 @@
"$schema": "https://json.schemastore.org/package.json",
"name": "opencode",
"description": "AI-powered development tool",
"version": "2.0.21",
"version": "2.0.22",
"private": true,
"type": "module",
"packageManager": "bun@1.4.2",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "2.0.21",
"version": "2.0.22",
"name": "@opencode/ai",
"type": "module",
"license": "MIT",
@@ -8,6 +8,29 @@ import { expectSessionTitle } from "../utils/waits"
test.use({ viewport: { width: 1440, height: 900 } })
for (const view of ["desktop", "mobile"] as const) {
test(`offers Git initialization for a project without VCS (${view})`, async ({ page }) => {
if (view === "mobile") await page.setViewportSize({ width: 390, height: 844 })
const requests: { directory: string; provider?: string }[] = []
const workspace = await openSession(page, {
name: "ReviewWithoutGit",
project: { vcs: undefined },
onVcsInit: (input) => requests.push(input),
})
if (view === "desktop") await page.getByRole("button", { name: "Toggle review" }).click()
else await page.getByRole("tablist", { name: "Session view" }).getByRole("tab", { name: "Changes" }).click()
const panel = view === "desktop" ? page.locator("#review-panel") : page.locator('[data-component="session-review"]')
await expect(panel.getByText("Track, review, and undo changes in this project")).toBeVisible()
await expect(panel.getByRole("button", { name: "Git changes" })).toHaveCount(0)
const init = panel.getByRole("button", { name: "Create Git repository" })
await init.click()
await expect.poll(() => requests).toEqual([{ directory: workspace.directory, provider: "git" }])
await expect(panel.getByRole("button", { name: "Git changes" })).toBeVisible()
await expect(init).toHaveCount(0)
})
}
test("open file tab browses, searches, and tracks missing files", async ({ page }) => {
const searches: { query: string; dirs?: string; limit?: number }[] = []
const directory = "C:/OpenCode/ReviewOpenFile"
@@ -543,6 +543,26 @@ test.describe("Working", () => {
await expect(working).toHaveCount(0)
})
test("keeps Working instead of Used 1 Thought while reasoning is the only activity", async ({ page }) => {
const timeline = await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([reasoningPart("prt_only_thought", "Still considering the answer.")], { completed: false }),
],
settings: { timelineDetail: detailed },
})
const working = page.locator('[data-component="session-working"]')
const group = page.locator('[data-component="collapsed-tool-group"]')
await expect(working).toBeVisible()
await expect(group).toHaveCount(0)
await expect(page.getByRole("button", { name: "Used 1 Thought" })).toHaveCount(0)
await timeline.send(partUpdated(toolPart("prt_after_thought", "read", "running", { filePath: "package.json" })))
await expect(group).toBeVisible()
await expect(group).toHaveAttribute("data-timeline-part-ids", `${assistantID}:reasoning:0,prt_after_thought`)
await expect(working).toBeVisible()
})
for (const name of ["shell", "patch", "subagent"] as const) {
test(`hides Working during ${name} input and execution, then restores it on completion`, async ({ page }) => {
const timeline = await setupTimeline(page, {
+1
View File
@@ -169,6 +169,7 @@ const Group = HttpApiGroup.make("mock")
)
.add(HttpApiEndpoint.get("formRequests", "/api/form", { success: Json }))
.add(HttpApiEndpoint.get("vcs", "/api/vcs", { success: Json }))
.add(HttpApiEndpoint.post("vcsInit", "/api/vcs/init", { success: NoContent, error: Unsupported }))
.add(HttpApiEndpoint.get("vcsStatus", "/api/vcs/status", { success: Json }))
.add(HttpApiEndpoint.get("vcsBranches", "/api/vcs/branch", { success: Json }))
.add(HttpApiEndpoint.get("vcsDiff", "/api/vcs/diff", { query: Query, success: Json }))
+20
View File
@@ -43,6 +43,8 @@ export interface MockServerConfig {
cursor?: string
}
vcs?: { current: string; default: string }
// Initializes the mock project's VCS; without a handler this mutation answers 501.
onVcsInit?: (input: { directory: string; provider?: string }) => void
vcsDiff?: unknown[] | ((input: { mode?: string }) => unknown[])
// Benchmark latency only. Tests hold message pages with `beforeMessagesResponse`.
messageDelay?: number
@@ -817,6 +819,24 @@ function mockHandlers(
location: location(config),
data: { branch: config.vcs ?? { current: "main", default: "main" } },
}),
vcsInit: (ctx) => {
if (!config.onVcsInit) return unsupported("initialize VCS", "onVcsInit")
return Effect.sync(() => {
const url = new URL(ctx.request.url, "http://localhost")
const provider = url.searchParams.get("provider") ?? undefined
config.onVcsInit?.({ directory: requestDirectory(config, ctx.request), provider })
const project = config.project as { id: string; vcs?: string }
project.vcs = provider ?? "git"
state.emit([
{
id: "evt_vcs_initialized",
type: "worktree.updated",
created: Date.now(),
data: { projectID: project.id },
},
])
}).pipe(Effect.andThen(noContent))
},
vcsStatus: () => Effect.succeed({ location: location(config), data: [] }),
vcsBranches: () => Effect.succeed({ location: location(config), data: ["main"] }),
vcsDiff: (ctx) =>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/app",
"version": "2.0.21",
"version": "2.0.22",
"description": "",
"type": "module",
"exports": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/cli",
"version": "2.0.21",
"version": "2.0.22",
"type": "module",
"license": "MIT",
"bin": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/client",
"version": "2.0.21",
"version": "2.0.22",
"type": "module",
"license": "MIT",
"repository": {
+8
View File
@@ -2270,6 +2270,13 @@ export interface WorktreeApi<E = never> {
readonly refresh: WorktreeRefreshOperation<E>
}
export type VcsInitInput = {
readonly location?: { readonly directory?: string | undefined } | undefined
readonly provider?: string | undefined
}
export type VcsInitOutput = void
export type VcsInitOperation<E = never> = (input?: VcsInitInput) => Effect.Effect<VcsInitOutput, E>
export type VcsGetInput = { readonly location?: { readonly directory?: string | undefined } | undefined }
export type VcsGetOutput = { readonly location: Location.PublicRef; readonly data: Vcs.Info }
export type VcsGetOperation<E = never> = (input?: VcsGetInput) => Effect.Effect<VcsGetOutput, E>
@@ -2300,6 +2307,7 @@ export type VcsDiffOutput = { readonly location: Location.PublicRef; readonly da
export type VcsDiffOperation<E = never> = (input: VcsDiffInput) => Effect.Effect<VcsDiffOutput, E>
export interface VcsApi<E = never> {
readonly init: VcsInitOperation<E>
readonly get: VcsGetOperation<E>
readonly base: VcsBaseOperation<E>
readonly status: VcsStatusOperation<E>
@@ -242,6 +242,8 @@ import type {
WorktreeRemoveOutput,
WorktreeRefreshInput,
WorktreeRefreshOutput,
VcsInitInput,
VcsInitOutput,
VcsGetInput,
VcsGetOutput,
VcsBaseInput,
@@ -1481,6 +1483,13 @@ const adaptGroupWorktree = (raw: RawClient["server.worktree"]) => ({
refresh: EndpointWorktreeRefresh(raw),
})
const EndpointVcsInit = (raw: RawClient["server.vcs"]) => (input?: VcsInitInput) =>
preserveEffect<VcsInitOutput>()(
raw["vcs.init"]({ query: { location: input?.["location"], provider: input?.["provider"] } }).pipe(
Effect.mapError(mapClientError),
),
)
const EndpointVcsGet = (raw: RawClient["server.vcs"]) => (input?: VcsGetInput) =>
preserveEffect<VcsGetOutput>()(
raw["vcs.get"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)),
@@ -1511,6 +1520,7 @@ const EndpointVcsDiff = (raw: RawClient["server.vcs"]) => (input: VcsDiffInput)
)
const adaptGroupVcs = (raw: RawClient["server.vcs"]) => ({
init: EndpointVcsInit(raw),
get: EndpointVcsGet(raw),
base: EndpointVcsBase(raw),
status: EndpointVcsStatus(raw),
@@ -240,6 +240,8 @@ import type {
WorktreeRemoveOutput,
WorktreeRefreshInput,
WorktreeRefreshOutput,
VcsInitInput,
VcsInitOutput,
VcsGetInput,
VcsGetOutput,
VcsBaseInput,
@@ -2040,6 +2042,18 @@ export function make(options: ClientOptions) {
),
},
vcs: {
init: (input?: VcsInitInput, requestOptions?: RequestOptions) =>
request<VcsInitOutput>(
{
method: "POST",
path: `/api/vcs/init`,
query: { location: input?.["location"], provider: input?.["provider"] },
successStatus: 204,
declaredStatuses: [400, 401, 409, 501, 503],
empty: true,
},
requestOptions,
),
get: (input?: VcsGetInput, requestOptions?: RequestOptions) =>
request<VcsGetOutput>(
{
+23 -1
View File
@@ -2735,11 +2735,20 @@ export const isShellNotFoundError = (value: unknown): value is ShellNotFoundErro
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ShellNotFoundError"
export type WorktreeError = {
readonly _tag: "WorktreeError"
readonly name: "WorktreeError"
readonly data: { readonly message: string; readonly forceRequired?: boolean | undefined }
}
export const isWorktreeError = (value: unknown): value is WorktreeError =>
typeof value === "object" && value !== null && "name" in value && value["name"] === "WorktreeError"
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "WorktreeError"
export type VcsInitNotSupportedError = {
readonly _tag: "VcsInitNotSupportedError"
readonly providerID: string
readonly message: string
}
export const isVcsInitNotSupportedError = (value: unknown): value is VcsInitNotSupportedError =>
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "VcsInitNotSupportedError"
export type ServerInfoOutput = ServerInfo
@@ -6494,6 +6503,19 @@ export type WorktreeRefreshInput = { readonly projectID: { readonly projectID: s
export type WorktreeRefreshOutput = void
export type VcsInitInput = {
readonly location?: {
readonly location?: { readonly directory?: string | undefined } | undefined
readonly provider?: string | undefined
}["location"]
readonly provider?: {
readonly location?: { readonly directory?: string | undefined } | undefined
readonly provider?: string | undefined
}["provider"]
}
export type VcsInitOutput = void
export type VcsGetInput = {
readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
}
+19 -15
View File
@@ -174,21 +174,25 @@ test("declared errors are thrown as Error instances that keep the body", async (
expect(error).toMatchObject({ _tag: "InvalidRequestError", kind: "integration_authorization" })
})
test("declared errors with a data envelope read the nested message", async () => {
const client = OpenCode.make({
baseUrl: "http://localhost:3000",
fetch: async () =>
Response.json({ name: "WorktreeError", data: { message: "Worktree directory unavailable" } }, { status: 400 }),
})
const error = await client.worktree.create({ projectID: "prj_test" }).then(
() => undefined,
(cause: unknown) => cause,
)
expect(error).toBeInstanceOf(Error)
if (!(error instanceof Error)) throw error
expect(error.message).toBe("Worktree directory unavailable")
expect(error.name).toBe("WorktreeError")
expect(error).toMatchObject({ name: "WorktreeError", data: { message: "Worktree directory unavailable" } })
test("worktree errors retain the existing envelope with or without a tag", async () => {
for (const body of [
{ name: "WorktreeError", data: { message: "Worktree directory unavailable" } },
{ _tag: "WorktreeError", name: "WorktreeError", data: { message: "Worktree directory unavailable" } },
]) {
const client = OpenCode.make({
baseUrl: "http://localhost:3000",
fetch: async () => Response.json(body, { status: 400 }),
})
const error = await client.worktree.create({ projectID: "prj_test" }).then(
() => undefined,
(cause: unknown) => cause,
)
expect(error).toBeInstanceOf(Error)
if (!(error instanceof Error)) throw error
expect(error.message).toBe("Worktree directory unavailable")
expect(error.name).toBe("WorktreeError")
expect(error).toMatchObject(body)
}
})
test("client errors keep the reason and describe the failure in the message", async () => {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/codemode",
"version": "2.0.21",
"version": "2.0.22",
"description": "Effect-native confined code execution over schema-described tools",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/console-app",
"version": "2.0.21",
"version": "2.0.22",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/console-core",
"version": "2.0.21",
"version": "2.0.22",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/console-function",
"version": "2.0.21",
"version": "2.0.22",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/console-mail",
"version": "2.0.21",
"version": "2.0.22",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/console-support",
"version": "2.0.21",
"version": "2.0.22",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "2.0.21",
"version": "2.0.22",
"name": "@opencode/core",
"type": "module",
"license": "MIT",
-4
View File
@@ -593,10 +593,6 @@ function toolMessage(input: LLMRequest["messages"][number]) {
}
}
function text(part: ContentPart) {
return part.type === "text" ? [part.text] : []
}
function userPart(part: ContentPart): UserContent {
if (part.type === "text") return [{ type: "text", text: part.text }]
if (part.type === "media")
@@ -6,7 +6,6 @@ import path from "path"
import { Effect, FiberMap, PubSub, Semaphore, Stream } from "effect"
import { Config } from "../../config.js"
import { Watcher } from "../../filesystem/watcher.js"
import { AbsolutePath } from "../../schema.js"
import { Skill } from "../../skill.js"
import { SkillFile } from "./skill-file.js"
-48
View File
@@ -1,48 +0,0 @@
const FOLDERS = new Set([
"node_modules",
"bower_components",
".pnpm-store",
"vendor",
".npm",
"dist",
"build",
"out",
".next",
"target",
"bin",
"obj",
".git",
".svn",
".hg",
".vscode",
".idea",
".turbo",
".output",
"desktop",
".sst",
".cache",
".webkit-cache",
"__pycache__",
".pytest_cache",
"mypy_cache",
".history",
".gradle",
])
const FILES = [
"**/*.swp",
"**/*.swo",
"**/*.pyc",
"**/.DS_Store",
"**/Thumbs.db",
"**/logs/**",
"**/tmp/**",
"**/temp/**",
"**/*.log",
"**/coverage/**",
"**/.nyc_output/**",
]
export const PATTERNS = [...FILES, ...FOLDERS, `**/{${Array.from(FOLDERS).join(",")}}/**`]
export * as Ignore from "./ignore.js"
@@ -1,7 +1,6 @@
import { Effect } from "effect"
import { define } from "@opencode/plugin/effect/plugin"
import { Integration } from "../../integration.js"
import { Provider } from "../../provider.js"
export const LLMGatewayPlugin = define({
id: "opencode.provider.llmgateway",
+9 -1
View File
@@ -23,7 +23,8 @@ export const Plugin = define({
id: "opencode.vcs.git",
effect: Effect.fn("VcsGitPlugin")(function* (ctx) {
const location = yield* Location.Service
if (location.vcs?.type !== "git") return
// Markerless locations need this definition to initialize their first repository.
if (location.vcs && location.vcs.type !== "git") return
const processes = yield* AppProcess.Service
const adapter = make(processes, {
@@ -35,6 +36,13 @@ export const Plugin = define({
editor.add({
id: "git",
name: "Git",
init: (input) =>
Effect.gen(function* () {
const result = yield* processes.run(
ChildProcess.make(gitExecutable, ["init"], { cwd: input.worktree, stdin: "ignore" }),
)
if (result.exitCode !== 0) return yield* Effect.fail(new Error("Git initialization failed"))
}),
info: () => adapter.info(),
base: () => adapter.base(),
branches: (input) => adapter.branches({ search: input.search, limit: input.limit }),
+14 -1
View File
@@ -26,7 +26,8 @@ export const Plugin = define({
id: "opencode.vcs.hg",
effect: Effect.fn("VcsHgPlugin")(function* (ctx) {
const location = yield* Location.Service
if (location.vcs?.type !== "hg") return
// Markerless locations need this definition to initialize their first repository.
if (location.vcs && location.vcs.type !== "hg") return
const processes = yield* AppProcess.Service
const fs = yield* FSUtil.Service
@@ -39,6 +40,18 @@ export const Plugin = define({
editor.add({
id: "hg",
name: "Mercurial",
init: (input) =>
Effect.gen(function* () {
const result = yield* processes.run(
ChildProcess.make("hg", ["init"], {
cwd: input.worktree,
env: { HGPLAIN: "1" },
extendEnv: true,
stdin: "ignore",
}),
)
if (result.exitCode !== 0) return yield* Effect.fail(new Error("Mercurial initialization failed"))
}),
info: () => adapter.info(),
branches: (input) => adapter.branches({ search: input.search, limit: input.limit }),
status: () => adapter.status(),
+2 -5
View File
@@ -32,7 +32,7 @@ import { SessionModelRequest } from "./model-request.js"
import { SessionProviderContext } from "./provider-context.js"
import { SessionRunnerRetry } from "./runner/retry.js"
import { toLLMMessages } from "./runner/to-llm-message.js"
import { toSessionError } from "./to-session-error.js"
import { contentFilterError, toSessionError } from "./to-session-error.js"
import { SessionUsage } from "./usage.js"
export type Settings = {
@@ -540,10 +540,7 @@ export const layer = Layer.effect(
message: "Compaction summary reached the output token limit",
})
case "content-filter":
return unusable({
type: "provider.content-filter",
message: "Compaction summary was blocked by the provider",
})
return unusable(contentFilterError("Compaction summary was blocked by the provider", event.reason))
default:
return Effect.succeed(streamed)
}
-2
View File
@@ -13,8 +13,6 @@ import { SessionMessage } from "./message.js"
import { SessionSchema } from "./schema.js"
import { SessionMessageTable } from "./sql.js"
export { MessageNotFoundError }
interface BoundaryInput {
readonly sessionID: SessionSchema.ID
readonly messageID: SessionMessage.ID
@@ -12,6 +12,7 @@ import { SessionSchema } from "../schema.js"
import { SessionError } from "@opencode/schema/session-error"
import { Money } from "@opencode/schema/money"
import { SessionUsage } from "../usage.js"
import { contentFilterError } from "../to-session-error.js"
import type { Tool } from "../../tool.js"
type Input = {
@@ -543,15 +544,7 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
}
if (event.reason.normalized === "content-filter") {
providerFailed = true
yield* failAssistant({
type: "provider.content-filter",
message: [
`Provider blocked the response${event.reason.category === undefined ? "" : ` (${event.reason.category})`}`,
event.reason.explanation,
]
.filter(Boolean)
.join(": "),
})
yield* failAssistant(contentFilterError("Provider blocked the response", event.reason))
return
}
return
-1
View File
@@ -15,7 +15,6 @@ import {
BusyError,
CompactionConflictError,
InboxConflictError,
MessageNotFoundError,
NotFoundError,
PromptConflictError,
SyntheticConflictError,
+10 -1
View File
@@ -1,4 +1,4 @@
import { AIError, ToolFailure } from "@opencode/ai"
import { AIError, ToolFailure, type FinishReasonDetails } from "@opencode/ai"
import { Tool } from "@opencode/schema/tool"
import { SessionError } from "@opencode/schema/session-error"
import { Permission } from "../permission.js"
@@ -85,6 +85,15 @@ export function toSessionError(cause: unknown): SessionError.Error {
return { type: "unknown", message: cause instanceof Error ? cause.message : String(cause) }
}
export function contentFilterError(summary: string, reason: FinishReasonDetails): SessionError.Error {
return {
type: "provider.content-filter",
message: [reason.category === undefined ? summary : `${summary} (${reason.category})`, reason.explanation]
.filter(Boolean)
.join(": "),
}
}
function providerError(type: string, reason: AIError["reason"]): SessionError.Error {
const status = reason.http?.status
return {
+27 -1
View File
@@ -10,6 +10,7 @@ import { VcsEvent } from "@opencode/schema/vcs-event"
import { makeLocationNode } from "@opencode/util/effect/app-node"
import { FSUtil } from "@opencode/util/fs-util"
import { Location } from "./location.js"
import { Project } from "./project.js"
import { Bus } from "./bus.js"
import { State } from "./state.js"
import { emptyPatch, MAX_TOTAL_PATCH_BYTES, PATCH_CONTEXT_LINES } from "./vcs/patch.js"
@@ -20,6 +21,10 @@ export class DiffError extends Schema.TaggedError<DiffError>()("Vcs.DiffError",
message: Schema.String,
}) {}
export class InitializeError extends Schema.TaggedError<InitializeError>()("Vcs.InitializeError", {
kind: Schema.Literals(["missing", "conflict", "unknown", "unsupported", "failed"]),
}) {}
export interface DiffOptions {
readonly context?: number
readonly base?: string
@@ -40,6 +45,7 @@ export interface Adapter {
export interface Interface extends Adapter, State.Transformable<VcsEditor> {
readonly base: () => Effect.Effect<Base | null, DiffError>
readonly initialize: (providerID: string) => Effect.Effect<void, InitializeError>
}
interface Data {
@@ -124,7 +130,9 @@ const layer = Layer.effect(
const provider = selected()
const next: Info = provider
? {
...(yield* protect(provider, "info", provider.info(scope).pipe(Effect.flatMap(decodeInfo)), { branch: {} })),
...(yield* protect(provider, "info", provider.info(scope).pipe(Effect.flatMap(decodeInfo)), {
branch: {},
})),
provider: provider.id,
}
: { branch: {} }
@@ -166,6 +174,24 @@ const layer = Layer.effect(
return Service.of({
transform: state.transform,
reload: state.reload,
initialize: Effect.fn("Vcs.initialize")(function* (providerID: string) {
if (!(yield* fs.isDir(location.project.directory))) return yield* new InitializeError({ kind: "missing" })
if (yield* Project.root(fs, location.project.directory)) return yield* new InitializeError({ kind: "conflict" })
const provider = state.get().providers.get(providerID)
if (!provider) return yield* new InitializeError({ kind: "unknown" })
if (!provider.init) return yield* new InitializeError({ kind: "unsupported" })
yield* provider
.init(scope)
.pipe(
Effect.catchCause((cause) =>
Cause.hasInterrupts(cause)
? Effect.failCause(cause).pipe(Effect.orDie)
: Effect.logWarning("vcs initialization failed", { provider: providerID, cause }).pipe(
Effect.andThen(Effect.fail(new InitializeError({ kind: "failed" }))),
),
),
)
}),
info: Effect.fn("Vcs.info")(function* () {
return current.info
}),
@@ -1,31 +0,0 @@
import { expect, test } from "bun:test"
import { Ignore } from "@opencode/core/filesystem/ignore"
// @ts-ignore
import { createWrapper } from "@parcel/watcher/wrapper.js"
test("parcel patterns ignore built-in folders at any depth", async () => {
let ignoreGlobs: string[] = []
const watcher = createWrapper({
subscribe: async (
_directory: string,
_callback: (...args: unknown[]) => unknown,
options: { ignoreGlobs?: string[] },
) => {
ignoreGlobs = options.ignoreGlobs ?? []
},
})
await watcher.subscribe("/tmp/project", () => {}, { ignore: Ignore.PATTERNS })
const patterns = ignoreGlobs.map((source) => new RegExp(source))
for (const path of [
"nested/node_modules",
"nested/node_modules/package/index.js",
"nested/.git",
"nested/.git/HEAD",
"nested/dist",
"nested/dist/index.js",
]) {
expect(patterns.some((pattern) => pattern.test(path))).toBe(true)
}
expect(patterns.some((pattern) => pattern.test("nested/src/index.ts"))).toBe(false)
})
@@ -0,0 +1,48 @@
import { expect } from "bun:test"
import { Plugin } from "@opencode/plugin"
import type { VcsDefinition } from "@opencode/plugin/effect/vcs"
import { PluginPromise } from "@opencode/core/plugin/promise"
import { State } from "@opencode/core/state"
import { Effect } from "effect"
import { it } from "../lib/effect"
import { host } from "./host"
it.live("Promise VCS initializer reaches the registered provider with its location scope", () =>
Effect.gen(function* () {
const calls: string[] = []
const state = State.create({
initial: () => new Map<string, VcsDefinition>(),
editor: (providers) => ({
add: (definition: VcsDefinition) => providers.set(definition.id, definition),
default: { get: () => undefined, set: (_selection: string) => {} },
}),
})
const context = host()
const plugin = PluginPromise.fromPromise(
Plugin.define({
id: "init-vcs",
async setup(ctx) {
await ctx.vcs.transform((editor) =>
editor.add({
id: "custom",
name: "Custom VCS",
init: async (scope, { signal }) => {
expect(signal.aborted).toBe(false)
calls.push(scope.worktree)
},
info: async () => ({ branch: {} }),
branches: async () => [],
status: async () => [],
diff: async () => [],
}),
)
},
}),
)
yield* plugin.effect(host({ vcs: { ...context.vcs, transform: state.transform, reload: state.reload } }))
const init = state.get().get("custom")?.init
if (!init) return yield* Effect.die("Initializer was not registered")
yield* init({ directory: "/workspace", worktree: "/workspace", canonical: "/workspace" })
expect(calls).toEqual(["/workspace"])
}),
)
+26
View File
@@ -2716,6 +2716,32 @@ describe("SessionRunnerLLM", () => {
)
})
scenario("explains a compaction blocked by the provider", function* (s) {
yield* s.llm.push(TestLLM.text("Earlier answer", "history"))
yield* s.runPrompt("Earlier question")
yield* s.llm.push(
TestLLM.complete({
reason: {
normalized: "content-filter",
raw: "refusal",
category: "cyber",
explanation: "This request was declined because it could enable cyber harm.",
},
}),
)
const compaction = yield* s.session.compact({ sessionID })
yield* s.resume
expect((yield* s.messages).find((message) => message.id === compaction.id)).toMatchObject({
status: "failed",
error: {
type: "provider.content-filter",
message:
"Compaction summary was blocked by the provider (cyber): This request was declined because it could enable cyber harm.",
},
})
})
for (const header of [false, true]) {
scenario(`stops compaction retries through the ${header ? "provider header" : "retry hook"}`, function* (s) {
yield* s.llm.push(TestLLM.text("Earlier answer", "history"))
+23
View File
@@ -103,6 +103,29 @@ const provider = (input: Partial<VcsDefinition> = {}) =>
}) satisfies VcsDefinition
describe("Vcs", () => {
it.live("routes initialization through the requested provider before a repository exists", () =>
withTmp((directory) =>
Effect.gen(function* () {
const vcs = yield* Vcs.Service
const calls: string[] = []
yield* vcs.transform((editor) => {
editor.add(provider({ id: "read-only" }))
editor.add(
provider({
id: "custom",
init: (input) => Effect.sync(() => void calls.push(input.worktree)),
}),
)
})
expect(yield* vcs.initialize("missing").pipe(Effect.flip)).toMatchObject({ kind: "unknown" })
expect(yield* vcs.initialize("read-only").pipe(Effect.flip)).toMatchObject({ kind: "unsupported" })
expect(calls).toEqual([])
yield* vcs.initialize("custom")
expect(calls).toEqual([directory])
}).pipe(provide(directory)),
),
)
it.live("returns empty results outside version control", () =>
withTmp((directory) =>
Effect.gen(function* () {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode/desktop",
"private": true,
"version": "2.0.21",
"version": "2.0.22",
"type": "module",
"license": "MIT",
"homepage": "https://opencode.ai",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/enterprise",
"version": "2.0.21",
"version": "2.0.22",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/function",
"version": "2.0.21",
"version": "2.0.22",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/gui-extensions",
"version": "2.0.20",
"version": "2.0.22",
"private": true,
"type": "module",
"license": "MIT",
@@ -2,6 +2,7 @@ import type { FileDiffInfo } from "@opencode/client/promise"
import type { SessionReviewLineComment } from "@opencode/session-ui/session-review"
import { previewSelectedLines } from "@opencode/session-ui/pierre/selection-bridge"
import { checksum } from "@opencode/util/encode"
import { showToast } from "@opencode/ui/toast"
import { createQuery, useQueryClient } from "@tanstack/solid-query"
import { debounce } from "@solid-primitives/scheduled"
import { createComputed, createEffect, createMemo, on, onCleanup } from "solid-js"
@@ -46,6 +47,7 @@ export function createReviewModel(input: { ctx: Context; view: SessionView; dema
scroll: undefined as HTMLDivElement | undefined,
pendingFile: undefined as string | undefined,
deferRender: false,
initializingGit: false,
// The filter is transient by design: a persisted filter would silently hide
// files after a reload.
filter: "",
@@ -218,6 +220,45 @@ export function createReviewModel(input: { ctx: Context; view: SessionView; dema
if (!stored()) return false
return !diffQuery.isPending
}
const lifetime = { disposed: false }
onCleanup(() => {
lifetime.disposed = true
})
const initializeGit = () => {
if (state.initializingGit) return
const location = view.location
if (!location || !view.server.connected) {
showToast({ variant: "error", title: ctx.t("common.requestFailed") })
return
}
const key = view.key
const sessionID = view.id
setState("initializingGit", true)
void view.server.client.vcs
.init({ location, provider: "git" })
.then(async () => {
if (lifetime.disposed || ctx.signal.aborted || view.key !== key) return
const data = view.server.data
data.project.invalidate()
data.session.invalidate(sessionID)
data.location.invalidate(location)
data.location.vcs.invalidate(location)
await data.project.sync()
await data.session.sync(sessionID)
await Promise.all([data.location.syncInfo(location), data.location.vcs.sync(location)])
})
.catch((error: unknown) => {
if (lifetime.disposed || ctx.signal.aborted || view.key !== key) return
showToast({
variant: "error",
title: ctx.t("common.requestFailed"),
description: error instanceof Error ? error.message : undefined,
})
})
.finally(() => {
if (!lifetime.disposed && !ctx.signal.aborted && view.key === key) setState("initializingGit", false)
})
}
const loadDiff = async (path: string, version?: number): Promise<FileDiffInfo | undefined> => {
const source = diffs().find((diff) => diff.file === path)
const valid = (diff: FileDiffInfo | undefined): FileDiffInfo | undefined => {
@@ -445,6 +486,8 @@ export function createReviewModel(input: { ctx: Context; view: SessionView; dema
kinds,
focusFile,
hasChanges,
initializeGit,
initializingGit: () => state.initializingGit,
loadDiff,
mode,
noGit: createMemo(() => !!view.project && !view.project.vcs),
+7 -15
View File
@@ -1,5 +1,6 @@
import { Match, Show, Switch } from "solid-js"
import { SessionReviewEmptyChangesV2 } from "@opencode/session-ui/v2/session-review-empty-changes-v2"
import { SessionReviewEmptyNoGitV2 } from "@opencode/session-ui/v2/session-review-empty-no-git-v2"
import { Select } from "@opencode/ui/select"
import { useExtension } from "../sdk"
import type { ChangeMode, ReviewModel } from "./model"
@@ -12,7 +13,7 @@ export function ReviewTitle(props: { review: ReviewModel }) {
return ctx.t("ui.sessionReview.title.lastTurn")
}
return (
<Show when={props.review.canReview()}>
<Show when={props.review.canReview() && props.review.options().length > 0}>
<Select
options={props.review.options()}
current={props.review.mode()}
@@ -28,7 +29,6 @@ export function ReviewTitle(props: { review: ReviewModel }) {
export function ReviewEmpty(props: { review: ReviewModel; loadingClass: string }) {
const ctx = useExtension()
const loading = () => !props.review.ready()
const noGit = () => props.review.noGit()
const text = () => {
if (props.review.mode() === "git") return ctx.t("empty.git")
if (props.review.mode() === "branch") return ctx.t("empty.branch")
@@ -39,14 +39,9 @@ export function ReviewEmpty(props: { review: ReviewModel; loadingClass: string }
<Match when={loading()}>
<div class={props.loadingClass}>{ctx.t("loadingChanges")}</div>
</Match>
<Match when={noGit()}>
<div class="h-full pb-64 -mt-4 flex flex-col items-center justify-center text-center gap-6">
<div class="flex flex-col gap-3">
<div class="text-14-medium text-text-strong">{ctx.t("git.title")}</div>
<div class="text-14-regular text-text-base max-w-md" style={{ "line-height": "var(--line-height-normal)" }}>
{ctx.t("git.description")}
</div>
</div>
<Match when={props.review.noGit()}>
<div class="h-full flex flex-col">
<SessionReviewEmptyNoGitV2 pending={props.review.initializingGit()} onInitGit={props.review.initializeGit} />
</div>
</Match>
<Match when={true}>
@@ -61,16 +56,13 @@ export function ReviewEmpty(props: { review: ReviewModel; loadingClass: string }
export function ReviewPanelEmpty(props: { review: ReviewModel }) {
const ctx = useExtension()
const loading = () => !props.review.ready()
const noGit = () => props.review.noGit()
return (
<Switch>
<Match when={loading()}>
<div class="px-6 py-4 text-text-weak">{ctx.t("loadingChanges")}</div>
</Match>
<Match when={noGit()}>
<div class="h-full pb-64 -mt-4 flex flex-col items-center justify-center text-center gap-6">
<div class="text-14-regular text-text-weak max-w-56">{ctx.t("git.description")}</div>
</div>
<Match when={props.review.noGit()}>
<SessionReviewEmptyNoGitV2 pending={props.review.initializingGit()} onInitGit={props.review.initializeGit} />
</Match>
<Match when={true}>
<SessionReviewEmptyChangesV2 />
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "2.0.21",
"version": "2.0.22",
"name": "@opencode/http-recorder",
"description": "Record and replay Effect HTTP and WebSocket traffic with deterministic cassettes",
"type": "module",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/httpapi-codegen",
"version": "2.0.21",
"version": "2.0.22",
"private": true,
"type": "module",
"exports": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/latex",
"version": "2.0.21",
"version": "2.0.22",
"private": true,
"type": "module",
"exports": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/merman",
"version": "2.0.21",
"version": "2.0.22",
"private": true,
"type": "module",
"exports": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/plugin-browser",
"version": "2.0.21",
"version": "2.0.22",
"description": "OpenCode's desktop browser plugin",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/plugin",
"version": "2.0.21",
"version": "2.0.22",
"type": "module",
"license": "MIT",
"scripts": {
+2 -1
View File
@@ -26,6 +26,7 @@ export interface VcsDiffInput extends VcsScope {
export interface VcsDefinition {
readonly id: string
readonly name: string
readonly init?: (input: VcsScope) => Effect.Effect<void, unknown>
readonly info: (input: VcsScope) => Effect.Effect<Vcs.Info, unknown>
readonly base?: (input: VcsScope) => Effect.Effect<Vcs.Base | null, unknown>
readonly branches: (input: VcsBranchesInput) => Effect.Effect<Vcs.BranchList, unknown>
@@ -33,7 +34,7 @@ export interface VcsDefinition {
readonly diff: (input: VcsDiffInput) => Effect.Effect<readonly FileDiff.Info[], unknown>
}
export interface VcsDomain extends VcsApi<unknown> {
export interface VcsDomain extends Omit<VcsApi<unknown>, "init"> {
readonly transform: Transform<VcsEditor>
readonly reload: () => Effect.Effect<void>
}
+2
View File
@@ -518,10 +518,12 @@ export function fromPromise(plugin: Plugin) {
host.vcs.transform((editor) => {
callback({
add: (definition) => {
const init = definition.init?.bind(definition)
const base = definition.base?.bind(definition)
editor.add({
id: definition.id,
name: definition.name,
init: init ? (input) => attempt((signal) => init(input, { signal })) : undefined,
info: (input) => attempt((signal) => definition.info(input, { signal })),
base: base ? (input) => attempt((signal) => base(input, { signal })) : undefined,
branches: (input) => attempt((signal) => definition.branches(input, { signal })),
+2 -1
View File
@@ -25,6 +25,7 @@ export interface VcsDiffInput extends VcsScope {
export interface VcsDefinition {
readonly id: string
readonly name: string
readonly init?: (input: VcsScope, context: { readonly signal: AbortSignal }) => Promise<void>
readonly info: (input: VcsScope, context: { readonly signal: AbortSignal }) => Promise<Vcs.Info>
readonly base?: (input: VcsScope, context: { readonly signal: AbortSignal }) => Promise<Vcs.Base | null>
readonly branches: (input: VcsBranchesInput, context: { readonly signal: AbortSignal }) => Promise<Vcs.BranchList>
@@ -32,7 +33,7 @@ export interface VcsDefinition {
readonly diff: (input: VcsDiffInput, context: { readonly signal: AbortSignal }) => Promise<readonly FileDiff.Info[]>
}
export interface VcsDomain extends VcsApi {
export interface VcsDomain extends Omit<VcsApi, "init"> {
readonly transform: Transform<VcsEditor>
readonly reload: () => Promise<void>
}
+628 -20
View File
@@ -48,6 +48,106 @@
"summary": "Get server info"
}
},
"/api/pair": {
"post": {
"tags": ["server"],
"operationId": "server.pair",
"parameters": [],
"security": [],
"responses": {
"200": {
"description": "PairingCode",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PairingCode"
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestErrorEncoded"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedErrorEncoded"
}
}
}
}
},
"description": "Create a short-lived, single-use code for a /auth/connect/:code pairing link.",
"summary": "Create pairing code"
}
},
"/auth/connect/{code}": {
"get": {
"tags": ["server"],
"operationId": "server.connect",
"parameters": [
{
"name": "code",
"in": "path",
"schema": {
"type": "string"
},
"required": true
}
],
"security": [],
"responses": {
"200": {
"description": "PairingSession",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PairingSession"
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestErrorEncoded"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/UnauthorizedErrorEncoded"
},
{
"$ref": "#/components/schemas/UnauthorizedErrorEncoded"
}
]
}
}
}
}
},
"description": "Redeem a pairing code. Browsers receive a session cookie and a redirect to the web app; requests that accept JSON receive a session token to use as the password.",
"summary": "Redeem pairing code"
}
},
"/api/location": {
"get": {
"tags": ["location"],
@@ -886,9 +986,19 @@
}
}
}
},
"404": {
"description": "SessionNotFoundError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SessionNotFoundErrorEncoded"
}
}
}
}
},
"description": "Create a session at the requested location.",
"description": "Create a session at the requested location. A parentID creates a linked child session at its parent's location.",
"summary": "Create session",
"requestBody": {
"content": {
@@ -907,6 +1017,17 @@
}
]
},
"parentID": {
"anyOf": [
{
"type": "string",
"pattern": "^ses"
},
{
"type": "null"
}
]
},
"title": {
"anyOf": [
{
@@ -4508,6 +4629,21 @@
"pattern": "^frm_"
},
"required": true
},
{
"name": "message",
"in": "query",
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"required": false
}
],
"security": [],
@@ -4591,21 +4727,6 @@
"pattern": "^frm_"
},
"required": true
},
{
"name": "message",
"in": "query",
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"required": false
}
],
"security": [],
@@ -7197,6 +7318,125 @@
"summary": "List MCP resources"
}
},
"/api/credential": {
"get": {
"tags": ["credential"],
"operationId": "credential.list",
"parameters": [],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Credential.Entry"
}
}
},
"required": ["data"],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestErrorEncoded"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedErrorEncoded"
}
}
}
}
},
"description": "List every stored integration credential, including its secret value.",
"summary": "List credentials"
},
"post": {
"tags": ["credential"],
"operationId": "credential.create",
"parameters": [],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"$ref": "#/components/schemas/Credential.Entry"
}
},
"required": ["data"],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestErrorEncoded"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedErrorEncoded"
}
}
}
},
"409": {
"description": "ConflictError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConflictErrorEncoded"
}
}
}
}
},
"description": "Store an integration credential. It becomes the integration's active credential unless activate is false and the integration already has one. Fails with a conflict when the requested ID already exists.",
"summary": "Create credential",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Credential.CreateInput"
}
}
},
"required": true
}
}
},
"/api/credential/{credentialID}": {
"patch": {
"tags": ["credential"],
@@ -11451,6 +11691,124 @@
}
}
},
"/api/vcs/init": {
"post": {
"tags": ["vcs"],
"operationId": "vcs.init",
"parameters": [
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
},
{
"name": "provider",
"in": "query",
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"required": false
}
],
"security": [],
"responses": {
"204": {
"description": "<No Content>"
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/InvalidRequestErrorEncoded"
},
{
"$ref": "#/components/schemas/InvalidRequestErrorEncoded"
}
]
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedErrorEncoded"
}
}
}
},
"409": {
"description": "ConflictError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConflictErrorEncoded"
}
}
}
},
"501": {
"description": "VcsInitNotSupportedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/VcsInitNotSupportedErrorEncoded"
}
}
}
},
"503": {
"description": "ServiceUnavailableError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ServiceUnavailableErrorEncoded"
}
}
}
}
},
"description": "Initialize a repository using the selected VCS provider in a markerless project's directory and refresh its location services. Omitting provider defaults to git; built-in git and hg providers support initialization. An unknown provider returns 400; a registered provider without init returns 501.",
"summary": "Initialize VCS repository"
}
},
"/api/vcs": {
"get": {
"tags": ["vcs"],
@@ -13237,6 +13595,27 @@
"required": ["command"],
"additionalProperties": false
},
"Config.Model.Capabilities": {
"type": "object",
"properties": {
"tools": {
"type": "boolean"
},
"input": {
"type": "array",
"items": {
"type": "string"
}
},
"output": {
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": false
},
"Config.Model.CostEncoded": {
"type": "object",
"properties": {
@@ -13328,7 +13707,7 @@
"type": "object"
},
"capabilities": {
"$ref": "#/components/schemas/Model.Capabilities"
"$ref": "#/components/schemas/Config.Model.Capabilities"
},
"variants": {
"type": "array",
@@ -13433,8 +13812,27 @@
}
]
},
"headerTimeout": {
"anyOf": [
{
"type": "number"
},
{
"type": "boolean",
"enum": [false]
}
]
},
"chunkTimeout": {
"type": "number"
"anyOf": [
{
"type": "number"
},
{
"type": "boolean",
"enum": [false]
}
]
},
"compaction": {
"$ref": "#/components/schemas/Provider.Compaction"
@@ -13631,6 +14029,9 @@
"method": {
"type": "string",
"enum": ["key", "oauth"]
},
"status": {
"$ref": "#/components/schemas/Connection.Status"
}
},
"required": ["type", "id", "label", "method"],
@@ -13645,6 +14046,9 @@
},
"name": {
"type": "string"
},
"status": {
"$ref": "#/components/schemas/Connection.Status"
}
},
"required": ["type", "name"],
@@ -13660,6 +14064,128 @@
}
]
},
"Connection.Status": {
"type": "object",
"properties": {
"status": {
"anyOf": [
{
"type": "string",
"enum": ["needs_auth"]
}
]
},
"message": {
"type": "string"
},
"url": {
"type": "string"
}
},
"required": ["status", "message"],
"additionalProperties": false
},
"Credential.CreateInput": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"integrationID": {
"type": "string"
},
"label": {
"type": "string"
},
"value": {
"$ref": "#/components/schemas/Credential.Value"
},
"activate": {
"type": "boolean"
}
},
"required": ["integrationID", "value"],
"additionalProperties": false
},
"Credential.Entry": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"integrationID": {
"type": "string"
},
"label": {
"type": "string"
},
"active": {
"type": "boolean"
},
"value": {
"$ref": "#/components/schemas/Credential.Value"
}
},
"required": ["id", "integrationID", "label", "active", "value"],
"additionalProperties": false
},
"Credential.Key": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["key"]
},
"key": {
"type": "string"
},
"metadata": {
"type": "object"
},
"configuration": {
"$ref": "#/components/schemas/Form.Answer_1"
}
},
"required": ["type", "key"],
"additionalProperties": false
},
"Credential.OAuth": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["oauth"]
},
"methodID": {
"type": "string"
},
"refresh": {
"type": "string"
},
"access": {
"type": "string"
},
"expires": {
"type": "integer",
"minimum": 0
},
"metadata": {
"type": "object"
}
},
"required": ["type", "methodID", "refresh", "access", "expires"],
"additionalProperties": false
},
"Credential.Value": {
"anyOf": [
{
"$ref": "#/components/schemas/Credential.OAuth"
},
{
"$ref": "#/components/schemas/Credential.Key"
}
]
},
"FileDiff.Info": {
"type": "object",
"properties": {
@@ -13746,6 +14272,12 @@
"$ref": "#/components/schemas/Form.Value"
}
},
"Form.Answer_1": {
"type": "object",
"additionalProperties": {
"$ref": "#/components/schemas/Form.Value"
}
},
"Form.BooleanField": {
"type": "object",
"properties": {
@@ -15709,6 +16241,29 @@
"Money.USDPerMillionTokens": {
"type": "number"
},
"PairingCode": {
"type": "object",
"properties": {
"code": {
"type": "string"
},
"expires_in": {
"type": "integer"
}
},
"required": ["code", "expires_in"],
"additionalProperties": false
},
"PairingSession": {
"type": "object",
"properties": {
"token": {
"type": "string"
}
},
"required": ["token"],
"additionalProperties": false
},
"Permission.Effect": {
"type": "string",
"enum": ["allow", "deny", "ask"]
@@ -16601,8 +17156,27 @@
}
]
},
"headerTimeout": {
"anyOf": [
{
"type": "number"
},
{
"type": "boolean",
"enum": [false]
}
]
},
"chunkTimeout": {
"type": "number"
"anyOf": [
{
"type": "number"
},
{
"type": "boolean",
"enum": [false]
}
]
},
"compaction": {
"$ref": "#/components/schemas/Provider.Compaction"
@@ -18234,6 +18808,16 @@
"type": "integer",
"minimum": 100,
"maximum": 599
},
"response": {
"type": "object",
"properties": {
"body": {
"type": "string"
}
},
"required": ["body"],
"additionalProperties": false
}
},
"required": ["type", "message"],
@@ -18653,6 +19237,9 @@
"exit": {
"type": "number"
},
"signal": {
"type": "string"
},
"metadata": {
"type": "object"
},
@@ -18929,6 +19516,23 @@
"type": "string",
"enum": ["working", "branch", "committed"]
},
"VcsInitNotSupportedErrorEncoded": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": ["VcsInitNotSupportedError"]
},
"providerID": {
"type": "string"
},
"message": {
"type": "string"
}
},
"required": ["_tag", "providerID", "message"],
"additionalProperties": false
},
"WebSearch.Provider": {
"type": "object",
"properties": {
@@ -19053,6 +19657,10 @@
"WorktreeErrorEncoded": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": ["WorktreeError"]
},
"name": {
"type": "string",
"enum": ["WorktreeError"]
@@ -19078,7 +19686,7 @@
"additionalProperties": false
}
},
"required": ["name", "data"],
"required": ["_tag", "name", "data"],
"additionalProperties": false
}
},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/protocol",
"version": "2.0.21",
"version": "2.0.22",
"type": "module",
"license": "MIT",
"repository": {
+29 -2
View File
@@ -3,9 +3,20 @@ import { Location } from "@opencode/schema/location"
import { NonNegativeInt, PositiveInt, optional } from "@opencode/schema/schema"
import { Vcs } from "@opencode/schema/vcs"
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
import { LocationQuery, locationQueryOpenApi } from "./location.js"
import { ServiceUnavailableError } from "../errors.js"
import { ConflictError, InvalidRequestError, ServiceUnavailableError } from "../errors.js"
const InitQuery = Schema.Struct({
...LocationQuery.fields,
provider: Schema.optional(Schema.String),
})
export class VcsInitNotSupportedError extends Schema.TaggedError<VcsInitNotSupportedError>()(
"VcsInitNotSupportedError",
{ providerID: Schema.String, message: Schema.String },
{ httpApiStatus: 501 },
) {}
const BranchesQuery = Schema.Struct({
...LocationQuery.fields,
@@ -21,6 +32,22 @@ const DiffQuery = Schema.Struct({
})
export const VcsGroup = HttpApiGroup.make("server.vcs")
.add(
HttpApiEndpoint.post("vcs.init", "/api/vcs/init", {
query: InitQuery,
success: HttpApiSchema.NoContent,
error: [ConflictError, InvalidRequestError, VcsInitNotSupportedError, ServiceUnavailableError],
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "vcs.init",
summary: "Initialize VCS repository",
description:
"Initialize a repository using the selected VCS provider in a markerless project's directory and refresh its location services. Omitting provider defaults to git; built-in git and hg providers support initialization. An unknown provider returns 400; a registered provider without init returns 501.",
}),
),
)
.add(
HttpApiEndpoint.get("vcs.get", "/api/vcs", {
query: LocationQuery,
+2 -1
View File
@@ -6,7 +6,8 @@ import { ProjectNotFoundError } from "../errors.js"
const root = "/api/worktree"
export class WorktreeError extends Schema.Error<WorktreeError>("WorktreeError")(
export class WorktreeError extends Schema.TaggedError<WorktreeError>()(
"WorktreeError",
{
name: Schema.Literal("WorktreeError"),
data: Schema.Struct({
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/schema",
"version": "2.0.21",
"version": "2.0.22",
"type": "module",
"license": "MIT",
"repository": {
-13
View File
@@ -1,13 +0,0 @@
export * as IdeEvent from "./ide-event.js"
import { Schema } from "effect"
import { Event } from "./event.js"
export const Installed = Event.ephemeral({
type: "ide.installed",
schema: {
ide: Schema.String,
},
})
export const Definitions = Event.inventory(Installed)
-9
View File
@@ -490,15 +490,6 @@ export type Assistant = Omit<Types.DeepMutable<Schema.Schema.Type<typeof Assista
export const Info = Schema.Union([User, Assistant]).annotate({ discriminator: "role", identifier: "SessionV1.Message" })
export type Info = User | Assistant
export const WithParts = Schema.Struct({
info: Info,
parts: Schema.Array(Part),
})
export type WithParts = {
info: Info
parts: Part[]
}
const options = {
durable: {
aggregate: "sessionID",
@@ -16,7 +16,6 @@ import {
} from "../src/index.js"
import { EventManifest } from "../src/event-manifest.js"
import { FileSystemV1 } from "../src/filesystem-v1.js"
import { IdeEvent } from "../src/ide-event.js"
import { McpEvent } from "../src/mcp-event.js"
import { SessionEvent } from "../src/session-event.js"
import { SessionID } from "../src/session-id.js"
@@ -71,8 +70,6 @@ describe("public event manifest", () => {
expect(Reference.Event.Definitions).toEqual([Reference.Event.Updated])
expect(McpEvent.Definitions).toEqual([McpEvent.ToolsChanged, McpEvent.ResourcesChanged, McpEvent.StatusChanged])
expect(EventManifest.Latest.has("mcp.browser.open.failed")).toBe(false)
expect(EventManifest.Latest.has("ide.installed")).toBe(false)
expect(IdeEvent.Definitions).toEqual([IdeEvent.Installed])
expect(EventManifest.Durable.get("session.step.ended.1")).toBe(SessionEvent.Step.Ended)
expect(EventManifest.Durable.has("session.step.ended.2")).toBe(false)
})
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "2.0.21",
"version": "2.0.22",
"name": "@opencode/sdk",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/server",
"version": "2.0.21",
"version": "2.0.22",
"type": "module",
"license": "MIT",
"repository": {
+48 -2
View File
@@ -1,13 +1,59 @@
import { Vcs } from "@opencode/core/vcs"
import { ServiceUnavailableError } from "@opencode/protocol/errors"
import { Project } from "@opencode/core/project"
import { Worktree } from "@opencode/core/worktree"
import { Bus } from "@opencode/core/bus"
import { Plugin } from "@opencode/core/plugin"
import { Location } from "@opencode/core/location"
import { LocationServiceMap } from "@opencode/core/location-services"
import { ConflictError, InvalidRequestError, ServiceUnavailableError } from "@opencode/protocol/errors"
import { VcsInitNotSupportedError } from "@opencode/protocol/groups/vcs"
import { Effect } from "effect"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
import { Api } from "../api"
import { response } from "../location"
export const VcsHandler = HttpApiBuilder.group(Api, "server.vcs", (handlers) =>
Effect.gen(function* () {
const project = yield* Project.Service
const bus = yield* Bus.Service
const locations = yield* LocationServiceMap.Service
return handlers
.handle("vcs.init", (ctx) =>
Effect.gen(function* () {
const location = yield* Location.Service
const directory = location.project.directory
const providerID = ctx.query.provider ?? "git"
yield* Plugin.awaitActivation
const vcs = yield* Vcs.Service
yield* vcs.initialize(providerID).pipe(
Effect.mapError((error) => {
if (error.kind === "missing")
return new InvalidRequestError({ message: "Project directory does not exist", field: "location" })
if (error.kind === "conflict")
return new ConflictError({ message: "Project already has version control", resource: directory })
if (error.kind === "unknown")
return new InvalidRequestError({
message: "Unknown VCS provider",
field: "provider",
})
if (error.kind === "unsupported")
return new VcsInitNotSupportedError({
providerID,
message: "VCS provider does not support initialization",
})
return new ServiceUnavailableError({ service: providerID, message: "VCS initialization failed" })
}),
)
const resolved = yield* project.resolve(directory)
if (!resolved.vcs)
return yield* new ServiceUnavailableError({ service: providerID, message: "VCS initialization failed" })
yield* locations.invalidate(
Location.Ref.make({ directory: location.directory, workspaceID: location.workspaceID }),
)
yield* bus.publish(Worktree.Event.Updated, { projectID: resolved.id })
return HttpApiSchema.NoContent.make()
}),
)
.handle("vcs.get", () =>
response(
Effect.gen(function* () {
+1
View File
@@ -96,6 +96,7 @@ const vcsLayer = Layer.succeed(
Vcs.Service,
Vcs.Service.of({
base: () => Effect.succeed(null),
initialize: () => Effect.fail(new Vcs.InitializeError({ kind: "unsupported" })),
transform: () => Effect.succeed({ dispose: Effect.void }),
reload: () => Effect.void,
info: () => Effect.succeed({ branch: {} }),
+199 -1
View File
@@ -1,6 +1,6 @@
import path from "node:path"
import { $ } from "bun"
import { expect } from "bun:test"
import { describe, expect } from "bun:test"
import { SdkPlugins } from "@opencode/core/plugin/sdk"
import { Effect, Layer, Schedule } from "effect"
import { tmpdir } from "../../core/test/fixture/tmpdir"
@@ -8,6 +8,204 @@ import { it } from "../../core/test/lib/effect"
import { startServer } from "./fixture/server"
import { ServerFetch } from "../src/fetch"
it.live(
"initializes Git in a markerless project and refreshes its location",
() =>
Effect.gen(function* () {
const tmp = yield* Effect.acquireDisposable(Effect.promise(() => tmpdir("opencode-vcs-init-")))
yield* Effect.promise(() => Bun.write(path.join(tmp.path, "hello.txt"), "hello\n"))
const server = yield* startServer(path.join(tmp.path, "config"))
const url = new URL("/api/vcs/init", server.base)
url.searchParams.set("location[directory]", tmp.path)
const before = yield* Effect.promise(() =>
fetch(new URL(`/api/location${url.search}`, server.base), { headers: server.headers }),
)
expect(before.status).toBe(200)
const original = yield* Effect.promise(() => before.json())
const initialized = yield* Effect.promise(() => fetch(url, { method: "POST", headers: server.headers }))
expect(initialized.status).toBe(204)
expect(yield* Effect.promise(() => $`git -C ${tmp.path} rev-parse --is-inside-work-tree`.text())).toBe("true\n")
url.pathname = "/api/location"
const location = yield* Effect.promise(() => fetch(url, { headers: server.headers }))
expect(location.status).toBe(200)
const refreshed = yield* Effect.promise(() => location.json())
expect(refreshed).toMatchObject({ project: { directory: tmp.path } })
expect(refreshed.project.id).not.toBe(original.project.id)
const projects = yield* Effect.promise(() =>
fetch(new URL("/api/project", server.base), { headers: server.headers }),
)
expect(yield* Effect.promise(() => projects.json())).toContainEqual(
expect.objectContaining({ id: refreshed.project.id, vcs: "git" }),
)
url.pathname = "/api/vcs"
const vcs = yield* Effect.tryPromise({
try: async () => {
const response = await fetch(url, { headers: server.headers })
const body: unknown = await response.json()
if (!isRecord(body) || !isRecord(body.data) || body.data.provider !== "git")
throw new Error("Git provider not ready")
return body
},
catch: (cause) => cause,
}).pipe(Effect.retry(Schedule.spaced("10 millis")), Effect.timeout("2 seconds"))
expect(vcs).toMatchObject({ data: { provider: "git" } })
url.pathname = "/api/vcs/diff"
url.searchParams.set("mode", "working")
const diff = yield* Effect.promise(() => fetch(url, { headers: server.headers }))
expect(diff.status).toBe(200)
expect(yield* Effect.promise(() => diff.json())).toMatchObject({
data: expect.arrayContaining([expect.objectContaining({ file: "hello.txt" })]),
})
url.searchParams.delete("mode")
url.pathname = "/api/vcs/init"
const repeated = yield* Effect.promise(() => fetch(url, { method: "POST", headers: server.headers }))
expect(repeated.status).toBe(409)
}),
15_000,
)
it.live(
"does not create a project directory while initializing Git",
() =>
Effect.gen(function* () {
const tmp = yield* Effect.acquireDisposable(Effect.promise(() => tmpdir("opencode-vcs-missing-")))
const server = yield* startServer(path.join(tmp.path, "config"))
const directory = path.join(tmp.path, "absent")
const url = new URL("/api/vcs/init", server.base)
url.searchParams.set("location[directory]", directory)
const response = yield* Effect.promise(() => fetch(url, { method: "POST", headers: server.headers }))
expect(response.status).not.toBe(204)
expect(yield* Effect.promise(() => Bun.file(path.join(directory, ".git", "HEAD")).exists())).toBe(false)
}),
15_000,
)
it.live(
"accepts an explicit Git provider and rejects unknown providers without initializing",
() =>
Effect.gen(function* () {
const tmp = yield* Effect.acquireDisposable(Effect.promise(() => tmpdir("opencode-vcs-provider-")))
const server = yield* startServer(path.join(tmp.path, "config"))
const url = new URL("/api/vcs/init", server.base)
url.searchParams.set("location[directory]", tmp.path)
url.searchParams.set("provider", "unknown")
const unsupported = yield* Effect.promise(() => fetch(url, { method: "POST", headers: server.headers }))
expect(unsupported.status).toBe(400)
expect(yield* Effect.promise(() => unsupported.json())).toMatchObject({
_tag: "InvalidRequestError",
field: "provider",
})
expect(yield* Effect.promise(() => Bun.file(path.join(tmp.path, ".git", "HEAD")).exists())).toBe(false)
expect(yield* Effect.promise(() => Bun.file(path.join(tmp.path, ".hg", "requires")).exists())).toBe(false)
url.searchParams.set("provider", "git")
const initialized = yield* Effect.promise(() => fetch(url, { method: "POST", headers: server.headers }))
expect(initialized.status).toBe(204)
expect(yield* Effect.promise(() => Bun.file(path.join(tmp.path, ".git", "HEAD")).exists())).toBe(true)
}),
15_000,
)
it.live("returns 501 when a registered VCS provider has no initializer", () =>
Effect.gen(function* () {
const tmp = yield* Effect.acquireDisposable(Effect.promise(() => tmpdir("opencode-vcs-no-init-")))
const handler = yield* ServerFetch.make(
{
database: { path: ":memory:" },
config: { directory: tmp.path },
fs: { filewatcher: false },
models: { fetch: false },
},
{
overrides: [
SdkPlugins.node.replace(
Layer.succeed(
SdkPlugins.Service,
SdkPlugins.Service.of({
register: () => Effect.void,
all: () => [
{
id: "read-only-vcs",
revision: "test",
effect: (ctx) =>
ctx.vcs
.transform((editor) => {
editor.add({
id: "read-only",
name: "Read-only VCS",
info: () => Effect.succeed({ branch: {} }),
branches: () => Effect.succeed([]),
status: () => Effect.succeed([]),
diff: () => Effect.succeed([]),
})
})
.pipe(Effect.asVoid),
},
],
}),
),
),
],
},
)
const url = new URL("http://opencode.local/api/vcs/init")
url.searchParams.set("location[directory]", tmp.path)
url.searchParams.set("provider", "read-only")
const response = yield* Effect.promise(() => handler(new Request(url, { method: "POST" })))
expect(response.status).toBe(501)
expect(yield* Effect.promise(() => response.json())).toMatchObject({
_tag: "VcsInitNotSupportedError",
providerID: "read-only",
})
expect(yield* Effect.promise(() => Bun.file(path.join(tmp.path, ".git", "HEAD")).exists())).toBe(false)
expect(yield* Effect.promise(() => Bun.file(path.join(tmp.path, ".hg", "requires")).exists())).toBe(false)
}),
)
const describeHg = Bun.which("hg") ? describe : describe.skip
describeHg("Mercurial initialization", () => {
it.live(
"initializes and serves an untracked-file diff through the Hg provider",
() =>
Effect.gen(function* () {
const tmp = yield* Effect.acquireDisposable(Effect.promise(() => tmpdir("opencode-vcs-hg-init-")))
yield* Effect.promise(() => Bun.write(path.join(tmp.path, "hello.txt"), "hello\n"))
const server = yield* startServer(path.join(tmp.path, "config"))
const url = new URL("/api/vcs/init", server.base)
url.searchParams.set("location[directory]", tmp.path)
url.searchParams.set("provider", "hg")
const initialized = yield* Effect.promise(() => fetch(url, { method: "POST", headers: server.headers }))
expect(initialized.status).toBe(204)
expect(yield* Effect.promise(() => Bun.file(path.join(tmp.path, ".hg", "requires")).exists())).toBe(true)
url.pathname = "/api/vcs"
const info = yield* Effect.tryPromise({
try: async () => {
const response = await fetch(url, { headers: server.headers })
const body: unknown = await response.json()
if (!isRecord(body) || !isRecord(body.data) || body.data.provider !== "hg")
throw new Error("Mercurial provider not ready")
return body
},
catch: (cause) => cause,
}).pipe(Effect.retry(Schedule.spaced("10 millis")), Effect.timeout("2 seconds"))
expect(info).toMatchObject({ data: { provider: "hg" } })
url.pathname = "/api/vcs/diff"
url.searchParams.set("mode", "working")
const diff = yield* Effect.promise(() => fetch(url, { headers: server.headers }))
expect(diff.status).toBe(200)
expect(yield* Effect.promise(() => diff.json())).toMatchObject({
data: expect.arrayContaining([expect.objectContaining({ file: "hello.txt" })]),
})
url.pathname = "/api/vcs/init"
url.searchParams.delete("mode")
url.searchParams.set("provider", "git")
const repeated = yield* Effect.promise(() => fetch(url, { method: "POST", headers: server.headers }))
expect(repeated.status).toBe(409)
}),
15_000,
)
})
it.live(
"serves lazy review bases, committed diffs, and unavailable-base errors",
() =>
+8 -2
View File
@@ -25,10 +25,12 @@ it.live("list reads saved inventory even when its checkout is missing, without b
await fs.rm(directory, { recursive: true })
expect(await api.worktree.list({ projectID: session.projectID })).toEqual([{ directory }])
await expect(api.worktree.create({ projectID: session.projectID })).rejects.toMatchObject({
_tag: "WorktreeError",
name: "WorktreeError",
data: { message: `Worktree directory unavailable: ${directory}` },
})
await expect(api.worktree.refresh({ projectID: session.projectID })).rejects.toMatchObject({
_tag: "WorktreeError",
name: "WorktreeError",
data: { message: `Worktree directory unavailable: ${directory}` },
})
@@ -109,7 +111,7 @@ it.live("remove loads canonical strategies and enforces project ownership", () =
const foreign = await api.session.create({ location: { directory: other } })
await expect(
api.worktree.remove({ projectID: foreign.projectID, directory: linked, force: true }),
).rejects.toMatchObject({ name: "WorktreeError" })
).rejects.toMatchObject({ _tag: "WorktreeError", name: "WorktreeError" })
expect(await fs.stat(linked).then((stat) => stat.isDirectory())).toBe(true)
await api.worktree.remove({ projectID: session.projectID, directory: linked, force: false })
expect(await api.debug.location.list()).toContainEqual({ directory })
@@ -267,7 +269,11 @@ it.live(
body: JSON.stringify({ projectID, directory: custom.directory, force: false }),
})
expect(failure.status).toBe(400)
expect(await failure.json()).toMatchObject({ data: { forceRequired: true } })
expect(await failure.json()).toMatchObject({
_tag: "WorktreeError",
name: "WorktreeError",
data: { forceRequired: true },
})
expect(await Bun.file(path.join(custom.directory, "dirty.txt")).text()).toBe("keep me")
expect(await api.worktree.list({ projectID })).toEqual(rows)
expect(await api.debug.location.list()).toContainEqual({ directory: first })
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/session-ui",
"version": "2.0.21",
"version": "2.0.22",
"private": true,
"type": "module",
"license": "MIT",
+14 -3
View File
@@ -308,15 +308,25 @@ export namespace Timeline {
const previousUserMessage = index > 0
const compaction = entries.some((entry) => entry.type === "notice" && entry.message.type === "compaction")
const lastContent = lastAssistant?.content.at(-1)
const thinking =
(detail ? detail.thinking.placement === "separate" : showReasoning) &&
const working =
isActive &&
status.type === "busy" &&
lastAssistant?.time.completed === undefined &&
!lastAssistant?.error &&
!lastAssistant?.retry &&
!lastAssistant?.retry
const thinking =
working &&
(detail ? detail.thinking.placement === "separate" : showReasoning) &&
lastContent?.type === "reasoning" &&
lastContent.time?.completed === undefined
const thoughtOnly =
working &&
detail?.thinking.placement === "grouped" &&
assistantMessages.every((message) =>
message.content.every(
(content) => content.type === "reasoning" || !isRenderable(content, showReasoning, detail),
),
)
if (previousUserMessage) rows.push(new TimelineRow.TurnGap({ userMessageID: turnID }))
if (userMessage) rows.push(new TimelineRow.UserMessage({ userMessageID: turnID }))
@@ -326,6 +336,7 @@ export namespace Timeline {
// An assistant message can produce several rows because its content parts are
// rendered separately. Notices end a segment so none of those rows cross it.
const appendAssistantSegment = (messages: SessionMessageAssistant[]) => {
if (thoughtOnly) return
const refs = messages.flatMap((message, messageIndex) =>
contentEntries(message)
.filter(
@@ -264,6 +264,54 @@ describe("current session timeline rows", () => {
})
})
test("omits a grouped reasoning-only row while busy until other content or completion", () => {
const thought = { type: "reasoning", text: "Thinking", time: { created: 1, completed: 2 } } as const
const rows = (content: SessionMessageAssistant["content"], busy: boolean) =>
createTimelineProjection({
sessionMessages: storyDocument(content, busy).messages,
status: { type: busy ? "busy" : "idle" },
reasoningMode: "compact",
timelineDetail: timelinePresets[2].value,
}).rows
expect(rows([{ ...thought, time: { created: 1 } }], true).map((row) => row._tag)).toEqual(["UserMessage"])
expect(rows([thought], true).map((row) => row._tag)).toEqual(["UserMessage"])
expect(rows([thought, { type: "text", text: "" }], true).map((row) => row._tag)).toEqual(["UserMessage"])
expect(rows([thought, { type: "text", text: "Answer" }], true).map((row) => row._tag)).toEqual([
"UserMessage",
"AssistantPart",
"AssistantPart",
])
expect(
rows([thought, storyTool("read", "read", "running", { filePath: "package.json" })], true).map((row) => row._tag),
).toEqual(["UserMessage", "AssistantPart"])
expect(rows([thought], false).map((row) => row._tag)).toEqual(["UserMessage", "AssistantPart"])
const failed = storyDocument([thought], true)
const messages = failed.messages.map((message) =>
message.type === "assistant" ? { ...message, error: { type: "provider.error", message: "Failed" } } : message,
)
expect(
createTimelineProjection({
sessionMessages: messages,
status: failed.status,
reasoningMode: "compact",
timelineDetail: timelinePresets[2].value,
}).rows.map((row) => row._tag),
).toEqual(["UserMessage", "AssistantPart", "Error"])
expect(
createTimelineProjection({
sessionMessages: failed.messages.map((message) =>
message.type === "assistant" ? { ...message, error: { type: "Interrupted", message: "Stopped" } } : message,
),
status: failed.status,
reasoningMode: "compact",
timelineDetail: { ...timelinePresets[2].value, notices: { placement: "hidden" } },
}).rows.map((row) => row._tag),
).toEqual(["UserMessage", "AssistantPart"])
})
test("stops thinking on idle, message completion, errors and retries", () => {
const document = storyDocument([{ type: "reasoning", text: "Current thought" }], true)
expect(
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/simulation",
"version": "2.0.21",
"version": "2.0.22",
"type": "module",
"license": "MIT",
"repository": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/stats-app",
"version": "2.0.21",
"version": "2.0.22",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/stats-core",
"version": "2.0.21",
"version": "2.0.22",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/stats-server",
"version": "2.0.21",
"version": "2.0.22",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/theme",
"version": "2.0.21",
"version": "2.0.22",
"type": "module",
"license": "MIT",
"repository": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/tui",
"version": "2.0.21",
"version": "2.0.22",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/ui",
"version": "2.0.21",
"version": "2.0.22",
"type": "module",
"license": "MIT",
"repository": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/util",
"version": "2.0.21",
"version": "2.0.22",
"type": "module",
"license": "MIT",
"repository": {
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "@opencode/web",
"type": "module",
"license": "MIT",
"version": "2.0.21",
"version": "2.0.22",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "2.0.21",
"version": "2.0.22",
"publisher": "sst-dev",
"repository": {
"type": "git",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/update",
"version": "2.0.21",
"version": "2.0.22",
"private": true,
"type": "module",
"scripts": {
+628 -20
View File
@@ -48,6 +48,106 @@
"summary": "Get server info"
}
},
"/api/pair": {
"post": {
"tags": ["server"],
"operationId": "server.pair",
"parameters": [],
"security": [],
"responses": {
"200": {
"description": "PairingCode",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PairingCode"
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestErrorEncoded"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedErrorEncoded"
}
}
}
}
},
"description": "Create a short-lived, single-use code for a /auth/connect/:code pairing link.",
"summary": "Create pairing code"
}
},
"/auth/connect/{code}": {
"get": {
"tags": ["server"],
"operationId": "server.connect",
"parameters": [
{
"name": "code",
"in": "path",
"schema": {
"type": "string"
},
"required": true
}
],
"security": [],
"responses": {
"200": {
"description": "PairingSession",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PairingSession"
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestErrorEncoded"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/UnauthorizedErrorEncoded"
},
{
"$ref": "#/components/schemas/UnauthorizedErrorEncoded"
}
]
}
}
}
}
},
"description": "Redeem a pairing code. Browsers receive a session cookie and a redirect to the web app; requests that accept JSON receive a session token to use as the password.",
"summary": "Redeem pairing code"
}
},
"/api/location": {
"get": {
"tags": ["location"],
@@ -886,9 +986,19 @@
}
}
}
},
"404": {
"description": "SessionNotFoundError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SessionNotFoundErrorEncoded"
}
}
}
}
},
"description": "Create a session at the requested location.",
"description": "Create a session at the requested location. A parentID creates a linked child session at its parent's location.",
"summary": "Create session",
"requestBody": {
"content": {
@@ -907,6 +1017,17 @@
}
]
},
"parentID": {
"anyOf": [
{
"type": "string",
"pattern": "^ses"
},
{
"type": "null"
}
]
},
"title": {
"anyOf": [
{
@@ -4508,6 +4629,21 @@
"pattern": "^frm_"
},
"required": true
},
{
"name": "message",
"in": "query",
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"required": false
}
],
"security": [],
@@ -4591,21 +4727,6 @@
"pattern": "^frm_"
},
"required": true
},
{
"name": "message",
"in": "query",
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"required": false
}
],
"security": [],
@@ -7197,6 +7318,125 @@
"summary": "List MCP resources"
}
},
"/api/credential": {
"get": {
"tags": ["credential"],
"operationId": "credential.list",
"parameters": [],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Credential.Entry"
}
}
},
"required": ["data"],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestErrorEncoded"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedErrorEncoded"
}
}
}
}
},
"description": "List every stored integration credential, including its secret value.",
"summary": "List credentials"
},
"post": {
"tags": ["credential"],
"operationId": "credential.create",
"parameters": [],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"$ref": "#/components/schemas/Credential.Entry"
}
},
"required": ["data"],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestErrorEncoded"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedErrorEncoded"
}
}
}
},
"409": {
"description": "ConflictError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConflictErrorEncoded"
}
}
}
}
},
"description": "Store an integration credential. It becomes the integration's active credential unless activate is false and the integration already has one. Fails with a conflict when the requested ID already exists.",
"summary": "Create credential",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Credential.CreateInput"
}
}
},
"required": true
}
}
},
"/api/credential/{credentialID}": {
"patch": {
"tags": ["credential"],
@@ -11451,6 +11691,124 @@
}
}
},
"/api/vcs/init": {
"post": {
"tags": ["vcs"],
"operationId": "vcs.init",
"parameters": [
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
},
{
"name": "provider",
"in": "query",
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"required": false
}
],
"security": [],
"responses": {
"204": {
"description": "<No Content>"
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/InvalidRequestErrorEncoded"
},
{
"$ref": "#/components/schemas/InvalidRequestErrorEncoded"
}
]
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedErrorEncoded"
}
}
}
},
"409": {
"description": "ConflictError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConflictErrorEncoded"
}
}
}
},
"501": {
"description": "VcsInitNotSupportedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/VcsInitNotSupportedErrorEncoded"
}
}
}
},
"503": {
"description": "ServiceUnavailableError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ServiceUnavailableErrorEncoded"
}
}
}
}
},
"description": "Initialize a repository using the selected VCS provider in a markerless project's directory and refresh its location services. Omitting provider defaults to git; built-in git and hg providers support initialization. An unknown provider returns 400; a registered provider without init returns 501.",
"summary": "Initialize VCS repository"
}
},
"/api/vcs": {
"get": {
"tags": ["vcs"],
@@ -13237,6 +13595,27 @@
"required": ["command"],
"additionalProperties": false
},
"Config.Model.Capabilities": {
"type": "object",
"properties": {
"tools": {
"type": "boolean"
},
"input": {
"type": "array",
"items": {
"type": "string"
}
},
"output": {
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": false
},
"Config.Model.CostEncoded": {
"type": "object",
"properties": {
@@ -13328,7 +13707,7 @@
"type": "object"
},
"capabilities": {
"$ref": "#/components/schemas/Model.Capabilities"
"$ref": "#/components/schemas/Config.Model.Capabilities"
},
"variants": {
"type": "array",
@@ -13433,8 +13812,27 @@
}
]
},
"headerTimeout": {
"anyOf": [
{
"type": "number"
},
{
"type": "boolean",
"enum": [false]
}
]
},
"chunkTimeout": {
"type": "number"
"anyOf": [
{
"type": "number"
},
{
"type": "boolean",
"enum": [false]
}
]
},
"compaction": {
"$ref": "#/components/schemas/Provider.Compaction"
@@ -13631,6 +14029,9 @@
"method": {
"type": "string",
"enum": ["key", "oauth"]
},
"status": {
"$ref": "#/components/schemas/Connection.Status"
}
},
"required": ["type", "id", "label", "method"],
@@ -13645,6 +14046,9 @@
},
"name": {
"type": "string"
},
"status": {
"$ref": "#/components/schemas/Connection.Status"
}
},
"required": ["type", "name"],
@@ -13660,6 +14064,128 @@
}
]
},
"Connection.Status": {
"type": "object",
"properties": {
"status": {
"anyOf": [
{
"type": "string",
"enum": ["needs_auth"]
}
]
},
"message": {
"type": "string"
},
"url": {
"type": "string"
}
},
"required": ["status", "message"],
"additionalProperties": false
},
"Credential.CreateInput": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"integrationID": {
"type": "string"
},
"label": {
"type": "string"
},
"value": {
"$ref": "#/components/schemas/Credential.Value"
},
"activate": {
"type": "boolean"
}
},
"required": ["integrationID", "value"],
"additionalProperties": false
},
"Credential.Entry": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"integrationID": {
"type": "string"
},
"label": {
"type": "string"
},
"active": {
"type": "boolean"
},
"value": {
"$ref": "#/components/schemas/Credential.Value"
}
},
"required": ["id", "integrationID", "label", "active", "value"],
"additionalProperties": false
},
"Credential.Key": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["key"]
},
"key": {
"type": "string"
},
"metadata": {
"type": "object"
},
"configuration": {
"$ref": "#/components/schemas/Form.Answer_1"
}
},
"required": ["type", "key"],
"additionalProperties": false
},
"Credential.OAuth": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["oauth"]
},
"methodID": {
"type": "string"
},
"refresh": {
"type": "string"
},
"access": {
"type": "string"
},
"expires": {
"type": "integer",
"minimum": 0
},
"metadata": {
"type": "object"
}
},
"required": ["type", "methodID", "refresh", "access", "expires"],
"additionalProperties": false
},
"Credential.Value": {
"anyOf": [
{
"$ref": "#/components/schemas/Credential.OAuth"
},
{
"$ref": "#/components/schemas/Credential.Key"
}
]
},
"FileDiff.Info": {
"type": "object",
"properties": {
@@ -13746,6 +14272,12 @@
"$ref": "#/components/schemas/Form.Value"
}
},
"Form.Answer_1": {
"type": "object",
"additionalProperties": {
"$ref": "#/components/schemas/Form.Value"
}
},
"Form.BooleanField": {
"type": "object",
"properties": {
@@ -15709,6 +16241,29 @@
"Money.USDPerMillionTokens": {
"type": "number"
},
"PairingCode": {
"type": "object",
"properties": {
"code": {
"type": "string"
},
"expires_in": {
"type": "integer"
}
},
"required": ["code", "expires_in"],
"additionalProperties": false
},
"PairingSession": {
"type": "object",
"properties": {
"token": {
"type": "string"
}
},
"required": ["token"],
"additionalProperties": false
},
"Permission.Effect": {
"type": "string",
"enum": ["allow", "deny", "ask"]
@@ -16601,8 +17156,27 @@
}
]
},
"headerTimeout": {
"anyOf": [
{
"type": "number"
},
{
"type": "boolean",
"enum": [false]
}
]
},
"chunkTimeout": {
"type": "number"
"anyOf": [
{
"type": "number"
},
{
"type": "boolean",
"enum": [false]
}
]
},
"compaction": {
"$ref": "#/components/schemas/Provider.Compaction"
@@ -18234,6 +18808,16 @@
"type": "integer",
"minimum": 100,
"maximum": 599
},
"response": {
"type": "object",
"properties": {
"body": {
"type": "string"
}
},
"required": ["body"],
"additionalProperties": false
}
},
"required": ["type", "message"],
@@ -18653,6 +19237,9 @@
"exit": {
"type": "number"
},
"signal": {
"type": "string"
},
"metadata": {
"type": "object"
},
@@ -18929,6 +19516,23 @@
"type": "string",
"enum": ["working", "branch", "committed"]
},
"VcsInitNotSupportedErrorEncoded": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": ["VcsInitNotSupportedError"]
},
"providerID": {
"type": "string"
},
"message": {
"type": "string"
}
},
"required": ["_tag", "providerID", "message"],
"additionalProperties": false
},
"WebSearch.Provider": {
"type": "object",
"properties": {
@@ -19053,6 +19657,10 @@
"WorktreeErrorEncoded": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": ["WorktreeError"]
},
"name": {
"type": "string",
"enum": ["WorktreeError"]
@@ -19078,7 +19686,7 @@
"additionalProperties": false
}
},
"required": ["name", "data"],
"required": ["_tag", "name", "data"],
"additionalProperties": false
}
},
+628 -20
View File
@@ -48,6 +48,106 @@
"summary": "Get server info"
}
},
"/api/pair": {
"post": {
"tags": ["server"],
"operationId": "server.pair",
"parameters": [],
"security": [],
"responses": {
"200": {
"description": "PairingCode",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PairingCode"
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestErrorEncoded"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedErrorEncoded"
}
}
}
}
},
"description": "Create a short-lived, single-use code for a /auth/connect/:code pairing link.",
"summary": "Create pairing code"
}
},
"/auth/connect/{code}": {
"get": {
"tags": ["server"],
"operationId": "server.connect",
"parameters": [
{
"name": "code",
"in": "path",
"schema": {
"type": "string"
},
"required": true
}
],
"security": [],
"responses": {
"200": {
"description": "PairingSession",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PairingSession"
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestErrorEncoded"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/UnauthorizedErrorEncoded"
},
{
"$ref": "#/components/schemas/UnauthorizedErrorEncoded"
}
]
}
}
}
}
},
"description": "Redeem a pairing code. Browsers receive a session cookie and a redirect to the web app; requests that accept JSON receive a session token to use as the password.",
"summary": "Redeem pairing code"
}
},
"/api/location": {
"get": {
"tags": ["location"],
@@ -886,9 +986,19 @@
}
}
}
},
"404": {
"description": "SessionNotFoundError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SessionNotFoundErrorEncoded"
}
}
}
}
},
"description": "Create a session at the requested location.",
"description": "Create a session at the requested location. A parentID creates a linked child session at its parent's location.",
"summary": "Create session",
"requestBody": {
"content": {
@@ -907,6 +1017,17 @@
}
]
},
"parentID": {
"anyOf": [
{
"type": "string",
"pattern": "^ses"
},
{
"type": "null"
}
]
},
"title": {
"anyOf": [
{
@@ -4508,6 +4629,21 @@
"pattern": "^frm_"
},
"required": true
},
{
"name": "message",
"in": "query",
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"required": false
}
],
"security": [],
@@ -4591,21 +4727,6 @@
"pattern": "^frm_"
},
"required": true
},
{
"name": "message",
"in": "query",
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"required": false
}
],
"security": [],
@@ -7197,6 +7318,125 @@
"summary": "List MCP resources"
}
},
"/api/credential": {
"get": {
"tags": ["credential"],
"operationId": "credential.list",
"parameters": [],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Credential.Entry"
}
}
},
"required": ["data"],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestErrorEncoded"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedErrorEncoded"
}
}
}
}
},
"description": "List every stored integration credential, including its secret value.",
"summary": "List credentials"
},
"post": {
"tags": ["credential"],
"operationId": "credential.create",
"parameters": [],
"security": [],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"$ref": "#/components/schemas/Credential.Entry"
}
},
"required": ["data"],
"additionalProperties": false
}
}
}
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRequestErrorEncoded"
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedErrorEncoded"
}
}
}
},
"409": {
"description": "ConflictError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConflictErrorEncoded"
}
}
}
}
},
"description": "Store an integration credential. It becomes the integration's active credential unless activate is false and the integration already has one. Fails with a conflict when the requested ID already exists.",
"summary": "Create credential",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Credential.CreateInput"
}
}
},
"required": true
}
}
},
"/api/credential/{credentialID}": {
"patch": {
"tags": ["credential"],
@@ -11451,6 +11691,124 @@
}
}
},
"/api/vcs/init": {
"post": {
"tags": ["vcs"],
"operationId": "vcs.init",
"parameters": [
{
"name": "location",
"in": "query",
"schema": {
"anyOf": [
{
"type": "object",
"properties": {
"directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
{
"type": "null"
}
]
},
"required": false,
"style": "deepObject",
"explode": true
},
{
"name": "provider",
"in": "query",
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"required": false
}
],
"security": [],
"responses": {
"204": {
"description": "<No Content>"
},
"400": {
"description": "InvalidRequestError",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/InvalidRequestErrorEncoded"
},
{
"$ref": "#/components/schemas/InvalidRequestErrorEncoded"
}
]
}
}
}
},
"401": {
"description": "UnauthorizedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedErrorEncoded"
}
}
}
},
"409": {
"description": "ConflictError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConflictErrorEncoded"
}
}
}
},
"501": {
"description": "VcsInitNotSupportedError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/VcsInitNotSupportedErrorEncoded"
}
}
}
},
"503": {
"description": "ServiceUnavailableError",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ServiceUnavailableErrorEncoded"
}
}
}
}
},
"description": "Initialize a repository using the selected VCS provider in a markerless project's directory and refresh its location services. Omitting provider defaults to git; built-in git and hg providers support initialization. An unknown provider returns 400; a registered provider without init returns 501.",
"summary": "Initialize VCS repository"
}
},
"/api/vcs": {
"get": {
"tags": ["vcs"],
@@ -13237,6 +13595,27 @@
"required": ["command"],
"additionalProperties": false
},
"Config.Model.Capabilities": {
"type": "object",
"properties": {
"tools": {
"type": "boolean"
},
"input": {
"type": "array",
"items": {
"type": "string"
}
},
"output": {
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": false
},
"Config.Model.CostEncoded": {
"type": "object",
"properties": {
@@ -13328,7 +13707,7 @@
"type": "object"
},
"capabilities": {
"$ref": "#/components/schemas/Model.Capabilities"
"$ref": "#/components/schemas/Config.Model.Capabilities"
},
"variants": {
"type": "array",
@@ -13433,8 +13812,27 @@
}
]
},
"headerTimeout": {
"anyOf": [
{
"type": "number"
},
{
"type": "boolean",
"enum": [false]
}
]
},
"chunkTimeout": {
"type": "number"
"anyOf": [
{
"type": "number"
},
{
"type": "boolean",
"enum": [false]
}
]
},
"compaction": {
"$ref": "#/components/schemas/Provider.Compaction"
@@ -13631,6 +14029,9 @@
"method": {
"type": "string",
"enum": ["key", "oauth"]
},
"status": {
"$ref": "#/components/schemas/Connection.Status"
}
},
"required": ["type", "id", "label", "method"],
@@ -13645,6 +14046,9 @@
},
"name": {
"type": "string"
},
"status": {
"$ref": "#/components/schemas/Connection.Status"
}
},
"required": ["type", "name"],
@@ -13660,6 +14064,128 @@
}
]
},
"Connection.Status": {
"type": "object",
"properties": {
"status": {
"anyOf": [
{
"type": "string",
"enum": ["needs_auth"]
}
]
},
"message": {
"type": "string"
},
"url": {
"type": "string"
}
},
"required": ["status", "message"],
"additionalProperties": false
},
"Credential.CreateInput": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"integrationID": {
"type": "string"
},
"label": {
"type": "string"
},
"value": {
"$ref": "#/components/schemas/Credential.Value"
},
"activate": {
"type": "boolean"
}
},
"required": ["integrationID", "value"],
"additionalProperties": false
},
"Credential.Entry": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"integrationID": {
"type": "string"
},
"label": {
"type": "string"
},
"active": {
"type": "boolean"
},
"value": {
"$ref": "#/components/schemas/Credential.Value"
}
},
"required": ["id", "integrationID", "label", "active", "value"],
"additionalProperties": false
},
"Credential.Key": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["key"]
},
"key": {
"type": "string"
},
"metadata": {
"type": "object"
},
"configuration": {
"$ref": "#/components/schemas/Form.Answer_1"
}
},
"required": ["type", "key"],
"additionalProperties": false
},
"Credential.OAuth": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["oauth"]
},
"methodID": {
"type": "string"
},
"refresh": {
"type": "string"
},
"access": {
"type": "string"
},
"expires": {
"type": "integer",
"minimum": 0
},
"metadata": {
"type": "object"
}
},
"required": ["type", "methodID", "refresh", "access", "expires"],
"additionalProperties": false
},
"Credential.Value": {
"anyOf": [
{
"$ref": "#/components/schemas/Credential.OAuth"
},
{
"$ref": "#/components/schemas/Credential.Key"
}
]
},
"FileDiff.Info": {
"type": "object",
"properties": {
@@ -13746,6 +14272,12 @@
"$ref": "#/components/schemas/Form.Value"
}
},
"Form.Answer_1": {
"type": "object",
"additionalProperties": {
"$ref": "#/components/schemas/Form.Value"
}
},
"Form.BooleanField": {
"type": "object",
"properties": {
@@ -15709,6 +16241,29 @@
"Money.USDPerMillionTokens": {
"type": "number"
},
"PairingCode": {
"type": "object",
"properties": {
"code": {
"type": "string"
},
"expires_in": {
"type": "integer"
}
},
"required": ["code", "expires_in"],
"additionalProperties": false
},
"PairingSession": {
"type": "object",
"properties": {
"token": {
"type": "string"
}
},
"required": ["token"],
"additionalProperties": false
},
"Permission.Effect": {
"type": "string",
"enum": ["allow", "deny", "ask"]
@@ -16601,8 +17156,27 @@
}
]
},
"headerTimeout": {
"anyOf": [
{
"type": "number"
},
{
"type": "boolean",
"enum": [false]
}
]
},
"chunkTimeout": {
"type": "number"
"anyOf": [
{
"type": "number"
},
{
"type": "boolean",
"enum": [false]
}
]
},
"compaction": {
"$ref": "#/components/schemas/Provider.Compaction"
@@ -18234,6 +18808,16 @@
"type": "integer",
"minimum": 100,
"maximum": 599
},
"response": {
"type": "object",
"properties": {
"body": {
"type": "string"
}
},
"required": ["body"],
"additionalProperties": false
}
},
"required": ["type", "message"],
@@ -18653,6 +19237,9 @@
"exit": {
"type": "number"
},
"signal": {
"type": "string"
},
"metadata": {
"type": "object"
},
@@ -18929,6 +19516,23 @@
"type": "string",
"enum": ["working", "branch", "committed"]
},
"VcsInitNotSupportedErrorEncoded": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": ["VcsInitNotSupportedError"]
},
"providerID": {
"type": "string"
},
"message": {
"type": "string"
}
},
"required": ["_tag", "providerID", "message"],
"additionalProperties": false
},
"WebSearch.Provider": {
"type": "object",
"properties": {
@@ -19053,6 +19657,10 @@
"WorktreeErrorEncoded": {
"type": "object",
"properties": {
"_tag": {
"type": "string",
"enum": ["WorktreeError"]
},
"name": {
"type": "string",
"enum": ["WorktreeError"]
@@ -19078,7 +19686,7 @@
"additionalProperties": false
}
},
"required": ["name", "data"],
"required": ["_tag", "name", "data"],
"additionalProperties": false
}
},
@@ -1086,6 +1086,11 @@ Provider callbacks receive the current location, working-copy root, canonical pr
store. Diff callbacks also receive the selected mode, requested context, and output byte budget. Repository discovery
continues to use OpenCode's built-in Git and Mercurial detectors.
Providers can optionally implement `init(scope)` to create a repository in `scope.worktree`. Initialization selects
the provider by ID (Git by default), not by the detected repository type. Register a provider with `init` even when
the location has no repository yet; OpenCode checks that the directory exists and has no repository before calling
it, then rediscovers the project. New repository formats still need discovery support beyond this callback.
Schemas: [`Vcs.Info`](/api#schema-Vcs.Info), [`Vcs.FileStatus`](/api#schema-Vcs.FileStatus),
[`FileDiff.Info`](/api#schema-FileDiff.Info).
@@ -1098,10 +1103,15 @@ interface VcsEditor {
}
}
interface VcsDomain extends VcsApi<unknown> {
interface VcsDomain extends Omit<VcsApi<unknown>, "init"> {
readonly transform: Transform<VcsEditor>
readonly reload: () => Effect.Effect<void>
}
interface VcsDefinition {
readonly init?: (scope: VcsScope) => Effect.Effect<void, unknown>
// Existing info, branches, status, and diff callbacks are required.
}
```
### Websearch
@@ -1045,6 +1045,11 @@ Provider callbacks receive the current location, working-copy root, canonical pr
store. Diff callbacks also receive the selected mode, requested context, and output byte budget. Repository discovery
continues to use OpenCode's built-in Git and Mercurial detectors.
Providers can optionally implement `init(scope, { signal })` to create a repository in `scope.worktree`. Initialization
selects the provider by ID (Git by default), not by the detected repository type. Register a provider with `init` even
when the location has no repository yet; OpenCode checks that the directory exists and has no repository before calling
it, then rediscovers the project. New repository formats still need discovery support beyond this callback.
#### Reference
Schemas: [`Vcs.Info`](/api#schema-Vcs.Info), [`Vcs.FileStatus`](/api#schema-Vcs.FileStatus),
@@ -1067,6 +1072,11 @@ interface VcsEditor {
set(providerID: string): void
}
}
interface VcsDefinition {
init?(scope: VcsScope, context: { signal: AbortSignal }): Promise<void>
// Existing info, branches, status, and diff callbacks are required.
}
```
### Worktrees