Skip to content

Commit 280c00b

Browse files
committed
Merge branch 'dev' into kit/snapshot-difffull-batching
2 parents 434511e + 650d0db commit 280c00b

File tree

107 files changed

+3681
-61971
lines changed

Some content is hidden

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

107 files changed

+3681
-61971
lines changed

.github/workflows/test.yml

Lines changed: 30 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,39 @@ 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+
if-no-files-found: ignore
79+
retention-days: 7
80+
path: packages/*/.artifacts/unit/junit.xml
81+
5682
e2e:
5783
name: e2e (${{ matrix.settings.name }})
5884
strategy:

.opencode/plugins/tui-smoke.tsx

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -653,31 +653,77 @@ const home = (api: TuiPluginApi, input: Cfg) => ({
653653
const skin = look(ctx.theme.current)
654654
type Prompt = (props: {
655655
workspaceID?: string
656+
visible?: boolean
657+
disabled?: boolean
658+
onSubmit?: () => void
656659
hint?: JSX.Element
660+
right?: JSX.Element
661+
showPlaceholder?: boolean
657662
placeholders?: {
658663
normal?: string[]
659664
shell?: string[]
660665
}
661666
}) => JSX.Element
662-
if (!("Prompt" in api.ui)) return null
663-
const view = api.ui.Prompt
664-
if (typeof view !== "function") return null
665-
const Prompt = view as Prompt
667+
type Slot = (
668+
props: { name: string; mode?: unknown; children?: JSX.Element } & Record<string, unknown>,
669+
) => JSX.Element | null
670+
const ui = api.ui as TuiPluginApi["ui"] & { Prompt: Prompt; Slot: Slot }
671+
const Prompt = ui.Prompt
672+
const Slot = ui.Slot
666673
const normal = [
667674
`[SMOKE] route check for ${input.label}`,
668675
"[SMOKE] confirm home_prompt slot override",
669-
"[SMOKE] verify api.ui.Prompt rendering",
676+
"[SMOKE] verify prompt-right slot passthrough",
670677
]
671678
const shell = ["printf '[SMOKE] home prompt\n'", "git status --short", "bun --version"]
672-
const Hint = (
679+
const hint = (
673680
<box flexShrink={0} flexDirection="row" gap={1}>
674681
<text fg={skin.muted}>
675682
<span style={{ fg: skin.accent }}></span> smoke home prompt
676683
</text>
677684
</box>
678685
)
679686

680-
return <Prompt workspaceID={value.workspace_id} hint={Hint} placeholders={{ normal, shell }} />
687+
return (
688+
<Prompt
689+
workspaceID={value.workspace_id}
690+
hint={hint}
691+
right={
692+
<box flexDirection="row" gap={1}>
693+
<Slot name="home_prompt_right" workspace_id={value.workspace_id} />
694+
<Slot name="smoke_prompt_right" workspace_id={value.workspace_id} label={input.label} />
695+
</box>
696+
}
697+
placeholders={{ normal, shell }}
698+
/>
699+
)
700+
},
701+
home_prompt_right(ctx, value) {
702+
const skin = look(ctx.theme.current)
703+
const id = value.workspace_id?.slice(0, 8) ?? "none"
704+
return (
705+
<text fg={skin.muted}>
706+
<span style={{ fg: skin.accent }}>{input.label}</span> home:{id}
707+
</text>
708+
)
709+
},
710+
session_prompt_right(ctx, value) {
711+
const skin = look(ctx.theme.current)
712+
return (
713+
<text fg={skin.muted}>
714+
<span style={{ fg: skin.accent }}>{input.label}</span> session:{value.session_id.slice(0, 8)}
715+
</text>
716+
)
717+
},
718+
smoke_prompt_right(ctx, value) {
719+
const skin = look(ctx.theme.current)
720+
const id = typeof value.workspace_id === "string" ? value.workspace_id.slice(0, 8) : "none"
721+
const label = typeof value.label === "string" ? value.label : input.label
722+
return (
723+
<text fg={skin.muted}>
724+
<span style={{ fg: skin.accent }}>{label}</span> custom:{id}
725+
</text>
726+
)
681727
},
682728
home_bottom(ctx) {
683729
const skin = look(ctx.theme.current)

bun.lock

Lines changed: 22 additions & 16 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-cMIblNlBgq3fJonaFywzT/VrusmFhrHThOKa5p6vIlw=",
4-
"aarch64-linux": "sha256-ougfUo4oqyyW2fBUK/i8U0//tqEvYnhNhnG2SR0s3B8=",
5-
"aarch64-darwin": "sha256-3n0X0GfEydQgbRTmXnFpnQTKFFE9bOjmHXaJpHji4JE=",
6-
"x86_64-darwin": "sha256-8KEV+Gy+UedqW25ene7O3M0aRPk8LdV8bAKrWCNfeLw="
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="
77
}
88
}

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"catalog": {
2828
"@effect/platform-node": "4.0.0-beta.43",
2929
"@types/bun": "1.3.11",
30+
"@types/cross-spawn": "6.0.6",
3031
"@octokit/rest": "22.0.0",
3132
"@hono/zod-validator": "0.4.2",
3233
"ulid": "3.0.1",
@@ -47,6 +48,7 @@
4748
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
4849
"effect": "4.0.0-beta.43",
4950
"ai": "6.0.138",
51+
"cross-spawn": "7.0.6",
5052
"hono": "4.10.7",
5153
"hono-openapi": "1.1.2",
5254
"fuzzysort": "3.1.0",
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+
})

0 commit comments

Comments
 (0)