Skip to content

Commit 504d65c

Browse files
authored
Merge branch 'anomalyco:dev' into dev
2 parents 45d5c02 + 157920b commit 504d65c

13 files changed

Lines changed: 268 additions & 72 deletions

File tree

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,15 @@ async function clearPermissionDock(page: any, label: RegExp) {
5050
}
5151
}
5252

53+
async function setAutoAccept(page: any, enabled: boolean) {
54+
const button = page.locator('[data-action="prompt-permissions"]').first()
55+
await expect(button).toBeVisible()
56+
const pressed = (await button.getAttribute("aria-pressed")) === "true"
57+
if (pressed === enabled) return
58+
await button.click()
59+
await expect(button).toHaveAttribute("aria-pressed", enabled ? "true" : "false")
60+
}
61+
5362
async function withMockPermission<T>(
5463
page: any,
5564
request: {
@@ -168,6 +177,7 @@ test("blocked question flow unblocks after submit", async ({ page, sdk, gotoSess
168177
test("blocked permission flow supports allow once", async ({ page, sdk, gotoSession }) => {
169178
await withDockSession(sdk, "e2e composer dock permission once", async (session) => {
170179
await gotoSession(session.id)
180+
await setAutoAccept(page, false)
171181
await withMockPermission(
172182
page,
173183
{
@@ -195,6 +205,7 @@ test("blocked permission flow supports allow once", async ({ page, sdk, gotoSess
195205
test("blocked permission flow supports reject", async ({ page, sdk, gotoSession }) => {
196206
await withDockSession(sdk, "e2e composer dock permission reject", async (session) => {
197207
await gotoSession(session.id)
208+
await setAutoAccept(page, false)
198209
await withMockPermission(
199210
page,
200211
{
@@ -221,6 +232,7 @@ test("blocked permission flow supports reject", async ({ page, sdk, gotoSession
221232
test("blocked permission flow supports allow always", async ({ page, sdk, gotoSession }) => {
222233
await withDockSession(sdk, "e2e composer dock permission always", async (session) => {
223234
await gotoSession(session.id)
235+
await setAutoAccept(page, false)
224236
await withMockPermission(
225237
page,
226238
{
@@ -300,6 +312,7 @@ test("child session permission request blocks parent dock and supports allow onc
300312
}) => {
301313
await withDockSession(sdk, "e2e composer dock child permission parent", async (session) => {
302314
await gotoSession(session.id)
315+
await setAutoAccept(page, false)
303316

304317
const child = await sdk.session
305318
.create({

packages/app/src/components/prompt-input.tsx

Lines changed: 37 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1511,43 +1511,45 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
15111511
</div>
15121512
</div>
15131513

1514-
<Show when={store.mode === "normal" && permission.permissionsEnabled() && params.id}>
1515-
<div class="pointer-events-none absolute bottom-2 left-2">
1516-
<div class="pointer-events-auto">
1517-
<TooltipKeybind
1518-
placement="top"
1519-
gutter={8}
1520-
title={language.t(
1521-
accepting() ? "command.permissions.autoaccept.disable" : "command.permissions.autoaccept.enable",
1522-
)}
1523-
keybind={command.keybind("permissions.autoaccept")}
1514+
<div class="pointer-events-none absolute bottom-2 left-2">
1515+
<div class="pointer-events-auto">
1516+
<TooltipKeybind
1517+
placement="top"
1518+
gutter={8}
1519+
title={language.t(
1520+
accepting() ? "command.permissions.autoaccept.disable" : "command.permissions.autoaccept.enable",
1521+
)}
1522+
keybind={command.keybind("permissions.autoaccept")}
1523+
>
1524+
<Button
1525+
data-action="prompt-permissions"
1526+
variant="ghost"
1527+
disabled={!params.id}
1528+
onClick={() => {
1529+
if (!params.id) return
1530+
permission.toggleAutoAccept(params.id, sdk.directory)
1531+
}}
1532+
classList={{
1533+
"size-6 flex items-center justify-center": true,
1534+
"text-text-base": !accepting(),
1535+
"hover:bg-surface-success-base": accepting(),
1536+
}}
1537+
aria-label={
1538+
accepting()
1539+
? language.t("command.permissions.autoaccept.disable")
1540+
: language.t("command.permissions.autoaccept.enable")
1541+
}
1542+
aria-pressed={accepting()}
15241543
>
1525-
<Button
1526-
data-action="prompt-permissions"
1527-
variant="ghost"
1528-
onClick={() => permission.toggleAutoAccept(params.id!, sdk.directory)}
1529-
classList={{
1530-
"_hidden group-hover/prompt-input:flex size-6 items-center justify-center": true,
1531-
"text-text-base": !accepting(),
1532-
"hover:bg-surface-success-base": accepting(),
1533-
}}
1534-
aria-label={
1535-
accepting()
1536-
? language.t("command.permissions.autoaccept.disable")
1537-
: language.t("command.permissions.autoaccept.enable")
1538-
}
1539-
aria-pressed={accepting()}
1540-
>
1541-
<Icon
1542-
name="chevron-double-right"
1543-
size="small"
1544-
classList={{ "text-icon-success-base": accepting() }}
1545-
/>
1546-
</Button>
1547-
</TooltipKeybind>
1548-
</div>
1544+
<Icon
1545+
name="chevron-double-right"
1546+
size="small"
1547+
classList={{ "text-icon-success-base": accepting() }}
1548+
/>
1549+
</Button>
1550+
</TooltipKeybind>
15491551
</div>
1550-
</Show>
1552+
</div>
15511553
</div>
15521554
</DockShellForm>
15531555
<Show when={store.mode === "normal" || store.mode === "shell"}>

packages/app/src/context/permission-auto-respond.test.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,33 @@ describe("autoRespondsPermission", () => {
3131
expect(autoRespondsPermission({ root: true }, sessions, permission("child"), "/tmp/project")).toBe(true)
3232
})
3333

34-
test("ignores auto-accept from unrelated sessions", () => {
34+
test("defaults to auto-accept when no lineage override exists", () => {
3535
const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" }), session({ id: "other" })]
3636
const autoAccept = {
3737
other: true,
3838
}
3939

40-
expect(autoRespondsPermission(autoAccept, sessions, permission("child"), "/tmp/project")).toBe(false)
40+
expect(autoRespondsPermission(autoAccept, sessions, permission("child"), "/tmp/project")).toBe(true)
41+
})
42+
43+
test("inherits a parent session's false override", () => {
44+
const directory = "/tmp/project"
45+
const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })]
46+
const autoAccept = {
47+
[`${base64Encode(directory)}/root`]: false,
48+
}
49+
50+
expect(autoRespondsPermission(autoAccept, sessions, permission("child"), directory)).toBe(false)
51+
})
52+
53+
test("prefers a child override over parent override", () => {
54+
const directory = "/tmp/project"
55+
const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })]
56+
const autoAccept = {
57+
[`${base64Encode(directory)}/root`]: false,
58+
[`${base64Encode(directory)}/child`]: true,
59+
}
60+
61+
expect(autoRespondsPermission(autoAccept, sessions, permission("child"), directory)).toBe(true)
4162
})
4263
})

packages/app/src/context/permission-auto-respond.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ export function acceptKey(sessionID: string, directory?: string) {
55
return `${base64Encode(directory)}/${sessionID}`
66
}
77

8+
function accepted(autoAccept: Record<string, boolean>, sessionID: string, directory?: string) {
9+
const key = acceptKey(sessionID, directory)
10+
return autoAccept[key] ?? autoAccept[sessionID]
11+
}
12+
813
function sessionLineage(session: { id: string; parentID?: string }[], sessionID: string) {
914
const parent = session.reduce((acc, item) => {
1015
if (item.parentID) acc.set(item.id, item.parentID)
@@ -29,8 +34,8 @@ export function autoRespondsPermission(
2934
permission: { sessionID: string },
3035
directory?: string,
3136
) {
32-
return sessionLineage(session, permission.sessionID).some((id) => {
33-
const key = acceptKey(id, directory)
34-
return autoAccept[key] ?? autoAccept[id] ?? false
35-
})
37+
const value = sessionLineage(session, permission.sessionID)
38+
.map((id) => accepted(autoAccept, id, directory))
39+
.find((item): item is boolean => item !== undefined)
40+
return value ?? true
3641
}

packages/app/src/context/permission.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,8 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
115115
}
116116

117117
function isAutoAccepting(sessionID: string, directory?: string) {
118-
const key = acceptKey(sessionID, directory)
119-
return store.autoAccept[key] ?? store.autoAccept[sessionID] ?? false
118+
const session = directory ? globalSync.child(directory, { bootstrap: false })[0].session : []
119+
return autoRespondsPermission(store.autoAccept, session, { sessionID }, directory)
120120
}
121121

122122
function shouldAutoRespond(permission: PermissionRequest, directory?: string) {
@@ -168,10 +168,11 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
168168

169169
function disable(sessionID: string, directory?: string) {
170170
bumpEnableVersion(sessionID, directory)
171-
const key = directory ? acceptKey(sessionID, directory) : undefined
171+
const key = directory ? acceptKey(sessionID, directory) : sessionID
172172
setStore(
173173
produce((draft) => {
174-
if (key) delete draft.autoAccept[key]
174+
draft.autoAccept[key] = false
175+
if (!directory) return
175176
delete draft.autoAccept[sessionID]
176177
}),
177178
)
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
#!/usr/bin/env bun
2+
3+
import { Buffer } from "node:buffer"
4+
import { $ } from "bun"
5+
6+
const { values } = parseArgs({
7+
args: Bun.argv.slice(2),
8+
options: {
9+
"dry-run": { type: "boolean", default: false },
10+
},
11+
})
12+
13+
const dryRun = values["dry-run"]
14+
15+
import { parseArgs } from "node:util"
16+
17+
const repo = process.env.GH_REPO
18+
if (!repo) throw new Error("GH_REPO is required")
19+
20+
const releaseId = process.env.OPENCODE_RELEASE
21+
if (!releaseId) throw new Error("OPENCODE_RELEASE is required")
22+
23+
const token = process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN
24+
if (!token) throw new Error("GH_TOKEN or GITHUB_TOKEN is required")
25+
26+
const apiHeaders = {
27+
Authorization: `token ${token}`,
28+
Accept: "application/vnd.github+json",
29+
}
30+
31+
const releaseRes = await fetch(`https://api.github.com/repos/${repo}/releases/${releaseId}`, {
32+
headers: apiHeaders,
33+
})
34+
35+
if (!releaseRes.ok) {
36+
throw new Error(`Failed to fetch release: ${releaseRes.status} ${releaseRes.statusText}`)
37+
}
38+
39+
type Asset = {
40+
name: string
41+
url: string
42+
browser_download_url: string
43+
}
44+
45+
type Release = {
46+
tag_name?: string
47+
assets?: Asset[]
48+
}
49+
50+
const release = (await releaseRes.json()) as Release
51+
const assets = release.assets ?? []
52+
const assetByName = new Map(assets.map((asset) => [asset.name, asset]))
53+
54+
const latestAsset = assetByName.get("latest.json")
55+
if (!latestAsset) throw new Error("latest.json asset not found")
56+
57+
const latestRes = await fetch(latestAsset.url, {
58+
headers: {
59+
Authorization: `token ${token}`,
60+
Accept: "application/octet-stream",
61+
},
62+
})
63+
64+
if (!latestRes.ok) {
65+
throw new Error(`Failed to fetch latest.json: ${latestRes.status} ${latestRes.statusText}`)
66+
}
67+
68+
const latestText = new TextDecoder().decode(await latestRes.arrayBuffer())
69+
const latest = JSON.parse(latestText)
70+
const base = { ...latest }
71+
delete base.platforms
72+
73+
const fetchSignature = async (asset: Asset) => {
74+
const res = await fetch(asset.url, {
75+
headers: {
76+
Authorization: `token ${token}`,
77+
Accept: "application/octet-stream",
78+
},
79+
})
80+
81+
if (!res.ok) {
82+
throw new Error(`Failed to fetch signature: ${res.status} ${res.statusText}`)
83+
}
84+
85+
return Buffer.from(await res.arrayBuffer()).toString()
86+
}
87+
88+
const entries: Record<string, { url: string; signature: string }> = {}
89+
const add = (key: string, asset: Asset, signature: string) => {
90+
if (entries[key]) return
91+
entries[key] = {
92+
url: asset.browser_download_url,
93+
signature,
94+
}
95+
}
96+
97+
const targets = [
98+
{ key: "linux-x86_64-deb", asset: "opencode-desktop-linux-amd64.deb" },
99+
{ key: "linux-x86_64-rpm", asset: "opencode-desktop-linux-x86_64.rpm" },
100+
{ key: "linux-aarch64-deb", asset: "opencode-desktop-linux-arm64.deb" },
101+
{ key: "linux-aarch64-rpm", asset: "opencode-desktop-linux-aarch64.rpm" },
102+
{ key: "windows-x86_64-nsis", asset: "opencode-desktop-windows-x64.exe" },
103+
{ key: "darwin-x86_64-app", asset: "opencode-desktop-darwin-x64.app.tar.gz" },
104+
{
105+
key: "darwin-aarch64-app",
106+
asset: "opencode-desktop-darwin-aarch64.app.tar.gz",
107+
},
108+
]
109+
110+
for (const target of targets) {
111+
const asset = assetByName.get(target.asset)
112+
if (!asset) continue
113+
114+
const sig = assetByName.get(`${target.asset}.sig`)
115+
if (!sig) continue
116+
117+
const signature = await fetchSignature(sig)
118+
add(target.key, asset, signature)
119+
}
120+
121+
const alias = (key: string, source: string) => {
122+
if (entries[key]) return
123+
const entry = entries[source]
124+
if (!entry) return
125+
entries[key] = entry
126+
}
127+
128+
alias("linux-x86_64", "linux-x86_64-deb")
129+
alias("linux-aarch64", "linux-aarch64-deb")
130+
alias("windows-x86_64", "windows-x86_64-nsis")
131+
alias("darwin-x86_64", "darwin-x86_64-app")
132+
alias("darwin-aarch64", "darwin-aarch64-app")
133+
134+
const platforms = Object.fromEntries(
135+
Object.keys(entries)
136+
.sort()
137+
.map((key) => [key, entries[key]]),
138+
)
139+
const output = {
140+
...base,
141+
platforms,
142+
}
143+
144+
const dir = process.env.RUNNER_TEMP ?? "/tmp"
145+
const file = `${dir}/latest.json`
146+
await Bun.write(file, JSON.stringify(output, null, 2))
147+
148+
const tag = release.tag_name
149+
if (!tag) throw new Error("Release tag not found")
150+
151+
if (dryRun) {
152+
console.log(`dry-run: wrote latest.json for ${tag} to ${file}`)
153+
process.exit(0)
154+
}
155+
await $`gh release upload ${tag} ${file} --clobber --repo ${repo}`
156+
157+
console.log(`finalized latest.json for ${tag}`)
Lines changed: 3 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)