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
2 changes: 1 addition & 1 deletion packages/app/e2e/app/server-default.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { test, expect } from "../fixtures"
import { serverNamePattern, serverUrls } from "../utils"
import { closeDialog, clickMenuItem } from "../actions"

const DEFAULT_SERVER_URL_KEY = "opencode.settings.dat:defaultServerUrl"
const DEFAULT_SERVER_URL_KEY = "pawwork.settings.dat:defaultServerUrl"

test("can set a default server on web", async ({ page, gotoSession }) => {
await page.addInitScript((key: string) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/app/e2e/commands/panels.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ test("desktop right-panel collapses shell tab labels below the compact threshold

test("legacy changes side-panel state restores into the review tab", async ({ page, gotoSession, slug }) => {
await page.addInitScript(({ slug }) => {
const key = "opencode.global.dat:layout"
const key = "pawwork.global.dat:layout"
const raw = localStorage.getItem(key)
const parsed = (() => {
if (!raw) return {}
Expand Down
6 changes: 3 additions & 3 deletions packages/app/e2e/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,7 @@ async function seedStorage(
extra: string[]
model: { providerID: string; modelID: string }
}) => {
const key = "opencode.global.dat:server"
const key = "pawwork.global.dat:server"
const raw = localStorage.getItem(key)
const parsed = (() => {
if (!raw) return undefined
Expand Down Expand Up @@ -586,7 +586,7 @@ async function seedStorage(
}

localStorage.setItem(key, JSON.stringify({ list: nextList, projects: next, lastProject }))
localStorage.setItem("opencode.settings.dat:defaultServerUrl", args.serverUrl)
localStorage.setItem("pawwork.settings.dat:defaultServerUrl", args.serverUrl)

const win = window as E2EWindow
win.__opencode_e2e = {
Expand All @@ -595,7 +595,7 @@ async function seedStorage(
prompt: { enabled: true },
terminal: { enabled: true, terminals: {} },
}
localStorage.setItem("opencode.global.dat:model", JSON.stringify({ recent: [args.model], user: [], variant: {} }))
localStorage.setItem("pawwork.global.dat:model", JSON.stringify({ recent: [args.model], user: [], variant: {} }))
},
{ directory: input.directory, serverUrl: origin, extra: input.extra ?? [], model: input.model ?? seedModel },
)
Expand Down
24 changes: 12 additions & 12 deletions packages/app/e2e/settings/settings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import {

test("@smoke new installs start with the PawWork theme", async ({ page, gotoSession }) => {
await page.addInitScript(() => {
localStorage.removeItem("opencode-theme-id")
localStorage.removeItem("opencode-color-scheme")
localStorage.removeItem("pawwork-theme-id")
localStorage.removeItem("pawwork-color-scheme")
})

await gotoSession()
Expand Down Expand Up @@ -51,7 +51,7 @@ test('@smoke PawWork settings opens as a full-pane surface, not a dialog', async

test("changing language updates settings labels", async ({ page, gotoSession }) => {
await page.addInitScript(() => {
localStorage.setItem("opencode.global.dat:language", JSON.stringify({ locale: "en" }))
localStorage.setItem("pawwork.global.dat:language", JSON.stringify({ locale: "en" }))
})

await gotoSession()
Expand Down Expand Up @@ -85,11 +85,11 @@ test.skip("changing theme persists in localStorage", async () => {
// and cannot exercise switching. Revisit once a second theme is added.
})

test("legacy theme ids migrate to pawwork and clear cached CSS", async ({ page, gotoSession }) => {
test("unknown theme ids migrate to pawwork and clear cached css", async ({ page, gotoSession }) => {
await page.addInitScript(() => {
localStorage.setItem("opencode-theme-id", "dracula")
localStorage.setItem("opencode-theme-css-light", "--background-base:#fff;")
localStorage.setItem("opencode-theme-css-dark", "--background-base:#000;")
localStorage.setItem("pawwork-theme-id", "dracula")
localStorage.setItem("pawwork-theme-css-light", "--background-base:#fff;")
localStorage.setItem("pawwork-theme-css-dark", "--background-base:#000;")
})

await gotoSession()
Expand All @@ -99,23 +99,23 @@ test("legacy theme ids migrate to pawwork and clear cached CSS", async ({ page,
await expect
.poll(async () => {
return await page.evaluate(() => {
return localStorage.getItem("opencode-theme-id")
return localStorage.getItem("pawwork-theme-id")
})
})
.toBe("pawwork")

await expect
.poll(async () => {
return await page.evaluate(() => {
return localStorage.getItem("opencode-theme-css-light")
return localStorage.getItem("pawwork-theme-css-light")
})
})
.toBeNull()

await expect
.poll(async () => {
return await page.evaluate(() => {
return localStorage.getItem("opencode-theme-css-dark")
return localStorage.getItem("pawwork-theme-css-dark")
})
})
.toBeNull()
Expand Down Expand Up @@ -316,8 +316,8 @@ test("clearing the UI font field restores the default placeholder and stack", as
test("code font and UI font rehydrate after reload", async ({ page, gotoSession }) => {
await page.addInitScript(() => {
if (sessionStorage.getItem("settings-rehydrate-init")) return
localStorage.setItem("opencode-theme-id", "pawwork")
localStorage.setItem("opencode-color-scheme", "light")
localStorage.setItem("pawwork-theme-id", "pawwork")
localStorage.setItem("pawwork-color-scheme", "light")
sessionStorage.setItem("settings-rehydrate-init", "1")
})

Expand Down
4 changes: 2 additions & 2 deletions packages/app/e2e/sidebar/sidebar-session-search.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ test("command palette prioritizes pinned and recent PawWork sessions", async ({

await page.addInitScript((sessionID) => {
localStorage.setItem(
"opencode.global.dat:layout.page",
"pawwork.global.dat:layout.page",
JSON.stringify({
pawworkPinnedSessions: [sessionID],
pawworkSortMode: "time",
Expand All @@ -23,7 +23,7 @@ test("command palette prioritizes pinned and recent PawWork sessions", async ({
await expect
.poll(() =>
page.evaluate(() => {
const raw = localStorage.getItem("opencode.global.dat:layout.page")
const raw = localStorage.getItem("pawwork.global.dat:layout.page")
const next = raw ? (JSON.parse(raw) as { pawworkPinnedSessions?: string[] }).pawworkPinnedSessions : []
return next ?? []
}),
Expand Down
2 changes: 1 addition & 1 deletion packages/app/e2e/sidebar/sidebar.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ test("sidebar collapsed state persists across navigation and reload", async ({ p
await expect(button).toHaveAttribute("aria-expanded", "false")

const opened = await page.evaluate(
() => JSON.parse(localStorage.getItem("opencode.global.dat:layout") ?? "{}").sidebar?.opened,
() => JSON.parse(localStorage.getItem("pawwork.global.dat:layout") ?? "{}").sidebar?.opened,
)
await expect(opened).toBe(false)
})
Expand Down
2 changes: 1 addition & 1 deletion packages/app/e2e/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,5 @@ export function sessionPath(directory: string, sessionID?: string) {
export function workspacePersistKey(directory: string, key: string) {
const head = (directory.slice(0, 12) || "workspace").replace(/[^a-zA-Z0-9._-]/g, "-")
const sum = checksum(directory) ?? "0"
return `opencode.workspace.${head}.${sum}.dat:workspace:${key}`
return `pawwork.workspace.${head}.${sum}.dat:workspace:${key}`
}
10 changes: 5 additions & 5 deletions packages/app/public/oc-theme-preload.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// Phase 1 ships only the `pawwork` theme, which is locked to light mode.
// When a dark-capable theme is bundled, this file must gain a theme-aware
// guard so `opencode-color-scheme` is forced to "light" ONLY for light-only
// guard so `pawwork-color-scheme` is forced to "light" ONLY for light-only
// themes; tracked in issue #23.
;(function () {
var key = "opencode-theme-id"
var schemeKey = "opencode-color-scheme"
var cssLightKey = "opencode-theme-css-light"
var cssDarkKey = "opencode-theme-css-dark"
var key = "pawwork-theme-id"
var schemeKey = "pawwork-color-scheme"
var cssLightKey = "pawwork-theme-css-light"
var cssDarkKey = "pawwork-theme-css-dark"

try {
var storedTheme = localStorage.getItem(key)
Expand Down
6 changes: 6 additions & 0 deletions packages/app/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,12 @@ export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) {
<Font />
<ThemeProvider
defaultTheme="pawwork"
storageKeys={{
themeId: "pawwork-theme-id",
colorScheme: "pawwork-color-scheme",
cssLight: "pawwork-theme-css-light",
cssDark: "pawwork-theme-css-dark",
}}
onThemeApplied={(_, mode) => {
void window.api?.setTitlebar?.({ mode })
}}
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/context/language.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export function normalizeLocale(value: unknown): Locale {
function readStoredLocale() {
if (typeof localStorage !== "object") return
try {
const raw = localStorage.getItem("opencode.global.dat:language")
const raw = localStorage.getItem("pawwork.global.dat:language")
if (!raw) return
const next = JSON.parse(raw) as { locale?: string }
if (typeof next?.locale !== "string") return
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/context/terminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,7 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
legacy: getLegacyTerminalStorageKeys(dir, params.id),
at: new Date().toISOString(),
})
const store = platform.storage?.("opencode.global.dat")
const store = platform.storage?.("pawwork.global.dat")
if (!store) return
void Promise.resolve(store.setItem("debug:terminal-invalid-all", payload)).catch(() => undefined)
})
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/entry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { handleNotificationClick } from "@/utils/notification-click"
import pkg from "../package.json"
import { ServerConnection } from "./context/server"

const DEFAULT_SERVER_URL_KEY = "opencode.settings.dat:defaultServerUrl"
const DEFAULT_SERVER_URL_KEY = "pawwork.settings.dat:defaultServerUrl"

const getLocale = () => {
if (typeof navigator !== "object") return "en" as const
Expand Down
12 changes: 12 additions & 0 deletions packages/app/src/hooks/use-providers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { expect, mock, test } from "bun:test"

mock.module("@solidjs/router", () => ({
useParams: () => ({}),
}))

const { popularProviders } = await import("./use-providers")

test("popular providers keep OpenCode Zen and OpenCode Go visible", () => {
expect(popularProviders).toContain("opencode")
expect(popularProviders).toContain("opencode-go")
})
2 changes: 1 addition & 1 deletion packages/app/src/pages/session/terminal-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ export function TerminalPanel(props: { embedded?: boolean }) {
active: terminal.active(),
at: new Date().toISOString(),
})
const store = platform.storage?.("opencode.global.dat")
const store = platform.storage?.("pawwork.global.dat")
if (store) {
void Promise.resolve(store.setItem("debug:terminal-panel-invalid-all", payload)).catch(() => undefined)
}
Expand Down
24 changes: 12 additions & 12 deletions packages/app/src/theme-preload.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,32 +26,32 @@ describe("theme preload", () => {
})

test("locks PawWork to light and rewrites stored color scheme", () => {
localStorage.setItem("opencode-theme-id", "pawwork")
localStorage.setItem("opencode-color-scheme", "dark")
localStorage.setItem("pawwork-theme-id", "pawwork")
localStorage.setItem("pawwork-color-scheme", "dark")

run()

expect(document.documentElement.dataset.theme).toBe("pawwork")
expect(document.documentElement.dataset.colorScheme).toBe("light")
expect(localStorage.getItem("opencode-theme-id")).toBe("pawwork")
expect(localStorage.getItem("opencode-color-scheme")).toBe("light")
expect(localStorage.getItem("pawwork-theme-id")).toBe("pawwork")
expect(localStorage.getItem("pawwork-color-scheme")).toBe("light")
})

for (const legacy of ["oc-1", "oc-2", "dracula", "nightowl", "amoled"]) {
test(`migrates legacy "${legacy}" theme to pawwork and clears cached css`, () => {
localStorage.setItem("opencode-theme-id", legacy)
localStorage.setItem("opencode-color-scheme", "dark")
localStorage.setItem("opencode-theme-css-light", "--background-base:#ffffff;")
localStorage.setItem("opencode-theme-css-dark", "--background-base:#000000;")
localStorage.setItem("pawwork-theme-id", legacy)
localStorage.setItem("pawwork-color-scheme", "dark")
localStorage.setItem("pawwork-theme-css-light", "--background-base:#ffffff;")
localStorage.setItem("pawwork-theme-css-dark", "--background-base:#000000;")

run()

expect(document.documentElement.dataset.theme).toBe("pawwork")
expect(document.documentElement.dataset.colorScheme).toBe("light")
expect(localStorage.getItem("opencode-theme-id")).toBe("pawwork")
expect(localStorage.getItem("opencode-color-scheme")).toBe("light")
expect(localStorage.getItem("opencode-theme-css-light")).toBeNull()
expect(localStorage.getItem("opencode-theme-css-dark")).toBeNull()
expect(localStorage.getItem("pawwork-theme-id")).toBe("pawwork")
expect(localStorage.getItem("pawwork-color-scheme")).toBe("light")
expect(localStorage.getItem("pawwork-theme-css-light")).toBeNull()
expect(localStorage.getItem("pawwork-theme-css-dark")).toBeNull()
})
}
})
48 changes: 37 additions & 11 deletions packages/app/src/utils/persist.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,17 @@ type PersistTestingType = typeof import("./persist").PersistTesting

class MemoryStorage implements Storage {
private values = new Map<string, string>()
private failingSets = new Map<string, number>()
readonly events: string[] = []
readonly calls = { get: 0, set: 0, remove: 0 }

clear() {
this.values.clear()
this.failingSets.clear()
}

failSet(key: string, times: number) {
this.failingSets.set(key, times)
}

get length() {
Expand All @@ -22,22 +28,27 @@ class MemoryStorage implements Storage {
getItem(key: string) {
this.calls.get += 1
this.events.push(`get:${key}`)
if (key.startsWith("opencode.throw")) throw new Error("storage get failed")
if (key.startsWith("pawwork.throw")) throw new Error("storage get failed")
return this.values.get(key) ?? null
}

setItem(key: string, value: string) {
this.calls.set += 1
this.events.push(`set:${key}`)
if (key.startsWith("opencode.quota")) throw new DOMException("quota", "QuotaExceededError")
if (key.startsWith("opencode.throw")) throw new Error("storage set failed")
const remaining = this.failingSets.get(key) ?? 0
if (remaining > 0) {
this.failingSets.set(key, remaining - 1)
throw new DOMException("quota", "QuotaExceededError")
}
if (key.startsWith("pawwork.quota")) throw new DOMException("quota", "QuotaExceededError")
if (key.startsWith("pawwork.throw")) throw new Error("storage set failed")
this.values.set(key, value)
}

removeItem(key: string) {
this.calls.remove += 1
this.events.push(`remove:${key}`)
if (key.startsWith("opencode.throw")) throw new Error("storage remove failed")
if (key.startsWith("pawwork.throw")) throw new Error("storage remove failed")
this.values.delete(key)
}
}
Expand Down Expand Up @@ -69,29 +80,29 @@ beforeEach(() => {

describe("persist localStorage resilience", () => {
test("does not cache values as persisted when quota write and eviction fail", () => {
const storageApi = persistTesting.localStorageWithPrefix("opencode.quota.scope")
const storageApi = persistTesting.localStorageWithPrefix("pawwork.quota.scope")
storageApi.setItem("value", '{"value":1}')

expect(storage.getItem("opencode.quota.scope:value")).toBeNull()
expect(storage.getItem("pawwork.quota.scope:value")).toBeNull()
expect(storageApi.getItem("value")).toBeNull()
})

test("disables only the failing scope when storage throws", () => {
const bad = persistTesting.localStorageWithPrefix("opencode.throw.scope")
const bad = persistTesting.localStorageWithPrefix("pawwork.throw.scope")
bad.setItem("value", '{"value":1}')

const before = storage.calls.set
bad.setItem("value", '{"value":2}')
expect(storage.calls.set).toBe(before)
expect(bad.getItem("value")).toBeNull()

const healthy = persistTesting.localStorageWithPrefix("opencode.safe.scope")
const healthy = persistTesting.localStorageWithPrefix("pawwork.safe.scope")
healthy.setItem("value", '{"value":3}')
expect(storage.getItem("opencode.safe.scope:value")).toBe('{"value":3}')
expect(storage.getItem("pawwork.safe.scope:value")).toBe('{"value":3}')
})

test("failing fallback scope does not poison direct storage scope", () => {
const broken = persistTesting.localStorageWithPrefix("opencode.throw.scope2")
const broken = persistTesting.localStorageWithPrefix("pawwork.throw.scope2")
broken.setItem("value", '{"value":1}')

const direct = persistTesting.localStorageDirect()
Expand All @@ -100,6 +111,21 @@ describe("persist localStorage resilience", () => {
expect(storage.getItem("direct-value")).toBe('{"value":5}')
})

test("quota eviction can remove legacy OpenCode local entries", () => {
storage.setItem("opencode.workspace.old.dat:value", "old workspace")
storage.setItem("opencode.global.dat:value", "old global")
storage.setItem("opencode.settings.dat:value", "old settings")
storage.failSet("pawwork.workspace.new.dat:value", 4)

const storageApi = persistTesting.localStorageWithPrefix("pawwork.workspace.new.dat")
storageApi.setItem("value", '{"value":1}')

expect(storage.getItem("opencode.workspace.old.dat:value")).toBeNull()
expect(storage.getItem("opencode.global.dat:value")).toBeNull()
expect(storage.getItem("opencode.settings.dat:value")).toBeNull()
expect(storage.getItem("pawwork.workspace.new.dat:value")).toBe('{"value":1}')
})

test("normalizer rejects malformed JSON payloads", () => {
const result = persistTesting.normalize({ value: "ok" }, '{"value":"\\x"}')
expect(result).toBeUndefined()
Expand All @@ -108,7 +134,7 @@ describe("persist localStorage resilience", () => {
test("workspace storage sanitizes Windows filename characters", () => {
const result = persistTesting.workspaceStorage("C:\\Users\\foo")

expect(result).toStartWith("opencode.workspace.")
expect(result).toStartWith("pawwork.workspace.")
expect(result.endsWith(".dat")).toBeTrue()
expect(/[:\\/]/.test(result)).toBeFalse()
})
Expand Down
Loading
Loading