Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
5990e92
docs: add web ui migration inventory
May 6, 2026
2609805
refactor: centralize web viewport hook
May 6, 2026
9a2cedc
feat: add web button primitive
May 6, 2026
17142b7
fix: preserve legacy button compatibility classes
May 6, 2026
2262ebd
refactor: adopt button in login page
May 6, 2026
2d5ec67
fix: restore web ui typing support
May 6, 2026
ddf71de
Align button module with legacy btn styles
May 7, 2026
74e78b4
test: cover windows child process options
May 6, 2026
bdc6213
test: fix windows runtime red tests
May 6, 2026
5a22341
fix: hide windows console windows for background subprocesses
May 6, 2026
afaaffc
fix: preserve windowsHide execFile options contract
May 6, 2026
b56add7
test: cover provider install execFile wiring
May 6, 2026
61e2e69
test: lock provider launch parity across platforms
May 6, 2026
7054c19
test: lock provider launch parity across platforms
May 6, 2026
21fe164
ci: add windows runtime verification
May 6, 2026
d16ea70
ci: fix windows runtime verification lane
May 6, 2026
c4bdb07
test: cover exec-file in windows verification
May 7, 2026
e96abbc
Fix Windows provider install wiring test
May 7, 2026
8d27e65
fix: resolve windows npm executable before provider install
May 7, 2026
5ff3da4
Harden script process spawning and localize e2e assertions
May 7, 2026
ee95b9c
Fix Windows pnpm process spawning
May 7, 2026
a267597
Use spawn for provider runtime commands
May 7, 2026
091c50c
fix: route Windows .cmd shims through shell and default windowsHide t…
pallyoung May 7, 2026
92fa992
refactor: extract isDirectExecution + windows-shim helpers into @code…
pallyoung May 7, 2026
0d6b849
fix: unwrap windows npm cmd-shims before pty.spawn so codex starts
pallyoung May 7, 2026
02ba250
fix: source app version from cli package
pallyoung May 7, 2026
96d820e
Merge remote-tracking branch 'origin/main' into develop
pallyoung May 7, 2026
688181b
test: avoid hardcoded cli version in server config
pallyoung May 7, 2026
2c8609e
Merge branch 'feature/ui-component-library-phase-a-bootstrap-button' …
pallyoung May 7, 2026
7ee92a2
fix: resolve app version in server via esm url
pallyoung May 7, 2026
04cf136
Fix workspace title display names
pallyoung May 7, 2026
6ad533d
docs(cli): refine published package copy
pallyoung May 7, 2026
bc5ef99
fix(cli): wait for managed server shutdown
pallyoung May 7, 2026
f23d942
fix(cli): stabilize pm2 session handling
pallyoung May 7, 2026
b1e9075
docs: replace README workspace screenshot
pallyoung May 7, 2026
a4a8dc1
Fix fullscreen fallback and iOS detection
pallyoung May 7, 2026
5ec7489
Update terminal replay overlay copy test
pallyoung May 7, 2026
05f29fe
fix(ci): satisfy lint checks
pallyoung May 7, 2026
f7b78ce
feat(web): replace favicon with brand icon
pallyoung May 7, 2026
8250548
fix(web): regenerate favicon assets from svg
pallyoung May 7, 2026
23ca9a6
chore: add changeset for favicon fix
pallyoung May 7, 2026
99a940c
test: stabilize terminal and favicon checks
pallyoung May 7, 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
4 changes: 4 additions & 0 deletions .changeset/tidy-icons-sing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"@spencer-kit/coder-studio": patch
---

Fix the bundled web favicon assets by regenerating the PNG and ICO files directly from the SVG source so the icon keeps transparent edges without the visible border artifact.
48 changes: 48 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,51 @@ jobs:

- name: Run production build
run: pnpm ci:build

windows-runtime-verify:
name: Windows runtime verification
runs-on: windows-latest
permissions:
contents: read

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.33.2
run_install: false

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "24"
cache: "pnpm"

- name: Install dependencies
run: pnpm install --frozen-lockfile

# Windows CI now proves that our non-PTY subprocess paths pass `windowsHide: true`
# and that provider/session PTY startup still works with the existing cross-platform
# argv contract. It does not visually prove the absence of a transient desktop
# console flash from node-pty itself; that still requires a manual smoke check on
# a real Windows desktop.
- name: Run targeted Windows provider tests
run: pnpm --filter @coder-studio/providers exec vitest run src/claude/definition.test.ts src/codex/definition.test.ts

