diff --git a/.changeset/fair-bears-tell.md b/.changeset/fair-bears-tell.md new file mode 100644 index 000000000..def1bad05 --- /dev/null +++ b/.changeset/fair-bears-tell.md @@ -0,0 +1,5 @@ +--- +"@spencer-kit/coder-studio": patch +--- + +Stabilize the web test suite and fix workspace UI and dev-browser regressions. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..b378f883d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: true +contact_links: + - name: Quick Start + url: https://github.com/spencerkit/coder-studio/blob/main/docs/wiki/Quick-Start.md + about: Install and launch Coder Studio. + - name: First Agent Run + url: https://github.com/spencerkit/coder-studio/blob/main/docs/wiki/First-Agent-Run.md + about: Run Claude or Codex and review your first diff. + - name: Security and Privacy + url: https://github.com/spencerkit/coder-studio/blob/main/docs/wiki/Security-and-Privacy.md + about: Understand local-first behavior and remote access risks. diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 000000000..5cf552a3f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,43 @@ +name: Feature request +description: Suggest a product improvement or missing workflow. +title: "Feature: " +labels: ["enhancement"] +body: + - type: textarea + id: workflow + attributes: + label: Workflow problem + description: What were you trying to do? + validations: + required: true + - type: textarea + id: current + attributes: + label: Current behavior + description: What makes the workflow hard today? + validations: + required: true + - type: textarea + id: desired + attributes: + label: Desired behavior + description: Describe the smallest useful version of the improvement. + validations: + required: true + - type: dropdown + id: area + attributes: + label: Area + options: + - First run / onboarding + - Agent sessions + - Git review + - Terminal + - Mobile / remote access + - Supervisor + - Work Analysis + - Skills + - Provider integrations + - Other + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/installation.yml b/.github/ISSUE_TEMPLATE/installation.yml new file mode 100644 index 000000000..bf7444a85 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/installation.yml @@ -0,0 +1,60 @@ +name: Installation issue +description: Report problems installing, launching, or opening Coder Studio. +title: "Installation: " +labels: ["installation"] +body: + - type: markdown + attributes: + value: | + Thanks for reporting a first-run issue. Please remove secrets, private paths, API keys, and sensitive logs. + - type: textarea + id: steps + attributes: + label: What did you try? + description: Include the exact commands you ran. + placeholder: | + npm install -g @spencer-kit/coder-studio + coder-studio open + validations: + required: true + - type: textarea + id: result + attributes: + label: What happened? + description: Paste the visible error or describe the behavior. + validations: + required: true + - type: textarea + id: expected + attributes: + label: What did you expect? + validations: + required: true + - type: input + id: node + attributes: + label: Node.js version + placeholder: "node --version" + validations: + required: true + - type: input + id: version + attributes: + label: Coder Studio version + placeholder: "coder-studio version" + validations: + required: true + - type: input + id: os + attributes: + label: Operating system + placeholder: "macOS 15, Ubuntu 24.04, Windows 11" + validations: + required: true + - type: textarea + id: status + attributes: + label: Service status and logs + description: Include `coder-studio status` and relevant `coder-studio logs` output. Remove secrets. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/provider-setup.yml b/.github/ISSUE_TEMPLATE/provider-setup.yml new file mode 100644 index 000000000..8c66a54f1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/provider-setup.yml @@ -0,0 +1,54 @@ +name: Provider setup issue +description: Report Claude, Codex, or other Provider CLI detection problems. +title: "Provider setup: " +labels: ["provider"] +body: + - type: markdown + attributes: + value: | + Use this when Coder Studio cannot find or start a Provider CLI. Remove secrets and private project details. + - type: dropdown + id: provider + attributes: + label: Provider + options: + - Claude Code + - Codex + - Gemini CLI + - Cursor Agent + - OpenCode + - Aider-style CLI + validations: + required: true + - type: textarea + id: commands + attributes: + label: Verification commands + description: Paste relevant output from `which`, `--version`, and Coder Studio logs. + placeholder: | + which claude + claude --version + coder-studio logs + validations: + required: true + - type: textarea + id: app + attributes: + label: What does Coder Studio show? + description: Describe the UI state or error message. + validations: + required: true + - type: input + id: shell + attributes: + label: Shell and terminal + placeholder: "zsh, bash, PowerShell, Windows Terminal" + validations: + required: false + - type: input + id: os + attributes: + label: Operating system + placeholder: "macOS 15, Ubuntu 24.04, Windows 11" + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/workflow-showcase.yml b/.github/ISSUE_TEMPLATE/workflow-showcase.yml new file mode 100644 index 000000000..921752343 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/workflow-showcase.yml @@ -0,0 +1,31 @@ +name: Show your workflow +description: Share how you use Coder Studio and what should improve. +title: "Workflow: " +labels: ["workflow"] +body: + - type: textarea + id: setup + attributes: + label: Your setup + description: Which agent, OS, device mix, and repository type are you using? + validations: + required: true + - type: textarea + id: workflow + attributes: + label: Workflow + description: Describe the task from start to finish. + validations: + required: true + - type: textarea + id: useful + attributes: + label: What worked well? + validations: + required: false + - type: textarea + id: friction + attributes: + label: What was confusing or slow? + validations: + required: false diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..04b8de631 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,69 @@ +# Contributing + +Thanks for helping improve Coder Studio. + +Coder Studio is a self-hosted browser workspace for AI coding agent workflows. The most useful early feedback is concrete: what you tried, what happened, what you expected, and where the first-run path became unclear. + +## Good First Feedback + +Please open an issue when you hit: + +- installation problems +- Provider CLI detection problems +- first agent session confusion +- mobile or remote access setup issues +- documentation that does not match the current UI +- a workflow that feels promising but needs one missing capability + +## Before Filing An Issue + +Run these commands when relevant: + +```bash +node --version +coder-studio version +coder-studio status +coder-studio logs +which claude +which codex +claude --version +codex --version +``` + +Do not paste secrets, API keys, private source code, or full logs that contain sensitive data. + +## Development Setup + +```bash +git clone https://github.com/spencerkit/coder-studio.git +cd coder-studio +pnpm install +pnpm dev +``` + +## Verification + +Before handing off code changes, run the relevant command: + +```bash +pnpm ci:verify +``` + +For docs-only changes, at least run: + +```bash +git diff --check +pnpm ci:lint +``` + +## Pull Request Expectations + +- Keep changes focused. +- Do not bundle unrelated refactors. +- Include screenshots or short recordings for UI changes. +- Update docs when behavior changes. +- Mention what verification you ran. + +## Security + +Do not report sensitive security issues in public issues if they include exploitable details. Open a minimal public issue asking for a private contact path, or contact the maintainer through the repository owner profile. diff --git a/README.md b/README.md index 8c1cf83bd..8b7470e19 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,11 @@ # Coder Studio -**The all-in-one vibe coding workspace for AI agents.** +**Self-hosted browser workspace for AI coding agents.** -Coder Studio brings your code editor, Git, terminals, AI coding agents, session review, notifications, work analysis, and Skills into one browser workspace. +Coder Studio brings Claude Code, Codex, terminals, files, Git diff review, Supervisor loops, Work Analysis, and Skills into one browser workspace you run on your own machine. -It helps keep agent context, progress, and follow-up work visible across desktop, tablet, and phone, so vibe coding feels less scattered and more controllable. +Use it when raw terminal-only AI coding starts to feel scattered: start an agent task on desktop, review the changed files and diff beside the session, monitor long-running work, and reopen the same workspace from a tablet or phone. Works with popular coding agents including Claude Code, Codex, Gemini CLI, Cursor Agent, OpenCode, and Aider-style CLI agents. @@ -17,9 +17,9 @@ Works with popular coding agents including Claude Code, Codex, Gemini CLI, Curso [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D24.0.0-green.svg)](https://nodejs.org/) [![GitHub Stars](https://img.shields.io/github/stars/spencerkit/coder-studio?style=social)](https://github.com/spencerkit/coder-studio/stargazers) -[View Workspace](docs/help/assets/screenshot-desktop-workspace-full.png) · [Quick Start](#quick-start) · [Star on GitHub](https://github.com/spencerkit/coder-studio) +[Watch Demo](docs/assets/demo.mp4) · [Quick Start](#quick-start) · [English Docs](docs/wiki/Quick-Start.md) · [Star on GitHub](https://github.com/spencerkit/coder-studio) -[中文说明](README.zh-CN.md) | [Documentation](docs/help/quick-start.md) +[中文说明](README.zh-CN.md) | [Security & Privacy](docs/wiki/Security-and-Privacy.md) | [Known Limitations](docs/wiki/Known-Limitations.md) @@ -27,6 +27,17 @@ Works with popular coding agents including Claude Code, Codex, Gemini CLI, Curso
Preview the full workspace layout built for agent runs, review, supervision, and device switching.
+## What You Can Try In 5 Minutes + +1. Install with `npm install -g @spencer-kit/coder-studio`. +2. Launch with `coder-studio open`. +3. Open one local repository. +4. Start an agent session. For a first trial, Claude or Codex is the recommended path. +5. Ask the agent for a small change, then review the Git diff beside the session. +6. Reopen the same workspace from a tablet or phone to check progress. + +Coder Studio is not a cloud IDE, not a VS Code replacement, and not an AI model provider. It is a self-hosted workbench around the AI coding agents and local tools you already use. + ## Why Coder Studio? Vibe coding feels fast until the agent output turns into real project work: you still need to run agents, inspect edits, manage Git, monitor long tasks, and improve the next run. Coder Studio keeps that loop in one programming workbench. @@ -136,17 +147,14 @@ The same workspace URL works across all devices — interface adapts automatical | Resource | Description | |----------|-------------| -| [Quick Start Guide](docs/help/quick-start.md) | Installation to first workspace | -| [App Overview](docs/help/app-overview.md) | Core concepts and features | -| [Agent CLI Setup](docs/help/providers.md) | Install and connect coding agent CLIs | -| [Desktop Guide](docs/help/desktop-guide.md) | PC interface and shortcuts | -| [Mobile & Remote Access Guide](docs/help/mobile-guide.md) | Phone / tablet usage, LAN access, Tailscale/ngrok/Cloudflare Tunnel | -| [Work Analysis](docs/help/work-analysis.md) | Review workspace activity, agent sessions, and improvement opportunities | -| [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 | -| [GitHub Wiki Source](docs/wiki/README.md) | Wiki source pages and publish flow | -| [AI Coding Terms](docs/wiki/AI-Coding-Terms.md) | Vibe coding, agentic harnesses, eval harnesses, and where Coder Studio fits | +| [Quick Start](docs/wiki/Quick-Start.md) | Install, launch, and open your first workspace | +| [First Agent Run](docs/wiki/First-Agent-Run.md) | Run a recommended first provider, inspect output, and review Git diff | +| [Agent Providers](docs/wiki/Agent-Providers.md) | Install and verify coding agent CLIs | +| [Mobile and Remote Access](docs/wiki/Mobile-and-Remote-Access.md) | LAN, Tailscale, ngrok, Cloudflare Tunnel, and phone/tablet usage | +| [Security and Privacy](docs/wiki/Security-and-Privacy.md) | Local-first model, provider boundaries, and remote access risks | +| [Known Limitations](docs/wiki/Known-Limitations.md) | Current requirements and product boundaries | +| [Troubleshooting](docs/wiki/Troubleshooting.md) | First-run problems, Provider CLI issues, and service recovery | +| [Chinese Help Center](docs/help/README.md) | 中文帮助中心 | --- @@ -164,7 +172,6 @@ The same workspace URL works across all devices — interface adapts automatical - [ ] Web-based terminal streaming optimization - [ ] Session replay and history navigation - [ ] Multi-workspace management -- [ ] Plugin system for custom integrations - [ ] Workspace preference sync --- diff --git a/README.zh-CN.md b/README.zh-CN.md index 42a7dfda3..67a94bf5a 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -4,11 +4,11 @@ # Coder Studio -**一站式 vibe coding 编程工作台。** +**自部署的 AI Coding 工作台。** -Coder Studio 把代码编辑器、Git、终端、AI coding agent、会话审查、消息提醒、工作复盘和 Skills 放进同一个浏览器工作区。 +Coder Studio 把 Claude Code、Codex、终端、文件、Git diff 审查、Supervisor 监督循环、工作分析和 Skills 放进同一个浏览器 workspace,并运行在你自己的机器上。 -它帮助你在桌面、平板和手机之间保持 Agent 上下文、任务进度和后续动作可见,让 vibe coding 不再散落在一堆窗口和工具里。 +当纯终端里的 AI coding 开始变得分散时,可以用它在桌面端启动 Agent 任务,在会话旁审查改动和 diff,监督长任务进度,并从平板或手机重新打开同一个工作区继续查看。 支持 Claude Code、Codex、Gemini CLI、Cursor Agent、OpenCode,以及 Aider 这类 CLI coding agent。 @@ -17,9 +17,9 @@ Coder Studio 把代码编辑器、Git、终端、AI coding agent、会话审查 [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D24.0.0-green.svg)](https://nodejs.org/) [![GitHub Stars](https://img.shields.io/github/stars/spencerkit/coder-studio?style=social)](https://github.com/spencerkit/coder-studio/stargazers) -[查看工作区](docs/help/assets/screenshot-desktop-workspace-full.png) · [快速开始](#快速开始) · [GitHub Star](https://github.com/spencerkit/coder-studio) +[观看 Demo](docs/assets/demo.mp4) · [快速开始](#快速开始) · [中文帮助中心](docs/help/README.md) · [GitHub Star](https://github.com/spencerkit/coder-studio) -[English](README.md) | [文档](docs/help/quick-start.md) +[English](README.md) | [安全与隐私](docs/help/security-and-privacy.md) | [当前限制](docs/help/known-limitations.md) @@ -27,6 +27,17 @@ Coder Studio 把代码编辑器、Git、终端、AI coding agent、会话审查
预览这个为 Agent 运行、改动审查、Supervisor 监督和跨设备切换而设计的完整工作区布局。
+## 5 分钟可以试到什么 + +1. 使用 `npm install -g @spencer-kit/coder-studio` 安装。 +2. 使用 `coder-studio open` 启动。 +3. 打开一个本地代码仓库。 +4. 创建 Agent 会话。首次试跑建议选择 Claude 或 Codex。 +5. 让 Agent 做一个小改动,然后在 Git diff 里审查结果。 +6. 用平板或手机重新打开同一个 workspace 查看进度。 + +Coder Studio 不是云 IDE,不是 VS Code 替代品,也不是 AI 模型提供方。它是围绕你已经在使用的 AI coding agent 和本地开发工具搭建的自部署工作台。 + ## 为什么选择 Coder Studio? vibe coding 一开始很快,但当 Agent 输出进入真实项目,后面还要运行 Agent、理解改动、审查 diff、管理 Git、监督长任务,并改进下一轮执行。Coder Studio 把这条链路收进一个编程工作台。 @@ -136,17 +147,14 @@ coder-studio open | 资源 | 描述 | |------|------| -| [快速开始](docs/help/quick-start.md) | 从安装到第一个工作区 | -| [功能总览](docs/help/app-overview.md) | 核心概念和功能 | -| [Agent CLI 配置](docs/help/providers.md) | 安装和连接 coding agent CLI | -| [桌面端指南](docs/help/desktop-guide.md) | PC 界面和快捷键 | -| [移动端与远程访问指南](docs/help/mobile-guide.md) | 手机/平板使用、局域网访问、Tailscale/ngrok/Cloudflare Tunnel | -| [工作分析](docs/help/work-analysis.md) | 复盘工作区活动、Agent 会话和改进机会 | -| [常用工作流](docs/help/workflows.md) | 任务式教程 | -| [故障排除](docs/help/troubleshooting.md) | 常见问题和修复 | -| [CLI 参考](docs/help/cli.md) | 命令行选项 | -| [GitHub Wiki 源文件](docs/wiki/README.md) | Wiki 源页面与发布流程 | -| [AI Coding 术语](docs/wiki/AI-Coding-Terms.md) | Vibe coding、agentic harness、eval harness,以及 Coder Studio 的定位 | +| [快速开始](docs/help/quick-start.md) | 安装、启动和打开第一个 workspace | +| [第一次 Agent 运行](docs/help/first-agent-run.md) | 创建推荐的首次 Provider 会话、查看输出并审查 Git diff | +| [Provider 配置](docs/help/providers.md) | 安装和验证 coding agent CLI | +| [移动端与远程访问](docs/help/mobile-guide.md) | 局域网、Tailscale、ngrok、Cloudflare Tunnel 和手机/平板使用 | +| [安全与隐私](docs/help/security-and-privacy.md) | 本地运行模型、Provider 边界和远程访问风险 | +| [当前限制](docs/help/known-limitations.md) | 当前要求、边界和适用场景 | +| [故障排除](docs/help/troubleshooting.md) | 首次运行、Provider、端口、认证和服务恢复 | +| [English Wiki](docs/wiki/README.md) | 英文 Wiki 源页面 | --- diff --git a/docs/PRD-agent-automation-skills.md b/docs/PRD-agent-automation-skills.md deleted file mode 100644 index 795c6bb32..000000000 --- a/docs/PRD-agent-automation-skills.md +++ /dev/null @@ -1,643 +0,0 @@ -# Product Requirements Document: Agent Automation Skills - -> Last updated: 2026-06-06 -> Scope: built-in skills, agent-facing automation discovery, CLI/API access, browser verification, and plugin/status extension surfaces. -> Status: proposed product direction for phased implementation. - -## 1. Background - -Coder Studio already provides a local browser workspace that combines agent sessions, terminals, files, Git, worktrees, Supervisor, settings, and work analysis. The current product is strong when a human operates the workspace through the UI. It is weaker when an agent or local automation tool needs to discover and use Coder Studio capabilities on its own. - -The gap appears in three related areas: - -- Agents do not have a stable way to discover Coder Studio commands and current workspace context. -- Agents can modify code, but they do not have a first-class Coder Studio browser verification loop for frontend changes. -- External tools and future plugins do not have a stable status/progress/sidebar contribution surface inside Coder Studio. - -This PRD defines a complete product direction that starts with built-in skills and grows into a broader automation platform. - -## 2. Product Goal - -Make Coder Studio understandable and usable by coding agents without requiring the agent to guess commands, read stale documentation, or rely on manual UI operation. - -The intended user experience is: - -1. A user starts Claude, Codex, or another provider inside Coder Studio. -2. Coder Studio automatically makes built-in skills available to that provider. -3. The agent detects it is running inside Coder Studio. -4. The agent uses a small stable entrypoint to identify the current workspace/session and discover available commands. -5. The agent calls Coder Studio automation commands for safe operations. -6. Risky operations require approval and are auditable. -7. Later phases add browser verification and plugin/status surfaces on the same automation foundation. - -## 3. Non-Goals - -This project does not replace the existing Coder Studio UI. - -This project does not require implementing a full VS Code-style plugin marketplace in the first release. - -This project does not grant agents unrestricted filesystem, terminal, Git, browser, or plugin permissions. - -This project does not require every provider to support skills equally. Provider-specific adapters may expose different support levels. - -This project does not make browser automation use a user's real browser profile by default. - -## 4. Target Users - -### 4.1 Developer Running Agents - -The developer wants Claude, Codex, or another provider to operate more effectively inside the Coder Studio workspace. - -Primary benefit: fewer manual copy/paste steps and better task completion quality. - -### 4.2 Power User Automating Local Workflows - -The user wants scripts that open workspaces, start sessions, run tests, inspect Git, or launch verification flows. - -Primary benefit: Coder Studio becomes scriptable instead of UI-only. - -### 4.3 Plugin or Integration Author - -The author wants to surface CI, test, review, deployment, issue tracker, or internal workflow status inside Coder Studio. - -Primary benefit: integrations can attach to a stable extension surface instead of patching the core UI. - -## 5. Core Concepts - -### 5.1 Built-in Skill - -A built-in skill is a Coder Studio-owned skill packaged with the application and mounted into supported provider skill directories. It teaches the agent when and how to use Coder Studio capabilities. - -Built-in skills are versioned, visible in the Skills UI, and can be disabled by the user. - -### 5.2 Agent Runtime Context - -Agent runtime context is the workspace/session/terminal/provider identity exposed to the running agent through environment variables and a machine-readable command. - -The agent should not infer current context from terminal paths or UI labels. - -### 5.3 Capabilities Discovery - -Capabilities discovery is a machine-readable list of current Coder Studio automation commands, input schemas, examples, permission requirements, and availability. - -The capabilities response is the source of truth for agents. Built-in skills should point agents to capabilities discovery instead of hardcoding many command details. - -### 5.4 Automation Permission - -Automation permissions define what an agent may do through Coder Studio automation commands. - -Permissions are separate from normal UI access. An operation can be available to the user in the UI but still require explicit approval when invoked by an agent. - -### 5.5 Browser Verification Surface - -The browser verification surface is a controlled browser panel that an agent can use to open local development URLs, inspect page state, click, fill forms, capture screenshots, and read console errors. - -### 5.6 Plugin/Status Surface - -The plugin/status surface is a stable way for external integrations to publish commands, status pills, progress, logs, and sidebar views into Coder Studio. - -## 6. Product Requirements - -## 6.1 Built-in Skills - -Coder Studio must ship with first-party built-in skills. - -Initial built-in skills: - -| Skill | Default | Purpose | -| --- | --- | --- | -| `coder-studio-automation` | Enabled | Teach agents to identify context and discover automation commands. | -| `coder-studio-browser-verification` | Enabled when browser automation is available | Teach agents to verify frontend/UI changes through Coder Studio browser automation. | -| `coder-studio-review` | Enabled | Teach agents to inspect Git diff, run relevant checks, and summarize verification before finishing. | - -Each built-in skill must include: - -- `SKILL.md` -- name -- description -- version -- source marker of `builtin` -- supported provider target rules -- user-visible enablement state - -Built-in skill content must remain short and stable. It should instruct agents to use: - -```bash -coder-studio identify --json -coder-studio capabilities --json -``` - -Built-in skill content must not duplicate a large command reference that can drift from implementation. - -## 6.2 Built-in Skill Distribution - -Coder Studio must automatically distribute supported built-in skills to supported provider skill directories. - -Distribution rules: - -- Auto-mount is enabled by default. -- Auto-mount only targets providers with configured skill mount directories. -- User-disabled mounts must not be re-enabled automatically. -- Built-in skill updates must refresh managed source content. -- Existing user-installed skills must not be overwritten. -- Failed mounts must surface health errors in the Skills UI. -- Custom providers are not auto-mounted by default unless the provider declares compatible skill support or the user enables auto-mount for that provider. - -The existing skill management concepts should remain recognizable: - -- skill library -- skill mount relation -- provider skill target -- health scan -- repair - -## 6.3 Skills UI - -The Skills UI must show built-in skills as a distinct section. - -Required UI information: - -- skill name -- built-in source badge -- version -- enabled/disabled state -- mounted provider list -- last synced time -- health state -- link/action to view skill content -- action to disable or re-enable auto-mount -- action to repair a failed mount - -The UI must make clear that disabling a built-in skill affects future agent guidance, not the availability of the underlying Coder Studio UI. - -## 6.4 Agent Runtime Environment - -When Coder Studio starts an agent session, it must inject runtime context environment variables when supported by the provider launch path. - -Minimum variables: - -```bash -CODER_STUDIO=1 -CODER_STUDIO_WORKSPACE_ID= -CODER_STUDIO_SESSION_ID= -CODER_STUDIO_PROVIDER_ID= -CODER_STUDIO_API_URL= -``` - -When a terminal id is known: - -```bash -CODER_STUDIO_TERMINAL_ID= -``` - -When token-scoped automation is implemented: - -```bash -CODER_STUDIO_TOKEN= -``` - -Environment injection must not leak long-lived administrative secrets. - -## 6.5 Identify Command - -Coder Studio must provide a machine-readable identify command: - -```bash -coder-studio identify --json -``` - -The command must return the current runtime context when called from a Coder Studio-managed agent environment. - -Expected response shape: - -```json -{ - "insideCoderStudio": true, - "workspaceId": "ws_123", - "sessionId": "sess_123", - "terminalId": "term_123", - "providerId": "codex", - "cwd": "/path/to/workspace", - "apiUrl": "http://127.0.0.1:4173", - "permissions": ["workspace:read", "terminal:read", "git:read"] -} -``` - -If called outside Coder Studio, the command must return a clear non-context response instead of pretending to know context: - -```json -{ - "insideCoderStudio": false -} -``` - -## 6.6 Capabilities Command - -Coder Studio must provide a machine-readable capabilities command: - -```bash -coder-studio capabilities --json -``` - -The command must list automation capabilities available to the caller. - -Required fields per capability: - -- name -- CLI form -- description -- input schema -- output shape summary -- permissions -- risk level -- examples -- availability state - -Example: - -```json -{ - "name": "git.status", - "cli": "coder-studio git status", - "description": "Read Git status for a workspace.", - "inputSchema": { - "workspaceId": "string" - }, - "permissions": ["git:read"], - "riskLevel": "read", - "examples": [ - "coder-studio git status --workspace ws_123 --json" - ], - "available": true -} -``` - -Capabilities must be filterable by current permission scope. An agent should not see privileged examples it cannot invoke. - -## 6.7 Initial Automation Command Families - -The first automation surface should expose these command families in a JSON-friendly way: - -| Family | Examples | Phase | -| --- | --- | --- | -| Context | `identify`, `capabilities` | MVP | -| Workspace | list/current/open metadata | MVP | -| Session | list/current/status | MVP | -| Terminal | list/read/send | MVP | -| Git | status/diff | MVP | -| Events | JSONL event stream | Phase 2 | -| Browser | open/snapshot/screenshot/console/click/fill/wait | Phase 2 | -| Plugin status | set-status/progress/log/list | Phase 3 | -| MCP | expose the same capabilities as MCP tools | Phase 3 | - -CLI commands and WebSocket/server commands should share a common command registry or metadata source where practical. - -## 6.8 Permissions and Approval - -Automation commands must declare risk level: - -| Risk | Behavior | -| --- | --- | -| `read` | Allowed when caller has read permission. | -| `write` | Allowed when caller has write permission; command is audited. | -| `dangerous` | Requires explicit user approval before execution. | - -Initial permission groups: - -- `workspace:read` -- `workspace:write` -- `session:read` -- `session:write` -- `terminal:read` -- `terminal:write` -- `git:read` -- `git:write` -- `browser:read` -- `browser:write` -- `plugin:read` -- `plugin:write` -- `events:read` - -Risk examples: - -| Operation | Risk | -| --- | --- | -| `git status` | `read` | -| `git diff` | `read` | -| read terminal output | `read` | -| send terminal input | `write` | -| browser screenshot | `read` | -| browser click/fill | `write` | -| `git push` | `dangerous` | -| delete files | `dangerous` | -| install plugin | `dangerous` | - -When a command requires approval and no approval is available yet, it must fail clearly: - -```json -{ - "code": "approval_required", - "message": "This operation requires user approval.", - "approval": { - "operation": "terminal.send", - "riskLevel": "dangerous" - } -} -``` - -Later phases may add an approval queue or Feed-style UI. - -## 6.9 Audit Log - -Agent-triggered automation commands must be auditable. - -Audit records must include: - -- timestamp -- workspace id -- session id when available -- provider id when available -- command name -- risk level -- permission decision -- success/failure -- sanitized arguments - -Audit logs must not store secret values, full prompts, access tokens, or password field contents. - -## 6.10 Browser Verification - -Coder Studio should add a browser verification surface for agent-controlled frontend validation. - -Required capabilities: - -- open URL -- navigate -- wait for text or selector -- capture screenshot -- read accessibility or DOM snapshot -- read console errors -- click -- fill -- press key -- inspect current URL/title - -Default security model: - -- use isolated browser profile -- do not use the user's personal browser cookies by default -- prefer localhost/workspace development URLs -- require approval for sensitive form filling -- expose screenshots through Coder Studio review UI when possible - -Browser verification must integrate with built-in skills. `coder-studio-browser-verification` should tell the agent to use capabilities discovery and then browser commands. - -## 6.11 Plugin/Status API - -Coder Studio should provide a lightweight extension surface before a full plugin runtime. - -Initial plugin/status contributions: - -- set status pill -- clear status pill -- set progress -- clear progress -- append log entry -- clear log -- list current extension state -- register Quick Actions command metadata - -Example command direction: - -```bash -coder-studio status set --workspace ws_123 --key ci --label "CI running" --state running -coder-studio progress set --workspace ws_123 --key tests --value 42 --max 100 -coder-studio log append --workspace ws_123 --key ci --message "Unit tests started" -``` - -The plugin/status API should later support sidebar views, but the first release should not require arbitrary third-party webviews. - -## 6.12 MCP Server - -Coder Studio should eventually expose its automation capabilities through MCP. - -The MCP server must reuse the same underlying capabilities registry and permission checks as the CLI. - -Expected command: - -```bash -coder-studio mcp -``` - -MCP is not the MVP path. CLI and internal automation metadata should land first. - -## 6.13 Documentation - -Documentation must include: - -- user-facing overview of built-in skills -- how to disable auto-mount -- how agents discover commands -- CLI reference for `identify` and `capabilities` -- security model and approval behavior -- troubleshooting for failed skill mounts -- examples for agent-friendly browser verification - -Docs must avoid implying that unavailable later-phase browser/plugin/MCP capabilities are already shipped. - -## 7. Phased Delivery - -## 7.1 MVP: Built-in Skills and Discovery Foundation - -Goal: agents can discover Coder Studio context and capabilities through built-in skills and machine-readable commands. - -Included: - -- built-in skill registry -- materialized built-in skill files -- built-in skills appear in skill library -- auto-mount to supported providers -- user disablement respected -- Skills UI built-in section -- session environment variables -- `coder-studio identify --json` -- `coder-studio capabilities --json` -- capabilities metadata for context/workspace/session/terminal/git read commands -- basic audit records for automation commands - -Excluded from MVP: - -- browser automation -- plugin/sidebar API -- MCP server -- full approval queue UI - -MVP acceptance criteria: - -- A fresh Coder Studio start shows the built-in skills in Skills UI. -- Codex and Claude receive mounted built-in skills when their skill directories are configured. -- A user can disable a built-in skill and it stays disabled after restart. -- An agent session includes `CODER_STUDIO=1` and workspace/session/provider environment variables. -- Inside the session, `coder-studio identify --json` returns current context. -- Inside the session, `coder-studio capabilities --json` returns available commands and permissions. -- Existing user-installed skills continue to install, mount, unmount, repair, and uninstall. - -## 7.2 Phase 2: Browser Verification and Events - -Goal: agents can validate browser-visible changes through Coder Studio. - -Included: - -- browser panel lifecycle -- browser command capabilities -- screenshot capture -- DOM or accessibility snapshot -- console error collection -- click/fill/wait commands -- JSONL event stream for automation-relevant events -- browser verification skill enabled when browser automation is available -- browser command audit records - -Acceptance criteria: - -- An agent can start a dev server in a terminal and open the local URL through a Coder Studio browser command. -- An agent can capture a screenshot and read console errors. -- An agent can wait for expected text and report success/failure. -- Browser actions are scoped, auditable, and do not use personal browser profile data by default. - -## 7.3 Phase 3: Plugin/Status API and Approval UI - -Goal: external tools can publish workflow state into Coder Studio, and risky automation has a visible approval flow. - -Included: - -- status/progress/log command families -- workspace/session scoped status state -- UI surfaces for status, progress, and logs -- Quick Actions contribution metadata -- approval queue UI for dangerous commands -- approval result returned to blocked automation caller where possible - -Acceptance criteria: - -- A local script can set and clear a workspace status pill. -- A local script can publish progress and append logs visible in the UI. -- A risky agent command can request approval and the user can approve or deny it from Coder Studio. -- Approval decisions are audited. - -## 7.4 Phase 4: MCP and Advanced Extension Surfaces - -Goal: expose Coder Studio automation to MCP-capable agents and support richer extension views. - -Included: - -- `coder-studio mcp` -- MCP tools generated from capabilities registry -- same permission and audit model as CLI -- sidebar view contribution model -- extension lifecycle management - -Acceptance criteria: - -- MCP-capable agents can discover and call Coder Studio tools. -- MCP tools match CLI permissions and risk behavior. -- A trusted extension can render a sidebar view without compromising the core UI. - -## 8. Data and State Requirements - -## 8.1 Built-in Skill State - -Coder Studio must persist: - -- built-in skill version -- materialized source path -- enabled state -- per-provider auto-mount state -- user disablement decisions -- last sync timestamp -- last health state - -## 8.2 Automation Command Metadata - -Coder Studio must maintain metadata for: - -- command name -- CLI mapping -- schema -- permission requirements -- risk level -- examples -- availability checks - -This metadata should be reusable by: - -- CLI help -- `capabilities --json` -- future MCP server -- docs generation where practical - -## 8.3 Audit State - -Audit records must be local-first and stored under the configured Coder Studio state directory. - -The audit store should support bounded retention to avoid unbounded disk growth. - -## 9. Security and Privacy Requirements - -Agents must not receive unrestricted server control by default. - -Automation tokens, when introduced, must be scoped to the running session and permissions. - -Sensitive values must be redacted from logs, audit records, and UI. - -Dangerous automation commands must require approval. - -Browser automation must default to isolated browser state. - -Public network exposure does not change the security model. If Coder Studio is reachable through LAN, VPN, tunnel, or public URL, automation permissions still apply. - -## 10. Success Metrics - -Product success can be evaluated with: - -- percentage of agent sessions with built-in skills mounted successfully -- number of successful `identify` and `capabilities` calls from agent sessions -- reduction in user manual copy/paste actions during agent workflows -- browser verification success/failure counts after Phase 2 -- plugin/status API adoption after Phase 3 -- number of risky operations correctly gated by approval -- support issues related to failed skill mounts or stale skill docs - -## 11. Product Decisions for Initial Planning - -The first implementation plan should use these defaults: - -1. Auto-mount is enabled by default for `coder-studio-automation` and `coder-studio-review`. -2. `coder-studio-browser-verification` is shipped in the built-in library during MVP, but it is only auto-mounted after browser automation is available. -3. Custom providers do not receive built-in skills automatically in MVP. They may be enabled manually through the Skills UI if they expose a skill mount directory. -4. `identify` and `capabilities` should exist as top-level CLI commands and should be backed by server-side metadata that can also serve WebSocket callers. -5. MVP audit logs are stored locally but do not need a full Settings UI. They must still be available for debugging through logs or a future command. -6. Phase 2 browser automation should use an isolated Playwright/Chromium profile unless implementation research proves a better local-first option. -7. Plugin/status commands should wait until Phase 3, after the automation metadata and audit model exist. -8. Approval UI is not required for MVP. MVP dangerous commands should return `approval_required` instead of executing. - -## 12. Recommended Implementation Order - -The recommended order is: - -1. Built-in skill registry and materialization. -2. Built-in skill library integration. -3. Auto-mount sync manager and user disablement persistence. -4. Skills UI built-in section. -5. Session environment injection. -6. `identify --json`. -7. `capabilities --json`. -8. Initial audit log. -9. Browser verification commands and UI. -10. Event stream. -11. Status/progress/log API. -12. Approval UI. -13. MCP server. -14. Rich sidebar extension views. - -This order keeps the MVP small while preserving the larger platform direction. diff --git a/docs/help/README.md b/docs/help/README.md index fb9d20231..f22eee7d6 100644 --- a/docs/help/README.md +++ b/docs/help/README.md @@ -2,9 +2,12 @@ ## 我是第一次使用 -1. [快速开始](quick-start.md) — 安装、启动、第一次使用 -2. [Provider 配置](providers.md) — 安装 Claude / Codex CLI -3. [排障指南](troubleshooting.md) — 遇到问题先看这里 +1. [快速开始](quick-start.md) — 安装、启动、打开第一个 workspace +2. [第一次 Agent 运行](first-agent-run.md) — 创建推荐的首次 Provider 会话并审查 Git diff +3. [Provider 配置](providers.md) — 安装和验证 coding agent CLI +4. [排障指南](troubleshooting.md) — 遇到问题先看这里 +5. [安全与隐私](security-and-privacy.md) — 本地运行模型、Provider 边界和远程访问风险 +6. [当前限制](known-limitations.md) — 了解当前要求和边界 ## 我主要在电脑上使用 diff --git a/docs/help/app-overview.md b/docs/help/app-overview.md index e675d42b5..994a2e035 100644 --- a/docs/help/app-overview.md +++ b/docs/help/app-overview.md @@ -27,7 +27,7 @@ Coder Studio 是一款**本地优先**的 AI 编码工作台。它将 AI Agent 会话是你在某个 Provider 下的一次 Agent 运行。每个会话: -- 对应一个 AI 编码 Agent(Claude 或 Codex) +- 对应一个 AI 编码 Agent Provider - 拥有独立的终端输出 - 可以独立启动、停止和恢复 @@ -35,10 +35,13 @@ Coder Studio 是一款**本地优先**的 AI 编码工作台。它将 AI Agent ### Provider -Provider 是 AI Agent 的运行环境。当前支持两种: +Provider 是 AI Agent 的运行环境。当前内置支持: -- **Claude**:Anthropic 的 Claude Code CLI +- **Claude Code**:Anthropic 的 Claude Code CLI - **Codex**:OpenAI 的 Codex CLI +- **Gemini CLI**:Google 的 Gemini CLI +- **Cursor Agent**:Cursor 的 agent CLI +- **OpenCode**:OpenCode CLI Provider 需要在本地独立安装。Coder Studio 会在启动会话时调用相应的 Provider CLI。 @@ -51,13 +54,13 @@ Provider 需要在本地独立安装。Coder Studio 会在启动会话时调用 ### Skills(技能) -技能是分发给 Agent 的本地说明文件,用来教 Agent 在特定场景下如何行动。Coder Studio 会内置一组第一方技能,并在服务启动时同步到支持技能挂载的 Provider。 +技能是分发给 Agent 的本地说明文件,用来教 Agent 在特定场景下如何行动。Coder Studio 支持安装、管理和挂载技能,也保留内置技能同步机制;当前默认不预装第一方内置技能。 -内置技能的 MVP 重点是: +与技能和 Agent 自动化相关的基础 CLI 能力包括: - 让 Agent 通过 `coder-studio identify --json` 识别当前 workspace、session 和 provider -- 让 Agent 通过 `coder-studio capabilities --json` 发现可用自动化命令 -- 在任务结束前提醒 Agent 检查 Git 变更、测试结果和残余风险 +- 让 Agent 通过 `coder-studio capabilities --json` 查看可用的只读验证命令清单 +- 让 Agent 读取终端输出、Git 状态和指定文件 diff,辅助最终检查 ### Settings(设置) @@ -86,7 +89,7 @@ Coder Studio 使用响应式界面,同一服务同时支持: ## 常见问题 **Q:一个工作区可以有多个会话吗?** -可以。你可以在同一工作区内同时运行 Claude 和 Codex 会话。 +可以。你可以在同一工作区内同时运行多个 Provider 会话。 **Q:可以打开多个项目吗?** 可以。通过顶栏的工作区标签切换,每个工作区独立管理自己的会话、文件和终端。 diff --git a/docs/help/cli.md b/docs/help/cli.md index cf90cffb9..c9cba8048 100644 --- a/docs/help/cli.md +++ b/docs/help/cli.md @@ -98,13 +98,13 @@ coder-studio identify --json ### coder-studio capabilities -输出面向 Agent 的自动化能力清单,包括命令名、CLI 用法、权限需求、风险等级和示例。 +输出面向 Agent 的只读自动化命令清单,包括命令名、CLI 用法、权限需求、风险等级和示例。 ```bash coder-studio capabilities --json ``` -Agent 应优先使用 JSON 输出做能力发现;如果某项能力不存在,说明当前版本还没有提供对应自动化入口。 +Agent 应优先使用 JSON 输出做能力发现;当前版本只返回工作区、会话、终端和 Git 读取命令,不包含浏览器驱动命令。 ### Agent 自动化只读命令 @@ -130,6 +130,8 @@ coder-studio git diff --workspace ws_123 --path src/a.ts --staged --json 这些命令当前只做读取,不会修改工作区、会话、终端或 Git 状态。Agent 应先运行 `coder-studio capabilities --json`,再根据返回的能力清单选择命令。 +如果你要做浏览器可见变更的验证,`capabilities` 只负责把可用的读取命令列出来;真正的浏览器检查仍然需要在浏览器里直接看结果,或者借助项目自己的 E2E / Playwright 流程。 + ### coder-studio help 显示完整的帮助信息,包括所有命令和选项。 @@ -153,7 +155,7 @@ coder-studio open # 启动并打开浏览器(最常用) coder-studio status # 随时检查服务状态 coder-studio logs # 出问题时查看日志 coder-studio identify --json # Agent 发现当前运行上下文 -coder-studio capabilities --json # Agent 发现可用自动化能力 +coder-studio capabilities --json # Agent 查看可用只读验证命令 coder-studio git status --workspace ws_123 --json # Agent 读取 Git 状态 coder-studio stop # 停止服务 ``` diff --git a/docs/help/desktop-guide.md b/docs/help/desktop-guide.md index 2a490d175..8b98b2dd8 100644 --- a/docs/help/desktop-guide.md +++ b/docs/help/desktop-guide.md @@ -121,7 +121,7 @@ fs.writeFileSync(file, JSON.stringify(doc, null, 2) + "\n"); ### 创建会话 -当没有文件处于编辑状态时,中央区域显示 Agent 面板。点击创建会话按钮,选择 Provider(Claude 或 Codex)即可启动。 +当没有文件处于编辑状态时,中央区域显示 Agent 面板。点击创建会话按钮,选择已检测到的 Provider 即可启动。首次试跑建议选择 Claude 或 Codex。 ### 切换会话 diff --git a/docs/help/first-agent-run.md b/docs/help/first-agent-run.md new file mode 100644 index 000000000..210911a08 --- /dev/null +++ b/docs/help/first-agent-run.md @@ -0,0 +1,108 @@ +# 第一次 Agent 运行 + +这篇文档带你完成第一次有效的 Coder Studio Agent 会话:打开项目、创建推荐的首次 Provider 会话、观察输出,并在 Git diff 中审查改动。 + +## 这篇文档解决什么问题 + +快速验证 Coder Studio 是否已经可以承接真实 AI coding 工作流,而不只是打开界面。 + +## 前置条件 + +- 已安装 Coder Studio:`npm install -g @spencer-kit/coder-studio` +- Node.js 版本为 24 或更新:`node --version` +- 至少安装一个 Provider CLI。首次试跑建议选择 Claude Code 或 Codex: + - Claude Code:`npm install -g @anthropic-ai/claude-code` + - Codex:`npm install -g @openai/codex` +- Provider CLI 可以在普通终端中执行: + - `claude --version` + - `codex --version` + +## 1. 启动 Coder Studio + +```bash +coder-studio open +``` + +浏览器会自动打开。如果没有自动打开,先执行: + +```bash +coder-studio status +``` + +然后手动访问命令输出里的本地地址。 + +## 2. 打开一个本地项目 + +在欢迎页点击 **打开工作区**,选择一个你可以安全试验的小型仓库。 + +建议第一次选择: + +- 有 README 的个人项目 +- 有 Git 仓库历史的项目 +- 不包含生产密钥或敏感配置的项目 + +## 3. 创建推荐的首次 Provider 会话 + +进入工作区后,点击 **创建会话**,选择已检测到的 Provider。首次试跑建议选择 Claude 或 Codex,因为对应安装和排障文档最完整。 + +如果 Provider 显示未安装: + +1. 回到普通终端安装对应 CLI。 +2. 用 `claude --version` 或 `codex --version` 验证。 +3. 刷新 Coder Studio 页面或重新打开工作区。 + +## 4. 使用一个安全的小任务 + +第一次运行不要让 Agent 直接做大规模重构。建议使用这个任务: + +```text +请阅读 README,补充一小段“本项目适合谁使用”的说明。改动保持在 1-2 段内,不要改代码。 +``` + +这个任务适合第一次验证,因为: + +- 改动范围小 +- Git diff 容易审查 +- 即使结果不理想也容易回滚 + +## 5. 查看输出和文件变化 + +会话运行时,观察 Agent 面板里的输出。任务结束后: + +1. 打开 Git 视图。 +2. 查看 changed files。 +3. 打开 diff。 +4. 判断改动是否符合你的要求。 + +如果你不满意,可以继续在同一个 Agent 会话中要求它调整,也可以直接手动编辑文件。 + +## 6. 在移动端查看进度 + +如果你想用手机或平板查看进度,先阅读 [移动端与远程访问指南](mobile-guide.md)。 + +移动端适合: + +- 查看 Agent 是否还在运行 +- 查看终端输出 +- 浏览文件和 diff +- 监督长任务 + +移动端不适合作为第一次试用时的主力编辑环境。第一次 Agent 运行建议先在桌面端完成。 + +## 常见问题 + +**Q:没装 Claude 或 Codex 能不能试?** +可以打开工作区、浏览文件、使用终端,但需要安装某个 Provider 的 CLI 后才能创建对应 Agent 会话。 + +**Q:安装了 Provider 但 Coder Studio 仍然找不到?** +在普通终端执行 `which ` 和 ` --version`。如果找不到,说明安装目录不在 PATH 中。按 [Provider 配置](providers.md) 排查。 + +**Q:Agent 改坏了怎么办?** +先看 Git diff。你可以手动编辑修正,也可以用 Git 丢弃不需要的改动。 + +## 下一步 + +- [Provider 配置](providers.md) +- [移动端与远程访问指南](mobile-guide.md) +- [排障指南](troubleshooting.md) +- [常见工作流](workflows.md) diff --git a/docs/help/known-limitations.md b/docs/help/known-limitations.md new file mode 100644 index 000000000..9f9858d20 --- /dev/null +++ b/docs/help/known-limitations.md @@ -0,0 +1,70 @@ +# 当前限制 + +这篇文档说明 Coder Studio 当前版本的要求、边界和不适合承诺的场景。 + +## 这篇文档解决什么问题 + +帮助新用户在试用前建立正确预期,避免把 Coder Studio 理解成云 IDE、VS Code 替代品或 AI 模型服务。 + +## 系统要求 + +- Node.js 24 或更新版本是必需的。 +- Coder Studio 通过 npm 全局安装。 +- AI Agent 会话依赖本机已安装的 Provider CLI。 + +## Provider 边界 + +Coder Studio 不内置 Claude、Codex 或其他模型。它负责在工作区中启动和管理本地 Provider CLI。 + +如果没有安装 Provider CLI: + +- 可以打开 workspace +- 可以浏览文件 +- 可以使用终端 +- 不能创建对应的 Agent 会话 + +## 移动端边界 + +移动端适合查看、监控和审查,不适合作为第一次试用或重度编码的主力入口。 + +推荐移动端使用场景: + +- 查看长任务进度 +- 阅读 Agent 输出 +- 浏览文件 +- 查看 Git diff +- 判断是否需要回到桌面端介入 + +## 远程访问边界 + +Coder Studio 默认更适合本机或可信网络访问。跨设备或公网访问需要你自己配置网络和认证。 + +远程访问前应当: + +```bash +coder-studio config --password <强密码> +coder-studio config --host 0.0.0.0 +coder-studio serve --restart +``` + +不要把无密码的 Coder Studio 暴露到公网。 + +## 当前不主打的能力 + +当前增长发布阶段不主打: + +- 云端托管工作区 +- 多人团队权限系统 +- 手机端完整编码替代桌面端 +- 插件市场 +- 会话回放 +- 偏好云同步 + +这些方向可能以后会演进,但不是第一次试用 Coder Studio 的核心价值。 + +## 下一步 + +- [快速开始](quick-start.md) +- [第一次 Agent 运行](first-agent-run.md) +- [移动端与远程访问指南](mobile-guide.md) +- [排障指南](troubleshooting.md) diff --git a/docs/help/mobile-guide.md b/docs/help/mobile-guide.md index dbe428219..6c850c066 100644 --- a/docs/help/mobile-guide.md +++ b/docs/help/mobile-guide.md @@ -26,6 +26,36 @@ 不建议直接把 Coder Studio 端口暴露到公网。至少先启用密码,并优先使用 Tailscale、ngrok 或 Cloudflare Tunnel 这类反向隧道/VPN 方案。 +## 移动端适合什么 + +移动端的主要价值是延续桌面端已经启动的工作流: + +- 查看 Agent 是否仍在运行 +- 阅读终端输出和状态变化 +- 浏览文件和 Git diff +- 检查 Supervisor 进度 +- 在离开电脑后决定是否需要介入 + +移动端不应该被理解成完整替代桌面端的主力编码环境。第一次安装、Provider 配置、大规模文件编辑和复杂 Git 操作建议先在桌面端完成。 + +## 访问本机开发服务 + +如果项目里启动了只能在电脑本机访问的开发服务,例如 `http://localhost:8000` 或 `http://127.0.0.1:5173`,手机浏览器通常不能直接打开这个地址。Coder Studio 的内置 Browser 可以为这类 loopback HTTP 服务创建一个受控代理入口,让你在 Coder Studio 页面内部查看页面。 + +使用方式: + +1. 在项目终端里启动你的开发服务。 +2. 在 Coder Studio 编辑器 header 点击 **Browser**,打开浏览器标签。 +3. 输入本机开发服务地址,例如 `http://localhost:8000`。 +4. 点击打开后,页面会在内置 Browser iframe 中加载,HTML、CSS、JS、图片等同源资源会通过 Coder Studio 的代理转发。 + +限制: + +- v1 仅支持 HTTP/HTTPS 页面和资源请求,不支持 WebSocket。 +- Vite、Next.js 等开发服务器的 HMR/WebSocket 连接会失败;需要刷新页面查看代码变更。 +- 该入口只在 Coder Studio 内置 Browser 中工作,不会把 `localhost:8000` 变成任意外部浏览器都能访问的公开地址。 +- 仅用于访问本机 loopback 开发服务,不是通用公网代理。 + ## 局域网访问 局域网访问适合手机和电脑在同一 Wi-Fi 下的情况。 @@ -211,6 +241,10 @@ https://coder.example.com -> http://localhost:<端口> - 该工作区的所有终端 - 新建终端的入口 +### 查看本机开发服务 + +在编辑器 header 点击 Browser 按钮,输入电脑上的 loopback 服务地址,例如 `http://localhost:8000`。页面会在内置 Browser 中通过 Coder Studio 代理加载。WebSocket/HMR 暂不支持,代码变更后需要手动刷新。 + ### Supervisor 与通知 如果 Supervisor 处于活跃状态,Agent 区域会显示 Supervisor 徽章。点击可以打开 Supervisor 详情面板,查看当前进度。 diff --git a/docs/help/providers.md b/docs/help/providers.md index 4eca1312d..8b8fe0897 100644 --- a/docs/help/providers.md +++ b/docs/help/providers.md @@ -4,7 +4,7 @@ ## 这篇文档解决什么问题 -如何安装 Claude 或 Codex CLI,以便在 Coder Studio 中创建 Agent 会话。 +如何安装常用 coding agent CLI,以便在 Coder Studio 中创建 Agent 会话。 ## 前置条件 @@ -13,12 +13,15 @@ ## 当前支持哪些 Provider -Coder Studio 目前支持两种 Provider: +Coder Studio 当前内置支持: -- **Claude**:Anthropic Claude Code CLI(Full mode) -- **Codex**:OpenAI Codex CLI(Limited mode) +- **Claude Code**:`claude`,Full,Stable +- **Codex**:`codex`,Full,Stable +- **Gemini CLI**:`gemini`,Full,Stable +- **Cursor Agent**:`agent`,Full,Stable +- **OpenCode**:`opencode`,Limited,Experimental -两者都需要在本地独立安装。 +这些 Provider 都需要在本地独立安装。Full 表示支持交互式会话、idle detection,以及 supervisor/session analysis 这类自动化工作流。Limited 表示可以运行交互式会话,但并非所有自动化能力都已接入。 ## 安装 Claude CLI @@ -44,9 +47,87 @@ npm install -g @openai/codex codex --version ``` +## 安装 Gemini CLI + +```bash +npm install -g @google/gemini-cli +``` + +安装后确认: + +```bash +gemini --version +``` + +## 安装 Cursor Agent + +macOS 和 Linux 可以使用 Cursor 官方安装脚本: + +```bash +curl https://cursor.com/install -fsS | bash +``` + +安装后确认: + +```bash +agent --version +``` + +## 安装 OpenCode + +```bash +npm install -g opencode-ai +``` + +安装后确认: + +```bash +opencode --version +``` + +## 验证 PATH + +安装 Provider CLI 后,Coder Studio 只能识别当前服务进程 PATH 中能找到的命令。先在普通终端验证: + +```bash +which claude +which codex +which gemini +which agent +which opencode +claude --version +codex --version +gemini --version +agent --version +opencode --version +``` + +如果 `which` 找不到命令,常见原因是全局 npm 命令目录没有加入 PATH。 + +查看全局 npm 前缀目录: + +```bash +npm config get prefix +npm prefix -g +``` + +在 macOS/Linux 上,全局命令 shim 通常在 `/bin`。把这个目录加入 shell 配置后,重新打开终端并重启 Coder Studio: + +```bash +coder-studio serve --restart +``` + +Windows 用户还需要确认 npm 的全局命令目录在用户或系统 PATH 中。常见目录是 `%APPDATA%\npm`。 + ## Coder Studio 如何识别 Provider -Coder Studio 启动工作区后会自动检测系统中是否有 `claude` 和 `codex` 命令。如果检测到,界面上就会显示对应的 Provider 入口。如果未检测到,创建会话时会显示安装提示。 +Coder Studio 启动工作区后会自动检测系统中是否有内置 Provider 需要的命令,例如 `claude`、`codex`、`gemini`、`agent` 和 `opencode`。如果检测到,界面上就会显示对应的 Provider 入口。如果未检测到,创建会话时会显示安装提示。 + +## Aider 和自定义 Provider + +Aider 不属于当前内置 Provider 列表。它适合通过 preset/custom-provider 工作流接入:先在本地安装 `aider` 命令,再用自定义 Provider 配置指定命令、参数和工作目录。 + +自定义 Provider 主要是交互式命令集成,不一定支持内置 Provider 的 supervisor、idle detection、Agent instructions 或自动安装能力。 ## 在设置页中配置 Provider @@ -71,7 +152,7 @@ Coder Studio 启动工作区后会自动检测系统中是否有 `claude` 和 `c ## 常见问题 **Q:安装了 Provider 但 Coder Studio 仍然说未找到?** -确保 `npm install -g` 安装的目录在 PATH 中。可以用 `which claude` 或 `which codex` 验证。 +确保安装目录在 PATH 中。可以用 `which ` 和 ` --version` 验证,例如 `which gemini`、`which opencode`。 **Q:两个 Provider 可以同时用吗?** -可以。Coder Studio 支持在同一工作区内并行运行 Claude 和 Codex 会话。 +可以。Coder Studio 支持在同一工作区内并行运行多个 Provider 会话。 diff --git a/docs/help/quick-start.md b/docs/help/quick-start.md index 32bad548b..a18616617 100644 --- a/docs/help/quick-start.md +++ b/docs/help/quick-start.md @@ -22,8 +22,8 @@ node --version Coder Studio 本身不包含 AI 引擎,需要安装 Provider CLI 才能创建 Agent 会话: -- **Claude**:需要安装 Claude Code CLI -- **Codex**:需要安装 OpenAI Codex CLI +- 推荐首次试跑:Claude Code 或 Codex +- 其他内置 Provider:Gemini CLI、Cursor Agent、OpenCode 安装步骤见 [Provider 配置指南](providers.md)。 @@ -64,7 +64,7 @@ coder-studio open ### 2. 创建第一个 Agent 会话 -打开工作区后,你会看到 Agent 工作区。点击 **"创建会话"**,选择 Claude 或 Codex 即可开始。 +打开工作区后,你会看到 Agent 工作区。点击 **"创建会话"**,选择已检测到的 Provider 即可开始。首次试跑建议选择 Claude 或 Codex。 如果 Provider 未安装,界面上会显示安装提示和指引。 @@ -98,6 +98,8 @@ Coder Studio 需要 Node.js >= 24.0.0。可以用 `nvm use 24` 或从 [nodejs.or ## 下一步看什么 +- [第一次 Agent 运行](first-agent-run.md) — 完成第一个推荐 Provider 会话并审查 diff - [App 功能总览](app-overview.md) — 了解核心概念 - [桌面端使用指南](desktop-guide.md) — 熟悉桌面端操作 - [移动端使用指南](mobile-guide.md) — 在手机端访问和使用 +- [排障指南](troubleshooting.md) — 解决 Node、Provider、端口和认证问题 diff --git a/docs/help/security-and-privacy.md b/docs/help/security-and-privacy.md new file mode 100644 index 000000000..815c95851 --- /dev/null +++ b/docs/help/security-and-privacy.md @@ -0,0 +1,45 @@ +# 安全与隐私 + +这篇文档说明 Coder Studio 的本地运行模型、Provider 数据边界和远程访问风险。 + +## 运行位置 + +- Coder Studio server 运行在你的机器上。 +- Web UI 由本机服务提供。 +- 你打开的 workspace 来自本机项目目录。 +- Agent 会话通过本机已安装的 Provider CLI 启动。 + +Coder Studio 本身不是托管代码服务,不会把你的 workspace 作为云端代码环境运行。 + +## Provider 数据边界 + +Claude Code、Codex 或其他 Provider CLI 可能会根据各自的行为和配置,发送 prompts、代码上下文、终端输出、文件片段或其他任务数据。 + +如果你需要严格的数据处理保证,请检查对应 Provider 的文档、账号设置和 CLI 配置。 + +## 远程访问风险 + +局域网或远程访问前先设置密码: + +```bash +coder-studio config --password <强密码> +coder-studio serve --restart +``` + +不要在没有认证的情况下把 Coder Studio 暴露到公网。 + +请把远程 Coder Studio 访问当作远程 shell 访问处理:任何能够通过认证的人,都可能用你的本地用户权限读取文件、运行终端命令,并触发 Provider 工具。 + +## 实用检查清单 + +- 开放给其他设备前设置强密码。 +- 个人跨设备访问优先使用 Tailscale。 +- 提交前审查 Git diff。 +- 临时隧道使用结束后及时停止。 +- 敏感仓库避免使用公开链接访问。 + +## 下一步 + +- [移动端与远程访问指南](mobile-guide.md) +- [当前限制](known-limitations.md) +- [排障指南](troubleshooting.md) diff --git a/docs/help/troubleshooting.md b/docs/help/troubleshooting.md index dac7244a3..053100e6c 100644 --- a/docs/help/troubleshooting.md +++ b/docs/help/troubleshooting.md @@ -11,6 +11,21 @@ - 已安装 Coder Studio 和 Node.js >= 24.0.0 - 能够执行 `coder-studio status`、`coder-studio logs` 等 CLI 命令 +## 首次运行快速排查 + +如果第一次试用没有顺利跑起来,按这个顺序排查: + +1. `node --version` 确认 Node.js >= 24.0.0。 +2. `coder-studio version` 确认 CLI 已安装。 +3. `coder-studio status` 确认服务正在运行。 +4. `coder-studio logs` 查看最近错误。 +5. `which ` 确认 Provider CLI 在 PATH 中。 +6. ` --version` 确认 Provider CLI 可执行。 +7. 如果浏览器打不开,手动访问 `coder-studio status` 输出的 URL。 +8. 如果移动端打不开,确认服务监听 `0.0.0.0` 且防火墙允许该端口。 + +首次试用建议先在桌面端完成,不要一开始就通过公网隧道或手机端排查所有问题。 + ## 服务启动失败 如果 `coder-studio serve` 或 `coder-studio open` 启动后没有正常响应: @@ -46,9 +61,25 @@ ```bash which claude which codex + which gemini + which agent + which opencode + claude --version + codex --version + gemini --version + agent --version + opencode --version + ``` +2. 如果未找到命令,按 [Provider 配置指南](providers.md) 安装或修复 PATH。 +3. 如果普通终端能找到命令,但 Coder Studio 找不到,重启服务: + ```bash + coder-studio serve --restart + ``` +4. 如果仍不可用,查看日志: + ```bash + coder-studio logs ``` -2. 如果未找到,按 [Provider 配置指南](providers.md) 进行安装 -3. 安装后刷新浏览器页面或重新打开工作区 +5. 提交问题时附上 Node 版本、Provider 版本、`which` 输出和日志片段。 ## 无法创建会话 diff --git a/docs/help/workflows.md b/docs/help/workflows.md index ad10c9daf..dd26e3f63 100644 --- a/docs/help/workflows.md +++ b/docs/help/workflows.md @@ -9,7 +9,7 @@ ## 前置条件 - Coder Studio 已安装并启动 -- 已配置至少一个 Provider(Claude 或 Codex) +- 已配置至少一个 Provider ## 打开一个本地项目并开始工作 diff --git a/docs/product-spec/modules/work-analysis.zh-CN.md b/docs/product-spec/modules/work-analysis.zh-CN.md index eee8f789f..98f602f08 100644 --- a/docs/product-spec/modules/work-analysis.zh-CN.md +++ b/docs/product-spec/modules/work-analysis.zh-CN.md @@ -51,10 +51,11 @@ 系统响应: - 页面创建 work analysis controller,并从 URL query 解析时间范围和目录筛选。 - 控制器加载 dashboard 后渲染状态条、KPI、token 趋势、贡献排行、任务/工具/skill 归因和小时热力图。 +- 没有可用或足够新的小时索引时,服务自动补齐索引后返回 dashboard。 - 没有 dashboard 时展示空态,并提供立即刷新按钮。 状态与边界: -- Loading:首次读取索引时显示读取提示。 +- Loading:首次读取或补齐索引时显示扫描中提示。 - Empty:没有索引或数据时显示“暂无工作分析索引”。 - Warning:scanState error 或 dashboard quality warnings 以 Notice 展示。 - URL sync:筛选条件变化后写回路由 query。 @@ -62,8 +63,9 @@ 验收标准: - Given 当前没有 dashboard 数据 - When 用户打开 Work Analysis 页面 -- Then 页面显示空态 -- And 提供立即刷新操作 +- Then 页面显示自动扫描中的状态 +- And 索引补齐后展示 dashboard +- And 仍提供立即刷新操作 代码索引: - `packages/web/src/features/work-analysis/page.tsx` diff --git a/docs/promotion/growth-launch-2026-06.en.md b/docs/promotion/growth-launch-2026-06.en.md new file mode 100644 index 000000000..4924b8b3a --- /dev/null +++ b/docs/promotion/growth-launch-2026-06.en.md @@ -0,0 +1,70 @@ +# Coder Studio English Launch Copy + +## Show HN Title + +Show HN: Coder Studio - a self-hosted browser workspace for AI coding agents + +## Short Description + +Coder Studio brings Claude Code, Codex, terminals, files, Git diff review, supervision, and cross-device continuation into one self-hosted browser workspace. + +## Long Post Draft + +I built Coder Studio because terminal-only AI coding gets scattered once the agent output turns into real project work. + +The agent runs in one terminal, files are in an editor, diffs are in another tool, and long tasks require repeatedly checking whether anything stalled. If you leave the machine where the CLI is running, the workflow gets even more awkward. + +Coder Studio is a self-hosted browser workspace around local AI coding agent CLIs. + +It brings together: + +- built-in session support for Claude Code, Codex, Gemini CLI, Cursor Agent, and OpenCode +- preset/custom-provider oriented workflows for tools such as Aider +- local terminals +- file tree and Monaco editor +- Git status, changed files, and diff review +- Supervisor loops for long tasks +- Work Analysis for reviewing what happened over time +- Skills management for reusable agent workflows +- desktop, tablet, and phone access to the same workspace + +Install: + +```bash +npm install -g @spencer-kit/coder-studio +coder-studio open +``` + +A good first trial: + +1. Open a local repository. +2. Start an agent session. For a first trial, Claude or Codex is the recommended path. +3. Ask the agent for a small README or UI-copy change. +4. Review the changed files and Git diff beside the session. +5. Reopen the same workspace from a phone or tablet to monitor progress. + +What it is not: + +- not a cloud IDE +- not a VS Code replacement +- not an AI model provider +- not a promise that phone screens replace desktop coding + +Coder Studio itself is not a hosted code service and opens local repositories. Provider CLIs may send prompts, code context, terminal output, file snippets, or other task data according to their own behavior and configuration. + +GitHub: + +https://github.com/spencerkit/coder-studio + +npm: + +https://www.npmjs.com/package/@spencer-kit/coder-studio + +I would especially like feedback from people running long Claude Code or Codex tasks, reviewing agent diffs frequently, or switching between desktop and mobile devices during AI coding work. + +## Reddit Notes + +- For `r/selfhosted`, lead with local-first and remote access boundaries. +- For AI coding communities, lead with Claude Code / Codex workflow pain. +- Avoid posting the same text to many communities on the same day. +- Answer security and Provider CLI questions directly. diff --git a/docs/promotion/growth-launch-2026-06.zh-CN.md b/docs/promotion/growth-launch-2026-06.zh-CN.md new file mode 100644 index 000000000..9a06b1a09 --- /dev/null +++ b/docs/promotion/growth-launch-2026-06.zh-CN.md @@ -0,0 +1,70 @@ +# Coder Studio 中文增长发布长帖 + +## 推荐标题 + +我做了一个自部署的 AI Coding 工作台,用来管理 Claude Code / Codex 长任务 + +## 正文草稿 + +很多 AI coding 工作流一开始都很轻:打开终端,启动 Claude Code 或 Codex,写一个 prompt,然后等它输出。 + +但进入真实项目后,问题会变得分散: + +- Agent 在终端里跑,文件在编辑器里看 +- 改动需要再切到 Git 工具里审查 +- 长任务经常要反复回来确认有没有卡住 +- 换到手机或平板后,很难看到完整 workspace 上下文 + +Coder Studio 想解决的不是“再做一个云 IDE”,而是给 AI coding agent 一个自部署的浏览器工作台。 + +它把这些东西放在同一个 workspace 里: + +- Claude Code / Codex 等 Agent 会话 +- 本地终端 +- 文件树和 Monaco 编辑器 +- Git 状态、changed files 和 diff +- Supervisor 长任务监督 +- Work Analysis 工作复盘 +- Skills 管理 +- 桌面、平板、手机的同一个 workspace 访问 + +安装方式: + +```bash +npm install -g @spencer-kit/coder-studio +coder-studio open +``` + +第一次试用建议做一个小任务: + +1. 打开一个本地仓库。 +2. 创建 Agent 会话。首次试跑建议选择 Claude 或 Codex。 +3. 让 Agent 改 README 的一小段说明。 +4. 在 Git diff 里审查结果。 +5. 用手机或平板打开同一个 workspace 查看进度。 + +它不是: + +- 不是云 IDE +- 不是 VS Code 替代品 +- 不是 AI 模型服务 +- 不是承诺手机完整替代桌面编码 + +它更像是围绕本地 repo、本地 shell 和 AI coding CLI 的一层工作台。Coder Studio 本身不是托管代码服务,打开的是本地仓库;Provider CLI 可能根据自己的行为和配置发送 prompts、代码上下文、终端输出、文件片段或其他任务数据。 + +项目地址: + +https://github.com/spencerkit/coder-studio + +npm: + +https://www.npmjs.com/package/@spencer-kit/coder-studio + +如果你主要使用 Claude Code / Codex,或者正在关注 Gemini CLI、Cursor Agent、OpenCode、Aider 这类 CLI agent 工作流,尤其经常跑长任务、审查 diff、或者希望离开电脑后还能看进度,这个项目应该值得试一下。 + +## V2EX 发布备注 + +- 标题保持技术导向,不使用夸张词。 +- 首帖重点解释“为什么不是云 IDE”。 +- 评论区优先收集安装、Provider、Node 版本和 PATH 问题。 +- 不在首帖承诺插件系统、团队协作或云同步。 diff --git a/docs/promotion/growth-launch-checklist-2026-06.md b/docs/promotion/growth-launch-checklist-2026-06.md new file mode 100644 index 000000000..b272ff3a4 --- /dev/null +++ b/docs/promotion/growth-launch-checklist-2026-06.md @@ -0,0 +1,67 @@ +# Growth Launch Checklist - 2026-06 + +Use this checklist before posting broadly in English communities. + +## Landing Pages + +- [ ] `README.md` explains the product in 30 seconds. +- [ ] `README.md` links English users to `docs/wiki/*`. +- [ ] `README.zh-CN.md` links Chinese users to `docs/help/*`. +- [ ] `packages/cli/README.md` explains install, first run, Provider CLIs, and boundaries. +- [ ] `packages/cli/package.json` description and keywords match the launch positioning. + +## First Trial + +- [ ] Quick Start explains install and `coder-studio open`. +- [ ] First Agent Run explains opening a repo, starting a recommended first provider, and reviewing Git diff. +- [ ] Provider docs explain install, `which`, `--version`, and PATH checks. +- [ ] Troubleshooting covers Node 24, service status, logs, Provider CLI, port, browser, auth, and mobile access. + +## Trust And Boundaries + +- [ ] Security docs explain local-first behavior and Provider CLI data boundaries. +- [ ] Known Limitations explain Node 24, Provider CLI dependency, mobile limits, and remote access responsibility. +- [ ] Mobile docs describe monitoring and review as the primary phone/tablet use case. +- [ ] README avoids claiming Coder Studio is a cloud IDE, VS Code replacement, AI provider, or full mobile coding replacement. + +## Feedback Intake + +- [ ] `CONTRIBUTING.md` exists. +- [ ] Installation issue template exists. +- [ ] Provider setup issue template exists. +- [ ] Feature request template exists. +- [ ] Workflow showcase template exists. + +## Promotion Materials + +- [ ] Chinese long-form post is ready. +- [ ] English launch post is ready. +- [ ] Social posts are ready. +- [ ] Launch FAQ is ready. +- [ ] Show HN title is ready. + +## Manual Trial + +- [ ] Install from npm in a clean shell. +- [ ] Run `coder-studio open`. +- [ ] Open a local repository. +- [ ] Start one recommended first-run provider session. +- [ ] Ask for a small documentation change. +- [ ] Review changed files and Git diff. +- [ ] Open the workspace from a second browser size or device. +- [ ] Record any confusion in the feedback log. + +## Verification Commands + +```bash +git diff --check +pnpm ci:lint +pnpm ci:test +pnpm ci:build +``` + +Before public English launch, prefer: + +```bash +pnpm ci:verify +``` diff --git a/docs/promotion/launch-faq-2026-06.md b/docs/promotion/launch-faq-2026-06.md new file mode 100644 index 000000000..6ec6339cd --- /dev/null +++ b/docs/promotion/launch-faq-2026-06.md @@ -0,0 +1,33 @@ +# Growth Launch FAQ + +## What is Coder Studio? + +Coder Studio is a self-hosted browser workspace for AI coding agent workflows. It brings agent sessions, terminals, files, Git diff review, supervision, work analysis, and cross-device continuation into one workspace. + +## Is it a cloud IDE? + +No. It runs on your machine and opens local project directories. Remote access is something you configure through LAN, Tailscale, ngrok, Cloudflare Tunnel, or another network path. Set a strong password and authentication before exposing it beyond your local machine. + +## Is it a VS Code replacement? + +No. It is focused on AI coding agent workflows: running agents, watching progress, reviewing changes, and continuing across devices. + +## Does it include AI models or Provider CLIs? + +No. You install Provider CLIs separately, such as Claude Code, Codex, Gemini CLI, Cursor Agent, or OpenCode. Coder Studio detects and launches those local commands. + +## Does code leave my machine? + +Coder Studio itself is not a hosted code service. Provider CLIs may send prompts, code context, terminal output, file snippets, or other task data according to the provider's own behavior and configuration. + +## What is mobile good for? + +Mobile is best for checking progress, reading output, browsing files, reviewing diffs, and deciding whether to intervene. Desktop remains the best environment for first setup and heavy editing. + +## Why require Node.js 24? + +The current package targets the runtime used by the server, CLI, and bundled dependencies. Users should check `node --version` before installing. + +## What feedback is most useful? + +Installation failures, Provider CLI detection issues, first agent run confusion, mobile setup problems, and real AI coding workflows that still feel awkward. diff --git a/docs/promotion/releases/README.md b/docs/promotion/releases/README.md index 7dff80f0b..6a52488c1 100644 --- a/docs/promotion/releases/README.md +++ b/docs/promotion/releases/README.md @@ -16,6 +16,13 @@ Add a release narrative when a version is published to GitHub and npm, especiall 4. Install or upgrade command 5. Full changelog comparison link +## Growth Launch Content Pack + +- [Chinese growth launch post](../growth-launch-2026-06.zh-CN.md) +- [English growth launch post](../growth-launch-2026-06.en.md) +- [Social posts](../social-posts-2026-06.md) +- [Launch FAQ](../launch-faq-2026-06.md) + ## Current releases - [v0.4.4](v0.4.4.md) diff --git a/docs/promotion/social-posts-2026-06.md b/docs/promotion/social-posts-2026-06.md new file mode 100644 index 000000000..60089d234 --- /dev/null +++ b/docs/promotion/social-posts-2026-06.md @@ -0,0 +1,35 @@ +# Growth Launch Social Posts + +## 中文短帖 + +我做了一个自部署的 AI Coding 工作台 Coder Studio。 + +它把 Claude Code / Codex、终端、文件、Git diff、Supervisor 长任务监督和移动端查看进度放进同一个浏览器 workspace。 + +不是云 IDE,也不是 VS Code 替代品。更像是给 AI coding CLI 加一个可审查、可监督、可跨设备继续的工作台。 + +```bash +npm install -g @spencer-kit/coder-studio +coder-studio open +``` + +GitHub: https://github.com/spencerkit/coder-studio + +## English Short Post + +I built Coder Studio, a self-hosted browser workspace for AI coding agents. + +It brings Claude Code, Codex, terminals, files, Git diff review, Supervisor loops, and cross-device continuation into one workspace. + +Not a cloud IDE, not a VS Code replacement, and not an AI provider. It is a workbench around local repos, local shells, and the agent CLIs you already use. + +```bash +npm install -g @spencer-kit/coder-studio +coder-studio open +``` + +GitHub: https://github.com/spencerkit/coder-studio + +## Demo Caption + +Start an agent task on desktop, review the changed files and Git diff beside the session, then reopen the same workspace from mobile to check progress. diff --git a/docs/superpowers/plans/2026-06-06-agent-automation-skills-mvp.md b/docs/superpowers/plans/2026-06-06-agent-automation-skills-mvp.md deleted file mode 100644 index 005f1bad8..000000000 --- a/docs/superpowers/plans/2026-06-06-agent-automation-skills-mvp.md +++ /dev/null @@ -1,2008 +0,0 @@ -# Agent Automation Skills MVP Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Implement the MVP from `docs/PRD-agent-automation-skills.md`: built-in Coder Studio skills, automatic provider mounting, agent runtime context, `identify`/`capabilities` discovery, and a minimal audit foundation. - -**Architecture:** Reuse the existing skill library/mount/health pipeline. Add a built-in skill registry that materializes first-party skills into the state directory, then uses the existing `SkillMountManager` to mount them into provider skill directories. Add an automation metadata layer that backs both WebSocket commands and top-level CLI output, and extend `SessionManager` to inject Coder Studio runtime context into agent terminals. - -**Tech Stack:** TypeScript, Vitest, Node.js fs APIs, existing Coder Studio command dispatch, Fastify runtime config, Jotai/React Skills UI. - ---- - -## File Structure - -Create these new files: - -- `packages/server/src/skills/builtin/registry.ts` - declares built-in skill metadata and markdown content. -- `packages/server/src/skills/builtin/materialize.ts` - writes built-in skill directories and `SKILL.md` files under state. -- `packages/server/src/skills/builtin/sync-manager.ts` - synchronizes built-ins into the skill library and mounts default-enabled skills. -- `packages/server/src/automation/capabilities.ts` - defines automation capability metadata and filtering. -- `packages/server/src/automation/identify.ts` - resolves current runtime context from environment/runtime data. -- `packages/server/src/automation/audit-log.ts` - append-only local audit writer with sanitization. -- `packages/server/src/commands/automation.ts` - WebSocket commands for `automation.identify` and `automation.capabilities`. -- `packages/cli/src/automation-client.ts` - CLI helpers for `identify` and `capabilities` output. -- `packages/server/src/__tests__/skills/builtin-registry.test.ts` -- `packages/server/src/__tests__/skills/builtin-sync-manager.test.ts` -- `packages/server/src/__tests__/automation/identify.test.ts` -- `packages/server/src/__tests__/automation/capabilities.test.ts` -- `packages/server/src/__tests__/automation/audit-log.test.ts` - -Modify these existing files: - -- `packages/core/src/domain/skill-management.ts` - add `builtin` as a skill source and built-in related fields. -- `packages/core/src/index.ts` - keep skill-management exports available. -- `packages/server/src/storage/repositories/skill-library-repo.ts` - preserve built-in entries and scanned local entries. -- `packages/server/src/skills/mount-manager.ts` - ensure mounted built-in skills follow the same symlink/copy behavior. -- `packages/server/src/commands/skills.ts` - add built-in sync/list/set-enabled commands and expose built-in metadata in library list. -- `packages/server/src/commands/index.ts` - import `automation.ts`. -- `packages/server/src/ws/dispatch.ts` - add optional `builtinSkillSyncMgr`, `automationAuditLog`, and `stateRoot` context dependencies. -- `packages/server/src/server.ts` - instantiate built-in sync manager, sync built-ins at startup, and wire dependencies. -- `packages/server/src/session/manager.ts` - inject `CODER_STUDIO_*` env values into agent terminals. -- `packages/server/src/session/types.ts` if needed for dependency typing. -- `packages/cli/src/parse-args.ts` - add `identify`, `capabilities`, `--json`, and `skills builtin ...` parsing. -- `packages/cli/src/cli.ts` - handle new commands. -- `docs/help/cli.md` - document `identify` and `capabilities`. -- `packages/web/src/features/workspace/actions/use-skills-panel.ts` - carry built-in metadata through UI state. -- `packages/web/src/features/workspace/views/shared/skills-panel.tsx` - show built-in source labels and built-in section or grouping. -- `packages/web/src/features/workspace/views/shared/skills-panel.test.tsx` - cover built-in labels and disabled state. - -Implementation notes: - -- Store user disablement in `SettingsRepo` under key `skills.builtin.disabledMounts`. -- Store materialized built-in sources under `/state/skills/builtin//SKILL.md`. -- Use TypeScript string constants for built-in skill content instead of relying on runtime copying of source markdown assets. -- MVP auto-mounts `coder-studio-automation` and `coder-studio-review`. It materializes `coder-studio-browser-verification` but does not auto-mount it until browser automation exists. -- Keep Browser, Plugin/status, MCP, and approval UI out of this MVP implementation. - ---- - -### Task 1: Extend Core Skill Types for Built-ins - -**Files:** -- Modify: `packages/core/src/domain/skill-management.ts` -- Test: `packages/core/src/domain/skill-management.test.ts` - -- [ ] **Step 1: Write the failing type/runtime test** - -Add a focused test in `packages/core/src/domain/skill-management.test.ts`: - -```ts -import { describe, expect, it } from "vitest"; -import { SKILL_LIBRARY_SOURCES } from "./skill-management"; - -describe("skill-management", () => { - it("includes builtin as a supported skill library source", () => { - expect(SKILL_LIBRARY_SOURCES).toContain("builtin"); - }); -}); -``` - -If the file already has a `describe("skill-management", ...)`, add only the `it(...)` block and import `SKILL_LIBRARY_SOURCES`. - -- [ ] **Step 2: Run the test and verify it fails** - -Run: - -```bash -pnpm vitest run packages/core/src/domain/skill-management.test.ts -``` - -Expected: FAIL because `SKILL_LIBRARY_SOURCES` is not exported or does not contain `builtin`. - -- [ ] **Step 3: Implement the core type change** - -Update `packages/core/src/domain/skill-management.ts`: - -```ts -export const SKILL_LIBRARY_SOURCES = ["skillhub", "local", "builtin"] as const; -type SkillLibrarySource = (typeof SKILL_LIBRARY_SOURCES)[number]; - -export interface SkillLibraryEntry { - slug: string; - displayName: string; - description?: string; - version: string; - source: SkillLibrarySource; - libraryPath: string; - installState: SkillInstallState; - installedAt: number; - updatedAt: number; - lastError?: string; - builtin?: { - defaultEnabled: boolean; - autoMount: boolean; - }; -} -``` - -Keep the existing constants and interfaces intact; only replace the hardcoded `source: "skillhub" | "local"` union and add the optional `builtin` metadata. - -- [ ] **Step 4: Run the test and verify it passes** - -Run: - -```bash -pnpm vitest run packages/core/src/domain/skill-management.test.ts -``` - -Expected: PASS. - -- [ ] **Step 5: Commit** - -```bash -git add packages/core/src/domain/skill-management.ts packages/core/src/domain/skill-management.test.ts -git commit -m "feat: add builtin skill source type" -``` - ---- - -### Task 2: Add Built-in Skill Registry and Materialization - -**Files:** -- Create: `packages/server/src/skills/builtin/registry.ts` -- Create: `packages/server/src/skills/builtin/materialize.ts` -- Test: `packages/server/src/__tests__/skills/builtin-registry.test.ts` - -- [ ] **Step 1: Write the failing registry/materialization tests** - -Create `packages/server/src/__tests__/skills/builtin-registry.test.ts`: - -```ts -import { mkdtemp, readFile, rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterEach, describe, expect, it } from "vitest"; -import { BUILTIN_SKILLS } from "../../skills/builtin/registry.js"; -import { materializeBuiltinSkills } from "../../skills/builtin/materialize.js"; - -describe("builtin skills", () => { - let tempDir: string | undefined; - - afterEach(async () => { - if (tempDir) { - await rm(tempDir, { recursive: true, force: true }); - } - }); - - it("declares the MVP built-in skills with stable defaults", () => { - expect(BUILTIN_SKILLS.map((skill) => skill.slug)).toEqual([ - "coder-studio-automation", - "coder-studio-browser-verification", - "coder-studio-review", - ]); - expect(BUILTIN_SKILLS.find((skill) => skill.slug === "coder-studio-automation")).toMatchObject({ - defaultEnabled: true, - autoMountInMvp: true, - }); - expect( - BUILTIN_SKILLS.find((skill) => skill.slug === "coder-studio-browser-verification") - ).toMatchObject({ - defaultEnabled: true, - autoMountInMvp: false, - }); - }); - - it("materializes built-in SKILL.md files into the state directory", async () => { - tempDir = await mkdtemp(join(tmpdir(), "coder-studio-builtin-skills-")); - - const entries = await materializeBuiltinSkills({ - builtinRoot: tempDir, - now: () => 1234, - }); - - expect(entries).toHaveLength(3); - expect(entries[0]).toMatchObject({ - source: "builtin", - installState: "installed", - installedAt: 1234, - updatedAt: 1234, - }); - - const automation = entries.find((entry) => entry.slug === "coder-studio-automation"); - expect(automation).toBeDefined(); - expect(await readFile(join(automation!.libraryPath, "SKILL.md"), "utf8")).toContain( - "coder-studio identify --json" - ); - }); -}); -``` - -- [ ] **Step 2: Run the test and verify it fails** - -Run: - -```bash -pnpm --filter @coder-studio/server test -- src/__tests__/skills/builtin-registry.test.ts -``` - -Expected: FAIL because `skills/builtin/registry.js` and `materialize.js` do not exist. - -- [ ] **Step 3: Implement `registry.ts`** - -Create `packages/server/src/skills/builtin/registry.ts`: - -```ts -export interface BuiltinSkillDefinition { - slug: string; - displayName: string; - description: string; - version: string; - defaultEnabled: boolean; - autoMountInMvp: boolean; - content: string; -} - -const AUTOMATION_SKILL = `--- -name: coder-studio-automation -description: Use when running inside Coder Studio and you need workspace, session, terminal, Git, or event automation. ---- - -# Coder Studio Automation - -When CODER_STUDIO=1 is present: - -1. Run \`coder-studio identify --json\` to inspect current context. -2. Run \`coder-studio capabilities --json\` to discover supported commands. -3. Prefer commands with \`--json\`. -4. Use current workspace and session IDs from identify instead of guessing. -5. Do not run destructive commands unless the user explicitly asked. -6. If a command returns approval_required, explain what approval is needed and wait. -`; - -const BROWSER_VERIFICATION_SKILL = `--- -name: coder-studio-browser-verification -description: Use after frontend, UI, CSS, route, form, or browser-visible changes to verify the app in Coder Studio's browser automation surface. ---- - -# Browser Verification - -For browser-visible changes: - -1. Use \`coder-studio identify --json\`. -2. Use \`coder-studio capabilities --json\` and find browser commands. -3. Start the dev server in a terminal when needed. -4. Open the local URL in a Coder Studio browser surface. -5. Wait for the expected text or selector. -6. Capture a screenshot. -7. Read console errors. -8. Report visible issues and fix them before final response. - -If browser capabilities are not available, say so and use the best available local verification. -`; - -const REVIEW_SKILL = `--- -name: coder-studio-review -description: Use before finishing a coding task in Coder Studio to inspect Git changes, tests, and residual risk. ---- - -# Coder Studio Review - -Before final response after code edits: - -1. Run \`coder-studio identify --json\`. -2. Use capabilities to find Git and terminal commands. -3. Inspect Git status and diff. -4. Run relevant tests when practical. -5. Report files changed, verification run, and any remaining risk. -`; - -export const BUILTIN_SKILLS: BuiltinSkillDefinition[] = [ - { - slug: "coder-studio-automation", - displayName: "Coder Studio Automation", - description: "Teach agents to identify Coder Studio context and discover automation commands.", - version: "1.0.0", - defaultEnabled: true, - autoMountInMvp: true, - content: AUTOMATION_SKILL, - }, - { - slug: "coder-studio-browser-verification", - displayName: "Coder Studio Browser Verification", - description: "Teach agents to verify browser-visible changes through Coder Studio automation.", - version: "1.0.0", - defaultEnabled: true, - autoMountInMvp: false, - content: BROWSER_VERIFICATION_SKILL, - }, - { - slug: "coder-studio-review", - displayName: "Coder Studio Review", - description: "Teach agents to review Git changes, tests, and residual risk before finishing.", - version: "1.0.0", - defaultEnabled: true, - autoMountInMvp: true, - content: REVIEW_SKILL, - }, -]; -``` - -- [ ] **Step 4: Implement `materialize.ts`** - -Create `packages/server/src/skills/builtin/materialize.ts`: - -```ts -import { mkdir, writeFile } from "node:fs/promises"; -import { join } from "node:path"; -import type { SkillLibraryEntry } from "@coder-studio/core"; -import { BUILTIN_SKILLS } from "./registry.js"; - -export interface MaterializeBuiltinSkillsInput { - builtinRoot: string; - now?: () => number; -} - -export async function materializeBuiltinSkills( - input: MaterializeBuiltinSkillsInput -): Promise { - const now = input.now?.() ?? Date.now(); - const entries: SkillLibraryEntry[] = []; - - for (const skill of BUILTIN_SKILLS) { - const libraryPath = join(input.builtinRoot, skill.slug); - await mkdir(libraryPath, { recursive: true }); - await writeFile(join(libraryPath, "SKILL.md"), skill.content.trimEnd() + "\n", "utf8"); - entries.push({ - slug: skill.slug, - displayName: skill.displayName, - description: skill.description, - version: skill.version, - source: "builtin", - libraryPath, - installState: "installed", - installedAt: now, - updatedAt: now, - builtin: { - defaultEnabled: skill.defaultEnabled, - autoMount: skill.autoMountInMvp, - }, - }); - } - - return entries; -} -``` - -- [ ] **Step 5: Run the test and verify it passes** - -Run: - -```bash -pnpm --filter @coder-studio/server test -- src/__tests__/skills/builtin-registry.test.ts -``` - -Expected: PASS. - -- [ ] **Step 6: Commit** - -```bash -git add packages/server/src/skills/builtin packages/server/src/__tests__/skills/builtin-registry.test.ts -git commit -m "feat: add built-in skill registry" -``` - ---- - -### Task 3: Sync Built-ins into Library and Auto-mount Defaults - -**Files:** -- Create: `packages/server/src/skills/builtin/sync-manager.ts` -- Modify: `packages/server/src/ws/dispatch.ts` -- Modify: `packages/server/src/server.ts` -- Test: `packages/server/src/__tests__/skills/builtin-sync-manager.test.ts` - -- [ ] **Step 1: Write the failing sync-manager tests** - -Create `packages/server/src/__tests__/skills/builtin-sync-manager.test.ts`: - -```ts -import { mkdtemp, rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import type { ProviderDefinition } from "@coder-studio/core"; -import { afterEach, describe, expect, it, vi } from "vitest"; -import { SettingsRepo } from "../../storage/repositories/settings-repo.js"; -import { SkillLibraryRepo } from "../../storage/repositories/skill-library-repo.js"; -import { SkillMountRepo } from "../../storage/repositories/skill-mount-repo.js"; -import { BuiltinSkillSyncManager } from "../../skills/builtin/sync-manager.js"; - -function provider(id: string, skillDir?: string): ProviderDefinition { - return { - id, - displayName: id, - badge: id, - kind: "built_in", - capability: "full", - capabilities: [], - install: { - prerequisites: [], - manualGuideKeys: [], - docUrls: { provider: "", prerequisites: {} }, - strategies: {}, - }, - buildCommand: () => ({ argv: [id], env: {}, cwd: "/tmp" }), - configSchema: { parse: (value: unknown) => value } as never, - defaultConfig: {}, - requiredCommands: [id], - skillMountDirectories: skillDir ? [skillDir] : undefined, - }; -} - -describe("BuiltinSkillSyncManager", () => { - let tempDir: string | undefined; - - afterEach(async () => { - if (tempDir) { - await rm(tempDir, { recursive: true, force: true }); - } - }); - - it("syncs built-ins into the library and mounts MVP defaults", async () => { - tempDir = await mkdtemp(join(tmpdir(), "coder-studio-builtin-sync-")); - const skillDir = join(tempDir, "codex-skills"); - const libraryRepo = new SkillLibraryRepo({ - filePath: join(tempDir, "library-index.json"), - }); - const mountRepo = new SkillMountRepo({ - filePath: join(tempDir, "mounts.json"), - }); - const settingsRepo = new SettingsRepo({ - filePath: join(tempDir, "settings.json"), - }); - - const manager = new BuiltinSkillSyncManager({ - builtinRoot: join(tempDir, "builtin"), - getProviderRegistry: () => [provider("codex", skillDir)], - skillLibraryRepo: libraryRepo, - skillMountRepo: mountRepo, - settingsRepo, - now: () => 1000, - }); - - const result = await manager.sync(); - - expect(result.libraryEntries.map((entry) => entry.slug)).toEqual([ - "coder-studio-automation", - "coder-studio-browser-verification", - "coder-studio-review", - ]); - expect(mountRepo.get("codex", "coder-studio-automation")).toMatchObject({ - enabled: true, - status: "mounted", - }); - expect(mountRepo.get("codex", "coder-studio-review")).toMatchObject({ - enabled: true, - status: "mounted", - }); - expect(mountRepo.get("codex", "coder-studio-browser-verification")).toBeUndefined(); - }); - - it("does not re-mount a user-disabled built-in skill", async () => { - tempDir = await mkdtemp(join(tmpdir(), "coder-studio-builtin-disabled-")); - const skillDir = join(tempDir, "codex-skills"); - const libraryRepo = new SkillLibraryRepo({ - filePath: join(tempDir, "library-index.json"), - }); - const mountRepo = new SkillMountRepo({ - filePath: join(tempDir, "mounts.json"), - }); - const settingsRepo = new SettingsRepo({ - filePath: join(tempDir, "settings.json"), - }); - settingsRepo.set("skills.builtin.disabledMounts", { - "codex:coder-studio-review": true, - }); - - const manager = new BuiltinSkillSyncManager({ - builtinRoot: join(tempDir, "builtin"), - getProviderRegistry: () => [provider("codex", skillDir)], - skillLibraryRepo: libraryRepo, - skillMountRepo: mountRepo, - settingsRepo, - now: () => 1000, - }); - - await manager.sync(); - - expect(mountRepo.get("codex", "coder-studio-automation")).toBeDefined(); - expect(mountRepo.get("codex", "coder-studio-review")).toBeUndefined(); - }); -}); -``` - -- [ ] **Step 2: Run the test and verify it fails** - -Run: - -```bash -pnpm --filter @coder-studio/server test -- src/__tests__/skills/builtin-sync-manager.test.ts -``` - -Expected: FAIL because `BuiltinSkillSyncManager` does not exist. - -- [ ] **Step 3: Implement `BuiltinSkillSyncManager`** - -Create `packages/server/src/skills/builtin/sync-manager.ts`: - -```ts -import { copyFile, mkdir, readdir, readlink, rm, symlink, unlink } from "node:fs/promises"; -import { dirname, join } from "node:path"; -import type { ProviderDefinition, SkillLibraryEntry, SkillMountRelation } from "@coder-studio/core"; -import type { SettingsRepo } from "../../storage/repositories/settings-repo.js"; -import type { SkillLibraryRepo } from "../../storage/repositories/skill-library-repo.js"; -import type { SkillMountRepo } from "../../storage/repositories/skill-mount-repo.js"; -import { materializeBuiltinSkills } from "./materialize.js"; - -const DISABLED_MOUNTS_SETTING_KEY = "skills.builtin.disabledMounts"; - -export interface BuiltinSkillSyncManagerDeps { - builtinRoot: string; - getProviderRegistry: () => ProviderDefinition[]; - skillLibraryRepo: SkillLibraryRepo; - skillMountRepo: SkillMountRepo; - settingsRepo: SettingsRepo; - now?: () => number; -} - -export interface BuiltinSkillSyncResult { - libraryEntries: SkillLibraryEntry[]; - mounted: SkillMountRelation[]; - skipped: Array<{ providerId: string; skillSlug: string; reason: string }>; -} - -export class BuiltinSkillSyncManager { - constructor(private readonly deps: BuiltinSkillSyncManagerDeps) {} - - async sync(): Promise { - const entries = await materializeBuiltinSkills({ - builtinRoot: this.deps.builtinRoot, - now: this.deps.now, - }); - for (const entry of entries) { - this.deps.skillLibraryRepo.set(entry); - } - - const disabled = this.readDisabledMounts(); - const mounted: SkillMountRelation[] = []; - const skipped: BuiltinSkillSyncResult["skipped"] = []; - - for (const provider of this.deps.getProviderRegistry()) { - const skillDir = provider.skillMountDirectories?.[0]; - if (!skillDir) { - continue; - } - - for (const entry of entries) { - if (!entry.builtin?.autoMount) { - skipped.push({ providerId: provider.id, skillSlug: entry.slug, reason: "not_mvp_auto" }); - continue; - } - - if (disabled[disabledKey(provider.id, entry.slug)]) { - skipped.push({ providerId: provider.id, skillSlug: entry.slug, reason: "disabled" }); - continue; - } - - const relation = await this.mountBuiltin(provider.id, skillDir, entry); - this.deps.skillMountRepo.upsert(relation); - mounted.push(relation); - } - } - - return { libraryEntries: entries, mounted, skipped }; - } - - setMountEnabled(providerId: string, skillSlug: string, enabled: boolean): void { - const disabled = this.readDisabledMounts(); - const key = disabledKey(providerId, skillSlug); - if (enabled) { - delete disabled[key]; - } else { - disabled[key] = true; - } - this.deps.settingsRepo.set(DISABLED_MOUNTS_SETTING_KEY, disabled); - } - - isMountDisabled(providerId: string, skillSlug: string): boolean { - return Boolean(this.readDisabledMounts()[disabledKey(providerId, skillSlug)]); - } - - private readDisabledMounts(): Record { - const raw = this.deps.settingsRepo.get>(DISABLED_MOUNTS_SETTING_KEY); - if (!raw || typeof raw !== "object" || Array.isArray(raw)) { - return {}; - } - return Object.fromEntries( - Object.entries(raw).filter(([, value]) => value === true) - ) as Record; - } - - private async mountBuiltin( - providerId: string, - skillDir: string, - entry: SkillLibraryEntry - ): Promise { - const targetPath = join(skillDir, entry.slug); - await mkdir(dirname(targetPath), { recursive: true }); - await rm(targetPath, { recursive: true, force: true }); - - let mountModeResolved: SkillMountRelation["mountModeResolved"] = "symlink"; - try { - await symlink(entry.libraryPath, targetPath); - } catch { - mountModeResolved = "copy"; - await copyRecursively(entry.libraryPath, targetPath); - } - - return { - providerId, - skillSlug: entry.slug, - enabled: true, - sourcePath: entry.libraryPath, - targetPath, - mountModeResolved, - status: "mounted", - lastSyncedAt: this.deps.now?.() ?? Date.now(), - }; - } -} - -function disabledKey(providerId: string, skillSlug: string): string { - return `${providerId}:${skillSlug}`; -} - -async function copyRecursively(source: string, target: string): Promise { - await mkdir(target, { recursive: true }); - const entries = await readdir(source, { withFileTypes: true }); - for (const entry of entries) { - const from = join(source, entry.name); - const to = join(target, entry.name); - if (entry.isDirectory()) { - await copyRecursively(from, to); - } else if (entry.isSymbolicLink()) { - const linkTarget = await readlink(from); - await symlink(linkTarget, to); - } else { - await copyFile(from, to); - } - } -} -``` - -Remove unused imports after implementation; if `unlink` is unused, do not keep it. - -- [ ] **Step 4: Run the test and verify it passes** - -Run: - -```bash -pnpm --filter @coder-studio/server test -- src/__tests__/skills/builtin-sync-manager.test.ts -``` - -Expected: PASS. - -- [ ] **Step 5: Wire manager into server context** - -Modify `packages/server/src/ws/dispatch.ts`: - -```ts -import type { BuiltinSkillSyncManager } from "../skills/builtin/sync-manager.js"; -``` - -Add to `CommandContext`: - -```ts -builtinSkillSyncMgr?: BuiltinSkillSyncManager; -stateRoot?: string; -``` - -Modify `packages/server/src/server.ts` imports: - -```ts -import { BuiltinSkillSyncManager } from "./skills/builtin/sync-manager.js"; -``` - -After `skillHealthMgr` is created, instantiate: - -```ts -const builtinSkillSyncMgr = new BuiltinSkillSyncManager({ - builtinRoot: join(stateRoot, "state", "skills", "builtin"), - getProviderRegistry: () => activeProviderRegistry, - skillLibraryRepo, - skillMountRepo, - settingsRepo, -}); -await builtinSkillSyncMgr.sync(); -``` - -Add `builtinSkillSyncMgr` and `stateRoot` to `commandContext`. - -- [ ] **Step 6: Run targeted server tests** - -Run: - -```bash -pnpm --filter @coder-studio/server test -- src/__tests__/skills/builtin-sync-manager.test.ts src/__tests__/skills/commands.test.ts -``` - -Expected: PASS. - -- [ ] **Step 7: Commit** - -```bash -git add packages/server/src/skills/builtin/sync-manager.ts packages/server/src/__tests__/skills/builtin-sync-manager.test.ts packages/server/src/ws/dispatch.ts packages/server/src/server.ts -git commit -m "feat: sync built-in skills" -``` - ---- - -### Task 4: Expose Built-in Skill Commands and Library Metadata - -**Files:** -- Modify: `packages/server/src/commands/skills.ts` -- Test: `packages/server/src/__tests__/skills/commands.test.ts` - -- [ ] **Step 1: Write failing command tests** - -Add tests to `packages/server/src/__tests__/skills/commands.test.ts`: - -```ts -it("returns builtin library metadata", async () => { - const ctx = createBaseContext({ - skillLibraryRepo: { - list: vi.fn(() => [ - { - slug: "coder-studio-automation", - displayName: "Coder Studio Automation", - description: "Teach agents", - version: "1.0.0", - source: "builtin", - libraryPath: "/skills/builtin/coder-studio-automation", - installState: "installed", - installedAt: 1, - updatedAt: 2, - builtin: { defaultEnabled: true, autoMount: true }, - }, - ]), - } as never, - skillMountRepo: { - listBySkillSlug: vi.fn(() => []), - } as never, - skillsHubClient: {} as never, - }); - - const result = await dispatch( - { - kind: "command", - id: "skills-library-builtin-1", - op: "skills.library.list", - args: {}, - }, - ctx - ); - - expect(result.ok).toBe(true); - expect(result.data).toEqual([ - expect.objectContaining({ - slug: "coder-studio-automation", - source: "builtin", - builtin: { defaultEnabled: true, autoMount: true }, - }), - ]); -}); - -it("syncs builtin skills through command dispatch", async () => { - const sync = vi.fn(async () => ({ - libraryEntries: [], - mounted: [], - skipped: [], - })); - const ctx = createBaseContext({ - builtinSkillSyncMgr: { sync } as never, - }); - - const result = await dispatch( - { - kind: "command", - id: "skills-builtin-sync-1", - op: "skills.builtin.sync", - args: {}, - }, - ctx - ); - - expect(result.ok).toBe(true); - expect(sync).toHaveBeenCalledTimes(1); -}); - -it("persists builtin mount disablement through command dispatch", async () => { - const setMountEnabled = vi.fn(); - const sync = vi.fn(async () => ({ - libraryEntries: [], - mounted: [], - skipped: [], - })); - const ctx = createBaseContext({ - builtinSkillSyncMgr: { setMountEnabled, sync } as never, - }); - - const result = await dispatch( - { - kind: "command", - id: "skills-builtin-set-enabled-1", - op: "skills.builtin.setMountEnabled", - args: { - providerId: "codex", - skillSlug: "coder-studio-review", - enabled: false, - }, - }, - ctx - ); - - expect(result.ok).toBe(true); - expect(setMountEnabled).toHaveBeenCalledWith("codex", "coder-studio-review", false); - expect(sync).toHaveBeenCalledTimes(1); -}); -``` - -- [ ] **Step 2: Run the tests and verify they fail** - -Run: - -```bash -pnpm --filter @coder-studio/server test -- src/__tests__/skills/commands.test.ts -``` - -Expected: FAIL for missing `skills.builtin.sync` and `skills.builtin.setMountEnabled` command handlers. - -- [ ] **Step 3: Add command handlers** - -Modify `packages/server/src/commands/skills.ts`: - -```ts -function requireBuiltinSkillSyncSupport(ctx: CommandContext): asserts ctx is CommandContext & { - builtinSkillSyncMgr: NonNullable; -} { - if (!ctx.builtinSkillSyncMgr) { - throw { - code: "builtin_skills_unavailable", - message: "Built-in skill sync is not configured", - }; - } -} -``` - -Add handlers: - -```ts -registerCommand("skills.builtin.sync", z.object({}), async (_args, ctx) => { - requireBuiltinSkillSyncSupport(ctx); - return ctx.builtinSkillSyncMgr.sync(); -}); - -registerCommand( - "skills.builtin.setMountEnabled", - z.object({ - providerId: z.string().trim().min(1), - skillSlug: z.string().trim().min(1), - enabled: z.boolean(), - }), - async (args, ctx) => { - requireBuiltinSkillSyncSupport(ctx); - ctx.builtinSkillSyncMgr.setMountEnabled(args.providerId, args.skillSlug, args.enabled); - return ctx.builtinSkillSyncMgr.sync(); - } -); -``` - -No extra implementation is needed for library metadata if `skills.library.list` returns `...entry`. - -- [ ] **Step 4: Run tests and verify they pass** - -Run: - -```bash -pnpm --filter @coder-studio/server test -- src/__tests__/skills/commands.test.ts -``` - -Expected: PASS. - -- [ ] **Step 5: Commit** - -```bash -git add packages/server/src/commands/skills.ts packages/server/src/__tests__/skills/commands.test.ts -git commit -m "feat: add built-in skill commands" -``` - ---- - -### Task 5: Inject Agent Runtime Context into Session Terminals - -**Files:** -- Modify: `packages/server/src/session/manager.ts` -- Modify: `packages/server/src/session/types.ts` if needed -- Test: `packages/server/src/__tests__/session-commands.test.ts` - -- [ ] **Step 1: Write the failing session env test** - -Add a test in `packages/server/src/__tests__/session-commands.test.ts` near the existing session create tests: - -```ts -it("injects Coder Studio runtime context into agent terminal env", async () => { - const testDir = join(tmpdir(), `coder-studio-session-env-${Date.now()}`); - mkdirSync(join(testDir, ".git"), { recursive: true }); - writeFileSync(join(testDir, ".git", "HEAD"), "ref: refs/heads/main\n"); - - const createdSpecs: Array<{ env?: Record }> = []; - terminalMgrStub = { - create: (spec: { env?: Record }) => { - createdSpecs.push(spec); - return { id: "terminal-env-1" }; - }, - kill: async () => {}, - close: async () => {}, - } as unknown as TerminalManager; - sessionMgr = new SessionManager({ - terminalMgr: terminalMgrStub, - eventBus, - db: sessionDbStub, - broadcaster, - providerRegistry: [], - providerConfigRepo: createProviderConfigRepo(join(stateDir, "provider-configs-env.json")), - runtimeContext: { - apiUrl: "http://127.0.0.1:4173", - }, - }); - ctx.sessionMgr = sessionMgr; - ctx.providerRegistry = providerRegistry as ProviderDefinition[]; - ctx.providerRuntimeDeps = { - commandExists: async (command: string) => command === "claude", - }; - - try { - const openResult = await dispatch( - { - kind: "command", - id: "workspace-env", - op: "workspace.open", - args: { path: testDir }, - }, - ctx - ); - - expect(openResult.ok).toBe(true); - - const result = await dispatch( - { - kind: "command", - id: "session-env", - op: "session.create", - args: { - workspaceId: openResult.data!.id, - providerId: "claude", - }, - }, - ctx - ); - - expect(result.ok).toBe(true); - expect(createdSpecs[0]?.env).toMatchObject({ - CODER_STUDIO: "1", - CODER_STUDIO_WORKSPACE_ID: openResult.data!.id, - CODER_STUDIO_SESSION_ID: expect.stringMatching(/^sess_/), - CODER_STUDIO_PROVIDER_ID: "claude", - CODER_STUDIO_API_URL: "http://127.0.0.1:4173", - }); - } finally { - rmSync(testDir, { recursive: true, force: true }); - } -}); -``` - -- [ ] **Step 2: Run the test and verify it fails** - -Run: - -```bash -pnpm --filter @coder-studio/server test -- src/__tests__/session-commands.test.ts -``` - -Expected: FAIL because `runtimeContext` is not accepted by `SessionManagerDeps` or env values are missing. - -- [ ] **Step 3: Extend SessionManager deps and env injection** - -Modify `packages/server/src/session/manager.ts`: - -```ts -export interface SessionRuntimeContext { - apiUrl?: string; -} - -export interface SessionManagerDeps { - terminalMgr: TerminalManager; - eventBus: EventBus; - db: SessionDatabase; - broadcaster: Broadcaster; - providerRegistry: ProviderDefinition[]; - providerConfigRepo: ProviderConfigRepo; - runtimeContext?: SessionRuntimeContext; - logger?: SessionLogger; -} -``` - -In `create`, update terminal env: - -```ts -env: { - ...cmd.env, - CODER_STUDIO: "1", - CODER_STUDIO_WORKSPACE_ID: req.workspaceId, - CODER_STUDIO_SESSION_ID: sessionId, - CODER_STUDIO_PROVIDER_ID: req.providerId, - ...(this.deps.runtimeContext?.apiUrl - ? { CODER_STUDIO_API_URL: this.deps.runtimeContext.apiUrl } - : {}), -}, -``` - -Do not inject `CODER_STUDIO_TERMINAL_ID` here because the terminal id is only known after `terminalMgr.create()`. Keep the PRD's terminal id variable for a later implementation that can safely update env through PTY launch changes. - -- [ ] **Step 4: Wire runtimeContext in server** - -Modify `packages/server/src/server.ts` where `SessionManager` is created: - -```ts -runtimeContext: { - apiUrl: `http://${config.host === "0.0.0.0" ? "127.0.0.1" : config.host}:${config.port}`, -}, -``` - -If there is an existing URL helper, prefer it; otherwise use the explicit expression above. - -- [ ] **Step 5: Run the test and verify it passes** - -Run: - -```bash -pnpm --filter @coder-studio/server test -- src/__tests__/session-commands.test.ts -``` - -Expected: PASS. - -- [ ] **Step 6: Commit** - -```bash -git add packages/server/src/session/manager.ts packages/server/src/server.ts packages/server/src/__tests__/session-commands.test.ts -git commit -m "feat: inject coder studio agent context" -``` - ---- - -### Task 6: Add Automation Identify and Capabilities Metadata - -**Files:** -- Create: `packages/server/src/automation/identify.ts` -- Create: `packages/server/src/automation/capabilities.ts` -- Create: `packages/server/src/commands/automation.ts` -- Modify: `packages/server/src/commands/index.ts` -- Test: `packages/server/src/__tests__/automation/identify.test.ts` -- Test: `packages/server/src/__tests__/automation/capabilities.test.ts` - -- [ ] **Step 1: Write failing automation tests** - -Create `packages/server/src/__tests__/automation/identify.test.ts`: - -```ts -import { describe, expect, it } from "vitest"; -import { buildIdentifyResult } from "../../automation/identify.js"; - -describe("automation identify", () => { - it("returns outside-Coder-Studio when env marker is absent", () => { - expect(buildIdentifyResult({ env: {} })).toEqual({ insideCoderStudio: false }); - }); - - it("returns runtime context from env", () => { - expect( - buildIdentifyResult({ - env: { - CODER_STUDIO: "1", - CODER_STUDIO_WORKSPACE_ID: "ws-1", - CODER_STUDIO_SESSION_ID: "sess-1", - CODER_STUDIO_PROVIDER_ID: "codex", - CODER_STUDIO_API_URL: "http://127.0.0.1:4173", - }, - cwd: "/repo", - }) - ).toEqual({ - insideCoderStudio: true, - workspaceId: "ws-1", - sessionId: "sess-1", - providerId: "codex", - cwd: "/repo", - apiUrl: "http://127.0.0.1:4173", - permissions: ["workspace:read", "session:read", "terminal:read", "git:read"], - }); - }); -}); -``` - -Create `packages/server/src/__tests__/automation/capabilities.test.ts`: - -```ts -import { describe, expect, it } from "vitest"; -import { listAutomationCapabilities } from "../../automation/capabilities.js"; - -describe("automation capabilities", () => { - it("lists MVP read capabilities with command examples", () => { - const capabilities = listAutomationCapabilities({ - permissions: ["workspace:read", "session:read", "terminal:read", "git:read"], - }); - - expect(capabilities.map((capability) => capability.name)).toContain("git.status"); - expect(capabilities.find((capability) => capability.name === "git.status")).toMatchObject({ - cli: "coder-studio git status", - riskLevel: "read", - permissions: ["git:read"], - available: true, - }); - }); - - it("filters capabilities by permissions", () => { - const capabilities = listAutomationCapabilities({ - permissions: ["workspace:read"], - }); - - expect(capabilities.map((capability) => capability.name)).toContain("workspace.list"); - expect(capabilities.map((capability) => capability.name)).not.toContain("git.status"); - }); -}); -``` - -- [ ] **Step 2: Run tests and verify they fail** - -Run: - -```bash -pnpm --filter @coder-studio/server test -- src/__tests__/automation/identify.test.ts src/__tests__/automation/capabilities.test.ts -``` - -Expected: FAIL because automation modules do not exist. - -- [ ] **Step 3: Implement identify** - -Create `packages/server/src/automation/identify.ts`: - -```ts -export interface IdentifyInput { - env?: NodeJS.ProcessEnv | Record; - cwd?: string; -} - -export interface IdentifyResult { - insideCoderStudio: boolean; - workspaceId?: string; - sessionId?: string; - terminalId?: string; - providerId?: string; - cwd?: string; - apiUrl?: string; - permissions?: string[]; -} - -const DEFAULT_AGENT_PERMISSIONS = ["workspace:read", "session:read", "terminal:read", "git:read"]; - -export function buildIdentifyResult(input: IdentifyInput = {}): IdentifyResult { - const env = input.env ?? process.env; - if (env.CODER_STUDIO !== "1") { - return { insideCoderStudio: false }; - } - - return { - insideCoderStudio: true, - workspaceId: env.CODER_STUDIO_WORKSPACE_ID, - sessionId: env.CODER_STUDIO_SESSION_ID, - terminalId: env.CODER_STUDIO_TERMINAL_ID, - providerId: env.CODER_STUDIO_PROVIDER_ID, - cwd: input.cwd ?? process.cwd(), - apiUrl: env.CODER_STUDIO_API_URL, - permissions: DEFAULT_AGENT_PERMISSIONS, - }; -} -``` - -- [ ] **Step 4: Implement capabilities** - -Create `packages/server/src/automation/capabilities.ts`: - -```ts -export type AutomationRiskLevel = "read" | "write" | "dangerous"; - -export interface AutomationCapability { - name: string; - cli: string; - description: string; - inputSchema: Record; - output: string; - permissions: string[]; - riskLevel: AutomationRiskLevel; - examples: string[]; - available: boolean; -} - -const MVP_CAPABILITIES: AutomationCapability[] = [ - { - name: "workspace.list", - cli: "coder-studio workspace list", - description: "List known workspaces.", - inputSchema: {}, - output: "Workspace summaries as JSON.", - permissions: ["workspace:read"], - riskLevel: "read", - examples: ["coder-studio workspace list --json"], - available: true, - }, - { - name: "session.list", - cli: "coder-studio session list", - description: "List sessions visible to the current caller.", - inputSchema: { workspaceId: "string optional" }, - output: "Session summaries as JSON.", - permissions: ["session:read"], - riskLevel: "read", - examples: ["coder-studio session list --workspace ws_123 --json"], - available: true, - }, - { - name: "terminal.read", - cli: "coder-studio terminal read", - description: "Read terminal output tail.", - inputSchema: { terminalId: "string", bytes: "number optional" }, - output: "Terminal text tail.", - permissions: ["terminal:read"], - riskLevel: "read", - examples: ["coder-studio terminal read --terminal term_123 --json"], - available: true, - }, - { - name: "git.status", - cli: "coder-studio git status", - description: "Read Git status for a workspace.", - inputSchema: { workspaceId: "string" }, - output: "Git status summary as JSON.", - permissions: ["git:read"], - riskLevel: "read", - examples: ["coder-studio git status --workspace ws_123 --json"], - available: true, - }, - { - name: "git.diff", - cli: "coder-studio git diff", - description: "Read Git diff for a workspace.", - inputSchema: { workspaceId: "string", path: "string optional" }, - output: "Git diff text or structured diff data.", - permissions: ["git:read"], - riskLevel: "read", - examples: ["coder-studio git diff --workspace ws_123 --json"], - available: true, - }, -]; - -export function listAutomationCapabilities(input: { permissions: string[] }): AutomationCapability[] { - const allowed = new Set(input.permissions); - return MVP_CAPABILITIES.filter((capability) => - capability.permissions.every((permission) => allowed.has(permission)) - ); -} -``` - -- [ ] **Step 5: Add WebSocket commands** - -Create `packages/server/src/commands/automation.ts`: - -```ts -import { z } from "zod"; -import { listAutomationCapabilities } from "../automation/capabilities.js"; -import { buildIdentifyResult } from "../automation/identify.js"; -import { registerCommand } from "../ws/dispatch.js"; - -registerCommand("automation.identify", z.object({}), async () => { - return buildIdentifyResult(); -}); - -registerCommand( - "automation.capabilities", - z.object({ - permissions: z.array(z.string()).optional(), - }), - async (args) => { - return { - version: 1, - commands: listAutomationCapabilities({ - permissions: args.permissions ?? ["workspace:read", "session:read", "terminal:read", "git:read"], - }), - }; - } -); -``` - -Modify `packages/server/src/commands/index.ts`: - -```ts -import "./automation.js"; -``` - -- [ ] **Step 6: Run tests and verify they pass** - -Run: - -```bash -pnpm --filter @coder-studio/server test -- src/__tests__/automation/identify.test.ts src/__tests__/automation/capabilities.test.ts -``` - -Expected: PASS. - -- [ ] **Step 7: Commit** - -```bash -git add packages/server/src/automation packages/server/src/commands/automation.ts packages/server/src/commands/index.ts packages/server/src/__tests__/automation -git commit -m "feat: add automation discovery metadata" -``` - ---- - -### Task 7: Add CLI `identify` and `capabilities` - -**Files:** -- Create: `packages/cli/src/automation-client.ts` -- Modify: `packages/cli/src/parse-args.ts` -- Modify: `packages/cli/src/cli.ts` -- Test: `packages/cli/src/bin.test.ts` - -- [ ] **Step 1: Write failing CLI tests** - -In `packages/cli/src/bin.test.ts`, add imports/mocks for automation helpers: - -```ts -const { - printCapabilities, - printIdentify, - // existing hoisted mocks... -} = vi.hoisted(() => ({ - printCapabilities: vi.fn(), - printIdentify: vi.fn(), - // existing mocks... -})); - -vi.mock("./automation-client.js", () => ({ - printCapabilities, - printIdentify, -})); -``` - -Add tests: - -```ts -it("parses identify command with --json", () => { - expect(parseArgs(["identify", "--json"])).toEqual({ - command: "identify", - json: true, - }); -}); - -it("prints identify output", async () => { - await main(["identify", "--json"]); - expect(printIdentify).toHaveBeenCalledWith({ json: true }); -}); - -it("prints capabilities output", async () => { - await main(["capabilities", "--json"]); - expect(printCapabilities).toHaveBeenCalledWith({ json: true }); -}); -``` - -Adjust the existing hoisted mock block carefully rather than creating a second conflicting `vi.hoisted`. - -- [ ] **Step 2: Run CLI tests and verify they fail** - -Run: - -```bash -pnpm --filter @spencer-kit/coder-studio test -- src/bin.test.ts -``` - -Expected: FAIL because parser and CLI do not know `identify` or `capabilities`. - -- [ ] **Step 3: Implement `automation-client.ts`** - -Create `packages/cli/src/automation-client.ts`: - -```ts -interface PrintOptions { - json?: boolean; -} - -function defaultPermissions(): string[] { - return ["workspace:read", "session:read", "terminal:read", "git:read"]; -} - -export function printIdentify(options: PrintOptions = {}): void { - const insideCoderStudio = process.env.CODER_STUDIO === "1"; - const result = insideCoderStudio - ? { - insideCoderStudio: true, - workspaceId: process.env.CODER_STUDIO_WORKSPACE_ID, - sessionId: process.env.CODER_STUDIO_SESSION_ID, - terminalId: process.env.CODER_STUDIO_TERMINAL_ID, - providerId: process.env.CODER_STUDIO_PROVIDER_ID, - cwd: process.cwd(), - apiUrl: process.env.CODER_STUDIO_API_URL, - permissions: defaultPermissions(), - } - : { insideCoderStudio: false }; - - if (options.json) { - console.log(JSON.stringify(result, null, 2)); - return; - } - - console.log(result.insideCoderStudio ? "Inside Coder Studio" : "Not running inside Coder Studio"); -} - -export function printCapabilities(options: PrintOptions = {}): void { - const result = { - version: 1, - commands: [ - { - name: "workspace.list", - cli: "coder-studio workspace list", - description: "List known workspaces.", - inputSchema: {}, - output: "Workspace summaries as JSON.", - permissions: ["workspace:read"], - riskLevel: "read", - examples: ["coder-studio workspace list --json"], - available: true, - }, - { - name: "git.status", - cli: "coder-studio git status", - description: "Read Git status for a workspace.", - inputSchema: { workspaceId: "string" }, - output: "Git status summary as JSON.", - permissions: ["git:read"], - riskLevel: "read", - examples: ["coder-studio git status --workspace ws_123 --json"], - available: true, - }, - ], - }; - - if (options.json) { - console.log(JSON.stringify(result, null, 2)); - return; - } - - console.log(result.commands.map((command) => `${command.name}: ${command.cli}`).join("\n")); -} -``` - -This duplicates a minimal MVP capabilities subset in the CLI package because the published CLI bundle cannot import server source directly without affecting bundling. A later refactor can move shared metadata into `@coder-studio/core`. - -- [ ] **Step 4: Extend parser** - -Modify `packages/cli/src/parse-args.ts`: - -```ts -type CliCommand = - | "serve" - | "open" - | "config" - | "stop" - | "status" - | "logs" - | "help" - | "version" - | "auth" - | "identify" - | "capabilities"; -``` - -Add to `CliArgs`: - -```ts -json?: boolean; -``` - -Add cases: - -```ts -case "identify": -case "capabilities": - setCommand(args, arg); - break; - -case "--json": - if (getActiveCommand(args) !== "identify" && getActiveCommand(args) !== "capabilities") { - throwUnknownOption(arg); - } - args.json = true; - break; -``` - -When `setCommand` switches away from `identify` or `capabilities`, it can leave `json` harmlessly or delete it for stricter parsing. Prefer deleting it unless the active command supports JSON. - -- [ ] **Step 5: Extend CLI main** - -Modify `packages/cli/src/cli.ts` imports: - -```ts -import { printCapabilities, printIdentify } from "./automation-client.js"; -``` - -In `showHelp`, add commands: - -```text - identify Print Coder Studio agent runtime context - capabilities Print agent-facing automation capabilities -``` - -In `main` after version/help handling: - -```ts -if (args.command === "identify") { - printIdentify({ json: args.json }); - return; -} - -if (args.command === "capabilities") { - printCapabilities({ json: args.json }); - return; -} -``` - -- [ ] **Step 6: Run CLI tests** - -Run: - -```bash -pnpm --filter @spencer-kit/coder-studio test -- src/bin.test.ts -``` - -Expected: PASS. - -- [ ] **Step 7: Commit** - -```bash -git add packages/cli/src/automation-client.ts packages/cli/src/parse-args.ts packages/cli/src/cli.ts packages/cli/src/bin.test.ts -git commit -m "feat: add automation discovery cli commands" -``` - ---- - -### Task 8: Add Minimal Automation Audit Log - -**Files:** -- Create: `packages/server/src/automation/audit-log.ts` -- Modify: `packages/server/src/ws/dispatch.ts` -- Modify: `packages/server/src/server.ts` -- Test: `packages/server/src/__tests__/automation/audit-log.test.ts` - -- [ ] **Step 1: Write failing audit log tests** - -Create `packages/server/src/__tests__/automation/audit-log.test.ts`: - -```ts -import { mkdtemp, readFile, rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterEach, describe, expect, it } from "vitest"; -import { AutomationAuditLog } from "../../automation/audit-log.js"; - -describe("AutomationAuditLog", () => { - let tempDir: string | undefined; - - afterEach(async () => { - if (tempDir) { - await rm(tempDir, { recursive: true, force: true }); - } - }); - - it("writes sanitized JSONL audit records", async () => { - tempDir = await mkdtemp(join(tmpdir(), "coder-studio-audit-")); - const log = new AutomationAuditLog({ - filePath: join(tempDir, "automation.jsonl"), - now: () => 1234, - }); - - await log.append({ - workspaceId: "ws-1", - sessionId: "sess-1", - providerId: "codex", - commandName: "terminal.send", - riskLevel: "write", - decision: "allowed", - success: true, - args: { - text: "secret-token", - token: "abc", - }, - }); - - const lines = (await readFile(join(tempDir, "automation.jsonl"), "utf8")).trim().split("\n"); - expect(lines).toHaveLength(1); - expect(JSON.parse(lines[0]!)).toMatchObject({ - timestamp: 1234, - workspaceId: "ws-1", - commandName: "terminal.send", - args: { - text: "secret-token", - token: "[redacted]", - }, - }); - }); -}); -``` - -- [ ] **Step 2: Run test and verify it fails** - -Run: - -```bash -pnpm --filter @coder-studio/server test -- src/__tests__/automation/audit-log.test.ts -``` - -Expected: FAIL because `audit-log.ts` does not exist. - -- [ ] **Step 3: Implement audit log** - -Create `packages/server/src/automation/audit-log.ts`: - -```ts -import { appendFile, mkdir } from "node:fs/promises"; -import { dirname } from "node:path"; - -export interface AutomationAuditRecordInput { - workspaceId?: string; - sessionId?: string; - providerId?: string; - commandName: string; - riskLevel: "read" | "write" | "dangerous"; - decision: "allowed" | "denied" | "approval_required"; - success: boolean; - args?: Record; -} - -export interface AutomationAuditLogDeps { - filePath: string; - now?: () => number; -} - -export class AutomationAuditLog { - constructor(private readonly deps: AutomationAuditLogDeps) {} - - async append(input: AutomationAuditRecordInput): Promise { - await mkdir(dirname(this.deps.filePath), { recursive: true }); - const record = { - timestamp: this.deps.now?.() ?? Date.now(), - ...input, - args: sanitizeArgs(input.args ?? {}), - }; - await appendFile(this.deps.filePath, JSON.stringify(record) + "\n", "utf8"); - } -} - -function sanitizeArgs(args: Record): Record { - return Object.fromEntries( - Object.entries(args).map(([key, value]) => [ - key, - /token|password|secret|apiKey|apikey|authorization/i.test(key) ? "[redacted]" : value, - ]) - ); -} -``` - -- [ ] **Step 4: Wire audit log into context** - -Modify `packages/server/src/ws/dispatch.ts`: - -```ts -import type { AutomationAuditLog } from "../automation/audit-log.js"; -``` - -Add to `CommandContext`: - -```ts -automationAuditLog?: AutomationAuditLog; -``` - -Modify `packages/server/src/server.ts`: - -```ts -import { AutomationAuditLog } from "./automation/audit-log.js"; -``` - -Instantiate: - -```ts -const automationAuditLog = new AutomationAuditLog({ - filePath: join(stateRoot, "state", "automation-audit.jsonl"), -}); -``` - -Add to `commandContext`. - -MVP does not need to audit every command globally. Later tasks can call `automationAuditLog.append` from agent-triggered command routes once those command routes exist. - -- [ ] **Step 5: Run test and verify it passes** - -Run: - -```bash -pnpm --filter @coder-studio/server test -- src/__tests__/automation/audit-log.test.ts -``` - -Expected: PASS. - -- [ ] **Step 6: Commit** - -```bash -git add packages/server/src/automation/audit-log.ts packages/server/src/__tests__/automation/audit-log.test.ts packages/server/src/ws/dispatch.ts packages/server/src/server.ts -git commit -m "feat: add automation audit log" -``` - ---- - -### Task 9: Surface Built-in Skills in Skills Panel - -**Files:** -- Modify: `packages/web/src/features/workspace/actions/use-skills-panel.ts` -- Modify: `packages/web/src/features/workspace/views/shared/skills-panel.tsx` -- Modify: `packages/web/src/features/workspace/views/shared/skills-panel.test.tsx` - -- [ ] **Step 1: Write failing UI test** - -In `packages/web/src/features/workspace/views/shared/skills-panel.test.tsx`, add translation fixture entries: - -```ts -"workspace.skills.source.builtin": "Built-in", -"skills.builtin_title": "Built-in Skills", -``` - -Add test: - -```tsx -it("shows built-in skills with a built-in source label", async () => { - mockDispatch((op) => { - if (op === "skills.library.list") { - return Promise.resolve({ - ok: true, - data: [ - { - slug: "coder-studio-automation", - displayName: "Coder Studio Automation", - description: "Teach agents", - version: "1.0.0", - source: "builtin", - libraryPath: "/skills/builtin/coder-studio-automation", - installState: "installed", - installedAt: 1, - updatedAt: 2, - mountedProviderIds: ["codex"], - mountStatus: "partially_mounted", - errorCount: 0, - builtin: { defaultEnabled: true, autoMount: true }, - }, - ], - }); - } - if (op === "skills.health.scan") { - return Promise.resolve({ ok: true, data: { targets: [], mounts: [] } }); - } - if (op === "skills.targets.list") { - return Promise.resolve({ ok: true, data: [] }); - } - return Promise.resolve({ ok: true, data: [] }); - }); - - renderPanel(); - - expect(await screen.findByText("Coder Studio Automation")).toBeInTheDocument(); - expect(screen.getByText("Built-in")).toBeInTheDocument(); -}); -``` - -Use the file's existing mock helper names. If it uses a different dispatch mocking helper than `mockDispatch`, adapt the test to the local helper. - -- [ ] **Step 2: Run UI test and verify it fails** - -Run: - -```bash -pnpm --filter @coder-studio/web test -- src/features/workspace/views/shared/skills-panel.test.tsx -``` - -Expected: FAIL because source `builtin` has no label handling. - -- [ ] **Step 3: Extend UI types** - -Modify `packages/web/src/features/workspace/actions/use-skills-panel.ts` only if TypeScript complains. Since `SkillLibraryEntry.source` comes from core, the type should update after Task 1. - -- [ ] **Step 4: Render built-in source label** - -Modify source label mapping in `packages/web/src/features/workspace/views/shared/skills-panel.tsx` wherever existing `skillhub`/`local` labels are rendered: - -```tsx -{item.source === "builtin" - ? t("workspace.skills.source.builtin") - : item.source === "local" - ? t("workspace.skills.source.local") - : t("workspace.skills.source.skillhub")} -``` - -If the component already has a helper function for source labels, update the helper instead of adding inline branching. - -- [ ] **Step 5: Add i18n keys** - -Find the locale files under `packages/web/src` with existing `workspace.skills.source.local` keys and add: - -```json -"workspace.skills.source.builtin": "Built-in" -``` - -For Chinese locale: - -```json -"workspace.skills.source.builtin": "内置" -``` - -If translations are in TypeScript records instead of JSON, update those records. - -- [ ] **Step 6: Run UI test and verify it passes** - -Run: - -```bash -pnpm --filter @coder-studio/web test -- src/features/workspace/views/shared/skills-panel.test.tsx -``` - -Expected: PASS. - -- [ ] **Step 7: Commit** - -```bash -git add packages/web/src/features/workspace/actions/use-skills-panel.ts packages/web/src/features/workspace/views/shared/skills-panel.tsx packages/web/src/features/workspace/views/shared/skills-panel.test.tsx packages/web/src -git commit -m "feat: show built-in skills in skills panel" -``` - ---- - -### Task 10: Document MVP CLI and Skill Behavior - -**Files:** -- Modify: `docs/help/cli.md` -- Modify: `docs/help/app-overview.md` -- Modify: `docs/PRD-agent-automation-skills.md` if implementation decisions changed during execution. - -- [ ] **Step 1: Update CLI docs** - -In `docs/help/cli.md`, add sections: - -```md -### coder-studio identify - -Prints machine-readable Coder Studio runtime context for an agent or local script. - -```bash -coder-studio identify --json -``` - -When called inside a Coder Studio-managed agent session, the JSON includes workspace, session, provider, API URL, and permission context. Outside Coder Studio it returns `{"insideCoderStudio": false}`. - -### coder-studio capabilities - -Prints machine-readable automation capabilities available to the caller. - -```bash -coder-studio capabilities --json -``` - -Agents should use this command instead of relying on a hardcoded command list. -``` -``` - -- [ ] **Step 2: Update app overview** - -In `docs/help/app-overview.md`, add a short section after Provider or Settings: - -```md -### Built-in Skills - -Coder Studio can distribute first-party skills to supported agent providers. These skills teach agents how to discover Coder Studio runtime context and automation capabilities through `coder-studio identify --json` and `coder-studio capabilities --json`. -``` - -- [ ] **Step 3: Review PRD alignment** - -Open `docs/PRD-agent-automation-skills.md` and update any MVP details that changed during implementation. Do not mark Phase 2 browser automation or Phase 3 plugin/status as shipped. - -- [ ] **Step 4: Commit** - -```bash -git add docs/help/cli.md docs/help/app-overview.md docs/PRD-agent-automation-skills.md -git commit -m "docs: document agent automation skills mvp" -``` - ---- - -### Task 11: Final Verification - -**Files:** -- No new files. - -- [ ] **Step 1: Run focused package tests** - -Run: - -```bash -pnpm vitest run packages/core/src/domain/skill-management.test.ts -pnpm --filter @coder-studio/server test -- src/__tests__/skills/builtin-registry.test.ts src/__tests__/skills/builtin-sync-manager.test.ts src/__tests__/skills/commands.test.ts src/__tests__/automation/identify.test.ts src/__tests__/automation/capabilities.test.ts src/__tests__/automation/audit-log.test.ts src/__tests__/session-commands.test.ts -pnpm --filter @spencer-kit/coder-studio test -- src/bin.test.ts -pnpm --filter @coder-studio/web test -- src/features/workspace/views/shared/skills-panel.test.tsx -``` - -Expected: all commands exit 0. - -- [ ] **Step 2: Run typecheck** - -Run: - -```bash -pnpm ci:typecheck -``` - -Expected: exit 0. - -- [ ] **Step 3: Run lint/check if typecheck passes** - -Run: - -```bash -pnpm exec biome check --diagnostic-level=error --max-diagnostics=none packages/core packages/server packages/cli packages/web docs/PRD-agent-automation-skills.md docs/help/cli.md docs/help/app-overview.md -``` - -Expected: exit 0. - -- [ ] **Step 4: Manual smoke test** - -Run: - -```bash -pnpm --filter @spencer-kit/coder-studio exec tsx src/bin.ts identify --json -pnpm --filter @spencer-kit/coder-studio exec tsx src/bin.ts capabilities --json -``` - -Expected: - -- `identify --json` outside a managed agent session prints JSON with `"insideCoderStudio": false`. -- `capabilities --json` prints JSON with a `commands` array containing `git.status`. - -- [ ] **Step 5: Inspect git status** - -Run: - -```bash -git status --short -``` - -Expected: only files related to this plan are modified, plus the existing PRD if it has not already been committed. - -- [ ] **Step 6: Final commit** - -If previous tasks were not committed individually, commit all MVP changes: - -```bash -git add packages/core packages/server packages/cli packages/web docs -git commit -m "feat: add agent automation skills mvp" -``` - -Do not commit unrelated user changes. diff --git a/docs/superpowers/plans/2026-06-08-supervisor-plan-tree.md b/docs/superpowers/plans/2026-06-08-supervisor-plan-tree.md new file mode 100644 index 000000000..27fd840be --- /dev/null +++ b/docs/superpowers/plans/2026-06-08-supervisor-plan-tree.md @@ -0,0 +1,101 @@ +# Supervisor Plan Tree Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Redesign the supervisor plan tree so it renders as an expandable hierarchy with clear indentation and active-node emphasis. + +**Architecture:** Keep the current `SupervisorTargetMemory.planTree` data model intact and update only the web detail view. Introduce local tree expansion state in the shared supervisor details component, defaulting to a collapsed tree with the active path opened when available. Update styles to make parent/child structure obvious and make the active node visually distinct. + +**Tech Stack:** React, TypeScript, Jotai, Vitest, Testing Library, existing app CSS, Lucide icons. + +--- + +### Task 1: Add regression tests for expandable supervisor tree behavior + +**Files:** +- Modify: `packages/web/src/features/supervisor/views/shared/supervisor-details-content.test.tsx` + +- [ ] **Step 1: Write the failing test** + +```tsx +it("keeps plan tree children collapsed until the user expands a node", () => { + // render a planTree with children and no active path + // expect the child node not to be visible initially + // click the root expand button + // expect the child node to become visible +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `pnpm --filter @coder-studio/web exec vitest run src/features/supervisor/views/shared/supervisor-details-content.test.tsx` +Expected: FAIL because the tree has no expand/collapse control yet and children are rendered immediately. + +- [ ] **Step 3: Write minimal implementation** + +No code yet. + +- [ ] **Step 4: Run test to verify it passes** + +Run: `pnpm --filter @coder-studio/web exec vitest run src/features/supervisor/views/shared/supervisor-details-content.test.tsx` +Expected: PASS after the tree interaction is implemented. + +- [ ] **Step 5: Commit** + +```bash +git add packages/web/src/features/supervisor/views/shared/supervisor-details-content.test.tsx +git commit -m "test: cover expandable supervisor plan tree" +``` + +### Task 2: Implement expandable supervisor plan tree UI + +**Files:** +- Modify: `packages/web/src/features/supervisor/views/shared/supervisor-details-content.tsx` +- Modify: `packages/web/src/styles/components.css` +- Modify: `packages/web/src/locales/en.json` +- Modify: `packages/web/src/locales/zh.json` + +- [ ] **Step 1: Write the failing test** + +Use the regression test from Task 1. + +- [ ] **Step 2: Run test to verify it fails** + +Run the same focused Vitest command and confirm the tree interaction is still missing. + +- [ ] **Step 3: Write minimal implementation** + +Add local expanded-node state, tree toggle buttons, active-path aware default expansion, and tree styling with indentation and highlight states. + +- [ ] **Step 4: Run test to verify it passes** + +Run: `pnpm --filter @coder-studio/web exec vitest run src/features/supervisor/views/shared/supervisor-details-content.test.tsx` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add packages/web/src/features/supervisor/views/shared/supervisor-details-content.tsx packages/web/src/styles/components.css packages/web/src/locales/en.json packages/web/src/locales/zh.json +git commit -m "feat: render supervisor plan tree as expandable hierarchy" +``` + +### Task 3: Verify related supervisor tests + +**Files:** +- Modify: none + +- [ ] **Step 1: Run the focused supervisor UI tests** + +Run: +`pnpm --filter @coder-studio/web exec vitest run src/features/supervisor/views/shared/supervisor-details-content.test.tsx src/features/supervisor/views/mobile/mobile-supervisor-sheet.test.tsx` + +- [ ] **Step 2: Confirm the supervisor error/detail regression still passes** + +Expected: both suites pass and the details panel still renders during error state. + +- [ ] **Step 3: Commit** + +```bash +git add -A +git commit -m "test: verify supervisor detail tree rendering" +``` diff --git a/docs/superpowers/plans/2026-06-11-dev-browser-editor-tab.md b/docs/superpowers/plans/2026-06-11-dev-browser-editor-tab.md new file mode 100644 index 000000000..23521a258 --- /dev/null +++ b/docs/superpowers/plans/2026-06-11-dev-browser-editor-tab.md @@ -0,0 +1,58 @@ +# Dev Browser Editor Tab Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Move the dev browser entry from workspace-level navigation into the editor header, opening a browser editor tab with its own URL toolbar. + +**Architecture:** Keep the existing server proxy and service worker implementation. Add a typed editor-tab layer in web state so file tabs remain file paths while the browser tab is a non-file target; render file and browser tabs through the existing editor header shell. + +**Tech Stack:** React 19, Jotai, Vitest, Testing Library, TypeScript, existing `DevBrowserSurface`. + +--- + +## File Structure + +- Modify `packages/web/src/features/workspace/atoms/files.ts`: add typed editor tab and active editor target atoms. +- Modify `packages/web/src/features/workspace/actions/open-editor-state.ts`: add helpers for file/browser tab normalization and cleanup. +- Modify `packages/web/src/features/code-editor/actions/use-code-editor-actions.ts`: expose editor-tab actions while preserving file-only state. +- Modify `packages/web/src/features/code-editor/views/shared/code-editor-tabs-header.tsx`: render file tabs plus browser tab and add a tabbar Browser action. +- Modify `packages/web/src/features/code-editor/views/shared/editor-surface.tsx`: render `DevBrowserSurface` when the browser tab is active. +- Modify `packages/web/src/features/code-editor/views/shared/code-editor-host.tsx`: suppress file mode controls when the browser tab is active. +- Modify desktop/mobile workspace views: remove Activity Bar/Dock browser entry points and keep browser inside the editor surface. +- Modify tests under `packages/web/src/features/code-editor` and `packages/web/src/features/workspace/views`: cover Browser header action, browser tab rendering, and removed workspace-level entries. +- Modify docs/help as needed to describe the editor-header entry. + +## Tasks + +### Task 1: Typed Editor Tab State + +- [ ] Add failing tests proving browser tabs are normalized separately from file paths. +- [ ] Add `WorkspaceEditorTab`, `activeEditorTargetAtomFamily`, and `openEditorTabsAtomFamily`. +- [ ] Keep existing file-path atoms as the file buffer compatibility layer. +- [ ] Run focused atom/helper tests. +- [ ] Commit. + +### Task 2: Editor Header Entry And Browser Tab Rendering + +- [ ] Add failing component tests for opening/focusing a Browser tab from the editor header. +- [ ] Extend `useCodeEditorActions` with `openBrowserTab`, `activateEditorTab`, and `closeEditorTab`. +- [ ] Extend `CodeEditorTabsHeader` to render file and browser tabs plus the Browser action. +- [ ] Render `DevBrowserSurface` from `EditorSurface` when the browser tab is active. +- [ ] Run focused code-editor tests. +- [ ] Commit. + +### Task 3: Remove Workspace-Level Browser Entry Points + +- [ ] Add/update failing desktop and mobile workspace tests proving Browser is not in Activity Bar/Dock. +- [ ] Remove `browser` from workspace sidebar/main-area mode and mobile sheet routing. +- [ ] Remove desktop `Ctrl/Cmd+6` Browser shortcut behavior if present. +- [ ] Update docs to say the entry is in the editor header. +- [ ] Run focused workspace tests. +- [ ] Commit. + +### Task 4: Verification + +- [ ] Run web focused tests for code editor, dev browser, desktop workspace, and mobile workspace. +- [ ] Run `pnpm --filter @coder-studio/web build`. +- [ ] Run `git diff --check`. +- [ ] Report full `pnpm ci:verify` status only if run; otherwise call out focused verification. diff --git a/docs/superpowers/plans/2026-06-11-dev-browser-loopback-proxy.md b/docs/superpowers/plans/2026-06-11-dev-browser-loopback-proxy.md new file mode 100644 index 000000000..a46035808 --- /dev/null +++ b/docs/superpowers/plans/2026-06-11-dev-browser-loopback-proxy.md @@ -0,0 +1,2198 @@ +# Dev Browser Loopback Proxy Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build a Coder Studio built-in browser that opens manually entered loopback dev-server URLs through an authenticated HTTP proxy and service-worker resource router. + +**Architecture:** Server owns loopback target validation, short-lived sessions, dev-browser shell HTML, and HTTP proxying under `/dev-browser/session/:id/proxy/*`. Web owns the workspace browser UI, service worker script, URL mapping, lifecycle cleanup, and visible unsupported states. WebSocket/HMR are explicitly unsupported in v1 and fail with a clear message. + +**Tech Stack:** Fastify 5, Node 24 `fetch`, React 19, Jotai, Vite public assets, Vitest, Testing Library, TypeScript. + +--- + +## File Structure + +- Create `packages/server/src/dev-browser/target-url.ts`: parse and validate manual loopback URLs. +- Create `packages/server/src/dev-browser/target-url.test.ts`: URL validation tests. +- Create `packages/server/src/dev-browser/session-store.ts`: in-memory short-lived dev browser session store. +- Create `packages/server/src/dev-browser/session-store.test.ts`: session lifecycle and expiry tests. +- Create `packages/server/src/dev-browser/proxy-headers.ts`: request and response header filtering plus redirect location rewriting. +- Create `packages/server/src/dev-browser/proxy-headers.test.ts`: hop-by-hop stripping and redirect rewrite tests. +- Create `packages/server/src/routes/dev-browser.ts`: create/read/delete session API, dev-browser shell route, HTTP proxy route. +- Create `packages/server/src/routes/dev-browser.test.ts`: route, proxy, and error handling tests. +- Modify `packages/server/src/app.ts`: register dev browser routes before static fallback routes. +- Modify `packages/server/src/app-routing.test.ts`: assert `/dev-browser/session/:id/proxy/*` is not swallowed by SPA fallback. +- Create `packages/web/public/dev-browser-sw.js`: service worker script scoped to `/dev-browser/`. +- Create `packages/web/src/features/dev-browser/dev-browser-sw.test.ts`: load the public SW script in a mocked worker global and test mapper behavior. +- Create `packages/web/src/features/dev-browser/api.ts`: client API for create/read/delete dev browser sessions. +- Create `packages/web/src/features/dev-browser/api.test.ts`: credentialed fetch and error handling tests. +- Create `packages/web/src/features/dev-browser/dev-browser-surface.tsx`: address bar, session lifecycle, iframe, and unsupported-state UI. +- Create `packages/web/src/features/dev-browser/dev-browser-surface.test.tsx`: form, iframe, cleanup, and unsupported-state tests. +- Modify `packages/web/src/features/workspace/atoms/layout.ts`: add `browser` desktop sidebar view. +- Modify `packages/web/src/features/workspace/actions/use-workspace-screen-model.ts`: add browser main-area mode and mobile sheet kind. +- Modify `packages/web/src/features/workspace/views/shared/workspace-activity-bar.tsx`: add desktop Browser activity item. +- Modify `packages/web/src/features/workspace/views/desktop/workspace-desktop-view.tsx`: render `DevBrowserSurface` in the desktop main stage. +- Modify `packages/web/src/features/workspace/views/mobile/mobile-dock.tsx`: add mobile Browser dock item. +- Modify `packages/web/src/features/workspace/views/mobile/workspace-mobile-view.tsx`: render Browser sheet. +- Modify related workspace view tests under `packages/web/src/features/workspace/views/**`: cover desktop and mobile browser entry points. +- Modify `packages/web/src/theme/icon-theme.ts`: add `nav.browser` and `mobile.dock.browser`. +- Modify `packages/web/src/locales/en.json` and `packages/web/src/locales/zh.json`: add dev browser labels and errors. +- Modify `packages/web/src/styles/components.css`: add dev browser layout styles. +- Modify `packages/web/vite.config.ts`: proxy `/dev-browser` during web dev so Vite-hosted UI can reach backend shell and proxy routes. +- Modify `docs/help/mobile-guide.md`: document manual local dev-server browser use and WebSocket/HMR limitation. + +--- + +### Task 1: Server Target URL Validation + +**Files:** +- Create: `packages/server/src/dev-browser/target-url.ts` +- Create: `packages/server/src/dev-browser/target-url.test.ts` + +- [ ] **Step 1: Write failing validation tests** + +Create `packages/server/src/dev-browser/target-url.test.ts` with: + +```ts +import { describe, expect, it } from "vitest"; +import { parseDevBrowserTargetUrl, DevBrowserTargetUrlError } from "./target-url.js"; + +describe("parseDevBrowserTargetUrl", () => { + it("accepts loopback HTTP URLs with explicit ports", () => { + expect(parseDevBrowserTargetUrl("http://localhost:8000/app?draft=1#top")).toMatchObject({ + targetOrigin: "http://127.0.0.1:8000", + targetPath: "/app?draft=1", + targetHash: "#top", + connectHost: "127.0.0.1", + port: 8000, + }); + + expect(parseDevBrowserTargetUrl("http://127.0.0.1:5173/")).toMatchObject({ + targetOrigin: "http://127.0.0.1:5173", + targetPath: "/", + targetHash: "", + connectHost: "127.0.0.1", + port: 5173, + }); + + expect(parseDevBrowserTargetUrl("http://[::1]:3000/docs")).toMatchObject({ + targetOrigin: "http://[::1]:3000", + targetPath: "/docs", + targetHash: "", + connectHost: "::1", + port: 3000, + }); + }); + + it("normalizes manual input without an explicit protocol to HTTP", () => { + expect(parseDevBrowserTargetUrl("localhost:8000")).toMatchObject({ + displayUrl: "http://localhost:8000/", + targetOrigin: "http://127.0.0.1:8000", + targetPath: "/", + }); + }); + + it("rejects non-loopback and unsafe targets", () => { + const invalidInputs = [ + "https://localhost:8000", + "http://localhost", + "http://localhost:0", + "http://localhost:8000@evil.test", + "http://user:pass@localhost:8000", + "http://192.168.1.20:8000", + "http://example.com:8000", + "file:///tmp/index.html", + "", + ]; + + for (const input of invalidInputs) { + expect(() => parseDevBrowserTargetUrl(input), input).toThrow(DevBrowserTargetUrlError); + } + }); +}); +``` + +- [ ] **Step 2: Run the validation test and confirm it fails** + +Run: + +```bash +pnpm --filter @coder-studio/server test src/dev-browser/target-url.test.ts +``` + +Expected: fails because `packages/server/src/dev-browser/target-url.ts` does not exist. + +- [ ] **Step 3: Implement the validator** + +Create `packages/server/src/dev-browser/target-url.ts` with: + +```ts +export class DevBrowserTargetUrlError extends Error { + constructor(message: string) { + super(message); + this.name = "DevBrowserTargetUrlError"; + } +} + +export interface DevBrowserTarget { + connectHost: "127.0.0.1" | "::1"; + displayUrl: string; + originalHost: "localhost" | "127.0.0.1" | "[::1]"; + port: number; + targetHash: string; + targetOrigin: string; + targetPath: string; +} + +const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]); + +function withDefaultProtocol(input: string): string { + const trimmed = input.trim(); + if (!trimmed) { + throw new DevBrowserTargetUrlError("empty_url"); + } + return /^[a-zA-Z][a-zA-Z\d+.-]*:\/\//.test(trimmed) ? trimmed : `http://${trimmed}`; +} + +function parsePort(url: URL): number { + if (!url.port) { + throw new DevBrowserTargetUrlError("missing_port"); + } + + const port = Number(url.port); + if (!Number.isInteger(port) || port < 1 || port > 65535) { + throw new DevBrowserTargetUrlError("invalid_port"); + } + + return port; +} + +export function parseDevBrowserTargetUrl(input: string): DevBrowserTarget { + let url: URL; + try { + url = new URL(withDefaultProtocol(input)); + } catch { + throw new DevBrowserTargetUrlError("invalid_url"); + } + + if (url.protocol !== "http:") { + throw new DevBrowserTargetUrlError("unsupported_protocol"); + } + + if (url.username || url.password) { + throw new DevBrowserTargetUrlError("credentials_not_allowed"); + } + + const host = url.hostname as "localhost" | "127.0.0.1" | "[::1]"; + if (!LOOPBACK_HOSTS.has(host)) { + throw new DevBrowserTargetUrlError("host_not_allowed"); + } + + const port = parsePort(url); + const connectHost = host === "[::1]" ? "::1" : "127.0.0.1"; + const targetOrigin = connectHost === "::1" ? `http://[::1]:${port}` : `http://127.0.0.1:${port}`; + const targetPath = `${url.pathname || "/"}${url.search}`; + + return { + connectHost, + displayUrl: url.href, + originalHost: host, + port, + targetHash: url.hash, + targetOrigin, + targetPath, + }; +} +``` + +- [ ] **Step 4: Run the validation test and confirm it passes** + +Run: + +```bash +pnpm --filter @coder-studio/server test src/dev-browser/target-url.test.ts +``` + +Expected: passes. + +- [ ] **Step 5: Commit** + +```bash +git add packages/server/src/dev-browser/target-url.ts packages/server/src/dev-browser/target-url.test.ts +git commit -m "feat: validate dev browser loopback targets" +``` + +--- + +### Task 2: Server Session Store + +**Files:** +- Create: `packages/server/src/dev-browser/session-store.ts` +- Create: `packages/server/src/dev-browser/session-store.test.ts` + +- [ ] **Step 1: Write failing session store tests** + +Create `packages/server/src/dev-browser/session-store.test.ts` with: + +```ts +import { describe, expect, it } from "vitest"; +import type { DevBrowserTarget } from "./target-url.js"; +import { DevBrowserSessionStore } from "./session-store.js"; + +function target(overrides: Partial = {}): DevBrowserTarget { + return { + connectHost: "127.0.0.1", + displayUrl: "http://localhost:8000/app", + originalHost: "localhost", + port: 8000, + targetHash: "", + targetOrigin: "http://127.0.0.1:8000", + targetPath: "/app", + ...overrides, + }; +} + +describe("DevBrowserSessionStore", () => { + it("creates and reads short-lived sessions", () => { + let now = 1_000; + const store = new DevBrowserSessionStore({ now: () => now, ttlMs: 10_000 }); + const session = store.create(target()); + + expect(session.id).toMatch(/^dev_/); + expect(session.createdAt).toBe(1_000); + expect(session.expiresAt).toBe(11_000); + expect(store.get(session.id)).toMatchObject({ + id: session.id, + targetOrigin: "http://127.0.0.1:8000", + targetPath: "/app", + }); + + now = 2_000; + expect(store.get(session.id)?.lastAccessedAt).toBe(2_000); + }); + + it("expires inactive sessions", () => { + let now = 1_000; + const store = new DevBrowserSessionStore({ now: () => now, ttlMs: 500 }); + const session = store.create(target()); + + now = 1_501; + + expect(store.get(session.id)).toBeNull(); + }); + + it("deletes sessions explicitly", () => { + const store = new DevBrowserSessionStore({ now: () => 1_000, ttlMs: 10_000 }); + const session = store.create(target()); + + expect(store.delete(session.id)).toBe(true); + expect(store.get(session.id)).toBeNull(); + }); +}); +``` + +- [ ] **Step 2: Run the session store test and confirm it fails** + +Run: + +```bash +pnpm --filter @coder-studio/server test src/dev-browser/session-store.test.ts +``` + +Expected: fails because `session-store.ts` does not exist. + +- [ ] **Step 3: Implement the session store** + +Create `packages/server/src/dev-browser/session-store.ts` with: + +```ts +import { randomUUID } from "node:crypto"; +import type { DevBrowserTarget } from "./target-url.js"; + +export interface DevBrowserSession extends DevBrowserTarget { + createdAt: number; + expiresAt: number; + id: string; + lastAccessedAt: number; +} + +export interface DevBrowserSessionStoreOptions { + now?: () => number; + ttlMs?: number; +} + +const DEFAULT_TTL_MS = 30 * 60 * 1_000; + +function cloneSession(session: DevBrowserSession): DevBrowserSession { + return { ...session }; +} + +export class DevBrowserSessionStore { + readonly #now: () => number; + readonly #ttlMs: number; + readonly #sessions = new Map(); + + constructor(options: DevBrowserSessionStoreOptions = {}) { + this.#now = options.now ?? (() => Date.now()); + this.#ttlMs = options.ttlMs ?? DEFAULT_TTL_MS; + } + + create(target: DevBrowserTarget): DevBrowserSession { + const now = this.#now(); + const session: DevBrowserSession = { + ...target, + id: `dev_${randomUUID()}`, + createdAt: now, + lastAccessedAt: now, + expiresAt: now + this.#ttlMs, + }; + this.#sessions.set(session.id, session); + return cloneSession(session); + } + + get(id: string): DevBrowserSession | null { + const session = this.#sessions.get(id); + if (!session) { + return null; + } + + const now = this.#now(); + if (session.expiresAt <= now) { + this.#sessions.delete(id); + return null; + } + + const nextSession = { + ...session, + lastAccessedAt: now, + expiresAt: now + this.#ttlMs, + }; + this.#sessions.set(id, nextSession); + return cloneSession(nextSession); + } + + delete(id: string): boolean { + return this.#sessions.delete(id); + } +} +``` + +- [ ] **Step 4: Run target and session tests** + +Run: + +```bash +pnpm --filter @coder-studio/server test src/dev-browser/target-url.test.ts src/dev-browser/session-store.test.ts +``` + +Expected: passes. + +- [ ] **Step 5: Commit** + +```bash +git add packages/server/src/dev-browser/session-store.ts packages/server/src/dev-browser/session-store.test.ts +git commit -m "feat: store dev browser proxy sessions" +``` + +--- + +### Task 3: Proxy Header Filtering + +**Files:** +- Create: `packages/server/src/dev-browser/proxy-headers.ts` +- Create: `packages/server/src/dev-browser/proxy-headers.test.ts` + +- [ ] **Step 1: Write failing header tests** + +Create `packages/server/src/dev-browser/proxy-headers.test.ts` with: + +```ts +import { describe, expect, it } from "vitest"; +import { + filterProxyRequestHeaders, + filterProxyResponseHeaders, + rewriteProxyLocationHeader, +} from "./proxy-headers.js"; + +describe("dev browser proxy headers", () => { + it("strips hop-by-hop request headers and sets the target host", () => { + expect( + Object.fromEntries( + filterProxyRequestHeaders( + { + connection: "upgrade", + host: "coder.example", + upgrade: "websocket", + cookie: "coder_studio_auth=secret", + accept: "text/html", + }, + "127.0.0.1:8000" + ) + ) + ).toEqual({ + accept: "text/html", + host: "127.0.0.1:8000", + }); + }); + + it("strips unsafe response headers", () => { + const headers = new Headers({ + "content-type": "text/html", + "content-length": "200", + "content-encoding": "gzip", + connection: "keep-alive", + "set-cookie": "sid=abc", + }); + + expect(Object.fromEntries(filterProxyResponseHeaders(headers))).toEqual({ + "content-type": "text/html", + }); + }); + + it("rewrites loopback redirect locations into browser proxy paths", () => { + expect( + rewriteProxyLocationHeader("http://localhost:8000/dashboard?tab=1", { + browserProxyBase: "/dev-browser/session/dev_1/proxy", + targetOrigin: "http://127.0.0.1:8000", + port: 8000, + }) + ).toBe("/dev-browser/session/dev_1/proxy/dashboard?tab=1"); + + expect( + rewriteProxyLocationHeader("/login", { + browserProxyBase: "/dev-browser/session/dev_1/proxy", + targetOrigin: "http://127.0.0.1:8000", + port: 8000, + }) + ).toBe("/dev-browser/session/dev_1/proxy/login"); + }); +}); +``` + +- [ ] **Step 2: Run the header test and confirm it fails** + +Run: + +```bash +pnpm --filter @coder-studio/server test src/dev-browser/proxy-headers.test.ts +``` + +Expected: fails because `proxy-headers.ts` does not exist. + +- [ ] **Step 3: Implement header filtering** + +Create `packages/server/src/dev-browser/proxy-headers.ts` with: + +```ts +import type { IncomingHttpHeaders } from "node:http"; + +const HOP_BY_HOP_HEADERS = new Set([ + "connection", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailer", + "transfer-encoding", + "upgrade", +]); + +const BLOCKED_REQUEST_HEADERS = new Set(["cookie", "authorization", "origin"]); +const BLOCKED_RESPONSE_HEADERS = new Set(["content-length", "content-encoding", "set-cookie"]); + +function appendHeader(target: Headers, key: string, value: string | string[] | undefined): void { + if (value === undefined) { + return; + } + + if (Array.isArray(value)) { + for (const entry of value) { + target.append(key, entry); + } + return; + } + + target.set(key, value); +} + +export function filterProxyRequestHeaders( + headers: IncomingHttpHeaders, + targetHost: string +): Headers { + const filtered = new Headers(); + + for (const [rawKey, value] of Object.entries(headers)) { + const key = rawKey.toLowerCase(); + if (HOP_BY_HOP_HEADERS.has(key) || BLOCKED_REQUEST_HEADERS.has(key) || key === "host") { + continue; + } + + appendHeader(filtered, key, value); + } + + filtered.set("host", targetHost); + return filtered; +} + +export function filterProxyResponseHeaders(headers: Headers): Headers { + const filtered = new Headers(); + + headers.forEach((value, rawKey) => { + const key = rawKey.toLowerCase(); + if (HOP_BY_HOP_HEADERS.has(key) || BLOCKED_RESPONSE_HEADERS.has(key)) { + return; + } + + filtered.set(key, value); + }); + + return filtered; +} + +export function rewriteProxyLocationHeader( + location: string, + input: { browserProxyBase: string; port: number; targetOrigin: string } +): string { + if (location.startsWith("/")) { + return `${input.browserProxyBase}${location}`; + } + + let parsed: URL; + try { + parsed = new URL(location); + } catch { + return location; + } + + const isLoopback = + parsed.protocol === "http:" && + Number(parsed.port) === input.port && + (parsed.hostname === "localhost" || + parsed.hostname === "127.0.0.1" || + parsed.hostname === "[::1]"); + + if (!isLoopback) { + return location; + } + + return `${input.browserProxyBase}${parsed.pathname}${parsed.search}${parsed.hash}`; +} +``` + +- [ ] **Step 4: Run dev-browser server unit tests** + +Run: + +```bash +pnpm --filter @coder-studio/server test src/dev-browser +``` + +Expected: passes. + +- [ ] **Step 5: Commit** + +```bash +git add packages/server/src/dev-browser/proxy-headers.ts packages/server/src/dev-browser/proxy-headers.test.ts +git commit -m "feat: filter dev browser proxy headers" +``` + +--- + +### Task 4: Server Routes And App Wiring + +**Files:** +- Create: `packages/server/src/routes/dev-browser.ts` +- Create: `packages/server/src/routes/dev-browser.test.ts` +- Modify: `packages/server/src/app.ts` +- Modify: `packages/server/src/app-routing.test.ts` + +- [ ] **Step 1: Write failing route tests** + +Create `packages/server/src/routes/dev-browser.test.ts` with focused tests for create, proxy, redirect, invalid target, and unsupported websocket: + +```ts +import Fastify from "fastify"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { DevBrowserSessionStore } from "../dev-browser/session-store.js"; +import { registerDevBrowserRoutes } from "./dev-browser.js"; + +describe("dev browser routes", () => { + let target: ReturnType; + let app: ReturnType; + let targetOrigin: string; + + beforeEach(async () => { + target = Fastify({ logger: false }); + target.get("/app/", async (_request, reply) => + reply + .type("text/html") + .send('') + ); + target.get("/assets/app.js", async (_request, reply) => + reply.type("application/javascript").send('window.loaded = true;') + ); + target.post("/api/echo", async (request) => request.body); + target.get("/redirect", async (_request, reply) => reply.redirect("/app/")); + await target.listen({ host: "127.0.0.1", port: 0 }); + + const address = target.server.address(); + if (!address || typeof address === "string") { + throw new Error("target listen failed"); + } + targetOrigin = `http://127.0.0.1:${address.port}`; + + app = Fastify({ logger: false }); + registerDevBrowserRoutes(app, { + sessions: new DevBrowserSessionStore(), + }); + await app.ready(); + }); + + afterEach(async () => { + await app.close(); + await target.close(); + }); + + async function createSession(path = "/app/") { + const response = await app.inject({ + method: "POST", + url: "/api/dev-proxy/session", + payload: { url: `${targetOrigin}${path}` }, + }); + expect(response.statusCode).toBe(200); + return response.json() as { + browserProxyBase: string; + browserUrl: string; + id: string; + targetOrigin: string; + }; + } + + it("creates sessions for loopback targets", async () => { + const created = await createSession(); + + expect(created.id).toMatch(/^dev_/); + expect(created.browserUrl).toBe(`/dev-browser/session/${created.id}/`); + expect(created.browserProxyBase).toBe(`/dev-browser/session/${created.id}/proxy`); + expect(created.targetOrigin).toBe(targetOrigin); + }); + + it("rejects invalid session targets", async () => { + const response = await app.inject({ + method: "POST", + url: "/api/dev-proxy/session", + payload: { url: "http://example.com:8000" }, + }); + + expect(response.statusCode).toBe(400); + expect(response.json()).toMatchObject({ error: "invalid_dev_browser_target" }); + }); + + it("serves shell HTML that registers the service worker", async () => { + const created = await createSession(); + const response = await app.inject({ + method: "GET", + url: created.browserUrl, + }); + + expect(response.statusCode).toBe(200); + expect(response.headers["content-type"]).toContain("text/html"); + expect(response.body).toContain("/dev-browser-sw.js"); + expect(response.body).toContain(created.browserProxyBase); + }); + + it("proxies HTML and injects the websocket failure bootstrap", async () => { + const created = await createSession(); + const response = await app.inject({ + method: "GET", + url: `${created.browserProxyBase}/app/`, + }); + + expect(response.statusCode).toBe(200); + expect(response.headers["content-type"]).toContain("text/html"); + expect(response.body).toContain("Coder Studio dev browser does not proxy WebSocket"); + expect(response.body).toContain(''); + }); + + it("proxies static assets and JSON posts", async () => { + const created = await createSession(); + const asset = await app.inject({ + method: "GET", + url: `${created.browserProxyBase}/assets/app.js`, + }); + const post = await app.inject({ + method: "POST", + url: `${created.browserProxyBase}/api/echo`, + headers: { "content-type": "application/json" }, + payload: { ok: true }, + }); + + expect(asset.statusCode).toBe(200); + expect(asset.headers["content-type"]).toContain("javascript"); + expect(asset.body).toContain("window.loaded"); + expect(post.statusCode).toBe(200); + expect(post.json()).toEqual({ ok: true }); + }); + + it("rewrites loopback redirects to browser proxy paths", async () => { + const created = await createSession("/redirect"); + const response = await app.inject({ + method: "GET", + url: `${created.browserProxyBase}/redirect`, + }); + + expect(response.statusCode).toBeGreaterThanOrEqual(300); + expect(response.statusCode).toBeLessThan(400); + expect(response.headers.location).toBe(`${created.browserProxyBase}/app/`); + }); + + it("rejects websocket upgrade attempts", async () => { + const created = await createSession(); + const response = await app.inject({ + method: "GET", + url: `${created.browserProxyBase}/socket`, + headers: { + connection: "upgrade", + upgrade: "websocket", + }, + }); + + expect(response.statusCode).toBe(400); + expect(response.json()).toMatchObject({ error: "websocket_not_supported" }); + }); +}); +``` + +- [ ] **Step 2: Run route tests and confirm they fail** + +Run: + +```bash +pnpm --filter @coder-studio/server test src/routes/dev-browser.test.ts +``` + +Expected: fails because `registerDevBrowserRoutes` does not exist. + +- [ ] **Step 3: Implement dev browser routes** + +Create `packages/server/src/routes/dev-browser.ts` with the following route shape: + +```ts +import { Readable } from "node:stream"; +import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; +import { z } from "zod"; +import { + filterProxyRequestHeaders, + filterProxyResponseHeaders, + rewriteProxyLocationHeader, +} from "../dev-browser/proxy-headers.js"; +import type { DevBrowserSession } from "../dev-browser/session-store.js"; +import { DevBrowserSessionStore } from "../dev-browser/session-store.js"; +import { DevBrowserTargetUrlError, parseDevBrowserTargetUrl } from "../dev-browser/target-url.js"; + +const createSessionSchema = z.object({ + url: z.string().min(1), +}); + +function browserProxyBase(id: string): string { + return `/dev-browser/session/${id}/proxy`; +} + +function browserUrl(id: string): string { + return `/dev-browser/session/${id}/`; +} + +function serializeSession(session: DevBrowserSession) { + return { + id: session.id, + browserUrl: browserUrl(session.id), + browserProxyBase: browserProxyBase(session.id), + displayUrl: session.displayUrl, + targetOrigin: session.targetOrigin, + targetPath: session.targetPath, + targetHash: session.targetHash, + expiresAt: session.expiresAt, + }; +} + +function escapeJsonForScript(value: unknown): string { + return JSON.stringify(value).replaceAll("<", "\\u003c"); +} + +function renderDevBrowserShell(session: DevBrowserSession): string { + const payload = serializeSession(session); + return ` + + + + + Coder Studio Dev Browser + + +

