Skip to content

Commit 5d47475

Browse files
authored
Merge branch 'dev' into fix/xhigh-mode-openrouter
2 parents eb1cbbe + 597ae57 commit 5d47475

160 files changed

Lines changed: 17153 additions & 9512 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/actions/setup-git-committer/action.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ runs:
2323
with:
2424
app-id: ${{ inputs.opencode-app-id }}
2525
private-key: ${{ inputs.opencode-app-secret }}
26+
owner: ${{ github.repository_owner }}
2627

2728
- name: Configure git user
2829
run: |

.github/workflows/beta.yml

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,15 @@
11
name: beta
22

33
on:
4-
push:
5-
branches: [dev]
6-
pull_request:
7-
types: [opened, synchronize, labeled, unlabeled]
4+
workflow_dispatch:
5+
schedule:
6+
- cron: "0 * * * *"
87

98
jobs:
109
sync:
11-
if: |
12-
github.event_name == 'push' ||
13-
(github.event_name == 'pull_request' &&
14-
contains(github.event.pull_request.labels.*.name, 'contributor'))
1510
runs-on: blacksmith-4vcpu-ubuntu-2404
1611
permissions:
1712
contents: write
18-
pull-requests: write
1913
steps:
2014
- name: Checkout repository
2115
uses: actions/checkout@v4

.github/workflows/publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ jobs:
103103
target: x86_64-pc-windows-msvc
104104
- host: blacksmith-4vcpu-ubuntu-2404
105105
target: x86_64-unknown-linux-gnu
106-
- host: blacksmith-4vcpu-ubuntu-2404-arm
106+
- host: blacksmith-8vcpu-ubuntu-2404-arm
107107
target: aarch64-unknown-linux-gnu
108108
runs-on: ${{ matrix.settings.host }}
109109
steps:

.github/workflows/test.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
name: test
22

33
on:
4+
push:
5+
branches:
6+
- dev
47
pull_request:
58
workflow_dispatch:
69
jobs:

bun.lock

Lines changed: 49 additions & 47 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-gUWzUsk81miIrjg0fZQmsIQG4pZYmEHgzN6BaXI+lfc=",
4-
"aarch64-linux": "sha256-gwEG75ha/ojTO2iAObTmLTtEkXIXJ7BThzfI5CqlJh8=",
5-
"aarch64-darwin": "sha256-20RGG2GkUItCzD67gDdoSLfexttM8abS//FKO9bfjoM=",
6-
"x86_64-darwin": "sha256-i2VawFuR1UbjPVYoybU6aJDJfFo0tcvtl1aM31Y2mTQ="
3+
"x86_64-linux": "sha256-3wRTDLo5FZoUc2Bwm1aAJZ4dNsekX8XoY6TwTmohgYo=",
4+
"aarch64-linux": "sha256-CKiuc6c52UV9cLEtccYEYS4QN0jYzNJv1fHSayqbHKo=",
5+
"aarch64-darwin": "sha256-pWfXomWTDvG8WpWmUCwNXdbSHw6hPlqoT0Q/XuNceMc=",
6+
"x86_64-darwin": "sha256-Dmg4+cUq2r6vZB2ta9tLpNAWqcl11ZCu4ZpieegRFrY="
77
}
88
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"@tailwindcss/vite": "4.1.11",
3939
"diff": "8.0.2",
4040
"dompurify": "3.3.1",
41-
"ai": "5.0.119",
41+
"ai": "5.0.124",
4242
"hono": "4.10.7",
4343
"hono-openapi": "1.1.2",
4444
"fuzzysort": "3.1.0",