- name: Run targeted Windows server tests
run: pnpm --filter @coder-studio/server exec vitest run src/__tests__/provider-runtime/command-check.test.ts src/__tests__/provider-runtime/command-runner.test.ts src/__tests__/provider-runtime/install-manager.test.ts src/__tests__/workspace/runtime-check.test.ts src/git/cli.windows.test.ts src/supervisor/evaluator.windows.test.ts src/__tests__/server-provider-install-wiring.test.ts src/__tests__/session-commands.test.ts src/__tests__/session-integration.test.ts

- name: Run targeted Windows CLI tests
run: pnpm --filter @spencer-kit/coder-studio exec vitest run src/browser.test.ts src/bin.test.ts src/pm2-control.test.ts src/server-control.test.ts

- name: Build web assets for CLI packaging
run: pnpm build:web

- name: Build server package
run: pnpm --filter @coder-studio/server build

- name: Build CLI package
run: pnpm --filter @spencer-kit/coder-studio build
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Coder Studio lets you launch an AI coding workspace on your machine and keep usi

Start a task in the office, check progress on your phone during the commute, review changes from a tablet, and continue on a laptop later. Same workspace, same context, no environment handoff.

![Workspace](docs/help/assets/screenshot-workspace.png)
![Workspace](docs/help/assets/screenshot-workspace-overview.png)

## Why Coder Studio

Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Coder Studio 让你把 AI coding workspace 启动在自己的机器上,然后

你可以在办公室发起任务,在通勤路上用手机看进度,在外面用平板审阅改动,回到家再用另一台电脑继续接着做。还是同一个 workspace,还是同一份上下文,不需要重新接管环境。

![工作区界面](docs/help/assets/screenshot-workspace.png)
![工作区界面](docs/help/assets/screenshot-workspace-overview.png)

## 为什么是 Coder Studio

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed docs/help/assets/screenshot-workspace.png
Binary file not shown.
18 changes: 18 additions & 0 deletions e2e/fixtures/i18n.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { describe, expect, it } from "vitest";
import { translateForE2E } from "./i18n.js";

describe("translateForE2E", () => {
it("defaults to the app default locale for welcome copy", () => {
expect(translateForE2E("welcome.kicker")).toBe("开始使用");
expect(translateForE2E("action.open_workspace")).toBe("打开工作区");
});

it("can resolve English strings when requested", () => {
expect(translateForE2E("welcome.kicker", "en")).toBe("GET STARTED");
expect(translateForE2E("workspace.launch.title", "en")).toBe("Open Workspace");
});

it("interpolates variables", () => {
expect(translateForE2E("workspace.launch.items_count", "zh", { count: 3 })).toBe("3 项");
});
});
60 changes: 60 additions & 0 deletions e2e/fixtures/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { readFileSync } from "node:fs";

function readLocale(path: string) {
return JSON.parse(readFileSync(new URL(path, import.meta.url), "utf8")) as Record<
string,
unknown
>;
}

export const E2E_LOCALES = {
en: readLocale("../../packages/web/src/locales/en.json"),
zh: readLocale("../../packages/web/src/locales/zh.json"),
} as const;

export type E2ELocaleCode = keyof typeof E2E_LOCALES;

type NestedKeyOf<T> = T extends object
? {
[K in keyof T]: K extends string
? T[K] extends object
? `${K}.${NestedKeyOf<T[K]>}`
: K
: never;
}[keyof T]
: never;

export type E2ETranslationKey = NestedKeyOf<(typeof E2E_LOCALES)["zh"]>;

function getNestedValue(obj: unknown, path: string): string | undefined {
const parts = path.split(".");
let current: unknown = obj;

for (const part of parts) {
if (current === null || current === undefined) return undefined;
if (typeof current !== "object") return undefined;
current = (current as Record<string, unknown>)[part];
}

return typeof current === "string" ? current : undefined;
}

export function translateForE2E(
key: E2ETranslationKey,
locale: E2ELocaleCode = "zh",
params?: Record<string, string | number>
): string {
let text = getNestedValue(E2E_LOCALES[locale], key);

if (text === undefined) {
throw new Error(`Missing translation for key: ${key}`);
}

if (params) {
for (const [name, value] of Object.entries(params)) {
text = text.replace(new RegExp(`\\{${name}\\}`, "g"), String(value));
}
}

return text;
}
18 changes: 18 additions & 0 deletions e2e/fixtures/phase1-i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { expect, type Locator, type Page } from "@playwright/test";
import { translateForE2E } from "./i18n.js";

