Skip to content

Commit 2943f2d

Browse files
authored
Merge pull request #24 from spencerkit/develop
Merge develop into main
2 parents fa99f12 + 99a940c commit 2943f2d

121 files changed

Lines changed: 3419 additions & 598 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/tidy-icons-sing.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"@spencer-kit/coder-studio": patch
2+
---
3+
4+
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.

.github/workflows/ci.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,51 @@ jobs:
4747

4848
- name: Run production build
4949
run: pnpm ci:build
50+
51+
windows-runtime-verify:
52+
name: Windows runtime verification
53+
runs-on: windows-latest
54+
permissions:
55+
contents: read
56+
57+
steps:
58+
- name: Checkout repository
59+
uses: actions/checkout@v4
60+
61+
- name: Setup pnpm
62+
uses: pnpm/action-setup@v4
63+
with:
64+
version: 10.33.2
65+
run_install: false
66+
67+
- name: Setup Node.js
68+
uses: actions/setup-node@v4
69+
with:
70+
node-version: "24"
71+
cache: "pnpm"
72+
73+
- name: Install dependencies
74+
run: pnpm install --frozen-lockfile
75+
76+
# Windows CI now proves that our non-PTY subprocess paths pass `windowsHide: true`
77+
# and that provider/session PTY startup still works with the existing cross-platform
78+
# argv contract. It does not visually prove the absence of a transient desktop
79+
# console flash from node-pty itself; that still requires a manual smoke check on
80+
# a real Windows desktop.
81+
- name: Run targeted Windows provider tests
82+
run: pnpm --filter @coder-studio/providers exec vitest run src/claude/definition.test.ts src/codex/definition.test.ts
83+
84+
- name: Run targeted Windows server tests
85+
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
86+
87+
- name: Run targeted Windows CLI tests
88+
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
89+
90+
- name: Build web assets for CLI packaging
91+
run: pnpm build:web
92+
93+
- name: Build server package
94+
run: pnpm --filter @coder-studio/server build
95+
96+
- name: Build CLI package
97+
run: pnpm --filter @spencer-kit/coder-studio build

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Coder Studio lets you launch an AI coding workspace on your machine and keep usi
1010

1111
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.
1212

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

1515
## Why Coder Studio
1616

README.zh-CN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Coder Studio 让你把 AI coding workspace 启动在自己的机器上,然后
1010

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

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

1515
## 为什么是 Coder Studio
1616

1.73 MB
Loading
-81.6 KB
Binary file not shown.

e2e/fixtures/i18n.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { describe, expect, it } from "vitest";
2+
import { translateForE2E } from "./i18n.js";
3+
4+
describe("translateForE2E", () => {
5+
it("defaults to the app default locale for welcome copy", () => {
6+
expect(translateForE2E("welcome.kicker")).toBe("开始使用");
7+
expect(translateForE2E("action.open_workspace")).toBe("打开工作区");
8+
});
9+
10+
it("can resolve English strings when requested", () => {
11+
expect(translateForE2E("welcome.kicker", "en")).toBe("GET STARTED");
12+
expect(translateForE2E("workspace.launch.title", "en")).toBe("Open Workspace");
13+
});
14+
15+
it("interpolates variables", () => {
16+
expect(translateForE2E("workspace.launch.items_count", "zh", { count: 3 })).toBe("3 项");
17+
});
18+
});

e2e/fixtures/i18n.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { readFileSync } from "node:fs";
2+
3+
function readLocale(path: string) {
4+
return JSON.parse(readFileSync(new URL(path, import.meta.url), "utf8")) as Record<
5+
string,
6+
unknown
7+
>;
8+
}
9+
10+
export const E2E_LOCALES = {
11+
en: readLocale("../../packages/web/src/locales/en.json"),
12+
zh: readLocale("../../packages/web/src/locales/zh.json"),
13+
} as const;
14+
15+
export type E2ELocaleCode = keyof typeof E2E_LOCALES;
16+
17+
type NestedKeyOf<T> = T extends object
18+
? {
19+
[K in keyof T]: K extends string
20+
? T[K] extends object
21+
? `${K}.${NestedKeyOf<T[K]>}`
22+
: K
23+
: never;
24+
}[keyof T]
25+
: never;
26+
27+
export type E2ETranslationKey = NestedKeyOf<(typeof E2E_LOCALES)["zh"]>;
28+
29+
function getNestedValue(obj: unknown, path: string): string | undefined {
30+
const parts = path.split(".");
31+
let current: unknown = obj;
32+
33+
for (const part of parts) {
34+
if (current === null || current === undefined) return undefined;
35+
if (typeof current !== "object") return undefined;
36+
current = (current as Record<string, unknown>)[part];
37+
}
38+
39+
return typeof current === "string" ? current : undefined;
40+
}
41+
42+
export function translateForE2E(
43+
key: E2ETranslationKey,
44+
locale: E2ELocaleCode = "zh",
45+
params?: Record<string, string | number>
46+
): string {
47+
let text = getNestedValue(E2E_LOCALES[locale], key);
48+
49+
if (text === undefined) {
50+
throw new Error(`Missing translation for key: ${key}`);
51+
}
52+
53+
if (params) {
54+
for (const [name, value] of Object.entries(params)) {
55+
text = text.replace(new RegExp(`\\{${name}\\}`, "g"), String(value));
56+
}
57+
}
58+
59+
return text;
60+
}