packages/app/e2e/actions.ts

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
import { expect, type Locator, type Page } from "@playwright/test"
2+
import fs from "node:fs/promises"
3+
import os from "node:os"
4+
import path from "node:path"
5+
import { execSync } from "node:child_process"
6+
import { modKey, serverUrl } from "./utils"
7+
import {
8+
sessionItemSelector,
9+
dropdownMenuTriggerSelector,
10+
dropdownMenuContentSelector,
11+
titlebarRightSelector,
12+
popoverBodySelector,
13+
listItemSelector,
14+
listItemKeySelector,
15+
listItemKeyStartsWithSelector,
16+
} from "./selectors"
17+
import type { createSdk } from "./utils"
18+
19+
export async function defocus(page: Page) {
20+
await page.mouse.click(5, 5)
21+
}
22+
23+
export async function openPalette(page: Page) {
24+
await defocus(page)
25+
await page.keyboard.press(`${modKey}+P`)
26+
27+
const dialog = page.getByRole("dialog")
28+
await expect(dialog).toBeVisible()
29+
await expect(dialog.getByRole("textbox").first()).toBeVisible()
30+
return dialog
31+
}
32+
33+
export async function closeDialog(page: Page, dialog: Locator) {
34+
await page.keyboard.press("Escape")
35+
const closed = await dialog
36+
.waitFor({ state: "detached", timeout: 1500 })
37+
.then(() => true)
38+
.catch(() => false)
39+
40+
if (closed) return
41+
42+
await page.keyboard.press("Escape")
43+
const closedSecond = await dialog
44+
.waitFor({ state: "detached", timeout: 1500 })
45+
.then(() => true)
46+
.catch(() => false)
47+
48+
if (closedSecond) return
49+
50+
await page.locator('[data-component="dialog-overlay"]').click({ position: { x: 5, y: 5 } })
51+
await expect(dialog).toHaveCount(0)
52+
}
53+
54+
export async function isSidebarClosed(page: Page) {
55+
const main = page.locator("main")
56+
const classes = (await main.getAttribute("class")) ?? ""
57+
return classes.includes("xl:border-l")
58+
}
59+
60+
export async function toggleSidebar(page: Page) {
61+
await defocus(page)
62+
await page.keyboard.press(`${modKey}+B`)
63+
}
64+
65+
export async function openSidebar(page: Page) {
66+
if (!(await isSidebarClosed(page))) return
67+
await toggleSidebar(page)
68+
await expect(page.locator("main")).not.toHaveClass(/xl:border-l/)
69+
}
70+
71+
export async function closeSidebar(page: Page) {
72+
if (await isSidebarClosed(page)) return
73+
await toggleSidebar(page)
74+
await expect(page.locator("main")).toHaveClass(/xl:border-l/)
75+
}
76+
77+
export async function openSettings(page: Page) {
78+
await defocus(page)
79+
80+
const dialog = page.getByRole("dialog")
81+
await page.keyboard.press(`${modKey}+Comma`).catch(() => undefined)
82+
83+
const opened = await dialog
84+
.waitFor({ state: "visible", timeout: 3000 })
85+
.then(() => true)
86+
.catch(() => false)
87+
88+
if (opened) return dialog
89+
90+
await page.getByRole("button", { name: "Settings" }).first().click()
91+
await expect(dialog).toBeVisible()
92+
return dialog
93+
}
94+
95+
export async function seedProjects(page: Page, input: { directory: string; extra?: string[] }) {
96+
await page.addInitScript(
97+
(args: { directory: string; serverUrl: string; extra: string[] }) => {
98+
const key = "opencode.global.dat:server"
99+
const raw = localStorage.getItem(key)
100+
const parsed = (() => {
101+
if (!raw) return undefined
102+
try {
103+
return JSON.parse(raw) as unknown
104+
} catch {
105+
return undefined
106+
}
107+
})()
108+
109+
const store = parsed && typeof parsed === "object" ? (parsed as Record<string, unknown>) : {}
110+
const list = Array.isArray(store.list) ? store.list : []
111+
const lastProject = store.lastProject && typeof store.lastProject === "object" ? store.lastProject : {}
112+
const projects = store.projects && typeof store.projects === "object" ? store.projects : {}
113+
const nextProjects = { ...(projects as Record<string, unknown>) }
114+
115+
const add = (origin: string, directory: string) => {
116+
const current = nextProjects[origin]
117+
const items = Array.isArray(current) ? current : []
118+
const existing = items.filter(
119+
(p): p is { worktree: string; expanded?: boolean } =>
120+
!!p &&
121+
typeof p === "object" &&
122+
"worktree" in p &&
123+
typeof (p as { worktree?: unknown }).worktree === "string",
124+
)
125+
126+
if (existing.some((p) => p.worktree === directory)) return
127+
nextProjects[origin] = [{ worktree: directory, expanded: true }, ...existing]
128+
}
129+
130+
const directories = [args.directory, ...args.extra]
131+
for (const directory of directories) {
132+
add("local", directory)
133+
add(args.serverUrl, directory)
134+
}
135+
136+
localStorage.setItem(
137+
key,
138+
JSON.stringify({
139+
list,
140+
projects: nextProjects,
141+
lastProject,
142+
}),
143+
)
144+
},
145+
{ directory: input.directory, serverUrl, extra: input.extra ?? [] },
146+
)
147+
}
148+
149+
export async function createTestProject() {
150+
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-e2e-project-"))
151+
152+
await fs.writeFile(path.join(root, "README.md"), "# e2e\n")
153+
154+
execSync("git init", { cwd: root, stdio: "ignore" })
155+
execSync("git add -A", { cwd: root, stdio: "ignore" })
156+
execSync('git -c user.name="e2e" -c user.email="[email protected]" commit -m "init" --allow-empty', {
157+
cwd: root,
158+
stdio: "ignore",
159+
})
160+
161+
return root
162+
}
163+
164+
export async function cleanupTestProject(directory: string) {
165+
await fs.rm(directory, { recursive: true, force: true }).catch(() => undefined)
166+
}
167+
168+
export function sessionIDFromUrl(url: string) {
169+
const match = /\/session\/([^/?#]+)/.exec(url)
170+
return match?.[1]
171+
}
172+
173+
export async function hoverSessionItem(page: Page, sessionID: string) {
174+
const sessionEl = page.locator(sessionItemSelector(sessionID)).first()
175+
await expect(sessionEl).toBeVisible()
176+
await sessionEl.hover()
177+
return sessionEl
178+
}
179+
180+
export async function openSessionMoreMenu(page: Page, sessionID: string) {
181+
const sessionEl = await hoverSessionItem(page, sessionID)
182+
183+
const menuTrigger = sessionEl.locator(dropdownMenuTriggerSelector).first()
184+
await expect(menuTrigger).toBeVisible()
185+
await menuTrigger.click()
186+
187+
const menu = page.locator(dropdownMenuContentSelector).first()
188+
await expect(menu).toBeVisible()
189+
return menu
190+
}
191+
192+
export async function clickMenuItem(menu: Locator, itemName: string | RegExp, options?: { force?: boolean }) {
193+
const item = menu.getByRole("menuitem").filter({ hasText: itemName }).first()
194+
await expect(item).toBeVisible()
195+
await item.click({ force: options?.force })
196+
}
197+
198+
export async function confirmDialog(page: Page, buttonName: string | RegExp) {
199+
const dialog = page.getByRole("dialog").first()
200+
await expect(dialog).toBeVisible()
201+
202+
const button = dialog.getByRole("button").filter({ hasText: buttonName }).first()
203+
await expect(button).toBeVisible()
204+
await button.click()
205+
}
206+
207+
export async function openSharePopover(page: Page) {
208+
const rightSection = page.locator(titlebarRightSelector)
209+
const shareButton = rightSection.getByRole("button", { name: "Share" }).first()
210+
await expect(shareButton).toBeVisible()
211+
212+
const popoverBody = page
213+
.locator(popoverBodySelector)
214+
.filter({ has: page.getByRole("button", { name: /^(Publish|Unpublish)$/ }) })
215+
.first()
216+
217+
const opened = await popoverBody
218+
.isVisible()
219+
.then((x) => x)
220+
.catch(() => false)
221+
222+
if (!opened) {
223+
await shareButton.click()
224+
await expect(popoverBody).toBeVisible()
225+
}
226+
return { rightSection, popoverBody }
227+
}
228+
229+
export async function clickPopoverButton(page: Page, buttonName: string | RegExp) {
230+
const button = page.getByRole("button").filter({ hasText: buttonName }).first()
231+
await expect(button).toBeVisible()
232+
await button.click()
233+
}
234+
235+
export async function clickListItem(
236+
container: Locator | Page,
237+
filter: string | RegExp | { key?: string; text?: string | RegExp; keyStartsWith?: string },
238+
): Promise<Locator> {
239+
let item: Locator
240+
241+
if (typeof filter === "string" || filter instanceof RegExp) {
242+
item = container.locator(listItemSelector).filter({ hasText: filter }).first()
243+
} else if (filter.keyStartsWith) {
244+
item = container.locator(listItemKeyStartsWithSelector(filter.keyStartsWith)).first()
245+
} else if (filter.key) {
246+
item = container.locator(listItemKeySelector(filter.key)).first()
247+
} else if (filter.text) {
248+
item = container.locator(listItemSelector).filter({ hasText: filter.text }).first()
249+
} else {
250+
throw new Error("Invalid filter provided to clickListItem")
251+
}
252+
253+
await expect(item).toBeVisible()
254+
await item.click()
255+
return item
256+
}
257+
258+
export async function withSession<T>(
259+
sdk: ReturnType<typeof createSdk>,
260+
title: string,
261+
callback: (session: { id: string; title: string }) => Promise<T>,
262+
): Promise<T> {
263+
const session = await sdk.session.create({ title }).then((r) => r.data)
264+
if (!session?.id) throw new Error("Session create did not return an id")
265+
266+
try {
267+
return await callback(session)
268+
} finally {
269+
await sdk.session.delete({ sessionID: session.id }).catch(() => undefined)
270+
}
271+
}

packages/app/e2e/app/navigation.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { test, expect } from "../fixtures"
2-
import { dirPath, promptSelector } from "../utils"
2+
import { promptSelector } from "../selectors"
3+
import { dirPath } from "../utils"
34

45
test("project route redirects to /session", async ({ page, directory, slug }) => {
56
await page.goto(dirPath(directory))

packages/app/e2e/app/palette.spec.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
import { test, expect } from "../fixtures"
2-
import { modKey } from "../utils"
2+
import { openPalette } from "../actions"
33

44
test("search palette opens and closes", async ({ page, gotoSession }) => {
55
await gotoSession()
66

7-
await page.keyboard.press(`${modKey}+P`)
8-
9-
const dialog = page.getByRole("dialog")
10-
await expect(dialog).toBeVisible()
11-
await expect(dialog.getByRole("textbox").first()).toBeVisible()
7+
const dialog = await openPalette(page)
128

139
await page.keyboard.press("Escape")
1410
await expect(dialog).toHaveCount(0)

0 commit comments

Comments
 (0)