Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,65 @@
pull_request:

jobs:
lint:
name: oxlint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: jdx/mise-action@dad1bfd3df957f44999b559dd69dc1671cb4e9ea # v4.2.1
with:
install: true
- run: bun install --frozen-lockfile
- run: bun run lint

typecheck:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
Comment on lines +10 to +20
name: typecheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: jdx/mise-action@dad1bfd3df957f44999b559dd69dc1671cb4e9ea # v4.2.1
with:
install: true
- run: bun install --frozen-lockfile
- run: bun run typecheck

unit:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
Comment on lines +21 to +31
name: unit tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: jdx/mise-action@dad1bfd3df957f44999b559dd69dc1671cb4e9ea # v4.2.1
with:
install: true
- run: bun install --frozen-lockfile
- run: bun run test:unit

integration:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
Comment on lines +32 to +42
name: integration tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: jdx/mise-action@dad1bfd3df957f44999b559dd69dc1671cb4e9ea # v4.2.1
with:
install: true
- run: bun install --frozen-lockfile
- run: bun run test:integration

build:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
name: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: jdx/mise-action@dad1bfd3df957f44999b559dd69dc1671cb4e9ea # v4.2.1
with:
install: true
- run: bun install --frozen-lockfile
- run: mise run build

all:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
Comment on lines 54 to +65
name: all checks pass
runs-on: ubuntu-latest
needs: [lint, typecheck, unit, integration, build]
if: ${{ always() }}
steps:
- run: echo "${{ needs.lint.result }} ${{ needs.typecheck.result }} ${{ needs.unit.result }} ${{ needs.integration.result }} ${{ needs.build.result }}"

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {}
34 changes: 34 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: e2e

on:
pull_request:
workflow_dispatch:

jobs:
e2e:
name: electron e2e
runs-on: macos-latest
env:
NODE_ENV: development
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: jdx/mise-action@dad1bfd3df957f44999b559dd69dc1671cb4e9ea # v4.2.1
with:
install: true
- run: bun install --frozen-lockfile
- run: npm install -g @earendil-works/pi-coding-agent
- run: bunx tsc -p tsconfig.main.json
- run: bunx tsc -p tsconfig.preload.json
- run: bunx vite build
- run: mise run dev:vite &
- run: bunx wait-on http://localhost:5173
- run: mise run test:e2e
- name: upload test artefacts
if: always()
uses: actions/upload-artifact@v4
with:
name: openpi-e2e
path: |
/tmp/pidex-*.png
test-results/
if-no-files-found: ignore

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment on lines +9 to +34
19 changes: 17 additions & 2 deletions .mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,12 @@ run = [
]

