Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6b91b25
feat: persist terminal copy-on-select setting
pallyoung May 10, 2026
e5d13e8
feat: hydrate terminal preferences at app startup
pallyoung May 10, 2026
62b5e13
test: isolate terminal preference hydration state
pallyoung May 10, 2026
a8c4fd9
feat: add settings UI for terminal copy-on-select
pallyoung May 10, 2026
3621948
fix: preserve terminal copy-on-select sync on stale loads
pallyoung May 10, 2026
ca1bfd7
docs: refresh workspace overview screenshot
pallyoung May 10, 2026
3873db4
Merge remote-tracking branch 'origin/main' into develop
pallyoung May 10, 2026
4fefdbd
fix: ignore stale copy-on-select defaults
pallyoung May 10, 2026
2d27c00
fix: use terminal preferences atom for settings sync
pallyoung May 10, 2026
a4d74b6
feat: copy terminal selection on desktop
pallyoung May 10, 2026
ade538d
fix: address review feedback for terminal copy-on-select
pallyoung May 11, 2026
3fcafce
Merge branch 'feat/terminal-copy-on-select' into develop
pallyoung May 11, 2026
5811130
fix: handle xterm selection disposable cleanup
pallyoung May 11, 2026
2fc06ec
Merge branch 'fix/terminal-selection-dispose' into develop
pallyoung May 11, 2026
8261fcb
refactor: move terminal settings into general
pallyoung May 11, 2026
92fe0aa
Merge branch 'feat/move-terminal-settings-general' into develop
pallyoung May 11, 2026
17c6cc5
fix: restore mobile settings detail scrolling
pallyoung May 11, 2026
a4140c0
Merge branch 'fix/mobile-settings-scroll' into develop
pallyoung May 11, 2026
4a04311
feat(web): add isolated e2e ui screenshot suite
pallyoung May 11, 2026
c6ec89d
Merge branch 'feat/e2e-ui' into develop
pallyoung May 11, 2026
9c63cee
fix(web): make e2e-ui command windows compatible
pallyoung May 11, 2026
3f4a395
fix(ws): recover terminal replay after silent disconnects
pallyoung May 11, 2026
3139ef4
chore(changeset): add patch release note for ws recovery
pallyoung May 11, 2026
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
5 changes: 5 additions & 0 deletions .changeset/swift-hounds-divide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@spencer-kit/coder-studio": patch
---

Fix terminal websocket recovery so buffered PTY output is replayed after silent disconnects, including probe-based recovery and keepalive handling.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ data-wl
# E2E test runner scripts (ad-hoc)
e2e/playwright.session.config.ts
e2e/run-session-tests.*
e2e-ui/output/
e2e-ui/test-results/
.playwright-cli/
/superpowers/
/.superpowers/
Expand Down
Binary file modified docs/help/assets/screenshot-workspace-overview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 41 additions & 0 deletions e2e-ui/fixtures/capture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { Locator, Page } from "@playwright/test";

export async function disableAnimations(page: Page) {
await page.emulateMedia({ reducedMotion: "reduce" });
await page.addStyleTag({
content: `
*,
*::before,
*::after {
animation: none !important;
transition: none !important;
caret-color: transparent !important;
}
`,
});
}

export async function waitForFonts(page: Page) {
await page.evaluate(async () => {
if (!document.fonts?.ready) {
return;
}
await document.fonts.ready;
});
}

export async function waitForStableScene(page: Page) {
await disableAnimations(page);
await waitForFonts(page);
await page.waitForTimeout(80);
}

export async function resolveCaptureTarget(page: Page, selector?: string): Promise<Locator | Page> {
if (!selector) {
return page;
}

const target = page.locator(selector).first();
await target.waitFor({ state: "visible" });
return target;
}
37 changes: 37 additions & 0 deletions e2e-ui/fixtures/prefs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { Page } from "@playwright/test";

export interface OpenPreviewSceneArgs {
sceneId: string;
device: "desktop" | "mobile";
theme: "dark" | "light";
locale: "zh" | "en";
}

async function seedPreviewPreferences(
page: Page,
args: Pick<OpenPreviewSceneArgs, "theme" | "locale">
) {
await page.goto("/ui-preview.html", {
waitUntil: "domcontentloaded",
});

await page.evaluate(({ theme, locale }) => {
window.localStorage.setItem("ui.theme", JSON.stringify(theme));
window.localStorage.setItem("ui.locale", JSON.stringify(locale));
}, args);
}

export async function openPreviewScene(page: Page, args: OpenPreviewSceneArgs) {
await seedPreviewPreferences(page, args);

const params = new URLSearchParams({
scene: args.sceneId,
device: args.device,
theme: args.theme,
locale: args.locale,
});

await page.goto(`/ui-preview.html?${params.toString()}`, {
waitUntil: "networkidle",
});
}
88 changes: 88 additions & 0 deletions e2e-ui/fixtures/scene-runner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import fs from "node:fs/promises";
import path from "node:path";
import type { Page } from "@playwright/test";
import type { UiCaptureScene, UiCaptureVariant } from "../scenes";
import { resolveCaptureTarget, waitForStableScene } from "./capture";
import { openPreviewScene } from "./prefs";