Opening local preview...

+ + +`; +} + +function createHtmlBootstrap(session: DevBrowserSession): string { + const payload = serializeSession(session); + return ``; +} + +function injectHtmlBootstrap(html: string, session: DevBrowserSession): string { + const bootstrap = createHtmlBootstrap(session); + return html.includes("") + ? html.replace("", `${bootstrap}`) + : `${bootstrap}${html}`; +} + +function isWebSocketUpgrade(request: FastifyRequest): boolean { + return ( + String(request.headers.upgrade ?? "").toLowerCase() === "websocket" || + String(request.headers.connection ?? "").toLowerCase().includes("upgrade") + ); +} + +function resolveProxyTargetUrl(session: DevBrowserSession, request: FastifyRequest): URL { + const incoming = new URL(request.url, "http://coder-studio.local"); + const prefix = `/dev-browser/session/${session.id}/proxy`; + const path = incoming.pathname.startsWith(prefix) + ? incoming.pathname.slice(prefix.length) || "/" + : "/"; + return new URL(`${path}${incoming.search}`, session.targetOrigin); +} + +async function proxyRequest( + request: FastifyRequest, + reply: FastifyReply, + session: DevBrowserSession +) { + if (isWebSocketUpgrade(request)) { + return reply.status(400).send({ error: "websocket_not_supported" }); + } + + const targetUrl = resolveProxyTargetUrl(session, request); + const targetHeaders = filterProxyRequestHeaders(request.headers, targetUrl.host); + const method = request.method.toUpperCase(); + const init: RequestInit = { + method, + headers: targetHeaders, + redirect: "manual", + }; + + if (method !== "GET" && method !== "HEAD" && request.body !== undefined) { + if (Buffer.isBuffer(request.body) || typeof request.body === "string") { + init.body = request.body; + } else { + init.body = JSON.stringify(request.body); + targetHeaders.set("content-type", "application/json"); + } + } + + let upstream: Response; + try { + upstream = await fetch(targetUrl, init); + } catch { + return reply.status(502).send({ error: "dev_browser_target_unavailable" }); + } + + const responseHeaders = filterProxyResponseHeaders(upstream.headers); + const location = upstream.headers.get("location"); + if (location) { + responseHeaders.set( + "location", + rewriteProxyLocationHeader(location, { + browserProxyBase: browserProxyBase(session.id), + port: session.port, + targetOrigin: session.targetOrigin, + }) + ); + } + + responseHeaders.forEach((value, key) => reply.header(key, value)); + reply.status(upstream.status); + + const contentType = upstream.headers.get("content-type") ?? ""; + if (contentType.includes("text/html")) { + const html = injectHtmlBootstrap(await upstream.text(), session); + return reply.type("text/html; charset=utf-8").send(html); + } + + if (!upstream.body) { + return reply.send(); + } + + return reply.send(Readable.fromWeb(upstream.body)); +} + +export function registerDevBrowserRoutes( + app: FastifyInstance, + deps: { sessions?: DevBrowserSessionStore } = {} +): void { + const sessions = deps.sessions ?? new DevBrowserSessionStore(); + + app.post("/api/dev-proxy/session", async (request, reply) => { + const parsed = createSessionSchema.safeParse(request.body); + if (!parsed.success) { + return reply.status(400).send({ error: "invalid_dev_browser_payload" }); + } + + try { + const session = sessions.create(parseDevBrowserTargetUrl(parsed.data.url)); + return reply.send(serializeSession(session)); + } catch (error) { + if (error instanceof DevBrowserTargetUrlError) { + return reply.status(400).send({ error: "invalid_dev_browser_target" }); + } + throw error; + } + }); + + app.get("/api/dev-proxy/session/:id", async (request, reply) => { + const { id } = request.params as { id: string }; + const session = sessions.get(id); + return session + ? reply.send(serializeSession(session)) + : reply.status(404).send({ error: "dev_browser_session_not_found" }); + }); + + app.delete("/api/dev-proxy/session/:id", async (request, reply) => { + const { id } = request.params as { id: string }; + sessions.delete(id); + return reply.send({ ok: true }); + }); + + app.get("/dev-browser/session/:id", async (request, reply) => { + const { id } = request.params as { id: string }; + return reply.redirect(`/dev-browser/session/${id}/`); + }); + + app.get("/dev-browser/session/:id/", async (request, reply) => { + const { id } = request.params as { id: string }; + const session = sessions.get(id); + if (!session) { + return reply.status(404).send({ error: "dev_browser_session_not_found" }); + } + return reply.type("text/html; charset=utf-8").send(renderDevBrowserShell(session)); + }); + + app.all("/dev-browser/session/:id/proxy/*", async (request, reply) => { + const { id } = request.params as { id: string; "*": string }; + const session = sessions.get(id); + if (!session) { + return reply.status(404).send({ error: "dev_browser_session_not_found" }); + } + return proxyRequest(request, reply, session); + }); +} +``` + +- [ ] **Step 4: Register routes in the app** + +Modify `packages/server/src/app.ts`: + +```ts +import { registerDevBrowserRoutes } from "./routes/dev-browser.js"; +``` + +Register before `registerPreviewRoutes` and before static fallback: + +```ts + registerDevBrowserRoutes(app); + + const previewSessions = new PreviewSessionStore(); + registerPreviewRoutes(app, { + workspaceMgr: deps.workspaceMgr, + previewSessions, + }); +``` + +- [ ] **Step 5: Extend app routing test** + +Add this test to `packages/server/src/app-routing.test.ts`: + +```ts + it("does not serve index.html for dev browser proxy paths", async () => { + const instance = await createApp(); + + const response = await instance.inject({ + method: "GET", + url: "/dev-browser/session/missing/proxy/app.js", + headers: { + accept: "application/javascript", + }, + }); + + expect(response.statusCode).toBe(404); + expect(response.body).not.toContain(""); + }); +``` + +- [ ] **Step 6: Run server route tests** + +Run: + +```bash +pnpm --filter @coder-studio/server test src/dev-browser src/routes/dev-browser.test.ts src/app-routing.test.ts +``` + +Expected: passes. + +- [ ] **Step 7: Commit** + +```bash +git add packages/server/src/dev-browser packages/server/src/routes/dev-browser.ts packages/server/src/routes/dev-browser.test.ts packages/server/src/app.ts packages/server/src/app-routing.test.ts +git commit -m "feat: proxy loopback dev browser HTTP requests" +``` + +--- + +### Task 5: Service Worker Routing + +**Files:** +- Create: `packages/web/public/dev-browser-sw.js` +- Create: `packages/web/src/features/dev-browser/dev-browser-sw.test.ts` + +- [ ] **Step 1: Write failing service worker tests** + +Create `packages/web/src/features/dev-browser/dev-browser-sw.test.ts` with: + +```ts +// @vitest-environment node + +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import vm from "node:vm"; +import { describe, expect, it } from "vitest"; + +interface SwHarness { + mapRequest(input: { + clientSessionId?: string; + referrer?: string; + requestUrl: string; + sessions: Record; + }): string | null; +} + +function loadHarness(): SwHarness { + const script = readFileSync(resolve(process.cwd(), "public/dev-browser-sw.js"), "utf8"); + const listeners: Record = {}; + const context = { + URL, + console, + clients: { get: async () => null }, + fetch: async () => new Response("ok"), + self: { + __coderStudioDevBrowserSwTest: undefined as SwHarness | undefined, + addEventListener: (name: string, handler: unknown) => { + listeners[name] = handler; + }, + location: { + origin: "https://studio.example", + }, + skipWaiting: () => undefined, + }, + }; + + vm.runInNewContext(script, context); + const harness = context.self.__coderStudioDevBrowserSwTest; + if (!harness) { + throw new Error("service worker test harness missing"); + } + return harness; +} + +describe("dev browser service worker mapper", () => { + const session = { + id: "dev_1", + browserProxyBase: "/dev-browser/session/dev_1/proxy", + targetOrigin: "http://127.0.0.1:8000", + targetPath: "/app/", + }; + + it("maps root-relative resource URLs using the proxied referrer", () => { + const harness = loadHarness(); + + expect( + harness.mapRequest({ + requestUrl: "https://studio.example/assets/app.js", + referrer: "https://studio.example/dev-browser/session/dev_1/proxy/app/", + sessions: { dev_1: session }, + }) + ).toBe("https://studio.example/dev-browser/session/dev_1/proxy/assets/app.js"); + }); + + it("maps hardcoded localhost URLs to the active proxy base", () => { + const harness = loadHarness(); + + expect( + harness.mapRequest({ + requestUrl: "http://localhost:8000/chunk.js?x=1", + referrer: "https://studio.example/dev-browser/session/dev_1/proxy/app/", + sessions: { dev_1: session }, + }) + ).toBe("https://studio.example/dev-browser/session/dev_1/proxy/chunk.js?x=1"); + }); + + it("does not rewrite unrelated Coder Studio requests", () => { + const harness = loadHarness(); + + expect( + harness.mapRequest({ + requestUrl: "https://studio.example/assets/main-app.js", + referrer: "https://studio.example/workspace", + sessions: { dev_1: session }, + }) + ).toBeNull(); + }); +}); +``` + +- [ ] **Step 2: Run the service worker test and confirm it fails** + +Run: + +```bash +pnpm --filter @coder-studio/web test src/features/dev-browser/dev-browser-sw.test.ts +``` + +Expected: fails because `public/dev-browser-sw.js` does not exist. + +- [ ] **Step 3: Implement the service worker** + +Create `packages/web/public/dev-browser-sw.js` with: + +```js +const SESSION_MESSAGE_TYPE = "coder-studio-dev-browser-session"; +const sessions = new Map(); +const clientSessions = new Map(); + +function getSessionIdFromProxyPath(pathname) { + const match = /^\/dev-browser\/session\/([^/]+)\/proxy(?:\/|$)/.exec(pathname); + return match ? decodeURIComponent(match[1]) : null; +} + +function getSessionForRequest(input) { + if (input.clientSessionId && input.sessions[input.clientSessionId]) { + return input.sessions[input.clientSessionId]; + } + + const requestUrl = new URL(input.requestUrl); + const fromRequest = getSessionIdFromProxyPath(requestUrl.pathname); + if (fromRequest && input.sessions[fromRequest]) { + return input.sessions[fromRequest]; + } + + if (input.referrer) { + const referrerUrl = new URL(input.referrer); + const fromReferrer = getSessionIdFromProxyPath(referrerUrl.pathname); + if (fromReferrer && input.sessions[fromReferrer]) { + return input.sessions[fromReferrer]; + } + } + + return null; +} + +function isLoopbackUrlForSession(url, session) { + if (url.protocol !== "http:") { + return false; + } + + const target = new URL(session.targetOrigin); + return ( + url.port === target.port && + (url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]") + ); +} + +function mapRequestToProxy(input) { + const requestUrl = new URL(input.requestUrl); + const session = getSessionForRequest(input); + if (!session) { + return null; + } + + if (requestUrl.pathname.startsWith(session.browserProxyBase)) { + return null; + } + + if (requestUrl.origin === self.location.origin) { + if (requestUrl.pathname.startsWith("/dev-browser/")) { + return null; + } + return `${self.location.origin}${session.browserProxyBase}${requestUrl.pathname}${requestUrl.search}`; + } + + if (isLoopbackUrlForSession(requestUrl, session)) { + return `${self.location.origin}${session.browserProxyBase}${requestUrl.pathname}${requestUrl.search}`; + } + + return null; +} + +self.addEventListener("install", (event) => { + self.skipWaiting(); +}); + +self.addEventListener("activate", (event) => { + event.waitUntil(self.clients.claim()); +}); + +self.addEventListener("message", (event) => { + if (event.data?.type !== SESSION_MESSAGE_TYPE || !event.data.session?.id) { + return; + } + + sessions.set(event.data.session.id, event.data.session); + if (event.source?.id) { + clientSessions.set(event.source.id, event.data.session.id); + } +}); + +self.addEventListener("fetch", (event) => { + const clientSessionId = event.clientId ? clientSessions.get(event.clientId) : undefined; + const mappedUrl = mapRequestToProxy({ + requestUrl: event.request.url, + referrer: event.request.referrer, + clientSessionId, + sessions: Object.fromEntries(sessions.entries()), + }); + + if (!mappedUrl) { + return; + } + + if (event.request.mode === "navigate") { + event.respondWith(Response.redirect(mappedUrl, 302)); + return; + } + + const proxiedRequest = new Request(mappedUrl, event.request); + event.respondWith(fetch(proxiedRequest)); +}); + +self.__coderStudioDevBrowserSwTest = { + mapRequest: mapRequestToProxy, +}; +``` + +- [ ] **Step 4: Run service worker tests** + +Run: + +```bash +pnpm --filter @coder-studio/web test src/features/dev-browser/dev-browser-sw.test.ts +``` + +Expected: passes. + +- [ ] **Step 5: Commit** + +```bash +git add packages/web/public/dev-browser-sw.js packages/web/src/features/dev-browser/dev-browser-sw.test.ts +git commit -m "feat: route dev browser resources with service worker" +``` + +--- + +### Task 6: Web API And Dev Browser Surface + +**Files:** +- Create: `packages/web/src/features/dev-browser/api.ts` +- Create: `packages/web/src/features/dev-browser/api.test.ts` +- Create: `packages/web/src/features/dev-browser/dev-browser-surface.tsx` +- Create: `packages/web/src/features/dev-browser/dev-browser-surface.test.tsx` + +- [ ] **Step 1: Write failing API tests** + +Create `packages/web/src/features/dev-browser/api.test.ts`: + +```ts +import { afterEach, describe, expect, it, vi } from "vitest"; +import { createDevBrowserSession, deleteDevBrowserSession } from "./api"; + +describe("dev browser api", () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("creates and deletes sessions with credentialed fetch", async () => { + const fetchMock = vi.fn(async (url: string, init?: RequestInit) => { + if (url === "/api/dev-proxy/session" && init?.method === "POST") { + return new Response( + JSON.stringify({ + id: "dev_1", + browserUrl: "/dev-browser/session/dev_1/", + browserProxyBase: "/dev-browser/session/dev_1/proxy", + targetOrigin: "http://127.0.0.1:8000", + }), + { status: 200, headers: { "content-type": "application/json" } } + ); + } + + if (url === "/api/dev-proxy/session/dev_1" && init?.method === "DELETE") { + return new Response(JSON.stringify({ ok: true }), { status: 200 }); + } + + return new Response("missing", { status: 404 }); + }); + vi.stubGlobal("fetch", fetchMock); + + const created = await createDevBrowserSession("localhost:8000"); + await deleteDevBrowserSession("dev_1"); + + expect(created.browserUrl).toBe("/dev-browser/session/dev_1/"); + expect(fetchMock).toHaveBeenCalledWith( + "/api/dev-proxy/session", + expect.objectContaining({ + method: "POST", + credentials: "include", + body: JSON.stringify({ url: "localhost:8000" }), + }) + ); + expect(fetchMock).toHaveBeenCalledWith( + "/api/dev-proxy/session/dev_1", + expect.objectContaining({ method: "DELETE", credentials: "include" }) + ); + }); +}); +``` + +- [ ] **Step 2: Implement API helpers** + +Create `packages/web/src/features/dev-browser/api.ts`: + +```ts +export interface DevBrowserSessionResponse { + browserProxyBase: string; + browserUrl: string; + displayUrl?: string; + expiresAt?: number; + id: string; + targetOrigin: string; +} + +async function readJson(response: Response): Promise { + if (!response.ok) { + throw new Error(`dev_browser_request_failed:${response.status}`); + } + return response.json() as Promise; +} + +export async function createDevBrowserSession(url: string): Promise { + const response = await fetch("/api/dev-proxy/session", { + method: "POST", + credentials: "include", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ url }), + }); + + return readJson(response); +} + +export async function deleteDevBrowserSession(sessionId: string): Promise { + const response = await fetch(`/api/dev-proxy/session/${sessionId}`, { + method: "DELETE", + credentials: "include", + }); + + await readJson<{ ok: true }>(response); +} +``` + +- [ ] **Step 3: Run API tests** + +Run: + +```bash +pnpm --filter @coder-studio/web test src/features/dev-browser/api.test.ts +``` + +Expected: passes. + +- [ ] **Step 4: Write failing surface tests** + +Create `packages/web/src/features/dev-browser/dev-browser-surface.test.tsx`: + +```tsx +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { DevBrowserSurface } from "./dev-browser-surface"; + +vi.mock("../../lib/i18n", () => ({ + useTranslation: () => (key: string) => { + const translations: Record = { + "dev_browser.title": "Browser", + "dev_browser.url_label": "Local URL", + "dev_browser.url_placeholder": "localhost:8000", + "dev_browser.open": "Open", + "dev_browser.loading": "Opening local preview", + "dev_browser.unsupported": "Service workers are unavailable", + "dev_browser.error": "Could not open local preview", + }; + return translations[key] ?? key; + }, +})); + +vi.mock("./api", () => ({ + createDevBrowserSession: vi.fn(async () => ({ + id: "dev_1", + browserUrl: "/dev-browser/session/dev_1/", + browserProxyBase: "/dev-browser/session/dev_1/proxy", + targetOrigin: "http://127.0.0.1:8000", + })), + deleteDevBrowserSession: vi.fn(async () => undefined), +})); + +describe("DevBrowserSurface", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("creates a session and renders the iframe", async () => { + Object.defineProperty(window, "isSecureContext", { configurable: true, value: true }); + Object.defineProperty(window.navigator, "serviceWorker", { + configurable: true, + value: {}, + }); + + render(); + + fireEvent.change(screen.getByLabelText("Local URL"), { + target: { value: "localhost:8000" }, + }); + fireEvent.click(screen.getByRole("button", { name: "Open" })); + + const frame = await screen.findByTitle("Browser"); + expect(frame).toHaveAttribute("src", "/dev-browser/session/dev_1/"); + }); + + it("shows unsupported state when service workers are unavailable", () => { + Object.defineProperty(window, "isSecureContext", { configurable: true, value: false }); + Object.defineProperty(window.navigator, "serviceWorker", { + configurable: true, + value: undefined, + }); + + render(); + + expect(screen.getByText("Service workers are unavailable")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Open" })).toBeDisabled(); + }); + + it("deletes the active session on unmount", async () => { + Object.defineProperty(window, "isSecureContext", { configurable: true, value: true }); + Object.defineProperty(window.navigator, "serviceWorker", { + configurable: true, + value: {}, + }); + const api = await import("./api"); + + const { unmount } = render(); + fireEvent.change(screen.getByLabelText("Local URL"), { + target: { value: "localhost:8000" }, + }); + fireEvent.click(screen.getByRole("button", { name: "Open" })); + await screen.findByTitle("Browser"); + + unmount(); + + await waitFor(() => expect(api.deleteDevBrowserSession).toHaveBeenCalledWith("dev_1")); + }); +}); +``` + +- [ ] **Step 5: Implement `DevBrowserSurface`** + +Create `packages/web/src/features/dev-browser/dev-browser-surface.tsx`: + +```tsx +import { type FormEvent, useEffect, useRef, useState } from "react"; +import { EmptyState, Notice } from "../../components/ui"; +import { useTranslation } from "../../lib/i18n"; +import { + createDevBrowserSession, + deleteDevBrowserSession, + type DevBrowserSessionResponse, +} from "./api"; + +function supportsDevBrowserServiceWorker(): boolean { + return ( + typeof window !== "undefined" && + window.isSecureContext && + typeof navigator !== "undefined" && + "serviceWorker" in navigator + ); +} + +export function DevBrowserSurface() { + const t = useTranslation(); + const [url, setUrl] = useState(""); + const [session, setSession] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const sessionRef = useRef(null); + const supported = supportsDevBrowserServiceWorker(); + + useEffect(() => { + sessionRef.current = session; + }, [session]); + + useEffect(() => { + return () => { + const activeSession = sessionRef.current; + if (activeSession) { + void deleteDevBrowserSession(activeSession.id).catch(() => undefined); + } + }; + }, []); + + const open = async (event: FormEvent) => { + event.preventDefault(); + if (!supported || loading || !url.trim()) { + return; + } + + setLoading(true); + setError(null); + const previousSession = sessionRef.current; + try { + const created = await createDevBrowserSession(url.trim()); + setSession(created); + if (previousSession) { + void deleteDevBrowserSession(previousSession.id).catch(() => undefined); + } + } catch (err) { + setError(err instanceof Error ? err.message : "dev_browser_open_failed"); + } finally { + setLoading(false); + } + }; + + return ( +
+
+ + +
+ + {!supported ? : null} + {error ? : null} + +
+ {session ? ( +