Skip to content

Commit 9c9aefc

Browse files
committed
feat(opencode): add dispatch controls to the task tool
Per-dispatch model override (permission-gated), resume that keeps model and variant, slug task_ids, per-dispatch variant, opaque metadata, an explicit resume consent gate, and timeout with fallback_model.
1 parent 7534d23 commit 9c9aefc

54 files changed

Lines changed: 1559 additions & 420 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

bun.lock

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

nix/hashes.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"nodeModules": {
3-
"x86_64-linux": "sha256-2suqkFAUeT2YgppKqP14s0C+GE3TR3uNKOOD8vWEiAg=",
4-
"aarch64-linux": "sha256-cJ/XYTodYmgs9ZFQhHQbfZPqxjIMmEvz062OsX6/6p4=",
5-
"aarch64-darwin": "sha256-tKyqHeI6r1aHEk4vBS39+eA6wh9KcIwsa2ctcktKkEc=",
6-
"x86_64-darwin": "sha256-1u7+dNjRcqKSvUTCEarVJ9fMrRUyg9y+4seZAm6Fmus="
3+
"x86_64-linux": "sha256-0kcwV34P2C3yKg2eG9W2nW+OedrSBb+1TdpuUeYtauY=",
4+
"aarch64-linux": "sha256-yHVygApQchAB34wrtFR4GU0CkmZOlLsl3wsp15u0xzs=",
5+
"aarch64-darwin": "sha256-DyalcwyK2Wn5R6249keFcNVECbgtjYNjscOFqTi88FI=",
6+
"x86_64-darwin": "sha256-BkGw0GWN9W9q+/g4FYR0MqxUuFP80BPoERO+ypz/arQ="
77
}
88
}

