mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-10-19 07:24:03 +00:00
docs: refresh README and workspace screenshot for 0.20 (#782)
## Summary - Update the main README for the shared OpenCode V2 service, current features, managed desktop setup, minimum/recommended OpenCode versions, and explicit service restart behavior. - Correct release links, actual Electron/Tauri artifact formats and architectures, Node guidance, remote authentication and SideCar configuration fields. - Align server and contributor documentation with the 0.20 baseline. - Replace the old hero screenshot with a current 1600x900 workspace rendered using real Solid components and synthetic demo data. - Include a reproducible Chromium capture script and provenance notes; external traffic is blocked and no user backend, daemon or provider is used. ## Preview  ## Validation - UI typecheck passes. - Chromium capture passes transcript, connected-state, agent, timeline and MCP readiness checks, with no uncaught renderer errors. - Resulting image inspected visually. - 20 local Markdown/image targets verified. - Staged git diff --check passes. Regenerate with: node packages/ui/scripts/capture-readme.mjs Documentation and capture tooling only; CI is not being monitored.
This commit is contained in:
parent
999ca9751d
commit
d873cfd2ee
8 changed files with 382 additions and 46 deletions
|
|
@ -4,8 +4,9 @@ Thank you for your interest in contributing! This guide will help you get starte
|
|||
|
||||
## Prerequisites
|
||||
|
||||
- **Node.js 18+** and npm
|
||||
- **OpenCode CLI** in your `PATH` (CodeNomad uses one shared native V2 service for all workspace locations)
|
||||
- **Node.js** from [`.node-version`](.node-version) and npm
|
||||
- **Git** available to the CodeNomad backend
|
||||
- **OpenCode V2 CLI** (`opencode`) in your `PATH` or selected in CodeNomad settings; see [Requirements](README.md#requirements) for minimum and recommended versions. CodeNomad uses one shared native V2 service for all workspace locations.
|
||||
|
||||
## Quick Start
|
||||
|
||||
|
|
|
|||
99
README.md
99
README.md
|
|
@ -2,29 +2,28 @@
|
|||
|
||||
## The AI Coding Cockpit for OpenCode
|
||||
|
||||
CodeNomad transforms OpenCode from a terminal tool into a **premium desktop workspace** — built for developers who live inside AI coding sessions for hours and need control, speed, and clarity.
|
||||
CodeNomad is a **desktop and browser workspace for OpenCode V2** — built for developers who work across projects, long conversations, and Git worktrees. It connects to OpenCode's shared background service, keeping your sessions accessible from CodeNomad and compatible terminal clients.
|
||||
|
||||
> OpenCode gives you the engine. CodeNomad gives you the cockpit.
|
||||
|
||||

|
||||

|
||||
|
||||
*The 0.20 interface, rendered with demonstration data. [Download the latest stable release](https://github.com/NeuralNomadsAI/CodeNomad/releases/latest).*
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- **🚀 Multi-Instance Workspace**
|
||||
- **🌐 Remote Access**
|
||||
- **🧠 Session Management**
|
||||
- **🎙️ Voice Input & Speech**
|
||||
- **🌳 Git Worktrees**
|
||||
- **💬 Rich Message Experience**
|
||||
- **🧩 SideCars**
|
||||
- **⌨️ Command Palette**
|
||||
- **📁 File System Browser**
|
||||
- **🔐 Authentication & Security**
|
||||
- **🔔 Notifications**
|
||||
- **🎨 Theming**
|
||||
- **🌍 Internationalization**
|
||||
- **🚀 Projects and desktop windows** — Keep multiple projects open, with per-window tabs, drafts, attachments, and view restoration.
|
||||
- **🧠 Sessions and full-history navigation** — Browse nested sessions, fork conversations, search beyond loaded messages, and jump through long histories with a timeline and previews.
|
||||
- **🌳 Git worktrees** — Create and select worktrees, move a session with its subsessions, and review Git changes without leaving the workspace.
|
||||
- **💬 Rich conversations** — Markdown, code, diffs, tool-result images, configurable reasoning/tool visibility, and `/btw` for a temporary side question.
|
||||
- **📎 Files and attachments** — Pick, paste, or drop device files; use `@` references for project files and browse your workspace from the file panel.
|
||||
- **🌐 Remote access** — Connect to a CodeNomad server from a desktop client or browser, with password authentication and HTTPS support.
|
||||
- **🧩 Tools and previews** — Monitor native background shells, manage MCP connections and Global/Project plugin activation, and open web tools as SideCars.
|
||||
- **🔎 Agent-driven browser previews** — Let your agent open and inspect a visible web preview in Electron or Windows Tauri, without a Developer Mode switch.
|
||||
- **🎙️ Voice input and speech** — Dictate prompts and listen to responses.
|
||||
- **⌨️ A workspace you can adapt** — Command palette, notifications, customizable panels, independent light/dark palettes, and multiple interface languages.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -34,25 +33,31 @@ CodeNomad transforms OpenCode from a terminal tool into a **premium desktop work
|
|||
|
||||
Available as both Electron and Tauri builds — choose based on your preference.
|
||||
|
||||
Download the latest installer for your platform from [Releases](https://github.com/shantur/CodeNomad/releases).
|
||||
Download the archive or package for your platform from [Releases](https://github.com/NeuralNomadsAI/CodeNomad/releases/latest).
|
||||
|
||||
| Platform | Formats |
|
||||
|----------|---------|
|
||||
| macOS | DMG, ZIP (Universal: Intel + Apple Silicon) |
|
||||
| Windows | NSIS Installer, ZIP (x64, ARM64) |
|
||||
| Linux | Tauri deb, Electron portable tar.gz (x64) |
|
||||
| Platform | Electron | Tauri |
|
||||
|----------|----------|-------|
|
||||
| macOS | ZIP, separate Intel and Apple Silicon builds | ZIP, separate Intel and Apple Silicon builds |
|
||||
| Windows | ZIP (x64) | ZIP containing the Windows installer (x64) |
|
||||
| Linux | Portable tar.gz (x64) | deb (x64) |
|
||||
|
||||
The Tauri deb is currently built and installation-tested on Ubuntu 24.04. Compatibility with older Debian-based distributions is not yet guaranteed.
|
||||
|
||||
Desktop builds bundle Node.js and npm; **you do not need to install Node.js separately**. On first launch, select an existing compatible OpenCode executable or install OpenCode through the setup screen. You can manage the executable, updates, and service later in **Preferences → OpenCode**.
|
||||
|
||||
Open a project folder, connect your model provider, and start a session. Git must be available to the CodeNomad backend for repository and worktree features; see [Requirements](#requirements).
|
||||
|
||||
### 💻 CodeNomad Server
|
||||
|
||||
Run as a local server and access via browser. Perfect for remote development.
|
||||
Run on your development machine and access it through a browser. Install Node.js/npm and OpenCode V2 on that host first; Node.js 24 LTS is the recommended runtime (see [Requirements](#requirements)).
|
||||
|
||||
```bash
|
||||
npx @neuralnomads/codenomad --password <your-password> --launch
|
||||
npx @neuralnomads/codenomad --password "your-password" --launch
|
||||
```
|
||||
|
||||
> **Authentication required:** The server requires a password on first run. You can pass it via `--password`, the `CODENOMAD_SERVER_PASSWORD` environment variable, or create an `auth.json` file (see [Server Documentation](packages/server/README.md)).
|
||||
The server binds to `127.0.0.1` by default. For remote access, add `--host 0.0.0.0` and connect to the printed HTTPS address; see [Server Documentation](packages/server/README.md#remote-access-binding-rules) for binding and TLS configuration.
|
||||
|
||||
> **Authentication:** Supply `--password` or `CODENOMAD_SERVER_PASSWORD` on each start, or use the UI's persistent password setup. `auth.json` is managed by CodeNomad; see [Authentication](packages/server/README.md#authentication).
|
||||
|
||||
> **Self-signed certificate:** On first launch with HTTPS enabled (the default), your browser will show a "Your connection is not private" warning. This is expected — the server generates a local self-signed certificate automatically. Click **Advanced → Proceed to localhost** to continue. For local-only use without the warning, run with `--https=false --http=true`.
|
||||
|
||||
|
|
@ -63,23 +68,37 @@ See [Server Documentation](packages/server/README.md) for flags, TLS, auth, and
|
|||
Bleeding-edge builds from the `dev` branch:
|
||||
|
||||
```bash
|
||||
npx @neuralnomads/codenomad-dev --password <your-password> --launch
|
||||
npx @neuralnomads/codenomad-dev --password "your-password" --launch
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## OpenCode V2 and upgrading from 0.19.x
|
||||
|
||||
CodeNomad **0.20.0 requires OpenCode V2**: **2.0.7 minimum**, with **2.0.16 recommended and qualified for this release**. OpenCode V1 and the former V2 beta protocol are not supported. The current requirements are maintained in [`runtime-support.ts`](packages/server/src/opencode/runtime-support.ts).
|
||||
|
||||
- **One shared OpenCode service** owns sessions and messages. Closing a CodeNomad tab, window, or backend leaves that service running. Desktop tabs, drafts, and layout are restored separately for each window.
|
||||
- **Use an existing CLI or install from CodeNomad.** Executables on `PATH` are preferred; the setup screen also supports a selected executable and a shared user npm installation. The former private CodeNomad OpenCode installation is no longer used.
|
||||
- **Updating the CLI and restarting its service are separate actions.** Use the explicit service controls in Preferences when you want the running daemon to use an updated executable.
|
||||
- **Remote users should upgrade their CodeNomad server too.** The 0.20 UI requires CodeNomad server 0.20.0 or later.
|
||||
|
||||
For a manual CLI installation, follow the [OpenCode V2 installation guide](https://opencode.ai/v2/docs/). The npm package is `@opencode/cli` and the command is `opencode`.
|
||||
|
||||
---
|
||||
|
||||
## SideCars
|
||||
|
||||
SideCars let you open local web tools inside CodeNomad as tabs.
|
||||
SideCars let you open web tools running on the CodeNomad server host inside CodeNomad as tabs. Start the service separately, then add it in **Preferences → SideCars**.
|
||||
|
||||
Previews use token-scoped URLs inside opaque-origin sandboxes. Loopback HTTP native previews receive a dedicated `.preview.localhost` origin so root routes, POSTs, and live reload behave normally; HTTPS, LAN, and web hosts use the capability path. Element comments use a source-checked message bridge.
|
||||
Session web previews are also available from the conversation toolbar. Agent-driven native browser automation is supported in Electron and Windows Tauri; other Tauri platforms use an iframe preview. See [Browser Automation](dev-docs/BROWSER_AUTOMATION.md) for platform details.
|
||||
|
||||
<details>
|
||||
<summary><strong>Configuration</strong></summary>
|
||||
|
||||
- **Name**: Display name used in CodeNomad
|
||||
- **Port**: Local HTTP or HTTPS service running on `127.0.0.1:<port>`
|
||||
- **Base path**: Mounted under `/sidecars/:id`
|
||||
- **Port**: Numeric port of the service on the server host, at `127.0.0.1:<port>`
|
||||
- **Protocol**: Select HTTP or HTTPS to match the service
|
||||
- **Base path**: Derived from the name and displayed as `/sidecars/:id`
|
||||
- **Prefix mode**:
|
||||
- **Preserve prefix** forwards the full `/sidecars/:id/...` path upstream
|
||||
- **Strip prefix** removes `/sidecars/:id` before forwarding the request upstream
|
||||
|
|
@ -92,13 +111,14 @@ Previews use token-scoped URLs inside opaque-origin sandboxes. Loopback HTTP nat
|
|||
Run with Docker:
|
||||
|
||||
```bash
|
||||
docker run -it --init -p 8000:3000 -v "${HOME}:${HOME}:cached" -e HOME=${HOME} gitpod/openvscode-server --server-base-path /sidecars/vscode
|
||||
docker run -it --init -p 127.0.0.1:8000:3000 -v "${HOME}:${HOME}:cached" -e HOME=${HOME} gitpod/openvscode-server --server-base-path /sidecars/vscode
|
||||
```
|
||||
|
||||
Add SideCar as:
|
||||
|
||||
- **Name**: `VSCode`
|
||||
- **Port**: `http://127.0.0.1:8000`
|
||||
- **Port**: `8000`
|
||||
- **Protocol**: `HTTP`
|
||||
- **Base path**: `/sidecars/vscode`
|
||||
- **Prefix mode**: `Preserve prefix`
|
||||
|
||||
|
|
@ -116,7 +136,8 @@ ttyd --writable zsh
|
|||
Add SideCar as:
|
||||
|
||||
- **Name**: `Terminal`
|
||||
- **Port**: `http://127.0.0.1:7681`
|
||||
- **Port**: `7681`
|
||||
- **Protocol**: `HTTP`
|
||||
- **Base path**: `/sidecars/terminal`
|
||||
- **Prefix mode**: `Strip prefix`
|
||||
|
||||
|
|
@ -126,9 +147,9 @@ Add SideCar as:
|
|||
|
||||
## Requirements
|
||||
|
||||
- **[OpenCode CLI](https://opencode.ai)** — must be installed and in your `PATH`
|
||||
- **[OpenCode V2](https://opencode.ai/v2/docs/)** — 2.0.7 minimum; 2.0.16 recommended for CodeNomad 0.20.0. Install/select it through desktop setup or provide a compatible CLI on the server host.
|
||||
- **[Git](https://git-scm.com/downloads)** — a prerequisite for full functionality (repository identity, Git operations and worktrees). Git must be executable in the CodeNomad backend's `PATH`; it is not bundled with the desktop installers.
|
||||
- **Node.js 18+** — for server mode or building from source
|
||||
- **Node.js and npm** — required for the standalone npm server and source builds, bundled with desktop releases. Use **Node.js 24 LTS**; development and release builds use the version in [`.node-version`](.node-version) (currently 24.20.0).
|
||||
|
||||
On Windows, install [Git for Windows](https://gitforwindows.org/) with the option to use Git from the command line and third-party software. A per-user installation is sufficient and does not require administrator rights; install into a user-writable directory and make Git available in the user `PATH`. On macOS or Linux, follow the [platform installation instructions](https://git-scm.com/downloads). Fully quit and restart CodeNomad after installation so its backend inherits the updated `PATH`. For a remote server, install Git on the server host for the account running CodeNomad and restart that server process, not just the browser. A Git installation available only inside WSL does not satisfy the Windows backend's requirement.
|
||||
|
||||
|
|
@ -145,17 +166,19 @@ CodeNomad is a monorepo built with:
|
|||
| **[packages/server](packages/server/README.md)** | Core logic & CLI — workspaces, OpenCode proxy, API, auth, speech |
|
||||
| **[packages/ui](packages/ui/README.md)** | SolidJS frontend — reactive, fast, beautiful |
|
||||
| **[packages/electron-app](packages/electron-app/README.md)** | Desktop shell — process management, IPC, native dialogs |
|
||||
| **[packages/tauri-app](packages/tauri-app)** | Tauri desktop shell (experimental) |
|
||||
| **[packages/tauri-app](packages/tauri-app)** | Tauri desktop shell — native windows, integration, and packaging |
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
git clone https://github.com/NeuralNomadsAI/CodeNomad.git
|
||||
git clone --branch dev https://github.com/NeuralNomadsAI/CodeNomad.git
|
||||
cd CodeNomad
|
||||
npm install
|
||||
npm ci --workspaces --include=optional
|
||||
npm run dev
|
||||
```
|
||||
|
||||
`npm run dev` starts the Electron development host. Use Node.js from [`.node-version`](.node-version), Git, and a compatible OpenCode V2 CLI. For Tauri, install the [platform build prerequisites](https://v2.tauri.app/start/prerequisites/) and run `npm run dev:tauri`. See [CONTRIBUTING.md](CONTRIBUTING.md) for the development and PR workflow.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
|
@ -181,7 +204,7 @@ WebKitGTK DMA-BUF/GBM issue. Run with:
|
|||
WEBKIT_DISABLE_DMABUF_RENDERER=1 codenomad-tauri
|
||||
```
|
||||
|
||||
See full workaround in the original README.
|
||||
This applies to the Tauri WebKitGTK build. Electron uses Chromium and can be used as an alternative on the same machine.
|
||||
</details>
|
||||
|
||||
---
|
||||
|
|
|
|||
36
docs/screenshots/README.md
Normal file
36
docs/screenshots/README.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# README workspace capture
|
||||
|
||||
`workspace-0.20.png` is a Chromium capture of the real CodeNomad project tabs,
|
||||
instance shell, session tree, transcript, composer, timeline, and Status panel.
|
||||
The fixture supplies synthetic project names, conversations, tool output, and
|
||||
service metadata. No personal sessions or provider credentials are used.
|
||||
|
||||
The scene contains eight project tabs, 40 sessions (including nested agent
|
||||
sessions), and a selected conversation with 244 native history records across
|
||||
61 exchanges. Transcript reads are paginated; the timeline uses the complete
|
||||
structural outline. The capture shows the latest exchange in that longer task.
|
||||
|
||||
Regenerate from the repository root after installing workspace dependencies:
|
||||
|
||||
```sh
|
||||
npx playwright install chromium
|
||||
node packages/ui/scripts/capture-readme.mjs
|
||||
```
|
||||
|
||||
`CODENOMAD_BROWSER_PATH` can select an existing Chromium executable. The script
|
||||
uses a fresh browser context, an isolated loopback Vite server, a 1600×900 viewport,
|
||||
the dark palette, English labels, and UTC timestamps. API responses are fixtures;
|
||||
external requests are blocked. It does not connect to a CodeNomad backend or an
|
||||
OpenCode daemon. It verifies the rendered conversation, connected state,
|
||||
agent control, eight project tabs, populated session list, long timeline and MCP
|
||||
row, then checks for uncaught renderer errors before saving.
|
||||
|
||||
The shell-only scene applies the page-level keyboard-hint preference normally
|
||||
set by `App.tsx`. The capture verifies that shortcut hints are hidden, matching
|
||||
the default application setting.
|
||||
|
||||
The scene is maintained in
|
||||
`packages/ui/tests/browser/fixtures/readme-workspace.tsx`, with the longer history
|
||||
and session/project names in the adjacent `readme-workspace-data.ts`. Update this
|
||||
data when refreshing documentation; keep product layout and styling in the real
|
||||
components.
|
||||
BIN
docs/screenshots/workspace-0.20.png
Normal file
BIN
docs/screenshots/workspace-0.20.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 146 KiB |
|
|
@ -1,6 +1,6 @@
|
|||
# CodeNomad Server
|
||||
|
||||
**CodeNomad Server** is the high-performance engine behind the CodeNomad cockpit. It transforms your machine into a robust development host, managing the lifecycle of multiple OpenCode instances and providing the low-latency data streams that long-haul builders demand. It bridges your local filesystem with the UI, ensuring that whether you are on localhost or a remote tunnel, you have the speed, clarity, and control of a native workspace.
|
||||
**CodeNomad Server** connects the desktop and browser UI to your projects and one shared OpenCode V2 background service. It provides workspace access, Git operations, authentication, and live event delivery. OpenCode owns the sessions, messages, and agent execution; closing CodeNomad leaves the shared OpenCode service running.
|
||||
|
||||
## Features & Capabilities
|
||||
|
||||
|
|
@ -13,18 +13,18 @@
|
|||
|
||||
### ⚡️ Workspace Power
|
||||
|
||||
- **Multi-Instance**: Juggle multiple OpenCode sessions side-by-side with per-instance tabs.
|
||||
- **Long-Context Native**: Scroll through massive transcripts without hitches.
|
||||
- **Multiple projects**: Work across project tabs and sessions over the same OpenCode service.
|
||||
- **Long conversations**: Bounded transcript loading, full-history search, and timeline navigation.
|
||||
- **Deep Task Awareness**: Monitor background tasks and child sessions without losing your flow.
|
||||
- **Command Palette**: A single, global palette to jump tabs, launch tools, and fire shortcuts.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Git**: A prerequisite in the backend process's `PATH` for repository identity, Git operations and worktree management. Without Git, directory-only conversations in the explicitly opened physical folder remain available so the user can ask the agent for installation help. Owned session prompts/custom commands update the native `codenomad.git-availability` instruction; it reports the backend platform and is removed when Git becomes available. Git/worktree operations still fail explicitly with `503`/`git_required`. Install from https://git-scm.com/downloads and restart the backend after changing `PATH`. A Windows per-user installation is sufficient; Git inside WSL alone is insufficient for a Windows backend.
|
||||
- **OpenCode V2**: Install a compatible `opencode2` CLI. CodeNomad uses the CLI's official `service status`, `service start`, and `service get password` lifecycle to connect to OpenCode's externally owned global daemon, then validates its authenticated loopback `/api/status` response rather than enforcing an exact CLI version string.
|
||||
- **OpenCode V2**: For CodeNomad 0.20.0, install `opencode` **2.0.7 or later**, with **2.0.16 recommended and qualified**. The npm package is `@opencode/cli`; see the [V2 installation guide](https://opencode.ai/v2/docs/). CodeNomad prefers the CLI on `PATH` and also supports an explicitly selected executable. It uses the official service lifecycle and validates authenticated daemon metadata and API compatibility. V1 and former V2 beta runtimes are unsupported; custom/prerelease versions are unverified and still undergo contract validation. See [`runtime-support.ts`](src/opencode/runtime-support.ts) for the current requirements.
|
||||
- **OpenCode data**: The global daemon owns its platform-default storage, database, and service registration. Configured startup environment applies only when CodeNomad starts a missing daemon; an existing daemon is unchanged.
|
||||
- **Windows to WSL**: A configured WSL UNC binary uses Linux `service status`, `service start`, and `service get password`; Windows must have WSL localhost forwarding enabled to reach its loopback service.
|
||||
- Node.js 18+ and npm (for running or building from source).
|
||||
- **Node.js and npm**: Use Node.js 24 LTS for the standalone server; source and release builds use [`.node-version`](../../.node-version) (currently 24.20.0). Desktop distributions bundle Node/npm.
|
||||
- A workspace folder on disk you want to serve.
|
||||
- Optional: a Chromium-based browser if you want `--launch` to open the UI automatically.
|
||||
|
||||
|
|
@ -225,7 +225,7 @@ When running as a server CodeNomad can also be installed as a PWA from any suppo
|
|||
- **OpenCode V2 sessions, messages, and service registration**: OpenCode's platform-default global locations.
|
||||
- **Desktop restore state**: `~/.codenomad/client-state/v2/`
|
||||
|
||||
CodeNomad owns no private OpenCode port, database, service registration, or daemon PID. Configured allowed `server.environmentVariables` and the current `NODE_EXTRA_CA_CERTS` apply only when CodeNomad starts a missing daemon. Existing daemons are unchanged; legacy `OPENCODE_DB` and `XDG_STATE_HOME` ownership variables are ignored. WSL lifecycle commands run inside Linux and never inspect or signal Linux PIDs from Windows.
|
||||
CodeNomad owns no private OpenCode port, database, service registration, or daemon PID. Allowed profile environment variables and the current `NODE_EXTRA_CA_CERTS` are passed when starting a missing daemon; connecting to an existing daemon does not change its process environment. Separately, CodeNomad applies the profile's complete execution-host environment to the native session before each prompt, custom command, or session shell request, including removal of previously configured values. Legacy `OPENCODE_DB` and `XDG_STATE_HOME` ownership variables are ignored. See [Session Environment](../../dev-docs/SESSION_ENVIRONMENT.md). WSL lifecycle commands run inside Linux and never inspect or signal Linux PIDs from Windows.
|
||||
|
||||
Explicit **Stop Workspace** evicts that location and its resources from the global service without stopping the daemon. Closing a UI tab or native window only detaches local state and never evicts. Backend shutdown clears only CodeNomad's in-memory connection state and never stops the global service.
|
||||
|
||||
|
|
|
|||
64
packages/ui/scripts/capture-readme.mjs
Normal file
64
packages/ui/scripts/capture-readme.mjs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import assert from "node:assert/strict"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { mkdir } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { chromium } from "playwright"
|
||||
import { createServer } from "vite"
|
||||
import solid from "vite-plugin-solid"
|
||||
|
||||
const root = fileURLToPath(new URL("..", import.meta.url))
|
||||
const output = path.resolve(root, "../../docs/screenshots/workspace-0.20.png")
|
||||
const server = await createServer({ configFile: false, root, logLevel: "error",
|
||||
plugins: [solid(), { name: "readme-capture", configureServer(server) {
|
||||
server.middlewares.use("/api/events", (req, res) => {
|
||||
res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" })
|
||||
res.write(": documentation fixture\n\n")
|
||||
req.on("close", () => res.end())
|
||||
})
|
||||
server.middlewares.use("/readme", async (_req, res) => {
|
||||
res.setHeader("Content-Type", "text/html")
|
||||
res.end(await server.transformIndexHtml("/readme", '<html lang="en"><head><title>CodeNomad — demo workspace</title></head><body><div id="root"></div><script type="module" src="/tests/browser/fixtures/readme-workspace.tsx"></script></body></html>'))
|
||||
})
|
||||
} }], resolve: { dedupe: ["solid-js"] }, optimizeDeps: { exclude: ["lucide-solid"] },
|
||||
server: { host: "127.0.0.1", port: 0, hmr: false, watch: null },
|
||||
})
|
||||
let browser
|
||||
try {
|
||||
await server.listen()
|
||||
const origin = `http://127.0.0.1:${server.httpServer.address().port}`
|
||||
browser = await chromium.launch({ executablePath: process.env.CODENOMAD_BROWSER_PATH || undefined })
|
||||
const page = await browser.newPage({ viewport: { width: 1600, height: 900 }, deviceScaleFactor: 1, colorScheme: "dark", timezoneId: "UTC" })
|
||||
const errors = []
|
||||
page.on("pageerror", error => { errors.push(error.message); console.error(error.message) })
|
||||
page.on("console", message => { if (message.type() === "error") console.error(message.text()) })
|
||||
// No user backend, shared daemon, provider, or external website is contacted.
|
||||
await page.route("**/*", async route => {
|
||||
const url = new URL(route.request().url())
|
||||
if (url.origin !== origin) return route.abort()
|
||||
if (url.pathname === "/api/events") return route.continue()
|
||||
if (url.pathname.includes("/api/") || url.pathname.startsWith("/workspaces/")) {
|
||||
return route.fulfill({ contentType: "application/json", body: "{}" })
|
||||
}
|
||||
return route.continue()
|
||||
})
|
||||
await page.goto(`${origin}/readme`)
|
||||
await page.getByText("Keyboard navigation is ready", { exact: true }).waitFor()
|
||||
await page.getByText("Connected", { exact: true }).waitFor()
|
||||
await page.getByText("project-docs", { exact: true }).waitFor()
|
||||
await page.getByRole("button", { name: "Agent: Build", exact: true }).waitFor()
|
||||
assert.equal(await page.locator("[data-app-tab-id]").count(), 8, "Show all eight demo projects")
|
||||
await page.getByText("Add workspace notifications", { exact: true }).waitFor()
|
||||
await page.waitForFunction(() => Number(document.querySelector(".message-timeline")?.getAttribute("data-segment-count")) >= 180)
|
||||
await page.locator('.message-timeline-segment[data-message-id="msg_01"]').waitFor()
|
||||
await page.evaluate(() => document.fonts.ready)
|
||||
await page.waitForTimeout(1500)
|
||||
assert.equal(await page.locator(".keyboard-hints:visible, .kbd-hint:visible").count(), 0,
|
||||
"Keyboard shortcut hints must remain hidden with the default preference")
|
||||
assert.deepEqual(errors, [], "The documentation fixture must render without uncaught errors")
|
||||
await mkdir(path.dirname(output), { recursive: true })
|
||||
await page.screenshot({ path: output, animations: "disabled" })
|
||||
console.log(output)
|
||||
} finally {
|
||||
await browser?.close()
|
||||
await server.close()
|
||||
}
|
||||
83
packages/ui/tests/browser/fixtures/readme-workspace-data.ts
Normal file
83
packages/ui/tests/browser/fixtures/readme-workspace-data.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
// Synthetic, public-safe history for the README's multi-project workspace.
|
||||
export const projectNames = ["Atlas", "API Gateway", "Mobile App", "Design System", "Documentation", "Infrastructure", "Analytics", "Billing"]
|
||||
|
||||
export const sessionTitles = [
|
||||
"Build the project workspace",
|
||||
"Explore the navigation architecture",
|
||||
"Review keyboard accessibility",
|
||||
"Browser regression tests",
|
||||
"Check focus restoration",
|
||||
"Audit responsive layouts",
|
||||
"Review the implementation",
|
||||
"Add full-history search",
|
||||
"Optimize timeline rendering",
|
||||
"Profile large conversations",
|
||||
"Validate scroll anchoring",
|
||||
"Improve project switching",
|
||||
"Persist window layouts",
|
||||
"Restore unsent drafts",
|
||||
"Review attachment handling",
|
||||
"Add command palette actions",
|
||||
"Fix reconnect recovery",
|
||||
"Test interrupted requests",
|
||||
"Polish the settings panel",
|
||||
"Add workspace notifications",
|
||||
"Review API pagination",
|
||||
"Improve empty states",
|
||||
"Update the contributor guide",
|
||||
"Refine light and dark palettes",
|
||||
"Review provider usage cards",
|
||||
"Add file search shortcuts",
|
||||
"Test worktree creation",
|
||||
"Validate session family moves",
|
||||
"Improve Markdown previews",
|
||||
"Review background shell output",
|
||||
"Add plugin activation controls",
|
||||
"Polish the mobile composer",
|
||||
"Audit localization coverage",
|
||||
"Review the release checklist",
|
||||
"Improve startup performance",
|
||||
"Add integration test fixtures",
|
||||
"Document remote server setup",
|
||||
"Review Git diff navigation",
|
||||
"Test clipboard attachments",
|
||||
"Prepare the next release",
|
||||
]
|
||||
|
||||
export function createReadmeHistory(model: { providerID: string; id: string }, endTime: number): any[] {
|
||||
const topics = [
|
||||
["project tabs", "project-tabs"], ["session navigation", "session-tree"],
|
||||
["history search", "history-search"], ["timeline previews", "timeline"],
|
||||
["draft restoration", "drafts"], ["attachment handling", "attachments"],
|
||||
["command palette", "command-palette"], ["responsive panels", "panels"],
|
||||
["worktree selection", "worktrees"], ["provider controls", "providers"],
|
||||
["file previews", "file-preview"], ["keyboard navigation", "project-switcher"],
|
||||
]
|
||||
const phases = ["Implement", "Review", "Test", "Refine", "Document"]
|
||||
return Array.from({ length: 60 }, (_, index) => {
|
||||
const [topic, file] = topics[index % topics.length]
|
||||
const phase = phases[Math.floor(index / topics.length)]
|
||||
const id = `msg_history_${String(index).padStart(3, "0")}`
|
||||
const time = endTime - (60 - index) * 180000
|
||||
const tool = index % 3 === 0 ? "read" : index % 3 === 1 ? "patch" : "shell"
|
||||
const input = tool === "read" ? { path: `src/components/${file}.tsx` }
|
||||
: tool === "patch" ? { patchText: `*** Begin Patch\n*** Update File: src/components/${file}.tsx\n@@\n-// Initial behavior\n+// Preserve selection and keyboard focus\n*** End Patch` }
|
||||
: { command: `npm run test:browser -- ${file}` }
|
||||
const output = tool === "shell" ? `PASS ${file}.test.ts\n6 tests passed`
|
||||
: tool === "read" ? `Read src/components/${file}.tsx, lines 1–180`
|
||||
: `Updated src/components/${file}.tsx`
|
||||
return [
|
||||
{ id: `${id}_user`, type: "user", time: { created: time },
|
||||
text: `${phase} ${topic} for the multi-project workspace. Preserve the active session and cover the important edge cases.` },
|
||||
{ id: `${id}_work`, type: "assistant", agent: "build", model, time: { created: time + 1000, completed: time + 45000 }, content: [
|
||||
{ type: "reasoning", text: `I will trace ${topic} through the existing components and check how selection, focus, and asynchronous updates interact.` },
|
||||
{ type: "tool", id: `${id}_tool`, name: tool, time: { created: time + 2000 },
|
||||
state: { status: "completed", input, content: [{ type: "text", text: output }] } },
|
||||
] },
|
||||
{ id: `${id}_summary`, type: "assistant", agent: "build", model, time: { created: time + 46000, completed: time + 60000 }, content: [
|
||||
{ type: "text", text: `### ${phase}: ${topic}\n\nThe workspace now preserves the selected project and session throughout this interaction.\n\n- Checked keyboard and pointer behavior.\n- Covered delayed responses and empty state transitions.\n- Verified the existing layout at desktop and compact widths.\n\nChanges are ready for the next review pass.` },
|
||||
] },
|
||||
{ id: `${id}_idle`, type: "idle", outcome: "completed", time: { created: time + 61000 } },
|
||||
]
|
||||
}).flat()
|
||||
}
|
||||
129
packages/ui/tests/browser/fixtures/readme-workspace.tsx
Normal file
129
packages/ui/tests/browser/fixtures/readme-workspace.tsx
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
// Public documentation fixture: real workspace components, synthetic session data.
|
||||
import { createEffect } from "solid-js"
|
||||
import { render } from "solid-js/web"
|
||||
import InstanceTabs from "../../../src/components/instance-tabs"
|
||||
import InstanceShell from "../../../src/components/instance/instance-shell2"
|
||||
import { ConfigProvider, updatePreferences, useConfig } from "../../../src/stores/preferences"
|
||||
import { I18nProvider } from "../../../src/lib/i18n"
|
||||
import { ThemeProvider } from "../../../src/lib/theme"
|
||||
import { serverApi } from "../../../src/lib/api-client"
|
||||
import { sdkManager } from "../../../src/lib/sdk-manager"
|
||||
import { sseManager } from "../../../src/lib/sse-manager"
|
||||
import { addInstance } from "../../../src/stores/instances"
|
||||
import { setSessions, setSessionPage, setActiveSession, setActiveParentSession, ensureSessionExpanded, setProviders, setAgents } from "../../../src/stores/session-state"
|
||||
import { initializeClientState, writeClientLayoutValue } from "../../../src/stores/client-state"
|
||||
import { LEFT_DRAWER_STORAGE_KEY, RIGHT_DRAWER_STORAGE_KEY, RIGHT_PANEL_TAB_STORAGE_KEY } from "../../../src/components/instance/shell/storage"
|
||||
import type { Session } from "../../../src/types/session"
|
||||
import { createReadmeHistory, projectNames, sessionTitles } from "./readme-workspace-data"
|
||||
import "../../../src/index.css"
|
||||
|
||||
const instanceId = "readme", sessionId = "workspace", directory = "/projects/atlas"
|
||||
const model = { providerID: "openai", id: "gpt-6-astra" }
|
||||
const time = Date.UTC(2026, 8, 25, 10)
|
||||
const tokens = { input: 82400, output: 6200, reasoning: 0, cache: { read: 34200, write: 0 } }
|
||||
const messages: any[] = [
|
||||
...createReadmeHistory(model, time),
|
||||
{ id: "msg_01", type: "user", time: { created: time }, text: "Add keyboard navigation to the project switcher. Keep the existing styling and cover the interaction with a browser test." },
|
||||
{ id: "msg_02", type: "assistant", agent: "build", model, time: { created: time + 1000, completed: time + 2000 }, content: [
|
||||
{ type: "text", text: "I'll check the existing focus behavior, add arrow-key navigation, then verify the interaction in the browser." },
|
||||
{ type: "tool", id: "tool_read", name: "read", time: { created: time + 1200 }, state: { status: "completed", input: { path: "src/components/project-switcher.tsx" }, content: [{ type: "text", text: "Read src/components/project-switcher.tsx, lines 1–142" }] } },
|
||||
] },
|
||||
{ id: "msg_03", type: "assistant", agent: "build", model, tokens, cost: 0.18, time: { created: time + 3000, completed: time + 5000 }, content: [
|
||||
{ type: "tool", id: "tool_shell", name: "shell", time: { created: time + 3100 }, state: { status: "completed", input: { command: "npm run test:browser -- project-switcher" }, content: [{ type: "text", text: "✓ Arrow keys move focus between projects\n✓ Enter selects the focused project\n✓ Escape closes the switcher and restores focus\n\n3 passed (1.2s)" }] } },
|
||||
{ type: "text", text: "## Keyboard navigation is ready\n\n- **Arrow keys** move between projects.\n- **Enter** selects the focused project.\n- **Escape** closes the switcher and returns focus to the trigger.\n\nThe existing pointer behavior and visual styling are preserved. All **3 browser tests pass**.\n\nChanged `project-switcher.tsx` and added `project-switcher.test.ts`." },
|
||||
] },
|
||||
{ id: "msg_04", type: "idle", outcome: "completed", time: { created: time + 6000 } },
|
||||
]
|
||||
const demoSessions: Session[] = sessionTitles.map((title, index) => ({
|
||||
id: index === 0 ? sessionId : `session-${index}`, instanceId, title,
|
||||
parentId: index === 4 ? "session-3" : index >= 1 && index <= 6 ? sessionId : index === 9 || index === 10 ? "session-8" : null,
|
||||
agent: index === 1 ? "explore" : "build", model: { providerId: model.providerID, modelId: model.id },
|
||||
status: "idle", retry: null, idleSince: null, generationRecovery: null, runtimeStatusKnown: true,
|
||||
version: "1", projectID: "atlas", location: { directory }, cost: 0.18, tokens,
|
||||
time: { created: time - index * 3600000, updated: time - index * 3600000 },
|
||||
}))
|
||||
const client: any = {
|
||||
session: { active: async () => ({}), inbox: { list: async () => ({ data: [] }) },
|
||||
get: async () => demoSessions[0], list: async () => ({ data: demoSessions, cursor: {} }),
|
||||
instructions: { entry: { remove: async () => {}, put: async () => {} } },
|
||||
},
|
||||
message: { list: async ({ cursor, limit = 200 }: { cursor?: string; limit?: number } = {}) => {
|
||||
const end = cursor ? Number(cursor) : messages.length
|
||||
const start = Math.max(0, end - limit)
|
||||
return { data: messages.slice(start, end).reverse(), cursor: start ? { next: String(start) } : {} }
|
||||
} },
|
||||
model: { default: async () => ({ data: model }) },
|
||||
shell: { list: async () => ({ data: [] }) },
|
||||
mcp: { status: async () => ({}) },
|
||||
}
|
||||
let config: any = { settings: { locale: "en", showMessageTimeline: true } }
|
||||
serverApi.fetchConfigOwner = async () => config
|
||||
serverApi.patchConfigOwner = async (_owner, patch: any) => (config = { ...config, ...patch, settings: { ...config.settings, ...patch.settings } })
|
||||
serverApi.fetchStateOwner = async () => ({} as any)
|
||||
serverApi.fetchWorktrees = async () => ({ isGitRepo: true, defaultDirectory: directory, worktrees: [
|
||||
{ slug: "root", directory, branch: "main", name: "Workspace", isRoot: true },
|
||||
] } as any)
|
||||
serverApi.getPluginControls = async () => ({ location: { directory }, runtime: [], configured: { sources: [], rules: [] }, targets: [],
|
||||
controls: ["codenomad.automation", "codenomad-session-pruning"].map(id => ({ id, builtin: false, effective: "enabled", global: "enabled", project: "default" })),
|
||||
} as any)
|
||||
const entries = messages.map((message, seq) => {
|
||||
const tools = message.content?.filter((part: any) => part.type === "tool") ?? []
|
||||
return { id: message.id, seq, type: message.type, tools: tools.length,
|
||||
reasoning: message.content?.filter((part: any) => part.type === "reasoning").length ?? 0,
|
||||
...(tools.length ? { toolName: tools[0].name } : {}),
|
||||
}
|
||||
})
|
||||
serverApi.fetchSessionOutline = async () => ({ status: "outline", total: entries.length, entries,
|
||||
checkpoints: [{ after: -1, through: entries.length - 1, digest: "0".repeat(64), changed: true }], cursor: null,
|
||||
} as any)
|
||||
serverApi.fetchOutlinePreviews = async (_instanceId, _sessionId, ids) => ({ status: "previews",
|
||||
entries: messages.filter(message => ids.includes(message.id)).map(message => ({ id: message.id,
|
||||
text: message.text ?? message.content?.filter((part: any) => part.type === "text").map((part: any) => part.text).join("\n\n") ?? "",
|
||||
tools: message.content?.filter((part: any) => part.type === "tool").map((part: any) => part.name).join(", ") ?? "",
|
||||
})),
|
||||
})
|
||||
|
||||
await initializeClientState()
|
||||
writeClientLayoutValue(LEFT_DRAWER_STORAGE_KEY, "290")
|
||||
writeClientLayoutValue(RIGHT_DRAWER_STORAGE_KEY, "350")
|
||||
writeClientLayoutValue(RIGHT_PANEL_TAB_STORAGE_KEY, "status")
|
||||
;(sdkManager as any).clients.set(`${instanceId}:/workspaces/${instanceId}/instance`, client)
|
||||
const instance = { id: instanceId, projectName: "Atlas", folder: directory, port: 0, pid: 0, proxyPath: "", status: "ready" as const, client,
|
||||
metadata: { mcpStatus: { location: { directory }, data: [{ name: "project-docs", status: { status: "connected" as const } }] } },
|
||||
}
|
||||
addInstance(instance)
|
||||
sseManager.seedStatus(instanceId, "connected")
|
||||
setSessions(new Map([[instanceId, new Map(demoSessions.map(session => [session.id, session]))]]))
|
||||
setSessionPage(instanceId, demoSessions.filter(session => !session.parentId).map(session => session.id), false, true)
|
||||
setProviders(new Map([[instanceId, [{ id: model.providerID, name: "OpenAI", models: [{ id: model.id, name: "GPT-6 Astra", providerId: model.providerID, limit: { context: 200000, output: 64000 }, cost: { input: 3, output: 15 } }] }]]]))
|
||||
setAgents(new Map([[instanceId, [{ id: "build", name: "Build", mode: "primary", description: "Build and implement" }, { id: "explore", name: "Explore", mode: "subagent", description: "Explore the codebase" }]]]))
|
||||
ensureSessionExpanded(instanceId, sessionId)
|
||||
ensureSessionExpanded(instanceId, "session-3")
|
||||
ensureSessionExpanded(instanceId, "session-8")
|
||||
setActiveParentSession(instanceId, sessionId)
|
||||
setActiveSession(instanceId, sessionId)
|
||||
await updatePreferences({ locale: "en", showMessageTimeline: true })
|
||||
|
||||
function CapturePreferences() {
|
||||
const { preferences } = useConfig()
|
||||
// App.tsx normally applies this page-level attribute. This desktop scene
|
||||
// mounts the workspace shell directly, so retain the real preference default.
|
||||
createEffect(() => {
|
||||
document.documentElement.dataset.keyboardHints = preferences().showKeyboardShortcutHints ? "show" : "hide"
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
render(() => <ConfigProvider><I18nProvider><ThemeProvider>
|
||||
<CapturePreferences />
|
||||
<div style={{ height: "100vh", display: "flex", "flex-direction": "column" }}>
|
||||
<InstanceTabs tabs={projectNames.map((name, index) => ({ id: index === 0 ? "instance:readme" : `instance:project-${index}`, kind: "instance",
|
||||
instance: index === 0 ? instance : { ...instance, id: `project-${index}`, projectName: name, folder: `/projects/${name.toLowerCase().replaceAll(" ", "-")}` },
|
||||
}))}
|
||||
activeTabId="instance:readme" onSelect={() => {}} onClose={() => {}} onNew={() => {}} onMoveTab={() => {}} />
|
||||
<InstanceShell instance={instance} isActiveInstance escapeInDebounce={false} paletteCommands={() => []}
|
||||
onCloseSession={() => {}} onNewSession={() => {}} handleSidebarAgentChange={async () => {}}
|
||||
handleSidebarModelChange={async () => {}} onExecuteCommand={() => {}} tabBarOffset={0}
|
||||
mobileFullscreenMode={false} onEnterMobileFullscreen={() => {}} onExitMobileFullscreen={() => {}} />
|
||||
</div>
|
||||
</ThemeProvider></I18nProvider></ConfigProvider>, document.getElementById("root")!)
|
||||
Loading…
Add table
Add a link
Reference in a new issue