e2e/fixtures/phase1-i18n.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { expect, type Locator, type Page } from "@playwright/test";
2+
import { translateForE2E } from "./i18n.js";
3+
4+
export async function expectWelcomeCopy(page: Page): Promise<void> {
5+
await expect(page.locator(".welcome-kicker")).toHaveText(translateForE2E("welcome.kicker"));
6+
await expect(page.locator(".welcome-title")).toHaveText(translateForE2E("welcome.title"));
7+
await expect(page.locator(".welcome-body")).toContainText(translateForE2E("welcome.description"));
8+
}
9+
10+
export async function expectOpenWorkspaceButton(locator: Locator): Promise<void> {
11+
await expect(locator).toBeVisible();
12+
await expect(locator.locator("span")).toContainText(translateForE2E("action.open_workspace"));
13+
}
14+
15+
export async function expectSettingsButton(locator: Locator): Promise<void> {
16+
await expect(locator).toBeVisible();
17+
await expect(locator.locator("span")).toContainText(translateForE2E("action.settings"));
18+
}

e2e/fixtures/phase2-i18n.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { type Page } from "@playwright/test";
2+
import { type E2ELocaleCode, translateForE2E } from "./i18n.js";
3+
4+
type SettingsSection = "general" | "appearance" | "providers" | "shortcuts";
5+
type ProviderSettingLabel =
6+
| "base"
7+
| "config_file"
8+
| "open_config_file_editor"
9+
| "back_to_base"
10+
| "startup_args";
11+
type SettingsGroupLabel = "notifications" | "theme" | "language";
12+
type ConfigFileLabel = "claude" | "codex";
13+
14+
const SETTINGS_SECTION_KEYS: Record<SettingsSection, Parameters<typeof translateForE2E>[0]> = {
15+
general: "settings.general",
16+
appearance: "settings.appearance",
17+
providers: "settings.providers",
18+
shortcuts: "settings.shortcuts.title",
19+
};
20+
21+
const PROVIDER_SETTING_KEYS: Record<ProviderSettingLabel, Parameters<typeof translateForE2E>[0]> = {
22+
base: "settings.provider.base",
23+
config_file: "settings.provider.config_file",
24+
open_config_file_editor: "settings.provider.open_config_file_editor",
25+
back_to_base: "settings.provider.back_to_base",
26+
startup_args: "settings.provider.startup_args",
27+
};
28+
29+
const SETTINGS_GROUP_KEYS: Record<SettingsGroupLabel, Parameters<typeof translateForE2E>[0]> = {
30+
notifications: "settings.notifications",
31+
theme: "settings.theme.title",
32+
language: "settings.language.title",
33+
};
34+
35+
const CONFIG_FILE_KEYS: Record<ConfigFileLabel, Parameters<typeof translateForE2E>[0]> = {
36+
claude: "settings.config_files.claude_config",
37+
codex: "settings.config_files.codex_config",
38+
};
39+
40+
export const AUTH_PREVIEW_URL = new URL("../../packages/web/auth-preview.html", import.meta.url)
41+
.href;
42+
43+
function escapeRegExp(value: string): string {
44+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
45+
}
46+
47+
function localizedPattern(
48+
key: Parameters<typeof translateForE2E>[0],
49+
params?: Record<string, string | number>
50+
): RegExp {
51+
const en = translateForE2E(key, "en", params);
52+
const zh = translateForE2E(key, "zh", params);
53+
const values = [...new Set([en, zh])].map(escapeRegExp);
54+
return new RegExp(`^(?:${values.join("|")})$`);
55+
}
56+
57+
export function settingsSectionLabel(
58+
section: SettingsSection,
59+
locale: E2ELocaleCode = "zh"
60+
): string {
61+
return translateForE2E(SETTINGS_SECTION_KEYS[section], locale);
62+
}
63+
64+
export function settingsSectionPattern(section: SettingsSection): RegExp {
65+
return localizedPattern(SETTINGS_SECTION_KEYS[section]);
66+
}
67+
68+
export async function openSettingsSection(
69+
page: Page,
70+
section: SettingsSection,
71+
locale?: E2ELocaleCode
72+
): Promise<void> {
73+
await page
74+
.getByRole("button", {
75+
name: locale ? settingsSectionLabel(section, locale) : settingsSectionPattern(section),
76+
})
77+
.click();
78+
}
79+
80+
export function providerSettingLabel(
81+
label: ProviderSettingLabel,
82+
locale: E2ELocaleCode = "zh"
83+
): string {
84+
return translateForE2E(PROVIDER_SETTING_KEYS[label], locale);
85+
}
86+
87+
export function providerSettingPattern(label: ProviderSettingLabel): RegExp {
88+
return localizedPattern(PROVIDER_SETTING_KEYS[label]);
89+
}
90+
91+
export function settingsGroupLabel(
92+
label: SettingsGroupLabel,
93+
locale: E2ELocaleCode = "zh"
94+
): string {
95+
return translateForE2E(SETTINGS_GROUP_KEYS[label], locale);
96+
}
97+
98+
export function settingsGroupPattern(label: SettingsGroupLabel): RegExp {
99+
return localizedPattern(SETTINGS_GROUP_KEYS[label]);
100+
}
101+
102+
export function configFileLabel(label: ConfigFileLabel, locale: E2ELocaleCode = "zh"): string {
103+
return translateForE2E(CONFIG_FILE_KEYS[label], locale);
104+
}
105+
106+
export function configFilePattern(label: ConfigFileLabel): RegExp {
107+
return localizedPattern(CONFIG_FILE_KEYS[label]);
108+
}

0 commit comments

Comments
 (0)