From 8fd3d6f0c43b52fe648eab38595231bf57780342 Mon Sep 17 00:00:00 2001 From: Spencer Date: Sun, 17 May 2026 13:36:34 +0000 Subject: [PATCH 01/63] docs: add mobile remote access guide --- README.md | 2 +- README.zh-CN.md | 2 +- docs/help/README.md | 2 +- docs/help/mobile-guide.md | 171 +++++++++++++++++- ...-activation-phase-3-mobile-continuation.md | 87 ++++----- 5 files changed, 203 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index a6670cfe2..e9bb21d54 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ The same workspace URL works across all devices — interface adapts automatical | [App Overview](docs/help/app-overview.md) | Core concepts and features | | [Provider Setup](docs/help/providers.md) | Claude Code / Codex CLI installation | | [Desktop Guide](docs/help/desktop-guide.md) | PC interface and shortcuts | -| [Mobile Guide](docs/help/mobile-guide.md) | Phone / tablet usage | +| [Mobile & Remote Access Guide](docs/help/mobile-guide.md) | Phone / tablet usage, LAN access, Tailscale/ngrok/Cloudflare Tunnel | | [Common Workflows](docs/help/workflows.md) | Task-based tutorials | | [Troubleshooting](docs/help/troubleshooting.md) | FAQ and known issues | | [CLI Reference](docs/help/cli.md) | Command-line options | diff --git a/README.zh-CN.md b/README.zh-CN.md index 74cf9bd12..c4858ae14 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -119,7 +119,7 @@ coder-studio open | [功能总览](docs/help/app-overview.md) | 核心概念和功能 | | [Provider 配置](docs/help/providers.md) | Claude Code / Codex CLI 安装 | | [桌面端指南](docs/help/desktop-guide.md) | PC 界面和快捷键 | -| [移动端指南](docs/help/mobile-guide.md) | 手机/平板使用 | +| [移动端与远程访问指南](docs/help/mobile-guide.md) | 手机/平板使用、局域网访问、Tailscale/ngrok/Cloudflare Tunnel | | [常用工作流](docs/help/workflows.md) | 任务式教程 | | [故障排除](docs/help/troubleshooting.md) | 常见问题和修复 | | [CLI 参考](docs/help/cli.md) | 命令行选项 | diff --git a/docs/help/README.md b/docs/help/README.md index 49b84e1df..c69bf4b1f 100644 --- a/docs/help/README.md +++ b/docs/help/README.md @@ -14,7 +14,7 @@ ## 我主要在手机上使用 -- [移动端使用指南](mobile-guide.md) — 手机端界面和操作 +- [移动端与远程访问指南](mobile-guide.md) — 手机端界面、局域网访问、Tailscale/ngrok/Cloudflare Tunnel ## 我想了解产品结构 diff --git a/docs/help/mobile-guide.md b/docs/help/mobile-guide.md index a340f1f22..dbe428219 100644 --- a/docs/help/mobile-guide.md +++ b/docs/help/mobile-guide.md @@ -1,18 +1,161 @@ -# 移动端使用指南 +# 移动端与远程访问指南 -这篇文档介绍如何通过手机浏览器使用 Coder Studio。移动端适合查看状态、监控 Agent 进度和轻量操作。 +这篇文档介绍如何通过手机浏览器使用 Coder Studio,以及如何把本机运行的 Coder Studio 安全地开放给其他设备或外网访问。移动端适合查看状态、监控 Agent 进度和轻量操作。 ## 这篇文档解决什么问题 -如何在手机上访问和使用 Coder Studio,包括界面导航、会话管理和文件查看。 +如何在手机上访问和使用 Coder Studio,包括局域网访问、远程访问、界面导航、会话管理和文件查看。 ## 前置条件 - Coder Studio 服务已在电脑上运行(`coder-studio serve` 或 `coder-studio open`) -- 手机和电脑在同一局域网 -- 手机浏览器访问 `http://<电脑IP>:<端口>` +- 如果要从其他设备访问,建议先设置密码:`coder-studio config --password <强密码>` +- 如果要从局域网其他设备访问,服务需要监听非 localhost 地址:`coder-studio config --host 0.0.0.0` +- 修改配置后重启服务:`coder-studio serve --restart` -> 如果不知道本机 IP 和端口,执行 `coder-studio status` 查看 "Listen URL"。 +> 如果不知道本机 IP 和端口,执行 `coder-studio status` 查看监听地址和端口。 + +## 选择访问方式 + +| 场景 | 推荐方式 | 适合谁 | +|------|----------|--------| +| 手机和电脑在同一个 Wi-Fi | 局域网 IP 访问 | 家里、办公室内网、临时查看 | +| 只有自己的设备需要远程访问 | Tailscale | 多设备开发者、长期使用 | +| 临时给外网一个访问地址 | ngrok | 演示、临时测试、Webhook | +| 有自己的域名并想长期暴露 | Cloudflare Tunnel | 固定域名、长期远程访问 | + +不建议直接把 Coder Studio 端口暴露到公网。至少先启用密码,并优先使用 Tailscale、ngrok 或 Cloudflare Tunnel 这类反向隧道/VPN 方案。 + +## 局域网访问 + +局域网访问适合手机和电脑在同一 Wi-Fi 下的情况。 + +### 1. 启用密码 + +```bash +coder-studio config --password <强密码> +``` + +### 2. 监听局域网地址 + +默认如果服务只监听 `localhost`,手机无法访问电脑上的服务。改成监听所有网卡: + +```bash +coder-studio config --host 0.0.0.0 +coder-studio serve --restart +``` + +### 3. 查看地址 + +```bash +coder-studio status +``` + +在手机浏览器中访问: + +```text +http://<电脑局域网IP>:<端口> +``` + +例如: + +```text +http://192.168.1.23:4173 +``` + +### 4. 如果打不开 + +- 确认手机和电脑在同一个 Wi-Fi,且没有开启访客网络隔离 +- 确认电脑防火墙允许该端口的入站连接 +- 确认 Coder Studio 不是只监听 `localhost` +- 如果正在使用 VPN、公司网络或校园网,网络可能禁止设备互访 +- 在电脑上重新执行 `coder-studio status`,确认端口没有变化 + +## Tailscale 访问 + +Tailscale 适合“只让自己的设备访问”。它会把你的电脑、手机、平板加入同一个私有 tailnet,不需要把服务直接暴露到公网。 + +基本流程: + +1. 在电脑和手机上安装并登录 Tailscale +2. 在电脑上启动 Coder Studio +3. 启用密码并重启服务 +4. 在手机上用电脑的 Tailscale IP 或 MagicDNS 名称访问 + +```bash +coder-studio config --password <强密码> +coder-studio config --host 0.0.0.0 +coder-studio serve --restart +tailscale status +``` + +然后在手机浏览器访问: + +```text +http://<电脑的 Tailscale IP>:<端口> +``` + +如果你希望使用 Tailscale 的 HTTPS/Serve/Funnel 能力,请参考官方文档: + +- Tailscale Serve:适合 tailnet 内设备访问 +- Tailscale Funnel:适合公开到互联网,风险更高,启用前确认访问控制和认证 + +## ngrok 临时外网访问 + +ngrok 适合临时把本机服务暴露给外网,例如演示或测试。它会生成一个公开 HTTPS 地址,并转发到本机端口。 + +先确认端口: + +```bash +coder-studio status +``` + +假设 Coder Studio 运行在 `4173`: + +```bash +ngrok http 4173 +``` + +ngrok 会显示一个 `https://...ngrok...` 地址。用手机打开这个地址即可。 + +使用 ngrok 前请注意: + +- 一定先设置 Coder Studio 密码 +- 不要把生成的公网地址发给不可信的人 +- 演示结束后停止 ngrok 进程 +- 免费/付费能力、固定域名和访问控制以 ngrok 官方说明为准 + +## Cloudflare Tunnel 长期外网访问 + +Cloudflare Tunnel 适合你有自己的域名,并希望长期通过 HTTPS 访问本机服务。它通过 `cloudflared` 从你的电脑主动连接 Cloudflare,不需要在路由器上开公网入站端口。 + +临时 Quick Tunnel 示例: + +```bash +cloudflared tunnel --url http://localhost:<端口> +``` + +长期使用建议在 Cloudflare Zero Trust 中创建受管理的 Tunnel,并绑定自己的域名,例如: + +```text +https://coder.example.com -> http://localhost:<端口> +``` + +使用 Cloudflare Tunnel 前请注意: + +- 一定先设置 Coder Studio 密码 +- 更推荐配合 Cloudflare Access 或其他身份校验 +- 如果电脑关机、休眠或 `cloudflared` 停止,远程地址也会不可用 +- 固定域名和访问策略以 Cloudflare 官方文档为准 + +## 安全建议 + +- 只在可信网络中使用局域网访问 +- 开放给其他设备前先执行 `coder-studio config --password <强密码>` +- 不要把无密码的 Coder Studio 暴露到公网 +- 不要在公开链接中打开敏感项目,除非你确认认证、网络和访问控制都已配置好 +- 如果只是自己跨设备使用,优先选择 Tailscale,而不是直接公网暴露 +- 临时分享结束后,停止 ngrok、Cloudflare Tunnel 或 Tailscale Funnel ## 移动端界面结构 @@ -89,7 +232,21 @@ 确保 Provider 已正确安装且服务在运行。 **Q:手机上怎么知道访问地址?** -在电脑上执行 `coder-studio status`,找到 "Local URL" 对应的地址。确保手机和电脑在同一网络。 +在电脑上执行 `coder-studio status`,找到当前监听地址和端口。如果手机和电脑在同一局域网,访问 `http://<电脑局域网IP>:<端口>`。如果使用 Tailscale,访问 `http://<电脑 Tailscale IP>:<端口>`。 + +**Q:为什么电脑能打开,手机打不开?** +常见原因是服务只监听 `localhost`、手机和电脑不在同一网络、Wi-Fi 开启了设备隔离,或电脑防火墙阻止端口访问。先执行 `coder-studio config --host 0.0.0.0`,再执行 `coder-studio serve --restart`。 + +**Q:可以直接把端口映射到公网吗?** +不建议。Coder Studio 能操作本机项目、终端和 AI 会话,公网暴露前至少要启用密码。更推荐使用 Tailscale、ngrok 或 Cloudflare Tunnel。 + +## 参考文档 + +- [Tailscale Funnel 文档](https://tailscale.com/docs/features/tailscale-funnel) +- [Tailscale Funnel CLI 参考](https://tailscale.com/docs/reference/tailscale-cli/funnel) +- [ngrok HTTP Endpoints 文档](https://ngrok.com/docs/http/) +- [Cloudflare Tunnel 文档](https://developers.cloudflare.com/tunnel/) +- [Cloudflare Quick Tunnels](https://try.cloudflare.com/) ## 下一步 diff --git a/docs/superpowers/plans/2026-05-14-conversion-first-activation-phase-3-mobile-continuation.md b/docs/superpowers/plans/2026-05-14-conversion-first-activation-phase-3-mobile-continuation.md index dbb744068..de16cbab3 100644 --- a/docs/superpowers/plans/2026-05-14-conversion-first-activation-phase-3-mobile-continuation.md +++ b/docs/superpowers/plans/2026-05-14-conversion-first-activation-phase-3-mobile-continuation.md @@ -6,11 +6,13 @@ > > **Spec:** `docs/superpowers/specs/2026-05-14-conversion-first-activation-design.md` -**Goal:** Productize phone continuation so the user can move from first success on desktop to the next differentiated outcome without reading docs. +**Goal:** Help users continue from desktop to phone or remote devices without overbuilding a low-value in-app assistant. -**Architecture:** Phase 3 builds on the setup success state and the mobile-access command introduced in Phase 1. It adds a reusable mobile assistant surface, exposes it from setup success and settings, and promotes `Continue on Phone` into the workspace shell so cross-device continuation becomes a normal next step instead of hidden product knowledge. +**Decision update:** The original Mobile Access Assistant mostly displayed LAN URLs and auth warnings. That is useful, but too thin to justify a dedicated feature surface right now. The higher-value user need is a clear remote-access guide that covers LAN access, auth, Tailscale, ngrok, Cloudflare Tunnel, and troubleshooting. Treat any product UI as a lightweight documentation entry point until real usage shows that a stateful assistant is needed. -**Tech Stack:** TypeScript, React, Jotai, Vitest, setup/mobile status command, existing workspace shell +**Architecture:** Phase 3 becomes documentation-first. Keep `setup.mobileAccessStatus` available as future infrastructure, but do not add `packages/web/src/features/mobile-access/*`, workspace-shell CTA, or a new setup success assistant in this phase. The product can later add a small settings/help link to the guide if the help surface supports it. + +**Tech Stack:** Markdown docs, existing help center, README links --- @@ -18,82 +20,65 @@ **Depends on:** -- Phase 1 readiness contract -- Phase 2 setup success state +- Existing help center +- Existing CLI config/status commands **Includes master task:** -- [Task 5](./2026-05-14-conversion-first-activation.md#task-5-add-the-mobile-access-assistant-and-continue-on-phone): mobile assistant and cross-device CTA +- Supersedes [Task 5](./2026-05-14-conversion-first-activation.md#task-5-add-the-mobile-access-assistant-and-continue-on-phone) for now. Do not implement the full mobile assistant until this decision is revisited. **Exit criteria:** -- mobile assistant renders candidate LAN URLs -- local-only or unauthenticated exposure states are explained in product language -- setup success shows `Continue on Phone` -- settings exposes the same assistant -- workspace shell can surface the same continuation CTA +- mobile guide explains LAN access from phone +- guide explains why `localhost` is not reachable from another device +- guide recommends enabling password before exposing the service +- guide covers Tailscale, ngrok, and Cloudflare Tunnel at a practical level +- README/help index points users to the guide ## Deliverables -- `packages/web/src/features/mobile-access/*` -- `SetupSuccessStep` -- settings integration for mobile continuation -- workspace-level `Continue on Phone` surface -- localized strings for mobile continuation states +- Update `docs/help/mobile-guide.md` +- Update `docs/help/README.md` +- Update README documentation table entries +- Keep the original in-app assistant as a deferred idea, not Phase 3 scope ## Tracking Checklist -- [ ] Add `useMobileAccess` -- [ ] Render LAN candidate URLs from `setup.mobileAccessStatus` -- [ ] Show the auth warning when only localhost/no password is configured -- [ ] Add `SetupSuccessStep` -- [ ] Show `Continue on Phone` in setup success -- [ ] Expose the same assistant in settings -- [ ] Expose the workspace-shell CTA -- [ ] Pass targeted mobile-access, settings, and workspace tests -- [ ] Commit Phase 3 changes +- [x] Re-evaluate the value of a dedicated Mobile Access Assistant +- [x] Change Phase 3 to documentation-first scope +- [x] Expand the mobile guide with LAN access instructions +- [x] Add Tailscale/ngrok/Cloudflare Tunnel guidance +- [x] Add security and troubleshooting notes +- [x] Update README/help links +- [ ] Commit Phase 3 documentation changes ## Files In Play -- Create: `packages/web/src/features/mobile-access/index.ts` -- Create: `packages/web/src/features/mobile-access/actions/use-mobile-access.ts` -- Create: `packages/web/src/features/mobile-access/views/mobile-access-assistant.tsx` -- Create: `packages/web/src/features/mobile-access/views/mobile-access-assistant.test.tsx` -- Modify: `packages/web/src/features/setup/views/setup-success-step.tsx` -- Modify: `packages/web/src/features/settings/components/settings-page.tsx` -- Modify: `packages/web/src/features/settings/components/settings-page.test.tsx` -- Modify: `packages/web/src/features/workspace/views/desktop/workspace-desktop-view.tsx` -- Create: `packages/web/src/features/workspace/views/desktop/workspace-desktop-view.test.tsx` -- Modify: `packages/web/src/locales/en.json` -- Modify: `packages/web/src/locales/zh.json` +- Modify: `docs/help/mobile-guide.md` +- Modify: `docs/help/README.md` +- Modify: `README.md` +- Modify: `README.zh-CN.md` +- Modify: `docs/superpowers/plans/2026-05-14-conversion-first-activation-phase-3-mobile-continuation.md` ## Verification -Run these before closing the phase: - -```bash -pnpm exec vitest run \ - packages/web/src/features/mobile-access/views/mobile-access-assistant.test.tsx \ - packages/web/src/features/settings/components/settings-page.test.tsx \ - packages/web/src/features/workspace/views/desktop/workspace-desktop-view.test.tsx -``` - -Expected outcome: the assistant is available from setup success and settings, and the workspace shell can promote phone continuation. +No code tests are required for this documentation-only phase. Verify the Markdown renders and the links point to the intended local docs and official tunnel-provider docs. ## Watchouts -- Keep the assistant productized; do not collapse back into doc-like instructions. -- Use the server-reported URLs and auth state directly rather than adding duplicated client inference. -- Avoid over-investing in QR or polish unless the core continuation path is clearly working. +- Do not reintroduce a workspace-shell `Continue on Phone` CTA without evidence that users need it. +- Do not encourage direct public port exposure. +- Keep third-party tunnel instructions practical but not exhaustive; link official docs for provider-specific details. +- If a future in-app assistant is revived, it should do more than static information display: status-aware diagnostics, copy actions, QR, and next-step fixes. ## Detailed Execution Source -Use the detailed step-by-step instructions in the master plan: +The master plan Task 5 is intentionally superseded for this phase: - [Task 5 detailed steps](./2026-05-14-conversion-first-activation.md#task-5-add-the-mobile-access-assistant-and-continue-on-phone) ## Suggested Commit Boundary ```bash -git commit -m "feat: add phone continuation flow" +git commit -m "docs: add mobile remote access guide" ``` From a972952afd8295f69c992bb548fa3b8a67763ee4 Mon Sep 17 00:00:00 2001 From: pallyoung Date: Sun, 17 May 2026 21:06:30 +0800 Subject: [PATCH 02/63] Align terminal shell with session layout --- packages/web/src/styles/components.css | 4 ++-- packages/web/src/styles/components.theme.test.ts | 15 ++++++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/web/src/styles/components.css b/packages/web/src/styles/components.css index 4b78fd525..cfdd6ed6c 100644 --- a/packages/web/src/styles/components.css +++ b/packages/web/src/styles/components.css @@ -6217,7 +6217,7 @@ textarea.input { .workspace-bottom-panel > .bottom-terminal { border: 1px solid color-mix(in srgb, var(--border-light) 64%, var(--accent-blue) 36%); - border-radius: 14px; + border-radius: 0; box-shadow: var(--shadow-lg); overflow: hidden; background: linear-gradient( @@ -7107,7 +7107,7 @@ textarea.input { flex-shrink: 0; min-height: 0; overflow: visible; - padding: 0 14px 14px; + padding: 0 0 14px; background: transparent; border-top: none; } diff --git a/packages/web/src/styles/components.theme.test.ts b/packages/web/src/styles/components.theme.test.ts index 40e5b9f0a..54d66cf3c 100644 --- a/packages/web/src/styles/components.theme.test.ts +++ b/packages/web/src/styles/components.theme.test.ts @@ -236,7 +236,11 @@ describe("components.css theme-sensitive surfaces", () => { const mainStage = getLastRuleBlock(".workspace-main-stage"); const sessionTerminal = getLastRuleBlock(".session-terminal"); const agentPanes = getLastRuleBlock(".workspace-main-stage > .agent-panes"); - const bottomTerminalShell = getLastRuleBlock(".workspace-bottom-panel > .bottom-terminal"); + const bottomPanel = getLastRuleBlock(".workspace-bottom-panel"); + const bottomTerminalShellRules = getRuleBlocksFrom( + stylesheet, + ".workspace-bottom-panel > .bottom-terminal" + ).join("\n"); expect(topbar).toContain("var(--bg-surface)"); expect(activeTab).toContain("var(--bg-active)"); @@ -250,10 +254,15 @@ describe("components.css theme-sensitive surfaces", () => { expect(mainStage).toContain("flex-direction: column"); expect(agentPanes).toContain("flex: 1"); expect(agentPanes).toContain("min-height: 0"); + expect(agentPanes).toContain("padding: 0"); expect(sessionTerminal).toContain("var(--bg-terminal)"); expect(sessionTerminal).not.toContain("rgba(11, 18, 24, 0.98)"); - expect(bottomTerminalShell).toContain("var(--bg-terminal)"); - expect(bottomTerminalShell).not.toContain("rgba(17, 24, 31, 0.96)"); + expect(bottomPanel).toContain("padding: 0 0 14px"); + expect(bottomPanel).not.toContain("padding: 0 14px 14px"); + expect(bottomTerminalShellRules).toContain("var(--bg-terminal)"); + expect(bottomTerminalShellRules).not.toContain("rgba(17, 24, 31, 0.96)"); + expect(bottomTerminalShellRules).toContain("border-radius: 0"); + expect(bottomTerminalShellRules).not.toContain("border-radius: 14px"); }); it("maps desktop chrome blocks to the dedicated desktop layout tokens", () => { From ae8150618fb771cf89d1ea3d4fac8b183294c355 Mon Sep 17 00:00:00 2001 From: pallyoung Date: Sun, 17 May 2026 21:07:14 +0800 Subject: [PATCH 03/63] docs: add terminal font size design spec --- .../2026-05-17-terminal-font-size-design.md | 281 ++++++++++++++++++ 1 file changed, 281 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-17-terminal-font-size-design.md diff --git a/docs/superpowers/specs/2026-05-17-terminal-font-size-design.md b/docs/superpowers/specs/2026-05-17-terminal-font-size-design.md new file mode 100644 index 000000000..a0db9d2b2 --- /dev/null +++ b/docs/superpowers/specs/2026-05-17-terminal-font-size-design.md @@ -0,0 +1,281 @@ +# Terminal Font Size — Design + +Date: 2026-05-17 +Status: Draft +Owner: Codex + +## Problem + +当前产品中的 shell terminal 和 agent session terminal 都复用同一个 [`XtermHost`](../../../packages/web/src/features/terminal-panel/views/shared/xterm-host.tsx),但字号仍写死为 `11`。用户希望在设置页中自定义终端字号,并且该设置要同时作用于主终端和会话终端,而不是分别配置。 + +现有设置链路已经支持通过 `settings.get/settings.update` 读取和保存 `appearance.*` 键,终端相关偏好也已经有一层轻量前端状态 [`terminalPreferencesAtom`](../../../packages/web/src/features/terminal-panel/preferences.ts)。因此本次设计应沿用现有设置架构,在不引入会话级状态的前提下增加一个全局终端字号设置。 + +## Goals + +- 提供一个全局终端字号设置入口。 +- 同时作用于 shell terminal 和 agent session terminal。 +- 设置刷新后持久保留,重启应用后仍然生效。 +- 修改设置后,已打开的终端即时更新字号。 +- 即时生效过程中不销毁终端实例,不清空滚动历史,不触发 replay 重建。 + +## Non-Goals + +- 不支持每个 session 独立字号。 +- 不实现终端内 `Ctrl/Cmd +/-` 临时缩放。 +- 不调整终端 `lineHeight`、字重、字族或配色。 +- 不重构全量 settings store。 +- 不顺带重排设置页分区结构。 + +## User Decisions Captured + +- 只做全局字号,不做会话级覆盖。 +- 设置入口放在“设置”里。 +- 首版使用数值输入,不使用滑杆,也不做 `Small / Default / Large` 档位。 +- 取值范围为 `10-18`,默认值为 `11`。 +- 修改后所有已打开终端都应即时生效。 + +## Approaches Considered + +### Option A: 数值型全局字号(推荐) + +新增 `appearance.terminalFontSize`,设置页用数字输入控件编辑,所有终端读取同一全局值。 + +优点: + +- 最符合用户需求,精度足够。 +- 与现有 `appearance.terminalRenderer` 和 `appearance.terminalCopyOnSelect` 模式一致。 +- 实现范围小,不引入新交互系统。 + +缺点: + +- 需要补充数值校验与非法值回退逻辑。 + +### Option B: 档位型字号 + +只提供 `Small / Default / Large` 三档,内部映射成固定数值。 + +优点: + +- UI 更简单。 +- 测试矩阵更小。 + +缺点: + +- 灵活性不足,容易出现“还差一点”的体验。 +- 与当前已有数值型设置控件风格不一致。 + +### Option C: 全局字号加快捷键缩放 + +在 Option A 基础上再支持终端内快捷缩放。 + +优点: + +- 交互体验最好。 + +缺点: + +- 会引入快捷键冲突、临时状态与持久化策略问题。 +- 明显超出本次“设置里支持字号自定义”的范围。 + +## Final Choice + +采用 Option A。 + +本次只增加一个全局数值设置,沿用现有的服务端 settings 持久化和前端终端偏好状态。这样可以以最小改动覆盖 shell terminal 与 agent session terminal,并保持行为一致。 + +## Final Design + +### 1. 设置模型 + +新增设置键: + +- `appearance.terminalFontSize: number` + +默认值: + +- `11` + +允许范围: + +- 最小值 `10` +- 最大值 `18` +- 仅接受整数 + +解析规则: + +- 缺失值回退到默认值 `11` +- 非数字、非整数、越界值全部按默认值 `11` 处理 + +服务端 schema 也必须约束为同样的整数范围,避免脏数据进入 `user_settings`。 + +### 2. 前端状态同步模型 + +当前 [`terminalPreferencesAtom`](../../../packages/web/src/features/terminal-panel/preferences.ts) 已承载 `copyOnSelect`,本次继续扩展这一轻量状态,而不是引入新的全量 settings store。 + +建议结构: + +- `copyOnSelect: boolean` +- `fontSize: number` + +建议新增的常量和解析函数: + +- `DEFAULT_TERMINAL_FONT_SIZE = 11` +- `MIN_TERMINAL_FONT_SIZE = 10` +- `MAX_TERMINAL_FONT_SIZE = 18` +- `resolveTerminalFontSizeSetting(settings): number` + +加载时机保持与当前终端偏好一致: + +- 应用级 settings 拉取后,将 `appearance.terminalFontSize` 写入 `terminalPreferencesAtom` +- 设置页本地修改后,先更新内存状态,再调用 `settings.update` +- 异步回读 settings 时,沿用当前 `appearanceSelectionVersionRef` 风格,避免用户刚改完字号就被旧值覆盖 + +### 3. 设置页交互 + +设置入口放在当前终端设置组内,与 terminal renderer 和 copy on select 同组展示。 + +注意:虽然存储键仍在 `appearance.*` 命名空间下,但首版不迁移设置页分区位置。这样改动范围最小,也更符合当前用户寻找终端相关设置的路径。 + +控件形态: + +- 复用现有 `Input` 数字输入控件 +- `type="number"` +- `inputMode="numeric"` +- `min={10}` +- `max={18}` +- `step={1}` + +提交模型: + +- 允许临时草稿值 +- 在 `blur` 或按下回车时提交 +- 非法值不保存,回退到上一个已保存值,并显示行内错误 +- 保存失败时也回退到上一个稳定值,并显示错误 + +建议文案: + +- 中文标题:`终端字号` +- 中文说明:`应用到所有终端和会话` +- 英文标题:`Terminal font size` +- 英文说明:`Applied to all terminals and sessions` + +### 4. 终端接入点 + +唯一接入点仍为: + +- [`packages/web/src/features/terminal-panel/views/shared/xterm-host.tsx`](../../../packages/web/src/features/terminal-panel/views/shared/xterm-host.tsx) + +理由: + +- shell terminal 和 agent session terminal 已经共用该组件。 +- 字号是终端宿主的渲染参数,不属于 server PTY、session manager 或 terminal panel 容器职责。 +- 只要 `XtermHost` 读取同一份全局字号,所有终端表现天然一致。 + +### 5. 即时生效模型 + +`XtermHost` 初始化时不再硬编码 `fontSize: 11`,而是改为读取当前全局终端字号。 + +对于已挂载的终端实例,字号变化时不重建 `Terminal` 实例,而是直接更新运行时选项: + +1. 监听 `terminalPreferences.fontSize` +2. 当值变化时执行: + - `terminalRef.current.options.fontSize = nextFontSize` + - `scheduleFit()` + +该模型必须满足: + +- 不销毁 `terminalRef.current` +- 不重新 `open()` 容器 +- 不触发 terminal snapshot / replay 恢复链路 +- 不影响现有 websocket 订阅与输入处理 + +这样可以保证: + +- 已打开的主终端和会话终端即时变化 +- 终端滚动历史保留 +- 不出现闪屏式重建 + +### 6. 兼容与渲染边界 + +为降低回归风险,本次明确以下边界: + +- `lineHeight` 保持当前默认值,不随字号一起变化,避免 TUI 框线字符出现断裂。 +- 字号增大后导致可见列数和行数下降是预期行为,不做额外补偿。 +- 尚未挂载完成的终端无需专门处理更新流程,创建时直接读取最新字号即可。 +- 新开终端和已开终端必须表现一致,不能一个走默认值、一个走新值。 +- `terminalRenderer`、`copyOnSelect`、上传遮罩、移动端输入条等现有行为不应因字号设置被改写。 + +### 7. 错误处理 + +前端: + +- 允许输入过程中的临时非法草稿值。 +- 提交时如果不是合法整数或不在范围内,恢复到上一个已保存值并显示行内错误。 +- 若 `settings.update` 失败,恢复到上一个稳定值并显示错误。 + +服务端: + +- `settings.update` 的 schema 应拒绝越界值、非整数值和非数字值。 +- `settings.get` 返回历史脏数据时,Web 端解析层兜底回退到默认值 `11`。 + +该双层校验可以同时覆盖: + +- 用户直接在 UI 中输入非法值 +- 测试脚本、旧客户端或手工写库引入的异常值 + +## Architecture + +```text +SettingsPage Input + -> dispatch("settings.update", { appearance: { terminalFontSize } }) + -> user_settings["appearance.terminalFontSize"] + +settings.get + -> resolveTerminalFontSizeSetting(settings) + -> terminalPreferencesAtom.fontSize + +XtermHost mount + -> new Terminal({ fontSize }) + +XtermHost runtime update + -> terminal.options.fontSize = nextFontSize + -> scheduleFit() +``` + +## Testing + +### Server + +- `settings.update` 接受 `10-18` 的整数值。 +- `settings.update` 拒绝 `9`、`19`、小数、字符串和 `null`。 +- `settings.get` 能返回已保存的 `appearance.terminalFontSize`。 + +### Web Settings + +- 设置页在无存量值时显示默认字号 `11`。 +- 设置页在有持久化值时显示该值。 +- 输入非法值后显示错误,不调用保存,字段回退到上次有效值。 +- 输入合法值后调用 `settings.update`,并更新本地 `terminalPreferencesAtom`。 +- 异步 settings 回读不会覆盖用户刚手动修改的字号。 + +### Xterm Host + +- 初始创建 `Terminal` 时使用解析后的全局字号。 +- 字号更新时直接写入 `terminal.options.fontSize`。 +- 字号更新后调用 `scheduleFit()`。 +- 字号变化不会销毁现有终端实例,不会触发 replay 重建。 + +### Integration / E2E + +- 在设置页修改字号后,shell terminal 与 session terminal 都能看到字号变化。 +- 刷新页面或重开应用后,字号仍然保持。 +- 新建终端时直接使用最新字号。 + +## Rollout Notes + +- 该功能可以直接启用,不需要 feature flag。 +- 若后续用户反馈还需要更快的临时缩放,再单独立项设计快捷键缩放,不在本次范围内。 + +## Open Questions + +无。 From acf923a5628a68182fd668f07da29d1d1d168cf9 Mon Sep 17 00:00:00 2001 From: pallyoung Date: Sun, 17 May 2026 21:14:07 +0800 Subject: [PATCH 04/63] docs: add typography token redesign spec --- .../specs/2026-05-17-typography-design.md | 413 ++++++++++++++++++ 1 file changed, 413 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-17-typography-design.md diff --git a/docs/superpowers/specs/2026-05-17-typography-design.md b/docs/superpowers/specs/2026-05-17-typography-design.md new file mode 100644 index 000000000..ff2e4f085 --- /dev/null +++ b/docs/superpowers/specs/2026-05-17-typography-design.md @@ -0,0 +1,413 @@ +# UI Typography Token Redesign — Design + +Date: 2026-05-17 +Status: Draft +Owner: Codex + +## Problem + +当前 Web UI 的排版体系存在两个结构性问题: + +- [`packages/web/src/styles/tokens.css`](../../../packages/web/src/styles/tokens.css) 只提供一套基础字号 token,移动端只覆写了触控尺寸,没有覆写排版尺度。 +- 运行态样式同时混用了基础 token 和大量裸写字号,导致标题、正文、标签、meta 信息之间的语义边界不稳定。 + +当前的典型症状包括: + +- `body`、`button`、`input` 默认仍绑定 `13px` 级别正文。 +- 局部标题、按钮、标签、辅助信息常落在同一组 `13px/14px` 尺度上,标题与正文拉不开层级。 +- 普通 UI 中存在 `9px`、`10px`、`11px` 等离群值,密度和可读性不一致。 +- 桌面与移动端只能共享同一套排版尺寸,无法通过 token 层分别控制。 + +因此,本次设计不是简单调整几个标题字号,而是重建一套可约束全局 UI 的排版 token 结构,让普通 UI 的字体使用回到语义驱动,而不是局部凑值。 + +## Goals + +- 为普通 UI 建立一套统一的语义排版 token。 +- 在 token 层支持 desktop / mobile 两套排版尺度。 +- 让标题、正文、标签、meta、内联代码有稳定且互不混用的角色边界。 +- 让 `body`、`button`、`input`、`textarea`、`select` 等基础元素默认绑定语义 token,而不是基础字号 token。 +- 清理普通 UI 中的裸写字号和直接消费基础字号 token 的模式。 +- 为后续批量迁移提供明确的优先级、禁止项和验收标准。 + +## Non-Goals + +- 不调整 `terminal`、`agent session`、`code editor`、`diff` 的代码型排版体系。 +- 不在本次设计中重做字体家族选择,继续沿用当前 sans / mono 字体栈。 +- 不在本次设计中引入新的主题 family 或更改颜色系统。 +- 不要求一次性重写全部组件实现;本次先明确规范和迁移路径。 +- 不把组件尺寸变体 `sm/md/lg` 等同为排版语义层级。 + +## User Decisions Captured + +- desktop 和 mobile 应分别拥有独立的排版尺度。 +- 除 `terminal`、会话、代码编辑器和 diff 之外,其他普通界面都应统一使用这套 typography token。 +- 运行态普通 UI 中现有裸写字号都应逐步迁移掉。 +- 迁移完成后,普通组件不应再直接依赖 `--text-xs/sm/base/lg...` 这类基础字号 token。 + +## Approaches Considered + +### Option A: 保留现有 `--text-*`,只整体调一轮数值 + +优点: + +- 改动最小。 +- 对现有组件命名兼容最好。 + +缺点: + +- `--text-*` 同时承担“数值尺度”和“语义角色”两种职责,组件仍会继续自由混用。 +- 后续新组件作者仍需要自己判断标题、正文、标签该用哪个 size,排版漂移会继续出现。 + +### Option B: 保留基础尺度 token,再新增一层语义排版 token(推荐) + +优点: + +- 组件统一消费语义 token,desktop / mobile 只在 token 映射层覆写。 +- 后续整体放大或压缩字体时,只需要调整 token 映射,不必逐个组件手改。 +- 能明确约束标题、正文、标签、meta、内联代码的使用边界。 + +缺点: + +- 首次迁移需要同时处理 token 定义、基础样式和组件用法。 +- 过渡期需要兼容旧 `--text-*` 体系,短期内会存在双层 token。 + +### Option C: 每个页面或组件族自行维护排版规则 + +优点: + +- 局部适配空间最大。 + +缺点: + +- 维护成本高。 +- 很快会回到“每块界面看起来都合理,但合在一起不统一”的状态。 + +## Final Choice + +采用 Option B。 + +保留基础尺度 token 作为纯数值层,再新增普通 UI 唯一允许消费的语义排版 token。desktop / mobile 的差异只在 token 层解决,组件层不再自行区分平台字号。 + +`terminal`、`agent session`、`code editor`、`diff` 继续维持独立的 monospace 排版体系,不并入这套普通 UI token。 + +## Final Design + +### 1. Scope + +本次 typography 规范适用范围: + +- 所有普通 UI 页面和组件 +- 顶栏、侧栏、设置页、sheet、modal、command palette、toast、welcome、auth、workspace 空状态等运行态界面 + +明确豁免范围: + +- shell terminal +- agent session terminal +- code editor +- diff / review code surfaces + +这些豁免区域可继续使用独立的 monospace token 与字号策略,不需要迁入普通 UI typography token。 + +### 2. Token Architecture + +排版 token 分为两层。 + +第一层是基础尺度 token,只负责数值,不直接给业务组件消费。建议命名为中性尺度名,例如: + +- `--font-size-100` +- `--font-size-200` +- `--font-size-300` +- `--font-size-400` +- `--font-size-500` +- `--font-size-600` +- `--font-size-700` + +第二层是语义排版 token,普通 UI 只能消费这一层,例如: + +- `--type-kicker-size` +- `--type-kicker-line-height` +- `--type-kicker-weight` +- `--type-kicker-letter-spacing` +- `--type-body-size` +- `--type-body-line-height` +- `--type-body-weight` +- `--type-section-title-size` +- `--type-page-title-size` +- `--type-code-inline-size` +- `--type-code-inline-family` + +约束规则: + +- 组件禁止直接使用基础字号 token。 +- 组件禁止继续直接使用旧 `--text-xs/sm/base/lg...` 作为运行态普通 UI 字号来源。 +- 过渡期可以保留旧 `--text-*` 作为兼容别名,但它们只用于 token 内部映射,不作为普通组件 API。 + +### 3. Desktop / Mobile Base Scale + +基础尺度建议如下。 + +| Base token | Desktop | Mobile | +| --- | --- | --- | +| `--font-size-100` | 11px | 12px | +| `--font-size-200` | 12px | 13px | +| `--font-size-300` | 14px | 15px | +| `--font-size-400` | 16px | 17px | +| `--font-size-500` | 18px | 20px | +| `--font-size-600` | 24px | 28px | +| `--font-size-700` | 32px | 36px | + +平台差异处理规则: + +- `:root` 定义 desktop 默认值。 +- 在现有移动端断点覆盖层中,为移动端覆写基础尺度 token。 +- 组件层不感知当前使用的是 desktop 还是 mobile 数值。 + +### 4. Semantic Typography Scale + +普通 UI 统一消费以下语义排版层。 + +| Semantic token | Desktop | Mobile | Weight | Typical usage | +| --- | --- | --- | --- | --- | +| `--type-kicker` | 11 / 1.2 | 12 / 1.2 | 600 | 眉题、区块前缀、小型状态标签 | +| `--type-label` | 12 / 1.35 | 13 / 1.35 | 500 | tab、字段标签、筛选项、chip | +| `--type-meta` | 12 / 1.45 | 13 / 1.45 | 400 | 辅助说明、次级信息、时间/状态补充 | +| `--type-body` | 14 / 1.5 | 15 / 1.55 | 400 | 正文、设置描述、列表说明 | +| `--type-body-strong` | 14 / 1.45 | 15 / 1.5 | 500 | 按钮文字、输入内容、列表主文案 | +| `--type-code-inline` | 12 / 1.4 | 13 / 1.4 | 500 | 普通 UI 中的路径、命令、快捷键、ID | +| `--type-app-title` | 16 / 1.25 | 17 / 1.25 | 600 | 顶栏标题、sheet 标题、面板头 | +| `--type-section-title` | 18 / 1.2 | 20 / 1.2 | 600 | 卡片标题、模块标题、弹窗标题 | +| `--type-page-title` | 24 / 1.1 | 28 / 1.1 | 600 | 页面主标题 | +| `--type-display` | 32 / 1.05 | 36 / 1.05 | 600 | hero、空状态主标题、品牌型标题 | + +附加规则: + +- 普通 UI 默认正文不再使用 `13px`,而是 desktop `14px`、mobile `15px`。 +- 普通 UI 不再使用 `9px` 或 `10px` 作为正文、按钮、标签等常规文本。 +- 标题至少比正文高一个语义层级,禁止出现“正文和标题同级甚至更大”的结构。 + +### 5. Base Element Mapping + +基础元素应统一绑定语义 token,而不是直接绑定基础字号。 + +建议映射: + +- `body` -> `--type-body` +- `button` -> `--type-body-strong` +- `input` / `textarea` / `select` -> `--type-body-strong` +- 默认 `p` -> `--type-body` +- 默认 `code` / `kbd` / `samp` -> `--type-code-inline` +- `h1` / `h2` / `h3` / `h4` / `h5` / `h6` 不再按固定旧 `--text-*` 线性映射,而是按实际运行态语义重新绑定 + +推荐基础标题绑定: + +- `h1` -> `--type-page-title` +- `h2` -> `--type-section-title` +- `h3` -> `--type-app-title` +- `h4` -> `--type-body-strong` +- `h5` -> `--type-label` +- `h6` -> `--type-meta` + +如果具体界面不适合直接使用默认标题映射,组件可覆写为更准确的语义 token,但不能回退为裸写字号。 + +### 6. Role Mapping for Existing UI + +以下映射用于指导当前项目中的常见界面角色收敛。 + +#### Page-level + +- welcome、auth、workspace 空状态的大标题 -> `--type-page-title` +- 展示型 hero 标题 -> `--type-display` +- 页面顶部 kicker、区块眉题 -> `--type-kicker` +- 页面简介、空态说明正文 -> `--type-body` + +#### Panel-level + +- modal、sheet、popover 主标题 -> `--type-section-title` +- topbar 当前区域标题、panel header -> `--type-app-title` +- 设置页或列表中的小分组名,如果只是导航分段,不应继续伪装为标题,应降级为 `--type-kicker` 或 `--type-label` + +#### Form and interaction + +- 按钮文字 -> `--type-body-strong` +- 输入内容 -> `--type-body-strong` +- 字段 label -> `--type-label` +- placeholder、help text、description -> `--type-meta` + +#### Lists and cards + +- 行标题、列表主文案 -> `--type-body-strong` +- 卡片或模块标题 -> `--type-app-title` 或 `--type-section-title` +- 路径、时间、状态补充、二级说明 -> `--type-meta` + +#### Inline code and shortcuts + +- 普通 UI 中出现的路径、命令、快捷键、ID -> `--type-code-inline` + +### 7. False Title Demotion Rules + +当前项目中相当一部分“标题”实际上不属于标题语义,而只是高权重正文、标签或状态文案。本次迁移必须主动做降级判断。 + +以下文本不应再使用标题层级: + +- 只是分组名的文字 +- 只是状态字的文字 +- 只是按钮或列表主文案的文字 +- 只是说明型副文案,但因为历史原因被写成 `600 + 13px/14px` + +判断规则: + +- 如果它并不承担页面、模块或面板的结构分层职责,它就不是标题。 +- 如果它和相邻正文处于同一信息层级,它应使用 `--type-body-strong`、`--type-label` 或 `--type-meta`。 +- 标题数量必须克制,不能通过“到处加粗加大”来解决视觉强调。 + +### 8. Authoring Rules + +普通 UI 的作者规范如下: + +- 组件只能消费 `--type-*` 语义 token。 +- 组件变体 `sm/md/lg` 优先调整容器高度、padding、图标尺寸,不优先切换排版语义层级。 +- 如现有语义 token 不足,应先新增 token,再写样式。 +- 所有 typography 相关属性应尽量成组来自 token,而不是只 token 化字号、其余属性继续裸写。 + +推荐做法: + +- `font-size`、`line-height`、`font-weight`、`letter-spacing` 尽量一起以语义角色定义。 +- `code-inline` 一类语义同时定义 `font-family` 和 size,避免组件各自拼装。 + +### 9. Forbidden Patterns + +普通 UI 中禁止以下模式: + +- 裸写 `font-size` +- 裸写 `line-height` +- 裸写 `font-weight` +- 裸写 `letter-spacing` +- 组件直接消费基础字号 token +- 组件继续直接消费旧 `--text-xs/sm/base/lg...` 作为最终运行态字号 +- 使用 `9px`、`10px` 作为普通 UI 正文、按钮、标签、说明文本 +- 让正文、标签、说明文本伪装成标题 +- 让组件尺寸变体替代语义层级 + +允许的例外: + +- 豁免范围中的 terminal、editor、diff、session code surfaces +- 需要独立字距的 `kicker` 或 `display` 等语义角色,但应通过 token,而不是组件局部临时写死 + +### 10. Migration Priority + +建议按以下顺序迁移。 + +#### Batch 1: Base and shared UI primitives + +- [`packages/web/src/styles/base.css`](../../../packages/web/src/styles/base.css) +- `button` +- `input` +- `textarea` +- `tabs` +- `tag` +- `badge` +- `pill` +- `tooltip` +- `notice` +- `modal` +- `empty-state` + +目标: + +- 先把基础元素和通用控件从旧 `--text-*` 与裸写字号上摘下来。 + +#### Batch 2: App chrome and navigation + +- topbar +- settings +- workspace shell +- popover +- sheet +- toast +- command palette + +目标: + +- 让主壳层和工具面板先形成统一语言。 + +#### Batch 3: Presentation pages + +- welcome +- auth +- workspace empty states +- mobile empty views + +目标: + +- 用新的 page / display 层级修复最明显的标题与正文错位。 + +#### Batch 4: Residual cleanup + +- 搜索框 +- 状态条 +- provider 列表 +- 各类 meta 文案 +- 各类残留 `13px/11px/10px` 离群点 + +目标: + +- 清理组件级、页面级散落的假标题和特例字号。 + +### 11. Testing and Acceptance Criteria + +验收标准: + +- 除豁免范围外,运行态普通 UI 中不再存在裸写字号。 +- 普通 UI 组件不再直接消费基础字号 token。 +- desktop / mobile 的字体差异只通过 token 映射完成,而不是组件分别写两套字号。 +- 标题、正文、标签、meta、内联代码角色边界清晰,不再混用。 + +建议测试覆盖: + +#### Stylesheet-level + +- `tokens.css` 定义 desktop / mobile 两套基础尺度 token。 +- 语义排版 token 全量存在,并映射到基础尺度 token。 + +#### Base/theme tests + +- `body`、`button`、`input`、`textarea`、`select` 都绑定语义 token。 +- 关键组件不再直接依赖旧 `--text-*` 作为最终字号来源。 + +#### Guardrails + +- 补充检测,确保普通 UI 样式文件中不再新增裸写字号。 +- 允许对豁免目录做白名单处理,避免误伤 terminal / editor / diff。 + +## Architecture + +```text +Desktop / Mobile base scale tokens + -> semantic typography tokens + -> base elements and shared UI primitives + -> feature-level components + +Exempt code surfaces + -> independent terminal/editor/diff typography tokens +``` + +## Open Questions Resolved + +- desktop 和 mobile 是否共用一套排版尺度 + - 不共用。通过 token 层分别覆写。 +- terminal、session、editor、diff 是否迁入统一 UI typography + - 不迁入。继续使用独立排版体系。 +- 是否允许普通组件继续直接用旧 `--text-*` + - 不允许。过渡期仅可作为 token 内部兼容别名。 + +## Risks + +- 首次迁移会触及较多基础组件,短期内可能带来视觉回归。 +- 如果没有同步加入测试护栏,后续仍可能重新出现裸写字号。 +- 若把所有当前“标题”一律放大,而不先做假标题降级,会把问题从层级混乱转成标题泛滥。 + +## Rollout Recommendation + +- 先完成 token 和 base element 改造,再推进共享组件。 +- 每批迁移都应伴随 theme / stylesheet 测试更新。 +- 每完成一批,优先 review welcome、settings、workspace shell、empty state 这几类最能暴露层级问题的界面。 From 76d451633143f64c4cc1132308892889c78c2b42 Mon Sep 17 00:00:00 2001 From: pallyoung Date: Sun, 17 May 2026 21:18:04 +0800 Subject: [PATCH 05/63] feat(server): validate terminal font size setting --- packages/server/src/commands/settings.test.ts | 95 +++++++++++++++++++ packages/server/src/commands/settings.ts | 1 + 2 files changed, 96 insertions(+) diff --git a/packages/server/src/commands/settings.test.ts b/packages/server/src/commands/settings.test.ts index 9bf53b28e..27396b4e9 100644 --- a/packages/server/src/commands/settings.test.ts +++ b/packages/server/src/commands/settings.test.ts @@ -137,6 +137,29 @@ describe("settings commands", () => { ).toEqual({ value: '"graphite-light"' }); }); + it("settings.update persists appearance.terminalFontSize into user_settings", async () => { + const result = await dispatch( + { + kind: "command", + id: "settings-update-terminal-font-size", + op: "settings.update", + args: { + settings: { + appearance: { + terminalFontSize: 16, + }, + }, + }, + }, + ctx + ); + + expect(result.ok).toBe(true); + expect( + db.prepare("SELECT value FROM user_settings WHERE key = ?").get("appearance.terminalFontSize") + ).toEqual({ value: "16" }); + }); + it("settings.update persists legacy appearance.theme light during themeId migration", async () => { const result = await dispatch( { @@ -238,6 +261,78 @@ describe("settings commands", () => { ).toBeUndefined(); }); + it("settings.update rejects terminalFontSize values below the supported minimum", async () => { + const result = await dispatch( + { + kind: "command", + id: "settings-update-terminal-font-size-too-small", + op: "settings.update", + args: { + settings: { + appearance: { + terminalFontSize: 9, + }, + }, + }, + }, + ctx + ); + + expect(result.ok).toBe(false); + expect(result.error?.code).toBe("validation_error"); + expect( + db.prepare("SELECT value FROM user_settings WHERE key = ?").get("appearance.terminalFontSize") + ).toBeUndefined(); + }); + + it("settings.update rejects terminalFontSize values above the supported maximum", async () => { + const result = await dispatch( + { + kind: "command", + id: "settings-update-terminal-font-size-too-large", + op: "settings.update", + args: { + settings: { + appearance: { + terminalFontSize: 19, + }, + }, + }, + }, + ctx + ); + + expect(result.ok).toBe(false); + expect(result.error?.code).toBe("validation_error"); + expect( + db.prepare("SELECT value FROM user_settings WHERE key = ?").get("appearance.terminalFontSize") + ).toBeUndefined(); + }); + + it("settings.update rejects fractional terminalFontSize values", async () => { + const result = await dispatch( + { + kind: "command", + id: "settings-update-terminal-font-size-fractional", + op: "settings.update", + args: { + settings: { + appearance: { + terminalFontSize: 15.5, + }, + }, + }, + }, + ctx + ); + + expect(result.ok).toBe(false); + expect(result.error?.code).toBe("validation_error"); + expect( + db.prepare("SELECT value FROM user_settings WHERE key = ?").get("appearance.terminalFontSize") + ).toBeUndefined(); + }); + it("settings.update persists provider startup command arguments per provider config", async () => { const result = await dispatch( { diff --git a/packages/server/src/commands/settings.ts b/packages/server/src/commands/settings.ts index d330adb7f..e2c983829 100644 --- a/packages/server/src/commands/settings.ts +++ b/packages/server/src/commands/settings.ts @@ -69,6 +69,7 @@ const SettingsSchema = z.object({ themeId: z.string().optional(), terminalRenderer: z.enum(["standard", "compatibility"]).optional(), terminalCopyOnSelect: z.boolean().optional(), + terminalFontSize: z.number().int().min(10).max(18).optional(), locale: z.enum(["zh", "en"]).optional(), }) .optional(), From eab77606dee1215af8f076945895c35219863acb Mon Sep 17 00:00:00 2001 From: pallyoung Date: Sun, 17 May 2026 21:25:01 +0800 Subject: [PATCH 06/63] test(server): cover terminal font size readback --- packages/server/src/commands/settings.test.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/server/src/commands/settings.test.ts b/packages/server/src/commands/settings.test.ts index 27396b4e9..2548db4a1 100644 --- a/packages/server/src/commands/settings.test.ts +++ b/packages/server/src/commands/settings.test.ts @@ -551,6 +551,28 @@ describe("settings commands", () => { }); }); + it("settings.get returns appearance.terminalFontSize from user_settings", async () => { + db.prepare("INSERT INTO user_settings (key, value) VALUES (?, ?)").run( + "appearance.terminalFontSize", + "16" + ); + + const result = await dispatch( + { + kind: "command", + id: "settings-get-terminal-font-size", + op: "settings.get", + args: {}, + }, + ctx + ); + + expect(result.ok).toBe(true); + expect(result.data).toMatchObject({ + "appearance.terminalFontSize": 16, + }); + }); + it("settings.get normalizes invalid persisted supervisor timeout values", async () => { db.prepare("INSERT INTO user_settings (key, value) VALUES (?, ?)").run( "supervisor.evaluationTimeoutSec", From 26db031150285420ab0c4d416028b8bfcccf5853 Mon Sep 17 00:00:00 2001 From: pallyoung Date: Sun, 17 May 2026 21:26:49 +0800 Subject: [PATCH 07/63] Refine workspace pane spacing --- .../components/session-card.test.tsx | 13 +-- .../views/shared/pane-layout.test.tsx | 8 +- .../agent-panes/views/shared/pane-layout.tsx | 4 +- .../agent-panes/views/shared/session-card.tsx | 58 +------------- packages/web/src/styles/components.css | 80 +++++++++++++++++-- .../web/src/styles/components.theme.test.ts | 29 ++++++- 6 files changed, 110 insertions(+), 82 deletions(-) diff --git a/packages/web/src/features/agent-panes/components/session-card.test.tsx b/packages/web/src/features/agent-panes/components/session-card.test.tsx index d0e62f698..9b99afa8f 100644 --- a/packages/web/src/features/agent-panes/components/session-card.test.tsx +++ b/packages/web/src/features/agent-panes/components/session-card.test.tsx @@ -194,7 +194,7 @@ describe("SessionCard", () => { ); }); - it("renders a decorative session status strip with legacy session classes", () => { + it("does not render the legacy session progress strip", () => { const { store } = createSessionStore({ terminalId: "term-live", state: "running", @@ -207,15 +207,8 @@ describe("SessionCard", () => { ); - const progress = container.querySelector(".session-progress"); - const fill = container.querySelector(".session-progress-bar.session-progress-running"); - - expect(progress).toHaveAttribute("aria-hidden", "true"); - expect(progress).not.toHaveAttribute("role"); - expect(progress).not.toHaveAttribute("aria-valuemin"); - expect(progress).not.toHaveAttribute("aria-valuemax"); - expect(progress).not.toHaveAttribute("aria-valuenow"); - expect(fill).toHaveStyle({ "--progress-bar-width": "42%" }); + expect(container.querySelector(".session-progress")).toBeNull(); + expect(container.querySelector(".session-progress-bar")).toBeNull(); }); it("renders migrated provider and state tags with legacy badge compatibility classes", () => { diff --git a/packages/web/src/features/agent-panes/views/shared/pane-layout.test.tsx b/packages/web/src/features/agent-panes/views/shared/pane-layout.test.tsx index 0ea55a7ee..cd492dd97 100644 --- a/packages/web/src/features/agent-panes/views/shared/pane-layout.test.tsx +++ b/packages/web/src/features/agent-panes/views/shared/pane-layout.test.tsx @@ -38,13 +38,13 @@ describe("PaneLayout", () => { const layout = container.firstElementChild as HTMLDivElement; const divider = container.querySelector(".pane-layout-divider"); - expect(layout.style.gridTemplateColumns).toBe("50% 8px 50%"); + expect(layout.style.gridTemplateColumns).toBe("50% 0px 50%"); fireEvent.mouseDown(divider!); fireEvent.mouseMove(document, { clientX: 250 }); fireEvent.mouseUp(document); - expect(layout.style.gridTemplateColumns).toBe("25% 8px 75%"); + expect(layout.style.gridTemplateColumns).toBe("25% 0px 75%"); rerender( @@ -54,7 +54,7 @@ describe("PaneLayout", () => { ); expect((container.firstElementChild as HTMLDivElement).style.gridTemplateColumns).toBe( - "50% 8px 50%" + "50% 0px 50%" ); }); @@ -75,7 +75,7 @@ describe("PaneLayout", () => { fireEvent.mouseDown(divider!); fireEvent.mouseMove(document, { clientX: 300 }); - expect(layout.style.gridTemplateColumns).toBe("30% 8px 70%"); + expect(layout.style.gridTemplateColumns).toBe("30% 0px 70%"); expect(onRatioCommit).not.toHaveBeenCalled(); fireEvent.mouseMove(document, { clientX: 350 }); diff --git a/packages/web/src/features/agent-panes/views/shared/pane-layout.tsx b/packages/web/src/features/agent-panes/views/shared/pane-layout.tsx index 765f3ea0f..c831ba16d 100644 --- a/packages/web/src/features/agent-panes/views/shared/pane-layout.tsx +++ b/packages/web/src/features/agent-panes/views/shared/pane-layout.tsx @@ -90,8 +90,8 @@ export const PaneLayout: FC = ({ const style = direction === "horizontal" - ? { gridTemplateColumns: `${currentRatio * 100}% 8px ${(1 - currentRatio) * 100}%` } - : { gridTemplateRows: `${currentRatio * 100}% 8px ${(1 - currentRatio) * 100}%` }; + ? { gridTemplateColumns: `${currentRatio * 100}% 0px ${(1 - currentRatio) * 100}%` } + : { gridTemplateRows: `${currentRatio * 100}% 0px ${(1 - currentRatio) * 100}%` }; return (
= ({ return null; } - const progressWidth = getProgressWidth(session.state); const sessionTitle = session.title?.trim() || formatSessionLabel(session.id); const providerLabel = formatProviderLabel(session.providerId); const sessionStateLabel = formatSessionStateLabel(session.state); @@ -121,15 +119,6 @@ export const SessionCard: FC = ({ data-session-id={sessionId} onClick={handleCardClick} > -