Skip to content

Commit 863d55d

Browse files
authored
Merge branch 'v2' into mcp-session-isolation
2 parents 108ded3 + 369aad3 commit 863d55d

92 files changed

Lines changed: 8395 additions & 218 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.

‎bun.lock‎

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

‎nix/hashes.json‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"nodeModules": {
3-
"x86_64-linux": "sha256-mrsVbX15SgLsQWBknYKmHSBpWhE6BnyUrYzxIWL35js=",
4-
"aarch64-linux": "sha256-uskMkbG9jeGoal1mHV4rpkkvBb3TDBvQcDdQT/bP9y4=",
5-
"aarch64-darwin": "sha256-/8nVBK6lBRO34Id/jKOUeEaEHHYJhh8cFESWfhzzln0="
3+
"x86_64-linux": "sha256-vzF5I8/QHutYM6j0R73ghqBCRntL6/fegzivyDTO9Ro=",
4+
"aarch64-linux": "sha256-foy1hI+cewpLJvNqTZCCNTqV4tnQSDK55ZRs87MvnI8=",
5+
"aarch64-darwin": "sha256-wl6+Omz25VxLRMsh0rvFZCqBErk0o3mh9cjuUYsYAP4="
66
}
77
}

‎packages/ai/src/protocols/open-responses.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,7 @@ const lowerReasoning = (part: ReasoningPart, providerMetadataKey: string): OpenR
507507
typeof metadata.reasoningEncryptedContent === "string" || metadata.reasoningEncryptedContent === null
508508
? metadata.reasoningEncryptedContent
509509
: undefined
510+
if (part.text.length === 0 && !encryptedContent) return undefined
510511
return {
511512
type: "reasoning",
512513
...(id === undefined ? {} : { id }),

‎packages/ai/test/provider/openai-responses.test.ts‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4011,6 +4011,46 @@ describe("OpenAI Responses route", () => {
40114011
}),
40124012
)
40134013

4014+
it.effect("drops empty reasoning items without summary or encrypted content", () =>
4015+
Effect.gen(function* () {
4016+
const prepared = yield* compileRequest(
4017+
LLM.request({
4018+
id: "req_reasoning_empty_shell",
4019+
model,
4020+
messages: [
4021+
Message.user("What changed?"),
4022+
Message.assistant([
4023+
{
4024+
type: "reasoning",
4025+
text: "",
4026+
providerMetadata: {
4027+
openai: {
4028+
itemId: "rs_6aa28a10c05cf9f566f44022:rs_01a08aeb51217b92a5b853a0cb5b20ca",
4029+
reasoningEncryptedContent: null,
4030+
},
4031+
},
4032+
},
4033+
{ type: "text", text: "The parser changed." },
4034+
]),
4035+
Message.user("Summarize it."),
4036+
],
4037+
providerOptions: { store: false },
4038+
}),
4039+
)
4040+
4041+
expect(prepared.body.input).toEqual([
4042+
{ type: "message", role: "user", content: [{ type: "input_text", text: "What changed?" }] },
4043+
{
4044+
type: "message",
4045+
role: "assistant",
4046+
status: "completed",
4047+
content: [{ type: "output_text", text: "The parser changed." }],
4048+
},
4049+
{ type: "message", role: "user", content: [{ type: "input_text", text: "Summarize it." }] },
4050+
])
4051+
}),
4052+
)
4053+
40144054
it.effect("assembles streamed function call input", () =>
40154055
Effect.gen(function* () {
40164056
const body = sseEvents(

‎packages/app/component-tests/browser-pane.spec.ts‎

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -282,13 +282,28 @@ story("restores the current URL each time the same submitted navigation is block
282282
await address.press("Enter")
283283
await expect(address).toHaveValue("https://blocked.example/")
284284
await root.getByRole("button", { name: "Block navigation", exact: true }).click()
285-
await expect(root.getByRole("alert")).toHaveText("Request failed")
285+
await expect(root.getByRole("alert")).toHaveText("Only web pages can open here.")
286286
await expect(address).toHaveValue("https://alpha.example/")
287287
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
288288
})
289289
}
290290
})
291291

