Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 26 additions & 18 deletions packages/app/e2e/service-worker/cache.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { expect, test, type Page } from "@playwright/test"
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises"
import { createServer, type ServerResponse } from "node:http"
import type { AddressInfo } from "node:net"
import { once } from "node:events"
import { createHash } from "node:crypto"
import { join, extname, relative, sep } from "node:path"
import { tmpdir } from "node:os"
import { fileURLToPath } from "node:url"
import { build } from "vite"
import { serviceWorker } from "../../vite.pwa"
import { onDemandAsset, serviceWorker } from "../../vite.pwa"

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

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

const types: Record<string, string> = {
".js": "text/javascript",
".html": "text/html",
".json": "application/json",
".wasm": "application/wasm",
}
const types = new Map([
[".js", "text/javascript"],
[".html", "text/html"],
[".json", "application/json"],
[".wasm", "application/wasm"],
])

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

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

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

server.listen(0, "127.0.0.1")
await once(server, "listening")
const address = server.address()

if (!address || typeof address === "string") throw new Error("Expected a TCP address")
// SAFETY: a server listening on a host and port reports an AddressInfo; only pipe servers report a string.
const address = server.address() as AddressInfo

try {
await use({
Expand Down Expand Up @@ -284,9 +284,11 @@ fixture(
await expect
.poll(() =>
replacement.evaluate(() => {
const registration = (self as unknown as { registration: ServiceWorkerRegistration }).registration
// This runs in the service worker, whose global scope holds its registration.
const registration =
"registration" in self && self.registration instanceof ServiceWorkerRegistration ? self.registration : undefined

return { waiting: !!registration.waiting, active: registration.active?.state }
return { waiting: !!registration?.waiting, active: registration?.active?.state }
}),
)
.toEqual({ waiting: false, active: "activated" })
Expand Down Expand Up @@ -378,15 +380,22 @@ fixture("does not substitute cached HTML for API or missing asset navigations",
expect(await asset?.text()).toBe("Not found")
})

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

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

const files = deployable.filter((path) => !onDemandAsset(path))

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

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

server.listen(0, "127.0.0.1")
await once(server, "listening")
const address = server.address()

if (!address || typeof address === "string") throw new Error("Expected a TCP address")
// SAFETY: a server listening on a host and port reports an AddressInfo; only pipe servers report a string.
const address = server.address() as AddressInfo
const url = `http://127.0.0.1:${address.port}`

try {
Expand Down
15 changes: 15 additions & 0 deletions packages/app/src/runtime/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,21 @@ export const dict = {
"settings.about.description": "OpenCode, the open source coding agent",
"settings.about.trademark": "OpenCode is a registered trademark of Anomaly Innovations, Inc.",
"settings.about.typeset": "Typeset in Inter and IBM Plex Mono",
"settings.about.notices.title": "Third-party notices",
"settings.about.notices.description": "OpenCode includes the following open source software.",
"settings.about.notices.license": "License text",
"settings.about.notices.fonts.title": "Fonts in the Office previews",
"settings.about.notices.fonts.description":
"Each family is distributed under the SIL Open Font License 1.1, with the copyright lines below.",
"settings.about.notices.eigenpal": "Portions of BetterOffice derive from it.",
"settings.about.notices.loadFailed": "Could not load the third-party notices",
"settings.about.notices.crates.title": "Rust crates in the Office engines",
"settings.about.notices.crates.description":
"BetterOffice compiles these crates into its Word, Excel and PowerPoint engines. Where a crate offers a choice of licenses, the highlighted license applies.",
"settings.about.notices.crates.licenses": "License texts",
"settings.about.notices.crates.count.one": "{{count}} crate",
"settings.about.notices.crates.count.other": "{{count}} crates",
"settings.about.notices.crates.appliesTo": "Applies to {{names}}",
"settings.about.tagline": "AI can’t build great software, without you",
"settings.about.copyright": "© Anomaly Innovations, Inc.",
"settings.preferences.description": "Customize preferences and theme and default behavior",
Expand Down
32 changes: 32 additions & 0 deletions packages/app/src/settings/about/about.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { createResource } from "solid-js"
import { useDialog } from "@opencode/ui/context/dialog"
import { useLanguage } from "@/runtime/i18n/language"
import { usePlatform } from "@/runtime/platform/platform"
import { ExternalLink } from "@/runtime/platform/external-link"
import { showToast } from "@/shell/notifications/toast"
import legal from "./legal.svg"
import anomalyBrush from "./anomaly-brush.svg"
import { AnimatedWordmark } from "./animated-wordmark"
Expand Down Expand Up @@ -31,6 +33,7 @@ const illustrators = ["usrnk1", "ludvigrask_", "arvsrn", "iamdavidhill"] as cons
export function SettingsAbout(props: { active: boolean }) {
const language = useLanguage()
const platform = usePlatform()
const dialog = useDialog()

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

let noticesButton: HTMLButtonElement | undefined

const showNotices = async () => {
// The license texts load only when someone reads them.
const loaded = await import("./notices/dialog").catch(() => undefined)

if (!loaded) {
showToast({ variant: "error", title: language.t("settings.about.notices.loadFailed") })

return
}

// dialog.show has no trigger for Kobalte to restore, so closing returns focus to the button by hand.
void dialog.show(() => (
<loaded.default
onCloseAutoFocus={(event) => {
if (!noticesButton?.isConnected) return
event.preventDefault()
noticesButton.focus({ preventScroll: true })
}}
/>
))
}

return (
<div class="settings-about-content">
<div class="settings-about-intro">
Expand Down Expand Up @@ -90,6 +117,11 @@ export function SettingsAbout(props: { active: boolean }) {
<p>{language.t("settings.about.description")}</p>
<p>{language.t("settings.about.trademark")}</p>
<p>{language.t("settings.about.typeset")}</p>
<p>
<button ref={noticesButton} type="button" class="settings-about-link" onClick={() => void showNotices()}>
{language.t("settings.about.notices.title")}
</button>
</p>
</div>

<p>{language.t("settings.about.tagline")}</p>
Expand Down
Loading
Loading