Skip to content

Commit 2eca96c

Browse files
author
Ryan Vogel
committed
Merge remote-tracking branch 'origin/dev' into opencode-remote-voice
2 parents 057208e + 988c989 commit 2eca96c

313 files changed

Lines changed: 8541 additions & 64866 deletions

File tree

Some content is hidden

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

.github/workflows/test.yml

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ concurrency:
1515

1616
permissions:
1717
contents: read
18+
checks: write
1819

1920
jobs:
2021
unit:
@@ -45,14 +46,40 @@ jobs:
4546
git config --global user.email "bot@opencode.ai"
4647
git config --global user.name "opencode"
4748
49+
- name: Cache Turbo
50+
uses: actions/cache@v4
51+
with:
52+
path: node_modules/.cache/turbo
53+
key: turbo-${{ runner.os }}-${{ hashFiles('turbo.json', '**/package.json') }}-${{ github.sha }}
54+
restore-keys: |
55+
turbo-${{ runner.os }}-${{ hashFiles('turbo.json', '**/package.json') }}-
56+
turbo-${{ runner.os }}-
57+
4858
- name: Run unit tests
49-
run: bun turbo test
59+
run: bun turbo test:ci
5060
env:
51-
# Bun 1.3.11 intermittently crashes on Windows during test teardown
52-
# inside the native @parcel/watcher binding. Unit CI does not rely on
53-
# the live watcher backend there, so disable it for that platform.
5461
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }}
5562

63+
- name: Publish unit reports
64+
if: always()
65+
uses: mikepenz/action-junit-report@v6
66+
with:
67+
report_paths: packages/*/.artifacts/unit/junit.xml
68+
check_name: "unit results (${{ matrix.settings.name }})"
69+
detailed_summary: true
70+
include_time_in_summary: true
71+
fail_on_failure: false
72+
73+
- name: Upload unit artifacts
74+
if: always()
75+
uses: actions/upload-artifact@v4
76+
with:
77+
name: unit-${{ matrix.settings.name }}-${{ github.run_attempt }}
78+
include-hidden-files: true
79+
if-no-files-found: ignore
80+
retention-days: 7
81+
path: packages/*/.artifacts/unit/junit.xml
82+
5683
e2e:
5784
name: e2e (${{ matrix.settings.name }})
5885
strategy:

bun.lock

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