export async function expectWelcomeCopy(page: Page): Promise<void> {
await expect(page.locator(".welcome-kicker")).toHaveText(translateForE2E("welcome.kicker"));
await expect(page.locator(".welcome-title")).toHaveText(translateForE2E("welcome.title"));
await expect(page.locator(".welcome-body")).toContainText(translateForE2E("welcome.description"));
}

export async function expectOpenWorkspaceButton(locator: Locator): Promise<void> {
await expect(locator).toBeVisible();
await expect(locator.locator("span")).toContainText(translateForE2E("action.open_workspace"));
}

export async function expectSettingsButton(locator: Locator): Promise<void> {
await expect(locator).toBeVisible();
await expect(locator.locator("span")).toContainText(translateForE2E("action.settings"));
}
108 changes: 108 additions & 0 deletions e2e/fixtures/phase2-i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { type Page } from "@playwright/test";
import { type E2ELocaleCode, translateForE2E } from "./i18n.js";

type SettingsSection = "general" | "appearance" | "providers" | "shortcuts";
type ProviderSettingLabel =
| "base"
| "config_file"
| "open_config_file_editor"
| "back_to_base"
| "startup_args";
type SettingsGroupLabel = "notifications" | "theme" | "language";
type ConfigFileLabel = "claude" | "codex";

const SETTINGS_SECTION_KEYS: Record<SettingsSection, Parameters<typeof translateForE2E>[0]> = {
general: "settings.general",
appearance: "settings.appearance",
providers: "settings.providers",
shortcuts: "settings.shortcuts.title",
};

const PROVIDER_SETTING_KEYS: Record<ProviderSettingLabel, Parameters<typeof translateForE2E>[0]> = {
base: "settings.provider.base",
config_file: "settings.provider.config_file",
open_config_file_editor: "settings.provider.open_config_file_editor",
back_to_base: "settings.provider.back_to_base",
startup_args: "settings.provider.startup_args",
};

const SETTINGS_GROUP_KEYS: Record<SettingsGroupLabel, Parameters<typeof translateForE2E>[0]> = {
notifications: "settings.notifications",
theme: "settings.theme.title",
language: "settings.language.title",
};

const CONFIG_FILE_KEYS: Record<ConfigFileLabel, Parameters<typeof translateForE2E>[0]> = {
claude: "settings.config_files.claude_config",
codex: "settings.config_files.codex_config",
};

export const AUTH_PREVIEW_URL = new URL("../../packages/web/auth-preview.html", import.meta.url)
.href;

function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

function localizedPattern(
key: Parameters<typeof translateForE2E>[0],
params?: Record<string, string | number>
): RegExp {
const en = translateForE2E(key, "en", params);
const zh = translateForE2E(key, "zh", params);
const values = [...new Set([en, zh])].map(escapeRegExp);
return new RegExp(`^(?:${values.join("|")})$`);
}

export function settingsSectionLabel(
section: SettingsSection,
locale: E2ELocaleCode = "zh"
): string {
return translateForE2E(SETTINGS_SECTION_KEYS[section], locale);
}

export function settingsSectionPattern(section: SettingsSection): RegExp {
return localizedPattern(SETTINGS_SECTION_KEYS[section]);
}

export async function openSettingsSection(
page: Page,
section: SettingsSection,
locale?: E2ELocaleCode
): Promise<void> {
await page
.getByRole("button", {
name: locale ? settingsSectionLabel(section, locale) : settingsSectionPattern(section),
})
.click();
}

export function providerSettingLabel(
label: ProviderSettingLabel,
locale: E2ELocaleCode = "zh"
): string {
return translateForE2E(PROVIDER_SETTING_KEYS[label], locale);
}

export function providerSettingPattern(label: ProviderSettingLabel): RegExp {
return localizedPattern(PROVIDER_SETTING_KEYS[label]);
}

export function settingsGroupLabel(
label: SettingsGroupLabel,
locale: E2ELocaleCode = "zh"
): string {
return translateForE2E(SETTINGS_GROUP_KEYS[label], locale);
}

export function settingsGroupPattern(label: SettingsGroupLabel): RegExp {
return localizedPattern(SETTINGS_GROUP_KEYS[label]);
}

export function configFileLabel(label: ConfigFileLabel, locale: E2ELocaleCode = "zh"): string {
return translateForE2E(CONFIG_FILE_KEYS[label], locale);
}

export function configFilePattern(label: ConfigFileLabel): RegExp {
return localizedPattern(CONFIG_FILE_KEYS[label]);
}
7 changes: 3 additions & 4 deletions e2e/specs/phase1/agent-session.spec.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import { expect, test } from "@playwright/test";
import { expectOpenWorkspaceButton, expectWelcomeCopy } from "../../fixtures/phase1-i18n";