[tasks.build]
description = "Production build: compile main/preload + bundle renderer"
description = "Production build: lint, typecheck, test, bundle"
run = [
"bun run lint",
"bun run typecheck",
"bun run test:unit",
"bun run test:integration",
"bunx tsc -p tsconfig.main.json",
"bunx tsc -p tsconfig.preload.json",
"bunx vite build",
Expand All @@ -31,5 +35,16 @@ run = [
description = "Run the production build"
run = "bunx electron ."

[tasks.test]
description = "Run unit + integration tests"
run = [
"bunx vitest run --project unit",
"bunx vitest run --project integration",
]

[tasks."test:e2e"]
description = "Run e2e tests against the live Electron app"
run = "bunx vitest run --config vitest.e2e.config.ts"

[settings]
experimental = true
experimental = true
21 changes: 21 additions & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"categories": {
"correctness": "error",
"suspicious": "allow",
"perf": "allow",
"style": "allow",
"restriction": "allow",
"nursery": "allow"
},
"env": {
"browser": true,
"node": true,
"es2022": true
},
"ignorePatterns": [
"dist/**",
"node_modules/**",
".vite/**",
"src/renderer/assets/fonts/**"
]
}
349 changes: 349 additions & 0 deletions bun.lock

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions docs/adr/0001-markdown-it-for-transcript.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# ADR 0001 — Use markdown-it for transcript rendering

- **Status**: Accepted (2026-07-19)
- **Context**: The Pi coding agent streams structured markdown to the renderer (assistant text, code blocks, lists, tables, blockquotes). The renderer needs a CommonMark-compliant parser that is safe by default and supports the streaming update pattern (re-parse on every token delta). Candidate libraries were `marked`, `remark`, `markdown-it`, and `micromark`. The previous decision point in this repo's research notes (`2026-06-26 - Spanner Layer - Search, Syntax, Markdown, Diff.md`) ruled out `marked` because it is unsafe by default and requires an additional sanitiser. We had been using `marked` plus `DOMPurify` as a workaround; the sanitiser is dead weight if the parser is safe by default.

- **Decision**: Switch the transcript renderer to `markdown-it` with `html: false`, `breaks: true`, `linkify: true`, `typographer: true`. Drop `DOMPurify` entirely.

- **Consequences**:
- No DOMPurify dependency, smaller bundle and surface area.
- Safe by default against HTML/script injection from agent output.
- Token-stream architecture re-parses cleanly on streaming deltas (each delta re-validates).
- Performance is ~8ms on 10k words vs `marked`'s ~3ms; negligible at transcript sizes.
- Plugins available for tables, footnotes, task lists, math later if needed.
- If math/MDX becomes a requirement, the path forward is `remark` + `rehype-sanitize`, not back to `marked`.
13 changes: 13 additions & 0 deletions docs/adr/0002-pierre-diffs-renderer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# ADR 0002 — Adopt @pierre/diffs as the diff renderer

- **Status**: Accepted (2026-07-19)
- **Context**: The workbench surfaces code changes from agent runs (file-level diffs from `bash`/`edit`/`write` tools). The change surface needs a renderer that handles large files, performs syntax highlighting consistently with code blocks, and supports future merge-conflict UI. The repo's research note (`2026-06-26 - Spanner Layer - Search, Syntax, Markdown, Diff.md`) ranked candidates: `@pierre/diffs`, `react-diff-viewer`, `react-virtualized-diff`, raw `jsdiff`. Pierre was the recommended primary — same engine used by Claude Code, Codex, Cursor, and OpenChamber — and pairs naturally with `shiki` for syntax highlighting. The earlier "Pidex - Pi Ecosystem Technology Stack Research" note (`2026-05-31`) flagged that `openchamber` already uses `@pierre/diffs 1.1.0-beta.13`.

- **Decision**: Adopt `@pierre/diffs` as the primary diff engine. Wrap its React `<PatchDiff>` component in `<PierreDiff>` to provide a small API (`oldFile`, `newFile`, `filename`) and a unified-patch generator. Use `theme: 'pierre-dark'` to match the OpenPi palette.

- **Consequences**:
- Diff renderer scales to 10k+ lines via per-line virtualisation in Shadow DOM.
- Same engine as OpenChamber / Claude Code / Codex / Cursor — visual parity for shared screenshots.
- Adds ~1MB of language packs to the bundle; acceptable for an agent workbench, with `chunks larger than 500kB` warning expected until we code-split Shiki languages.
- Plumbs into future "review session" views where merge-conflict UI will reuse the same renderer.
- Until file content plumbed from the main process, the renderer ships a `Fig. 02 | Pierre diff probe` sample in the empty state.
12 changes: 12 additions & 0 deletions docs/adr/0003-pretext-measurement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# ADR 0003 — Use @chenglou/pretext for text measurement only

- **Status**: Accepted (2026-07-19)
- **Context**: The workbench renders long session timelines (10k+ messages in long sessions). The renderer needs a way to know row heights for the status-bar measurement counter and, eventually, for virtualised row sizing before mounting DOM. `@chenglou/pretext` (Cheng Lou) provides Canvas-2D-based text measurement that is ~300× faster than DOM-based measurement for re-layout. The original OpenPi architecture (`docs/openpi-architecture.md`) called for Pretext specifically as a "layout dispatch" primitive.

- **Decision**: Use `@chenglou/pretext` strictly for text measurement — row counting for the status bar today, and (future) row heights for virtualised lists. Do not use it for glyph rendering; rely on `markdown-it` + browser text layout for that.

- **Consequences**:
- The status-bar `rows measured` counter is meaningful (it reflects an actual measurement, not a wall-clock approximation).
- No need to maintain a parallel `getBoundingClientRect` measurement path.
- When the renderer virtualises the timeline, Pretext's `layoutWithLines` is already wired in and ready.
- Avoids the canvas-vs-DOM font metrics drift by only measuring, never rendering.
16 changes: 16 additions & 0 deletions docs/adr/0004-three-lane-testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# ADR 0004 — Three-lane testing with Vitest and Playwright

- **Status**: Accepted (2026-07-19)
- **Context**: The workbench has multiple execution surfaces — pure TS reducers (timeline), React components (markdown rendering, diff probe), and a live Electron process (spawning `pi` over RPC). A single test lane cannot cover all three cost-effectively: pure unit tests don't catch DOM regressions, DOM tests don't catch RPC contract drift, and Playwright-only is too slow for tight feedback. The previous PRs shipped code without tests; this ADR sets the lane model.

- **Decision**: Three lanes under Vitest projects plus a Playwright Electron runner:
- **Unit** — Node environment, covers pure functions (`timeline.ts` reducer, `sessions.ts` parser). Fast, deterministic, runs in CI on every PR.
- **Integration** — jsdom environment with React Testing Library, covers component output (`Markdown.tsx` rendering, escaping, lists, tables). Runs in CI on every PR.
- **E2E** — Node environment with Playwright Electron, drives the full app against a real `pi` subprocess. Runs on `macos-latest` runners only (Electron binary shape) and is manual (`workflow_dispatch`) because the `pi` runtime requires user OAuth.

- **Consequences**:
- PR feedback loop is fast (unit + integration complete in seconds).
- E2E is gated to manual trigger and macos to avoid flaky CI runs.
- Tests live under `tests/{unit,integration,e2e}/`; config files in `vitest.{unit,integration,e2e}.config.ts`.
- Coverage targets left intentionally loose while we stabilise the surface.
- Future: split E2E into contract tests (mock pi subprocess) plus one full-driver smoke test.
12 changes: 12 additions & 0 deletions docs/adr/0005-oxlint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# ADR 0005 — Use oxlint as the lint tool

- **Status**: Accepted (2026-07-19)
- **Context**: The repo had no lint gate. ESLint is the default but slow to install and configure, and many of its plugins don't work cleanly with React 19 + Vitest. The OpenPi project has a single-language (TypeScript) codebase and needs correctness guarantees, not style debates.

- **Decision**: Adopt `oxlint` for CI lint. Configure it via `.oxlintrc.json` with the `correctness` category as error and the rest allowed. Style formatting is left to manual conventions.

- **Consequences**:
- Lint runs in CI under a separate job so it doesn't block other jobs.
- Only correctness failures block merges; suspicious/style warnings are noise we don't have time to debate today.
- When we need formatting, we'll reach for `biome` (single binary, formatter + lint) — not ESLint + Prettier.
- The repo can add ESLint for a small set of rules if a specific class of bug emerges that oxlint doesn't catch.
13 changes: 13 additions & 0 deletions docs/adr/0006-pi-runtime-rpc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# ADR 0006 — Pi runtime integration via JSONL RPC

- **Status**: Accepted (2026-07-19)
- **Context**: OpenPi is an Electron workbench for the Pi coding agent. Pi supports four runtime modes (`interactive`, `print/JSON`, `RPC`, `SDK`). The renderer needs streaming events and prompt submission. Embedding the SDK in-process bloats the renderer with Node-only dependencies; interactive mode is TUI-only. RPC mode is the documented headless protocol — JSON lines over stdin/stdout, `prompt`/`get_state`/`get_messages`/`get_commands` commands, `agent_start`/`message_update`/`tool_execution_*` events.

- **Decision**: Spawn `pi --mode rpc` from the Electron main process per active session tab. Line-buffer stdout on `\n` only (no `readline` — the protocol forbids U+2028/U+2029 splitting). Forward events to the renderer over `webContents.send`. Re-emit Pi errors as `openpi:session:exit`. Validate `sessionFile` is under `~/.pi/agent/sessions` before resume.

- **Consequences**:
- Strong process boundary; renderer never sees Node APIs beyond the typed bridge.
- One Pi subprocess per session tab keeps event routing simple.
- Sessions killed on tab close, on app quit, on window-all-closed.
- The renderer is fully testable without Pi: `applyEvent` is a pure reducer over RPC events.
- Future: a single RPC supervisor for many concurrent sessions is possible (`new_session` rather than respawn); we haven't needed it.
13 changes: 13 additions & 0 deletions docs/adr/0007-native-tab-bar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# ADR 0007 — Native tab bar with hidden-inset title bar

- **Status**: Accepted (2026-07-19)
- **Context**: The workbench has a workspace/session model with multiple open sessions. macOS users expect Chrome-style native tabs (`NSWindow tabbingMode`) OR a custom drag region with native traffic-light controls. The original OpenPi brief calls for "few clicks, fast switching, high awareness" and a "workspace-first" mental model — both benefit from a tab strip at the top of the window.

- **Decision**: Use `titleBarStyle: 'hiddenInset'` on the `BrowserWindow`. Reserve `84px` of left padding on the renderer tab strip to clear the macOS traffic lights at the inset position. Mark the tab strip with `-webkit-app-region: drag` so the user can drag the window by the strip; mark every interactive child with `no-drag` so buttons remain clickable. Render the tab strip in-app with custom styles.

- **Consequences**:
- Native window controls without losing the dark app background under them.
- User drags the window by the tab strip area.
- Avoids native NSWindow tab bar (which would push our renderer design into macOS chrome conventions).
- Windows/Linux: the inset still works but traffic-light buttons don't render — the strip just feels like a normal toolbar.
- Pair with custom buttons always inside `no-drag` regions, never the strip itself.
13 changes: 13 additions & 0 deletions docs/adr/0008-pi-dev-design-system.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# ADR 0008 — Adopt pi.dev design system for visual language

- **Status**: Accepted (2026-07-19)
- **Context**: The workbench is a desktop wrapper around the Pi coding agent. The Pi product (pi.dev) has its own distinctive visual language: deep ink canvas `#0d1116`, moonstone text, tidal-blue accent, terracotta rust, Departure Mono labels, Commit Mono code, bracketed buttons `[ ACTION ]`, corner-bracketed figure frames, lowercase `›` and `$` shell prompts. Building a separate design system would feel disjointed.

- **Decision**: Mirror the pi.dev design system in OpenPi. Bundle `Commit Mono` (OFL) and `Departure Mono` (OFL) locally; fall back to system serifs/mono for the licensed Plantin MT Pro. Tokens live in `src/renderer/design-system/tokens.css` and components in `components.css`.

- **Consequences**:
- Screenshots from OpenPi feel like native Pi product surfaces.
- Designers familiar with Pi can transfer work without re-learning a design system.
- Token names match Pi's published names (`--bg-deep`, `--accent`, `--panel`) — easy to grep and update.
- Local font assets ship ~850KB; acceptable for desktop.
- No licensing risk (both bundled fonts are OFL; Plantin fallback uses system Georgia).
13 changes: 13 additions & 0 deletions docs/adr/0009-calver-releases.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# ADR 0009 — Calendar-versioned releases via GitHub Actions

- **Status**: Accepted (2026-07-19)
- **Context**: This is a personal, low-traffic workbench. Semver (X.Y.Z) imposes friction for solo projects where breaking changes are frequent and there's no public API contract. Pi's CLI uses semver; we don't.

- **Decision**: Adopt CalVer (`YYYY.0M.0D`) with a `-N` suffix for same-day re-releases. On push to `main`, the `release.yml` workflow computes the tag (e.g. `2026.07.19`, `2026.07.19-2`) and creates a GitHub release with auto-generated notes.

- **Consequences**:
- Predictable, sortable, machine-friendly tags.
- No "is this a breaking change" ceremony — every push to main tags a release.
- Dropped `release-please` (semver, PR-based) for a small workflow that just calls `gh release create --generate-notes`.
- Inconsistent with Pi CLI semver — keep documentation clear that OpenPi tags are CalVer.
- If we ever publish a public API, we revisit and add semver alongside.
24 changes: 24 additions & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Architecture Decision Records

ADRs document significant architectural choices made in OpenPi.

Each ADR follows this shape:

- **Status** — Accepted / Superseded / Deprecated
- **Context** — What problem we faced and the options we considered
- **Decision** — What we chose
- **Consequences** — Trade-offs, costs, and follow-up work

## Index

| # | Decision | Status |
|---|----------|--------|
| [0001](0001-markdown-it-for-transcript.md) | Use markdown-it for transcript rendering | Accepted |
| [0002](0002-pierre-diffs-renderer.md) | Adopt @pierre/diffs as the diff renderer | Accepted |
| [0003](0003-pretext-measurement.md) | Use @chenglou/pretext for text measurement only | Accepted |
| [0004](0004-three-lane-testing.md) | Three-lane testing with Vitest and Playwright | Accepted |
| [0005](0005-oxlint.md) | Use oxlint as the lint tool | Accepted |
| [0006](0006-pi-runtime-rpc.md) | Pi runtime integration via JSONL RPC | Accepted |
| [0007](0007-native-tab-bar.md) | Native tab bar with hidden-inset title bar | Accepted |
| [0008](0008-pi-dev-design-system.md) | Adopt pi.dev design system for visual language | Accepted |
| [0009](0009-calver-releases.md) | Calendar-versioned releases via GitHub Actions | Accepted |
18 changes: 17 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,38 @@
"scripts": {
"dev": "mise run dev",
"build": "mise run build",
"start": "mise run start"
"start": "mise run start",
"lint": "oxlint src tests",
"typecheck": "bunx tsc -p tsconfig.json --noEmit && bunx tsc -p tsconfig.main.json --noEmit && bunx tsc -p tsconfig.preload.json --noEmit",
"test": "mise run test",
"test:unit": "bunx vitest run --project unit",
"test:integration": "bunx vitest run --project integration",
"test:e2e": "bunx vitest run --config vitest.e2e.config.ts",
"test:all": "bunx vitest run"
},
"dependencies": {
"@chenglou/pretext": "^0.0.6",
"@pierre/diffs": "^1.2.12",
"markdown-it": "^14.3.0",
"react": "^19.2.0",
"react-dom": "^19.2.0"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@types/markdown-it": "^14.1.2",
"@types/node": "^26.1.1",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^6.0.0",
"concurrently": "^9.1.0",
"electron": "^41.0.0",
"jsdom": "^29.1.1",
"oxlint": "^1.74.0",
"playwright-core": "^1.61.1",
"typescript": "^6.0.0",
"vite": "^8.0.0",
"vitest": "^4.1.10",
"wait-on": "^8.0.0"
}
}
Loading
Loading