infra/console.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,12 @@ const zenLiteCouponFirstMonth50 = new stripe.Coupon("ZenLiteCouponFirstMonth50",
109109
appliesToProducts: [zenLiteProduct.id],
110110
duration: "once",
111111
})
112+
const zenLiteCouponFirstMonth100 = new stripe.Coupon("ZenLiteCouponFirstMonth100", {
113+
name: "First month 100% off",
114+
percentOff: 100,
115+
appliesToProducts: [zenLiteProduct.id],
116+
duration: "once",
117+
})
112118
const zenLitePrice = new stripe.Price("ZenLitePrice", {
113119
product: zenLiteProduct.id,
114120
currency: "usd",
@@ -124,6 +130,7 @@ const ZEN_LITE_PRICE = new sst.Linkable("ZEN_LITE_PRICE", {
124130
price: zenLitePrice.id,
125131
priceInr: 92900,
126132
firstMonth50Coupon: zenLiteCouponFirstMonth50.id,
133+
firstMonth100Coupon: zenLiteCouponFirstMonth100.id,
127134
},
128135
})
129136

@@ -229,6 +236,7 @@ new sst.cloudflare.x.SolidStart("Console", {
229236
SALESFORCE_INSTANCE_URL,
230237
ZEN_BLACK_PRICE,
231238
ZEN_LITE_PRICE,
239+
new sst.Secret("ZEN_LITE_COUPON_FIRST_MONTH_100_INVITEES"),
232240
new sst.Secret("ZEN_LIMITS"),
233241
new sst.Secret("ZEN_SESSION_SECRET"),
234242
...ZEN_MODELS,

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-0jwPCu2Lod433GPQLHN8eEkhfpPviDFfkFJmuvkRdlE=",
4-
"aarch64-linux": "sha256-Qi0IkGkaIBKZsPLTO8kaTbCVL0cEfVOm/Y/6VUVI9TY=",
5-
"aarch64-darwin": "sha256-1eZBBLgYVkjg5RYN/etR1Mb5UjU3VelElBB5ug5hQdc=",
6-
"x86_64-darwin": "sha256-jdXgA+kZb/foFHR40UiPif6rsA2GDVCCVHnJR3jBUGI="
3+
"x86_64-linux": "sha256-85wpU1oCWbthPleNIOj5d5AOuuYZ6rM7gMLZR6YJ2WU=",
4+
"aarch64-linux": "sha256-C3A56SDQGJquCpIRj2JhIzr4A7N4cc9lxtEjl8bXDeM=",
5+
"aarch64-darwin": "sha256-/Ij3qhGRrcLlMfl9uEacDNnGK5URxhctuQFBW4Njrog=",
6+
"x86_64-darwin": "sha256-10sOPuN4eZ75orw4FI8ztCq1+AKS2e8aAfg3Z6Yn56w="
77
}
88
}

package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
1313
"dev:storybook": "bun --cwd packages/storybook storybook",
1414
"typecheck": "bun turbo typecheck",
15+
"postinstall": "bun run --cwd packages/opencode fix-node-pty",
1516
"prepare": "husky",
1617
"random": "echo 'Random script'",
1718
"hello": "echo 'Hello World!'",
@@ -47,7 +48,7 @@
4748
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
4849
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
4950
"effect": "4.0.0-beta.43",
50-
"ai": "6.0.138",
51+
"ai": "6.0.149",
5152
"cross-spawn": "7.0.6",
5253
"hono": "4.10.7",
5354
"hono-openapi": "1.1.2",
@@ -90,6 +91,7 @@
9091
"@opencode-ai/plugin": "workspace:*",
9192
"@opencode-ai/script": "workspace:*",
9293
"@opencode-ai/sdk": "workspace:*",
94+
"heap-snapshot-toolkit": "1.1.3",
9395
"typescript": "catalog:"
9496
},
9597
"repository": {
@@ -103,6 +105,7 @@
103105
},
104106
"trustedDependencies": [
105107
"esbuild",
108+
"node-pty",
106109
"protobufjs",
107110
"tree-sitter",
108111
"tree-sitter-bash",
@@ -116,8 +119,6 @@
116119
},
117120
"patchedDependencies": {
118121
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
119-
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
120-
"@ai-sdk/provider-utils@4.0.21": "patches/@ai-sdk%2Fprovider-utils@4.0.21.patch",
121-
"@ai-sdk/anthropic@3.0.64": "patches/@ai-sdk%2Fanthropic@3.0.64.patch"
122+
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch"
122123
}
123124
}