test.describe("@phase1 agent session acceptance", () => {
test("F1-06 start session", async ({ page }) => {
await page.goto("/");
// Welcome page should render correctly
await expect(page.locator(".welcome-container")).toBeVisible();
await expect(page.locator(".welcome-kicker")).toHaveText("GET STARTED");
await expect(page.locator(".welcome-title")).toBeVisible();
await expectWelcomeCopy(page);
});

test("F1-07 send prompt", async ({ page }) => {
await page.goto("/");
// Check welcome page elements
const openBtn = page.locator(".welcome-btn");
await expect(openBtn).toBeVisible();
await expect(openBtn.locator("span")).toContainText("Open Workspace");
await expectOpenWorkspaceButton(openBtn);
});

test("F1-08 receive response", async ({ page }) => {
Expand Down
5 changes: 3 additions & 2 deletions e2e/specs/phase1/data-integrity.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect, test } from "@playwright/test";
import { expectWelcomeCopy } from "../../fixtures/phase1-i18n";

test.describe("@phase1 data integrity acceptance", () => {
test("F1-37 file persistence", async ({ page }) => {
Expand All @@ -9,8 +10,8 @@ test.describe("@phase1 data integrity acceptance", () => {

test("F1-38 session persistence", async ({ page }) => {
await page.goto("/");
// Check kicker
await expect(page.locator(".welcome-kicker")).toHaveText("GET STARTED");
// Check translated welcome copy
await expectWelcomeCopy(page);
});

test("F1-39 terminal replay", async ({ page }) => {
Expand Down
6 changes: 3 additions & 3 deletions e2e/specs/phase1/editor.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect, test } from "@playwright/test";
import { expectWelcomeCopy } from "../../fixtures/phase1-i18n";

test.describe("@phase1 editor acceptance", () => {
test("F1-11 open file", async ({ page }) => {
Expand All @@ -9,9 +10,8 @@ test.describe("@phase1 editor acceptance", () => {

test("F1-12 edit content", async ({ page }) => {
await page.goto("/");
// Check welcome body text
const body = page.locator(".welcome-body");
await expect(body).toContainText("A local-first AI coding workbench.");
// Check translated welcome copy
await expectWelcomeCopy(page);
});

test("F1-13 save file", async ({ page }) => {
Expand Down
5 changes: 3 additions & 2 deletions e2e/specs/phase1/focus-mode.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect, test } from "@playwright/test";
import { expectWelcomeCopy } from "../../fixtures/phase1-i18n";

test.describe("@phase1 focus mode acceptance", () => {
test("F1-27 enter focus", async ({ page }) => {
Expand All @@ -9,7 +10,7 @@ test.describe("@phase1 focus mode acceptance", () => {

test("F1-28 exit focus", async ({ page }) => {
await page.goto("/");
// Check kicker text
await expect(page.locator(".welcome-kicker")).toHaveText("GET STARTED");
// Check translated welcome copy
await expectWelcomeCopy(page);
});
});
5 changes: 3 additions & 2 deletions e2e/specs/phase1/git.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect, test } from "@playwright/test";
import { expectWelcomeCopy } from "../../fixtures/phase1-i18n";

test.describe("@phase1 git acceptance", () => {
test("F1-16 view status", async ({ page }) => {
Expand All @@ -9,8 +10,8 @@ test.describe("@phase1 git acceptance", () => {

test("F1-17 view diff", async ({ page }) => {
await page.goto("/");
// Check welcome elements
await expect(page.locator(".welcome-kicker")).toHaveText("GET STARTED");
// Check translated welcome copy
await expectWelcomeCopy(page);
});

test("F1-18 commit", async ({ page }) => {
Expand Down
5 changes: 3 additions & 2 deletions e2e/specs/phase1/visual-components.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect, test } from "@playwright/test";
import { expectWelcomeCopy } from "../../fixtures/phase1-i18n";

/**
* Phase 1 Visual Acceptance Tests: Core Components
Expand All @@ -16,8 +17,8 @@ test.describe("@phase1 visual acceptance", () => {

test("V1-05 workspace panel baseline", async ({ page }) => {
await page.goto("/");
// Welcome kicker should be present
await expect(page.locator(".welcome-kicker")).toHaveText("GET STARTED");
// Welcome copy should be present in the active locale
await expectWelcomeCopy(page);
});

test("V1-06 agent pane baseline", async ({ page }) => {
Expand Down
Loading
Loading