packages/app/e2e/regression/review-state-persistence.spec.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ async function switchSession(page: Page, title: string) {
6565

6666
async function setup(page: Page) {
6767
await mockOpenCodeServer(page, {
68-
protocol: "v1",
68+
protocol: "v2",
6969
directory,
7070
project: {
7171
id: projectID,
@@ -93,18 +93,20 @@ async function setup(page: Page) {
9393
route.fulfill({
9494
status: 200,
9595
contentType: "application/json",
96-
body: JSON.stringify({ branch: "feature", default_branch: "dev" }),
96+
body: JSON.stringify({ location: { directory }, data: { branch: "feature", defaultBranch: "dev" } }),
9797
}),
9898
)
9999
await page.route("**/vcs/diff**", (route) =>
100100
route.fulfill({
101101
status: 200,
102102
contentType: "application/json",
103-
body: JSON.stringify(
104-
new URL(route.request().url()).searchParams.get("mode") === "branch"
105-
? [diff("src/alpha.ts"), diff("src/beta.ts")]
106-
: [diff("src/alpha.ts"), diff("src/gamma.ts")],
107-
),
103+
body: JSON.stringify({
104+
location: { directory },
105+
data:
106+
new URL(route.request().url()).searchParams.get("mode") === "branch"
107+
? [diff("src/alpha.ts"), diff("src/beta.ts")]
108+
: [diff("src/alpha.ts"), diff("src/gamma.ts")],
109+
}),
108110
}),
109111
)
110112
await page.addInitScript(

packages/app/e2e/regression/review-terminal-stacked.spec.ts

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ test("keeps the review tree and terminal sized when both panels are open", async
2525
let detailFailures = 1
2626
await page.setViewportSize({ width: 1400, height: 900 })
2727
await mockOpenCodeServer(page, {
28-
protocol: "v1",
28+
protocol: "v2",
2929
directory,
3030
project: {
3131
id: projectID,
@@ -67,31 +67,33 @@ test("keeps the review tree and terminal sized when both panels are open", async
6767
status: 200,
6868
contentType: "application/json",
6969
body: JSON.stringify({
70-
branch: "review-pane-performance",
71-
default_branch: "dev",
70+
location: { directory },
71+
data: { branch: "review-pane-performance", defaultBranch: "dev" },
7272
}),
7373
}),
7474
)
75-
await page.route("**/vcs/diff**", (route) => {
75+
await page.route("**/api/vcs/diff**", (route) => {
7676
const url = new URL(route.request().url())
77-
const scope = url.searchParams.get("directory")?.replaceAll("\\", "/")
77+
const scope = url.searchParams.get("location[directory]")?.replaceAll("\\", "/")
7878
const detail = scope?.endsWith("/src/branch/d00027")
7979
if (detail && detailFailures-- > 0) return route.fulfill({ status: 500, body: "retry detail" })
8080
return route.fulfill({
8181
status: 200,
8282
contentType: "application/json",
83-
body: JSON.stringify(
84-
url.searchParams.get("mode") === "branch"
85-
? detail
86-
? branchDiffs
87-
.filter((diff) => diff.file.startsWith("src/branch/d00027/"))
88-
.map((diff) => fileDiff(diff.file, diff.additions, true, detailVersion))
89-
: branchDiffs
90-
: Array.from({ length: 7 }, (_, index) => fileDiff(`src/git-${index}.ts`, 1)),
91-
),
83+
body: JSON.stringify({
84+
location: { directory: scope ?? directory, project: { id: projectID, directory } },
85+
data:
86+
url.searchParams.get("mode") === "branch"
87+
? detail
88+
? branchDiffs
89+
.filter((diff) => diff.file.startsWith("src/branch/d00027/"))
90+
.map((diff) => fileDiff(diff.file, diff.additions, true, detailVersion))
91+
: branchDiffs
92+
: Array.from({ length: 7 }, (_, index) => fileDiff(`src/git-${index}.ts`, 1)),
93+
}),
9294
})
9395
})
94-
await page.route("**/pty*", (route) =>
96+
await page.route("**/api/pty*", (route) =>
9597
route.fulfill({
9698
status: 200,
9799
contentType: "application/json",
@@ -109,7 +111,7 @@ test("keeps the review tree and terminal sized when both panels are open", async
109111
}),
110112
}),
111113
)
112-
await page.route("**/pty/pty_review_terminal*", (route) =>
114+
await page.route("**/api/pty/pty_review_terminal*", (route) =>
113115
route.fulfill({
114116
status: 200,
115117
contentType: "application/json",
@@ -127,7 +129,7 @@ test("keeps the review tree and terminal sized when both panels are open", async
127129
}),
128130
}),
129131
)
130-
await page.route("**/pty/pty_review_terminal/connect-token*", (route) =>
132+
await page.route("**/api/pty/pty_review_terminal/connect-token*", (route) =>
131133
route.fulfill({
132134
status: 200,
133135
contentType: "application/json",
@@ -137,7 +139,7 @@ test("keeps the review tree and terminal sized when both panels are open", async
137139
}),
138140
}),
139141
)
140-
await page.routeWebSocket("**/pty/pty_review_terminal/connect", () => undefined)
142+
await page.routeWebSocket("**/api/pty/pty_review_terminal/connect", () => undefined)
141143
await page.addInitScript(() => {
142144
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
143145
localStorage.setItem(
@@ -175,8 +177,8 @@ test("keeps the review tree and terminal sized when both panels are open", async
175177
const lazyDiff = page.waitForRequest((request) => {
176178
const url = new URL(request.url())
177179
return (
178-
url.pathname === "/vcs/diff" &&
179-
url.searchParams.get("directory")?.replaceAll("\\", "/").endsWith("/src/branch/d00027") === true
180+
url.pathname === "/api/vcs/diff" &&
181+
url.searchParams.get("location[directory]")?.replaceAll("\\", "/").endsWith("/src/branch/d00027") === true
180182
)
181183
})
182184
await lastFile.click()
@@ -190,8 +192,8 @@ test("keeps the review tree and terminal sized when both panels are open", async
190192
const refreshedDiff = page.waitForRequest((request) => {
191193
const url = new URL(request.url())
192194
return (
193-
url.pathname === "/vcs/diff" &&
194-
url.searchParams.get("directory")?.replaceAll("\\", "/").endsWith("/src/branch/d00027") === true
195+
url.pathname === "/api/vcs/diff" &&
196+
url.searchParams.get("location[directory]")?.replaceAll("\\", "/").endsWith("/src/branch/d00027") === true
195197
)
196198
})
197199
sessionStatus[sessionID] = { type: "idle" }

packages/app/e2e/regression/terminal-tab-switch.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ test("keeps the terminal session alive when switching session tabs in a workspac
3232
const connection = new URL(connections[0]!)
3333
expect(connection.pathname).toBe(`/api/pty/${ptyID}/connect`)
3434
expect(connection.searchParams.get("location[directory]")).toBe(directory)
35-
expect(connection.searchParams.get("ticket")).toBeNull()
35+
expect(connection.searchParams.get("ticket")).toBe("e2e-ticket")
3636
await writeProbe(page)
3737

3838
await switchTab(page, titleB)

packages/app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
"@dnd-kit/helpers": "0.5.0",
5454
"@dnd-kit/solid": "0.5.0",
5555
"@kobalte/core": "catalog:",
56-
"@opencode-ai/client": "file:vendor/opencode-ai-client-1.17.13-v2.tgz",
56+
"@opencode-ai/client": "file:vendor/opencode-ai-client-1.17.13.tgz",
5757
"@opencode-ai/core": "workspace:*",
5858
"@opencode-ai/schema": "workspace:*",
5959
"@opencode-ai/sdk": "workspace:*",

packages/app/src/components/dialog-connect-provider.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ function ProviderPicker(props: {
159159
const settings = useSettings()
160160
if (settings.general.newLayoutDesigns())
161161
return <ProviderPickerV2 directory={props.directory} onSelect={props.onSelect} onPrepare={props.onPrepare} />
162-
const providers = useProviders(() => props.directory?.())
162+
const providers = useProviders(props.directory)
163163
const language = useLanguage()
164164
const popularGroup = () => language.t("dialog.provider.group.popular")
165165
const otherGroup = () => language.t("dialog.provider.group.other")
@@ -231,7 +231,7 @@ function ProviderPickerV2(props: {
231231
onSelect: (provider: string) => void
232232
onPrepare?: () => void
233233
}) {
234-
const providers = useProviders(() => props.directory?.())
234+
const providers = useProviders(props.directory)
235235
const language = useLanguage()
236236
const [store, setStore] = createStore({
237237
filter: "",
@@ -391,7 +391,7 @@ function ProviderConnection(props: {
391391
const language = useLanguage()
392392
const settings = useSettings()
393393
const newLayout = settings.general.newLayoutDesigns
394-
const providers = useProviders(() => props.directory?.())
394+
const providers = useProviders(props.directory)
395395
const directory = () => props.directory?.() ?? decode64(params.dir)
396396
const location = () => {
397397
const value = directory()

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

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { createEffect, createMemo, createResource, createSignal, For, onCleanup,
88
import { useGlobal } from "@/context/global"
99
import { useLanguage } from "@/context/language"
1010
import { ServerConnection } from "@/context/server"
11-
import type { Path } from "@opencode-ai/sdk/v2/client"
1211
import {
1312
absoluteTreePath,
1413
activeTreeNavigation,
@@ -69,13 +68,7 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) {
6968
const missingBase = createMemo(() => !(sync.data.path.home || sync.data.path.directory))
7069
const [fallbackPath] = createResource(
7170
() => (missingBase() ? true : undefined),
72-
async (): Promise<Path | undefined> => {
73-
if ((await sdk.protocol) !== "v1") return
74-
return sdk.client.path
75-
.get()
76-
.then((result) => result.data)
77-
.catch(() => undefined)
78-
},
71+
() => sdk.api.path.get().catch(() => undefined),
7972
{ initialValue: undefined },
8073
)
8174
const home = createMemo(() => sync.data.path.home || fallbackPath()?.home || "")

packages/app/src/components/dialog-select-directory.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { useLanguage } from "@/context/language"
99
import { ServerConnection } from "@/context/server"
1010
import { useGlobal } from "@/context/global"
1111
import { cleanPickerInput, createDirectorySearch, displayPickerPath } from "./directory-picker-domain"
12-
import type { Path } from "@opencode-ai/sdk/v2/client"
1312

1413
interface DialogSelectDirectoryProps {
1514
title?: string
@@ -60,12 +59,8 @@ export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
6059
const missingBase = createMemo(() => !(sync.data.path.home || sync.data.path.directory))
6160
const [fallbackPath] = createResource(
6261
() => (missingBase() ? true : undefined),
63-
async (): Promise<Path | undefined> => {
64-
if ((await sdk.protocol) !== "v1") return
65-
return sdk.client.path
66-
.get()
67-
.then((result) => result.data)
68-
.catch(() => undefined)
62+
async () => {
63+
return sdk.api.path.get().catch(() => undefined)
6964
},
7065
{ initialValue: undefined },
7166
)

packages/app/src/components/edit-project.ts

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -71,23 +71,12 @@ export function createEditProjectModel(props: { project: LocalProject; server: S
7171
const start = store.startup.trim()
7272

7373
if (props.project.id && props.project.id !== "global") {
74-
if ((await serverCtx().sdk.protocol) !== "v1") return
75-
const project = await serverCtx()
76-
.sdk.client.project.update({
77-
projectID: props.project.id,
78-
directory: props.project.worktree,
79-
name,
80-
icon: { color: store.color || "", override: store.iconOverride || "" },
81-
commands: { start },
82-
})
83-
.then((result) => result.data)
84-
if (!project) return
85-
// const project = await serverCtx().sdk.api.project.update({
86-
// projectID: props.project.id,
87-
// name,
88-
// icon: { color: store.color || "", override: store.iconOverride || "" },
89-
// commands: { start },
90-
// })
74+
const project = await serverCtx().sdk.api.project.update({
75+
projectID: props.project.id,
76+
name,
77+
icon: { color: store.color || "", override: store.iconOverride || "" },
78+
commands: { start },
79+
})
9180
serverCtx().sync.set("project", (items) =>
9281
items.map((item) => (item.id === project.id ? normalizeProjectInfo(project) : item)),
9382
)

0 commit comments

Comments
 (0)