packages/app/e2e/actions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@ export async function createTestProject(input?: { serverUrl?: string }) {
320320
execSync("git init", { cwd: root, stdio: "ignore" })
321321
await fs.writeFile(path.join(root, ".git", "opencode"), id)
322322
execSync("git config core.fsmonitor false", { cwd: root, stdio: "ignore" })
323+
execSync("git config commit.gpgsign false", { cwd: root, stdio: "ignore" })
323324
execSync("git add -A", { cwd: root, stdio: "ignore" })
324325
execSync('git -c user.name="e2e" -c user.email="e2e@example.com" commit -m "init" --allow-empty', {
325326
cwd: root,
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import type { Locator, Page } from "@playwright/test"
2+
import { test, expect } from "../fixtures"
3+
import { promptAgentSelector, promptModelSelector, promptSelector } from "../selectors"
4+
5+
type Probe = {
6+
agent?: string
7+
model?: { providerID: string; modelID: string; name?: string }
8+
models?: Array<{ providerID: string; modelID: string; name: string }>
9+
agents?: Array<{ name: string }>
10+
}
11+
12+
async function probe(page: Page): Promise<Probe | null> {
13+
return page.evaluate(() => {
14+
const win = window as Window & {
15+
__opencode_e2e?: {
16+
model?: {
17+
current?: Probe
18+
}
19+
}
20+
}
21+
return win.__opencode_e2e?.model?.current ?? null
22+
})
23+
}
24+
25+
async function state(page: Page) {
26+
const value = await probe(page)
27+
if (!value) throw new Error("Failed to resolve model selection probe")
28+
return value
29+
}
30+
31+
async function ready(page: Page) {
32+
const prompt = page.locator(promptSelector)
33+
await prompt.click()
34+
await expect(prompt).toBeFocused()
35+
await prompt.pressSequentially("focus")
36+
return prompt
37+
}
38+
39+
async function body(prompt: Locator) {
40+
return prompt.evaluate((el) => (el as HTMLElement).innerText)
41+
}
42+
43+
test("agent select returns focus to the prompt", async ({ page, gotoSession }) => {
44+
await gotoSession()
45+
46+
const prompt = await ready(page)
47+
48+
const info = await state(page)
49+
const next = info.agents?.map((item) => item.name).find((name) => name !== info.agent)
50+
test.skip(!next, "only one agent available")
51+
if (!next) return
52+
53+
await page.locator(`${promptAgentSelector} [data-slot="select-select-trigger"]`).first().click()
54+
55+
const item = page.locator('[data-slot="select-select-item"]').filter({ hasText: next }).first()
56+
await expect(item).toBeVisible()
57+
await item.click({ force: true })
58+
59+
await expect(page.locator(`${promptAgentSelector} [data-slot="select-select-trigger-value"]`).first()).toHaveText(
60+
next,
61+
)
62+
await expect(prompt).toBeFocused()
63+
await prompt.pressSequentially(" agent")
64+
await expect.poll(() => body(prompt)).toContain("focus agent")
65+
})
66+
67+
test("model select returns focus to the prompt", async ({ page, gotoSession }) => {
68+
await gotoSession()
69+
70+
const prompt = await ready(page)
71+
72+
const info = await state(page)
73+
const key = info.model ? `${info.model.providerID}:${info.model.modelID}` : null
74+
const next = info.models?.find((item) => `${item.providerID}:${item.modelID}` !== key)
75+
test.skip(!next, "only one model available")
76+
if (!next) return
77+
78+
await page.locator(`${promptModelSelector} [data-action="prompt-model"]`).first().click()
79+
80+
const item = page.locator(`[data-slot="list-item"][data-key="${next.providerID}:${next.modelID}"]`).first()
81+
await expect(item).toBeVisible()
82+
await item.click({ force: true })
83+
84+
await expect(page.locator(`${promptModelSelector} [data-action="prompt-model"] span`).first()).toHaveText(next.name)
85+
await expect(prompt).toBeFocused()
86+
await prompt.pressSequentially(" model")
87+
await expect.poll(() => body(prompt)).toContain("focus model")
88+
})

packages/app/e2e/prompt/prompt-shell.spec.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { ToolPart } from "@opencode-ai/sdk/v2/client"
22
import { test, expect } from "../fixtures"
3-
import { withSession } from "../actions"
3+
import { closeDialog, openSettings, withSession } from "../actions"
44
import { promptModelSelector, promptSelector, promptVariantSelector } from "../selectors"
55

66
const isBash = (part: unknown): part is ToolPart => {
@@ -19,12 +19,15 @@ test("shell mode runs a command in the project directory", async ({ page, projec
1919
await withSession(project.sdk, `e2e shell ${Date.now()}`, async (session) => {
2020
project.trackSession(session.id)
2121
await project.gotoSession(session.id)
22-
const button = page.locator('[data-action="prompt-permissions"]').first()
23-
await expect(button).toBeVisible()
24-
if ((await button.getAttribute("aria-pressed")) !== "true") {
25-
await button.click()
26-
await expect(button).toHaveAttribute("aria-pressed", "true")
22+
const dialog = await openSettings(page)
23+
const toggle = dialog.locator('[data-action="settings-auto-accept-permissions"]').first()
24+
const input = toggle.locator('[data-slot="switch-input"]').first()
25+
await expect(toggle).toBeVisible()
26+
if ((await input.getAttribute("aria-checked")) !== "true") {
27+
await toggle.locator('[data-slot="switch-control"]').click()
28+
await expect(input).toHaveAttribute("aria-checked", "true")
2729
}
30+
await closeDialog(page, dialog)
2831
await project.shell(cmd)
2932

3033
await expect

packages/app/e2e/session/session-child-navigation.spec.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { seedSessionTask, withSession } from "../actions"
22
import { test, expect } from "../fixtures"
33
import { inputMatch } from "../prompt/mock"
4-
import { promptSelector } from "../selectors"
54

65
test("task tool child-session link does not trigger stale show errors", async ({ page, llm, project }) => {
76
test.setTimeout(120_000)
@@ -30,15 +29,33 @@ test("task tool child-session link does not trigger stale show errors", async ({
3029

3130
await project.gotoSession(session.id)
3231

33-
const link = page
34-
.locator("a.subagent-link")
32+
const header = page.locator("[data-session-title]")
33+
await expect(header.getByRole("button", { name: "More options" })).toBeVisible({ timeout: 30_000 })
34+
35+
const card = page
36+
.locator('[data-component="task-tool-card"]')
3537
.filter({ hasText: /open child session/i })
3638
.first()
37-
await expect(link).toBeVisible({ timeout: 30_000 })
38-
await link.click()
39+
await expect(card).toBeVisible({ timeout: 30_000 })
40+
await card.click()
3941

4042
await expect(page).toHaveURL(new RegExp(`/session/${child.sessionID}(?:[/?#]|$)`), { timeout: 30_000 })
41-
await expect(page.locator(promptSelector)).toBeVisible({ timeout: 30_000 })
43+
await expect(header.locator('[data-slot="session-title-parent"]')).toHaveText(session.title)
44+
await expect(header.locator('[data-slot="session-title-child"]')).toHaveText(taskInput.description)
45+
await expect(header.locator('[data-slot="session-title-separator"]')).toHaveText("/")
46+
await expect
47+
.poll(
48+
() =>
49+
header.locator('[data-slot="session-title-separator"]').evaluate((el) => ({
50+
left: getComputedStyle(el).paddingLeft,
51+
right: getComputedStyle(el).paddingRight,
52+
})),
53+
{ timeout: 30_000 },
54+
)
55+
.toEqual({ left: "8px", right: "8px" })
56+
await expect(header.getByRole("button", { name: "More options" })).toHaveCount(0)
57+
await expect(page.getByText("Subagent sessions cannot be prompted.")).toBeVisible({ timeout: 30_000 })
58+
await expect(page.getByRole("button", { name: "Back to main session." })).toBeVisible({ timeout: 30_000 })
4259
await expect.poll(() => errs, { timeout: 5_000 }).toEqual([])
4360
})
4461
} finally {

packages/app/e2e/session/session-composer-dock.spec.ts

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
type ComposerProbeState,
66
type ComposerWindow,
77
} from "../../src/testing/session-composer"
8-
import { cleanupSession, clearSessionDockSeed, seedSessionQuestion } from "../actions"
8+
import { cleanupSession, clearSessionDockSeed, closeDialog, openSettings, seedSessionQuestion } from "../actions"
99
import {
1010
permissionDockSelector,
1111
promptSelector,
@@ -65,12 +65,14 @@ async function clearPermissionDock(page: any, label: RegExp) {
6565
}
6666

6767
async function setAutoAccept(page: any, enabled: boolean) {
68-
const button = page.locator('[data-action="prompt-permissions"]').first()
69-
await expect(button).toBeVisible()
70-
const pressed = (await button.getAttribute("aria-pressed")) === "true"
71-
if (pressed === enabled) return
72-
await button.click()
73-
await expect(button).toHaveAttribute("aria-pressed", enabled ? "true" : "false")
68+
const dialog = await openSettings(page)
69+
const toggle = dialog.locator('[data-action="settings-auto-accept-permissions"]').first()
70+
const input = toggle.locator('[data-slot="switch-input"]').first()
71+
await expect(toggle).toBeVisible()
72+
const checked = (await input.getAttribute("aria-checked")) === "true"
73+
if (checked !== enabled) await toggle.locator('[data-slot="switch-control"]').click()
74+
await expect(input).toHaveAttribute("aria-checked", enabled ? "true" : "false")
75+
await closeDialog(page, dialog)
7476
}
7577

7678
async function expectQuestionBlocked(page: any) {
@@ -277,6 +279,7 @@ test("default dock shows prompt input", async ({ page, project }) => {
277279

278280
await expect(page.locator(sessionComposerDockSelector)).toBeVisible()
279281
await expect(page.locator(promptSelector)).toBeVisible()
282+
await expect(page.locator('[data-action="prompt-permissions"]')).toHaveCount(0)
280283
await expect(page.locator(questionDockSelector)).toHaveCount(0)
281284
await expect(page.locator(permissionDockSelector)).toHaveCount(0)
282285

@@ -290,10 +293,6 @@ test("default dock shows prompt input", async ({ page, project }) => {
290293
test("auto-accept toggle works before first submit", async ({ page, project }) => {
291294
await project.open()
292295

293-
const button = page.locator('[data-action="prompt-permissions"]').first()
294-
await expect(button).toBeVisible()
295-
await expect(button).toHaveAttribute("aria-pressed", "false")
296-
297296
await setAutoAccept(page, true)
298297
await setAutoAccept(page, false)
299298
})

0 commit comments

Comments
 (0)