const OUTPUT_ROOT = path.resolve(process.cwd(), "output");
const SCREENSHOT_ROOT = path.join(OUTPUT_ROOT, "screenshots");

export interface CaptureSceneVariantArgs {
scene: UiCaptureScene;
variant: UiCaptureVariant;
}

export function buildScreenshotPath(scene: UiCaptureScene, variant: UiCaptureVariant) {
return path.join(
SCREENSHOT_ROOT,
scene.category,
scene.id,
`${variant.device}__${variant.theme}__${variant.locale}.png`
);
}

export async function ensureParentDir(filePath: string) {
await fs.mkdir(path.dirname(filePath), { recursive: true });
}

async function openSettingsSection(
page: Page,
section: NonNullable<UiCaptureScene["settingsSection"]>,
device: UiCaptureVariant["device"]
) {
const sectionOrder = {
general: 0,
providers: 1,
appearance: 2,
shortcuts: 3,
} as const;

const index = sectionOrder[section];

if (device === "mobile") {
await page.locator(".settings-mobile-item").nth(index).click();
await page.locator(".settings-content--mobile, .settings-content").first().waitFor();
return;
}

if (section !== "general") {
await page.locator(".settings-nav-item").nth(index).click();
}
}

export async function captureSceneVariant(page: Page, args: CaptureSceneVariantArgs) {
await openPreviewScene(page, {
sceneId: args.scene.id,
device: args.variant.device,
theme: args.variant.theme,
locale: args.variant.locale,
});

await waitForStableScene(page);

if (args.scene.settingsSection) {
await openSettingsSection(page, args.scene.settingsSection, args.variant.device);
await waitForStableScene(page);
}

const filePath = buildScreenshotPath(args.scene, args.variant);
await ensureParentDir(filePath);

if (args.scene.fullPage) {
await page.screenshot({
path: filePath,
animations: "disabled",
fullPage: true,
scale: "device",
});
return;
}

const target = await resolveCaptureTarget(page, args.scene.targetSelector);
await target.screenshot({
path: filePath,
animations: "disabled",
scale: "device",
});
}
11 changes: 11 additions & 0 deletions e2e-ui/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "@coder-studio/e2e-ui",
"private": true,
"type": "module",
"devDependencies": {
"@playwright/test": "^1.59.1",
"tsx": "^4.21.0",
"typescript": "^6.0.3",
"vitest": "^4.1.5"
}
}
48 changes: 48 additions & 0 deletions e2e-ui/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { defineConfig, devices } from "@playwright/test";

const HOST = "127.0.0.1";
const PORT = 4174;
const BASE_URL = `http://${HOST}:${PORT}`;
const iPhone14 = devices["iPhone 14"];

export default defineConfig({
testDir: "./specs",
fullyParallel: false,
workers: 1,
retries: 0,
timeout: 60000,
reporter: [["list"]],
outputDir: "./test-results",
use: {
baseURL: BASE_URL,
trace: "off",
screenshot: "off",
video: "off",
},
projects: [
{
name: "desktop",
use: {
viewport: { width: 1440, height: 960 },
},
},
{
name: "mobile",
use: {
...iPhone14,
browserName: "chromium",
},
},
],
webServer: {
command: `pnpm --dir ../packages/web exec vite --host ${HOST} --port ${PORT}`,
cwd: ".",
url: `${BASE_URL}/ui-preview.html`,
reuseExistingServer: false,
timeout: 30000,
env: {
...process.env,
NODE_ENV: "development",
},
},
});
51 changes: 51 additions & 0 deletions e2e-ui/report/build-report.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { describe, expect, it } from "vitest";
import { buildManifestEntry, renderReportHtml } from "./build-report";

describe("build-report", () => {
it("builds a manifest entry from a screenshot path", () => {
const entry = buildManifestEntry({
scene: {
id: "welcome",
title: "Welcome",
category: "page",
source: "real-route",
description: "Welcome page",
},
screenshotPath: "screenshots/page/welcome/desktop__dark__zh.png",
variant: {
device: "desktop",
theme: "dark",
locale: "zh",
},
});

expect(entry).toMatchObject({
id: "welcome",
category: "page",
path: "screenshots/page/welcome/desktop__dark__zh.png",
device: "desktop",
theme: "dark",
locale: "zh",
});
});

it("renders a report html shell with filters and grouped scenes", () => {
const html = renderReportHtml([
{
id: "welcome",
title: "Welcome",
category: "page",
source: "real-route",
device: "desktop",
theme: "dark",
locale: "zh",
path: "screenshots/page/welcome/desktop__dark__zh.png",
description: "Welcome page",
},
]);

expect(html).toContain("UI Preview Report");
expect(html).toContain("screenshots/page/welcome/desktop__dark__zh.png");
expect(html).toContain('data-category="page"');
});
});
Loading
Loading