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: 2 additions & 0 deletions DIVERGENCES.md

Large diffs are not rendered by default.

3 changes: 0 additions & 3 deletions packages/app/e2e/performance/timeline-stability/fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,6 @@ export async function setupTimeline(
},
}),
)
if (settings.newLayoutDesigns === false) {
localStorage.setItem("app-version.v1", JSON.stringify({ version: "1.17.20" }))
}
}, input.settings ?? {})
if (input.locale) {
await page.addInitScript((locale) => {
Expand Down
129 changes: 129 additions & 0 deletions packages/app/e2e/regression/layout-theme-lock.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { expect, test, type Page } from "@playwright/test"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectAppVisible } from "../utils/waits"

const directory = "C:/OpenCode/LayoutThemeLock"

// The layout switch and the theme picker are both gone, and neither is merely
// hidden: a profile that still carries the old preference has to come up on the
// shipped layout and the shipped theme anyway. These specs seed exactly the
// stored state that used to select the retired ones.
async function seed(page: Page, storage: Record<string, string>) {
await mockOpenCodeServer(page, {
directory,
project: {
id: "proj_layout_theme_lock",
worktree: directory,
vcs: "git",
name: "layout-theme-lock",
time: { created: 1700000000000, updated: 1700000000000 },
sandboxes: [],
},
provider: { all: [], connected: [], default: {} },
sessions: [],
pageMessages: () => ({ items: [] }),
})
await page.addInitScript((entries) => {
for (const [key, value] of Object.entries(entries)) localStorage.setItem(key, value)
}, storage)
}

const sidebar = (page: Page) => page.locator('[data-component="app-sidebar"]')

test("boots the shipped layout for a profile that had the old one turned on", async ({ page }) => {
// Exactly what the retired transition wrote: the preference off, plus a
// launch version old enough that the upgrade migration would not flip it.
await seed(page, {
"settings.v3": JSON.stringify({ general: { newLayoutDesigns: false } }),
"app-version.v1": JSON.stringify({ version: "1.17.20" }),
})

await page.goto("/")

await expectAppVisible(sidebar(page))
// app.tsx stamps this straight off the setting, so it is the setting.
await expect(page.locator("body[data-new-layout]")).toHaveCount(1)
await expect(page.locator("body.text-12-regular")).toHaveCount(0)
})

test("migrates a theme picked before the picker was removed, before first paint", async ({ page }) => {
await seed(page, {
"opencode-theme-id": "nightowl",
"opencode-theme-css-light": "--background-base:#ff00ff;",
"opencode-theme-css-dark": "--background-base:#ff00ff;",
})
// The old palette would only be on screen between the pre-mount script and
// the app applying its own theme, and the app removes the element on the way
// past. Watching for it is the only way to see that window from out here.
await page.addInitScript(() => {
Object.assign(window, { __sawStalePalette: false })
new MutationObserver((records) => {
for (const record of records) {
for (const node of record.addedNodes) {
if (node instanceof Element && node.id === "oc-theme-preload") {
Object.assign(window, { __sawStalePalette: true })
}
}
}
// `document.documentElement` does not exist yet at document_start, so the
// observer has to be anchored on the document itself.
}).observe(document, { childList: true, subtree: true })
})

await page.goto("/")

await expectAppVisible(sidebar(page))
await expect(page.locator("html")).toHaveAttribute("data-theme", "oc-2")
expect(await page.evaluate(() => Reflect.get(window, "__sawStalePalette") === true)).toBe(false)
await expect
.poll(() =>
page.evaluate(() => ({
id: localStorage.getItem("opencode-theme-id"),
light: localStorage.getItem("opencode-theme-css-light"),
dark: localStorage.getItem("opencode-theme-css-dark"),
})),
)
.toEqual({ id: "oc-2", light: null, dark: null })
})

test("ignores a theme another tab writes", async ({ page }) => {
await seed(page, {})

await page.goto("/")
await expectAppVisible(sidebar(page))

// The cross-tab storage listener takes whatever landed in localStorage, so it
// is the one path into the theme that no UI change can close.
await page.evaluate(() => {
localStorage.setItem("opencode-theme-id", "dracula")
window.dispatchEvent(
new StorageEvent("storage", { key: "opencode-theme-id", newValue: "dracula", storageArea: localStorage }),
)
})

await expect(page.locator("html")).toHaveAttribute("data-theme", "oc-2")
await expect(page.locator("#oc-theme-preload")).toHaveCount(0)
})

test("offers no layout switch and no theme picker in settings", async ({ page }) => {
// `layoutTransitionEligible` is what the retired transition gated the switch
// on, so seeding it is what makes this a real assertion rather than one that
// passes because the profile was never offered the switch anyway.
await seed(page, {
"settings.v3": JSON.stringify({ general: { layoutTransitionEligible: true } }),
})

await page.goto("/")
await expectAppVisible(sidebar(page))
await sidebar(page).getByRole("button", { name: "Settings" }).click()

const dialog = page.locator(".settings-v2-dialog")
await expectAppVisible(dialog)
await expect(dialog.getByRole("heading", { name: "Appearance" })).toBeVisible()

await expect(dialog.locator('[data-action="settings-new-layout-designs"]')).toHaveCount(0)
await expect(dialog.getByText("New layout")).toHaveCount(0)
await expect(dialog.locator('[data-action="settings-theme"]')).toHaveCount(0)
// Light/dark survives as the only appearance choice.
await expect(dialog.locator('[data-action="settings-color-scheme"]')).toBeVisible()
})
41 changes: 0 additions & 41 deletions packages/app/e2e/regression/legacy-new-session.spec.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ test.describe("session timeline projection", () => {
await expect(page.locator('[data-timeline-row="TurnGap"]')).toBeVisible()
})

test("renders comment strips and historical diff summary overflow", async ({ page }) => {
test("renders inline comments and historical diff summary overflow", async ({ page }) => {
const user = userMessage(
[
userText("The user made the following comment regarding lines 4 through 8 of src/a.ts: Keep this stable", {
Expand All @@ -154,12 +154,13 @@ test.describe("session timeline projection", () => {
})
await setupTimeline(page, {
messages: [user, assistantMessage(), nextUser, nextAssistant],
settings: { newLayoutDesigns: false },
})
const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
await scroller.evaluate((element) => (element.scrollTop = 0))

await expect(page.locator('[data-timeline-row="CommentStrip"]')).toBeVisible()
// Comments render inline on the user message; the separate CommentStrip row
// was only ever produced by the retired layout.
await expect(page.locator('[data-timeline-row="CommentStrip"]')).toHaveCount(0)
await expect(page.getByText("Keep this stable", { exact: true })).toBeVisible()
await expect(page.locator('[data-timeline-row="DiffSummary"]')).toBeVisible()
await expect(page.getByText(/show all/i)).toBeVisible()
Expand Down
31 changes: 9 additions & 22 deletions packages/app/public/oc-theme-preload.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
;(function () {
var key = "opencode-theme-id"
var themeId = localStorage.getItem(key) || "oc-2"

if (themeId === "oc-1") {
themeId = "oc-2"
localStorage.setItem(key, themeId)
// The app ships a single theme (light/dark is the colour-scheme axis, not a
// second theme). Any other stored id is a leftover from before the picker was
// removed, so rewrite it and drop the cached palette here rather than after
// mount — otherwise the window paints the old theme for a frame on the first
// launch after the upgrade. With no other theme reachable there is no cached
// CSS left to inject, so the preload only has to set the mode attributes.
if (localStorage.getItem(key) !== "oc-2") {
localStorage.setItem(key, "oc-2")
localStorage.removeItem("opencode-theme-css-light")
localStorage.removeItem("opencode-theme-css-dark")
}
Expand All @@ -13,28 +17,11 @@
var isDark = scheme === "dark" || (scheme === "system" && matchMedia("(prefers-color-scheme: dark)").matches)
var mode = isDark ? "dark" : "light"

document.documentElement.dataset.theme = themeId
document.documentElement.dataset.theme = "oc-2"
document.documentElement.dataset.colorScheme = mode
document.documentElement.style.backgroundColor = isDark ? "#080808" : "#fafafa"

// Update theme-color meta tag to match app color scheme
var metas = document.querySelectorAll("meta[name='theme-color']")
if (metas.length > 0) metas[0].setAttribute("content", isDark ? "#080808" : "#fafafa")

if (themeId === "oc-2") return

var css = localStorage.getItem("opencode-theme-css-" + mode)
if (css) {
var style = document.createElement("style")
style.id = "oc-theme-preload"
style.textContent =
":root{color-scheme:" +
mode +
";--text-mix-blend-mode:" +
(isDark ? "plus-lighter" : "multiply") +
";" +
css +
"}"
document.head.appendChild(style)
}
})()
Loading
Loading