292+
story("explains why a typed address cannot open instead of searching for it", async ({ page }) => {
293+
const root = page.getByTestId("browser-pane-fixture")
294+
const address = root.getByRole("combobox", { name: "Browser address", exact: true })
295+
296+
for (const [typed, reason] of [
297+
["ftp://example.com", "Only web pages can open here."],
298+
["https://user:[email protected]", "Addresses with a user name or password can't open here."],
299+
]) {
300+
await address.fill(typed)
301+
await address.press("Enter")
302+
await expect(root.getByRole("alert")).toHaveText(reason)
303+
await expect(address).toHaveValue("https://alpha.example/")
304+
}
305+
})
306+
292307
story("shows a themed failure state for only the failed tab and allows retry", async ({ page }) => {
293308
const root = page.getByTestId("browser-pane-fixture")
294309
await root.getByRole("button", { name: "Failed page", exact: true }).click()
@@ -435,6 +450,9 @@ story("suggests visited pages and opens the first one that completes the typed a
435450
const options = page.getByRole("option")
436451
await expect(options).toHaveCount(1)
437452
await expect(options.first()).toHaveAttribute("aria-selected", "true")
453+
// Arrow keys that pick IME candidates leave the suggestions alone.
454+
await address.dispatchEvent("keydown", { key: "ArrowDown", isComposing: true })
455+
await expect(options.first()).toHaveAttribute("aria-selected", "true")
438456
await address.press("Enter")
439457
await expect(address).toHaveValue("http://localhost:5173/settings")
440458

@@ -451,14 +469,39 @@ story("suggests visited pages and opens the first one that completes the typed a
451469

452470
story("lists recent pages on a blank tab and opens one", async ({ page }) => {
453471
const root = page.getByTestId("browser-pane-fixture")
472+
const address = root.getByRole("combobox", { name: "Browser address", exact: true })
454473
await root.getByRole("button", { name: "Seed history", exact: true }).click()
455474
await root.getByRole("button", { name: "Blank page", exact: true }).click()
456475
await expect(root.getByText("Recent", { exact: true })).toBeVisible()
476+
// The globe at the field's start belongs to the field: a click there focuses it.
477+
await expect(address).not.toBeFocused()
478+
await root.locator('[data-component="browser-address"]').click({ position: { x: 16, y: 14 } })
479+
await expect(address).toBeFocused()
480+
await address.blur()
481+
457482
await root.getByRole("button", { name: /^Settings - Preview/ }).click()
458-
await expect(root.getByRole("combobox", { name: "Browser address", exact: true })).toHaveValue(
459-
"http://localhost:5173/settings",
460-
)
483+
await expect(address).toHaveValue("http://localhost:5173/settings")
461484
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
485+
// The opened page owns the field afterwards: an edit that is never submitted shows its URL again.
486+
await address.click()
487+
await address.fill("unsubmitted")
488+
await address.blur()
489+
await expect(address).toHaveValue("http://localhost:5173/settings")
490+
})
491+
492+
story("adds the whole page to the session and keeps the draft when the button is pressed again", async ({ page }) => {
493+
const root = page.getByTestId("browser-pane-fixture")
494+
const field = root.locator('[data-component="browser-address"]')
495+
const add = root.getByRole("button", { name: "Add page to session", exact: true })
496+
await field.hover()
497+
await add.click()
498+
const editor = root.locator('[data-slot="browser-comment-editor"] textarea')
499+
await editor.fill("Check the header layout")
500+
await field.hover()
501+
await add.click()
502+
await expect(editor).toHaveValue("Check the header layout")
503+
await editor.press("Enter")
504+
await expect(root.getByTestId("fixture-comments").getByRole("listitem")).toHaveText([/: Check the header layout$/])
462505
})
463506

464507
story("focuses the address field when the page uses the address shortcut", async ({ page }) => {
@@ -478,12 +521,22 @@ story("shows the page's cookies in the site information and clears them", async
478521
await expect(site).toContainText("0 cookies in use")
479522
})
480523

481-
story("zooms the page from the browser options and shows its zoom", async ({ page }) => {
524+
story("zooms the page from the browser options in view and keeps its zoom in the address field", async ({ page }) => {
482525
const root = page.getByTestId("browser-pane-fixture")
483-
await root.getByRole("button", { name: "Browser options", exact: true }).click()
526+
const options = root.getByRole("button", { name: "Browser options", exact: true })
527+
await options.click()
484528
await page.getByRole("menuitem", { name: /^Zoom in/ }).click()
485529
await expect(root.getByText("Zoom: 110", { exact: true })).toBeVisible()
530+
// The menu covered the page with a still; it closes so the zoomed page shows at once.
531+
await expect(page.getByRole("menu")).toBeHidden()
532+
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
533+
534+
const zoom = root.getByRole("button", { name: "110%", exact: true })
535+
await expect(zoom).toBeVisible()
536+
await options.click()
486537
await expect(page.getByRole("menuitem", { name: /^Reset zoom/ })).toContainText("110%")
487-
await page.getByRole("menuitem", { name: /^Reset zoom/ }).click()
538+
await page.keyboard.press("Escape")
539+
await zoom.click()
488540
await expect(root.getByText("Zoom: 100", { exact: true })).toBeVisible()
541+
await expect(zoom).toBeHidden()
489542
})

‎packages/app/e2e/service-worker/cache.spec.ts‎

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { expect, test, type Page } from "@playwright/test"
22
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises"
33
import { createServer, type ServerResponse } from "node:http"
4+
import type { AddressInfo } from "node:net"
45
import { once } from "node:events"
56
import { createHash } from "node:crypto"
67
import { join, extname, relative, sep } from "node:path"
78
import { tmpdir } from "node:os"
89
import { fileURLToPath } from "node:url"
910
import { build } from "vite"
10-
import { serviceWorker } from "../../vite.pwa"
11+
import { onDemandAsset, serviceWorker } from "../../vite.pwa"
1112

1213
type Site = {
1314
url: string
@@ -127,14 +128,14 @@ const fixture = test.extend<{ site: Site }, { builds: Record<string, Record<stri
127128

128129
const file = builds[state.version][path]
129130

130-
const types: Record<string, string> = {
131-
".js": "text/javascript",
132-
".html": "text/html",
133-
".json": "application/json",
134-
".wasm": "application/wasm",
135-
}
131+
const types = new Map([
132+
[".js", "text/javascript"],
133+
[".html", "text/html"],
134+
[".json", "application/json"],
135+
[".wasm", "application/wasm"],
136+
])
136137

137-
response.setHeader("content-type", types[extname(path)] ?? "application/octet-stream")
138+
response.setHeader("content-type", types.get(extname(path)) ?? "application/octet-stream")
138139

139140
if (file) return void response.end(file)
140141

@@ -145,9 +146,8 @@ const fixture = test.extend<{ site: Site }, { builds: Record<string, Record<stri
145146

146147
server.listen(0, "127.0.0.1")
147148
await once(server, "listening")
148-
const address = server.address()
149-
150-
if (!address || typeof address === "string") throw new Error("Expected a TCP address")
149+
// SAFETY: a server listening on a host and port reports an AddressInfo; only pipe servers report a string.
150+
const address = server.address() as AddressInfo
151151

152152
try {
153153
await use({
@@ -284,9 +284,11 @@ fixture(
284284
await expect
285285
.poll(() =>
286286
replacement.evaluate(() => {
287-
const registration = (self as unknown as { registration: ServiceWorkerRegistration }).registration
287+
// This runs in the service worker, whose global scope holds its registration.
288+
const registration =
289+
"registration" in self && self.registration instanceof ServiceWorkerRegistration ? self.registration : undefined
288290

289-
return { waiting: !!registration.waiting, active: registration.active?.state }
291+
return { waiting: !!registration?.waiting, active: registration?.active?.state }
290292
}),
291293
)
292294
.toEqual({ waiting: false, active: "activated" })
@@ -378,15 +380,22 @@ fixture("does not substitute cached HTML for API or missing asset navigations",
378380
expect(await asset?.text()).toBe("Not found")
379381
})
380382

381-
test("the production build precaches every deployable file", async ({ page, context }) => {
383+
test("the production build precaches every deployable file except on-demand Office assets", async ({
384+
page,
385+
context,
386+
}) => {
382387
const directory = new URL("../../dist/", import.meta.url)
383388

384-
const files = (await readdir(directory, { recursive: true, withFileTypes: true }))
389+
const deployable = (await readdir(directory, { recursive: true, withFileTypes: true }))
385390
.filter((entry) => entry.isFile())
386391
.map((entry) => "/" + relative(fileURLToPath(directory), join(entry.parentPath, entry.name)).split(sep).join("/"))
387392
.filter((path) => !path.endsWith(".map") && !["/_headers", "/_redirects", "/sw.js"].includes(path))
388393

394+
const files = deployable.filter((path) => !onDemandAsset(path))
395+
389396
expect(files.length).toBeGreaterThan(1)
397+
// The Office engines ship but load on demand, so a build without them would make this exclusion vacuous.
398+
expect(deployable.some((path) => path.endsWith(".wasm") && onDemandAsset(path))).toBe(true)
390399

391400
const server = createServer(async (request, response) => {
392401
const path = new URL(request.url ?? "/", "http://localhost").pathname
@@ -411,9 +420,8 @@ test("the production build precaches every deployable file", async ({ page, cont
411420

412421
server.listen(0, "127.0.0.1")
413422
await once(server, "listening")
414-
const address = server.address()
415-
416-
if (!address || typeof address === "string") throw new Error("Expected a TCP address")
423+
// SAFETY: a server listening on a host and port reports an AddressInfo; only pipe servers report a string.
424+
const address = server.address() as AddressInfo
417425
const url = `http://127.0.0.1:${address.port}`
418426

419427
try {

‎packages/app/src/runtime/i18n/en.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,21 @@ export const dict = {
662662
"settings.about.description": "OpenCode, the open source coding agent",
663663
"settings.about.trademark": "OpenCode is a registered trademark of Anomaly Innovations, Inc.",
664664
"settings.about.typeset": "Typeset in Inter and IBM Plex Mono",
665+
"settings.about.notices.title": "Third-party notices",
666+
"settings.about.notices.description": "OpenCode includes the following open source software.",
667+
"settings.about.notices.license": "License text",
668+
"settings.about.notices.fonts.title": "Fonts in the Office previews",
669+
"settings.about.notices.fonts.description":
670+
"Each family is distributed under the SIL Open Font License 1.1, with the copyright lines below.",
671+
"settings.about.notices.eigenpal": "Portions of BetterOffice derive from it.",
672+
"settings.about.notices.loadFailed": "Could not load the third-party notices",
673+
"settings.about.notices.crates.title": "Rust crates in the Office engines",
674+
"settings.about.notices.crates.description":
675+
"BetterOffice compiles these crates into its Word, Excel and PowerPoint engines. Where a crate offers a choice of licenses, the highlighted license applies.",
676+
"settings.about.notices.crates.licenses": "License texts",
677+
"settings.about.notices.crates.count.one": "{{count}} crate",
678+
"settings.about.notices.crates.count.other": "{{count}} crates",
679+
"settings.about.notices.crates.appliesTo": "Applies to {{names}}",
665680
"settings.about.tagline": "AI can’t build great software, without you",
666681
"settings.about.copyright": "© Anomaly Innovations, Inc.",
667682
"settings.preferences.description": "Customize preferences and theme and default behavior",

‎packages/app/src/settings/about/about.tsx‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { createResource } from "solid-js"
2+
import { useDialog } from "@opencode/ui/context/dialog"
23
import { useLanguage } from "@/runtime/i18n/language"
34
import { usePlatform } from "@/runtime/platform/platform"
45
import { ExternalLink } from "@/runtime/platform/external-link"
6+
import { showToast } from "@/shell/notifications/toast"
57
import legal from "./legal.svg"
68
import anomalyBrush from "./anomaly-brush.svg"
79
import { AnimatedWordmark } from "./animated-wordmark"
@@ -31,6 +33,7 @@ const illustrators = ["usrnk1", "ludvigrask_", "arvsrn", "iamdavidhill"] as cons
3133
export function SettingsAbout(props: { active: boolean }) {
3234
const language = useLanguage()
3335
const platform = usePlatform()
36+
const dialog = useDialog()
3437

3538
const [otherContributors] = createResource(
3639
() => props.active || undefined,
@@ -53,6 +56,30 @@ export function SettingsAbout(props: { active: boolean }) {
5356
</ExternalLink>,
5457
]
5558

59+
let noticesButton: HTMLButtonElement | undefined
60+
61+
const showNotices = async () => {
62+
// The license texts load only when someone reads them.
63+
const loaded = await import("./notices/dialog").catch(() => undefined)
64+
65+
if (!loaded) {
66+
showToast({ variant: "error", title: language.t("settings.about.notices.loadFailed") })
67+
68+
return
69+
}
70+
71+
// dialog.show has no trigger for Kobalte to restore, so closing returns focus to the button by hand.
72+
void dialog.show(() => (
73+
<loaded.default
74+
onCloseAutoFocus={(event) => {
75+
if (!noticesButton?.isConnected) return
76+
event.preventDefault()
77+
noticesButton.focus({ preventScroll: true })
78+
}}
79+
/>
80+
))
81+
}
82+
5683
return (
5784
<div class="settings-about-content">
5885
<div class="settings-about-intro">
@@ -90,6 +117,11 @@ export function SettingsAbout(props: { active: boolean }) {
90117
<p>{language.t("settings.about.description")}</p>
91118
<p>{language.t("settings.about.trademark")}</p>
92119
<p>{language.t("settings.about.typeset")}</p>
120+
<p>
121+
<button ref={noticesButton} type="button" class="settings-about-link" onClick={() => void showNotices()}>
122+
{language.t("settings.about.notices.title")}
123+
</button>
124+
</p>
93125
</div>
94126

95127
<p>{language.t("settings.about.tagline")}</p>

0 commit comments

Comments
 (0)