From 62ba072b2618b84ecca9351bd38d779eff39ccb1 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Wed, 19 Aug 2026 20:04:35 +0300 Subject: [PATCH] refactor(ai): ship Harness as optional companion (#561) * Revert "feat(ai): add HarnessAgent sidecar foundation (#560)" This reverts commit 83a5ea1b428748e1c52287ff3c10ac32449243bf. * Revert "Revert "feat(ai): add HarnessAgent sidecar foundation (#560)"" This reverts commit 0d8c03515888c62dc47186d4a3b0b7b04e78f8af. * refactor(ai): ship Harness as optional companion * fix(ai): support Harness companion on Windows * test(ai): restore navigator after Harness Windows test --- .github/workflows/build.yml | 10 ++--- .github/workflows/ci.yml | 3 -- AGENTS.md | 4 +- README.md | 2 + desktop/capabilities/default.json | 2 +- desktop/tauri.conf.json | 5 +-- package.json | 1 - packages/harness/README.md | 4 +- packages/harness/package.json | 16 +++++++- src/app/ai/harness/process.ts | 5 ++- tests/engine/tauri/harness-process.test.ts | 33 ++++++++++++++- tools/harness-sidecar/package.json | 5 --- tools/harness-sidecar/src/build.ts | 41 ------------------- tools/release-packages/src/publish-dirs.ts | 1 + .../tests/publish-dirs.test.ts | 1 + 15 files changed, 63 insertions(+), 70 deletions(-) delete mode 100644 tools/harness-sidecar/package.json delete mode 100644 tools/harness-sidecar/src/build.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9ca5fc311..117c9c37b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -53,10 +53,6 @@ jobs: - uses: ./.github/actions/setup-bun - - name: Build Harness sidecar - shell: bash - run: bun run build:harness-sidecar -- ${{ matrix.target }} - - name: Extract release notes if: startsWith(github.ref, 'refs/tags/v') id: changelog @@ -110,7 +106,7 @@ jobs: - name: Build packages for publish run: | - publish_packages="packages/scene-graph packages/pen packages/kiwi packages/fig packages/core packages/dom-css packages/cli packages/mcp packages/vue" + publish_packages="packages/scene-graph packages/pen packages/kiwi packages/fig packages/core packages/dom-css packages/cli packages/mcp packages/harness packages/vue" for dir in $publish_packages; do if [ -f "$dir/package.json" ] && node -p "require('./$dir/package.json').scripts?.build" 2>/dev/null | grep -qv undefined; then echo "Building $dir..." @@ -138,7 +134,7 @@ jobs: fi } root_dir="$PWD" - publish_packages="packages/scene-graph packages/pen packages/kiwi packages/fig packages/core packages/dom-css packages/cli packages/mcp packages/vue" + publish_packages="packages/scene-graph packages/pen packages/kiwi packages/fig packages/core packages/dom-css packages/cli packages/mcp packages/harness packages/vue" for dir in $publish_packages; do pack_if_new "$dir" done @@ -163,7 +159,7 @@ jobs: npm publish "./.npm-packages/${tarball}" --access public --provenance fi } - publish_packages="packages/scene-graph packages/pen packages/kiwi packages/fig packages/core packages/dom-css packages/cli packages/mcp packages/vue" + publish_packages="packages/scene-graph packages/pen packages/kiwi packages/fig packages/core packages/dom-css packages/cli packages/mcp packages/harness packages/vue" for dir in $publish_packages; do publish_if_new "$dir" done diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c55fd2d9..d2c941ce1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -135,9 +135,6 @@ jobs: - uses: ./.github/actions/setup-bun - - name: Build Harness sidecar - run: bun run build:harness-sidecar -- x86_64-unknown-linux-gnu - - name: Typecheck native interaction tests run: bun run check:native-test diff --git a/AGENTS.md b/AGENTS.md index 2b2ca3131..81f0d7c3a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,7 +17,7 @@ Bun workspace packages: - `packages/vue` — `@open-pencil/vue`: headless Vue 3 SDK (Reka UI-style) for building custom OpenPencil-powered editor shells and embedded editing surfaces. Renderless components and composables. The app is one consumer of the SDK. - `packages/cli` — `@open-pencil/cli`: headless CLI for .fig inspection, export, linting. Uses `citty` + `agentfmt`. - `packages/mcp` — `@open-pencil/mcp`: MCP server for AI coding tools. Stdio + HTTP (Hono). Reuses core tools. -- `packages/harness` — `@open-pencil/harness`: private Node sidecar runtime for backend-neutral coding-agent sessions. Owns HarnessAgent adapters, opaque resume-state persistence, and the JSONL host protocol; app/editor integration remains outside the package. +- `packages/harness` — `@open-pencil/harness`: optional Node companion CLI for backend-neutral coding-agent sessions. Owns HarnessAgent adapters, opaque resume-state persistence, and the JSONL host protocol; the desktop detects and launches the separately installed command instead of bundling a JavaScript runtime. - `packages/docs` — `@open-pencil/docs`: published VitePress documentation site. Use `bun run docs:dev` for authoring, `bun run docs:build` for the default fast local render check without minification or generated LLM files, and `bun run docs:build:production` for the complete deployment output. The root app (`src/`) is the Tauri/Vite desktop editor. App-specific editor, document, AI, collaboration, shell, tabs, demo, and automation code lives under `src/app/*`. The app consumes scene graph primitives from `@open-pencil/scene-graph`, editor/rendering services through targeted `@open-pencil/core` subpath exports, and `@open-pencil/vue` through the public Vue SDK entrypoint. @@ -153,7 +153,7 @@ Release commits are the exception: keep using `Release v0.x.y`. Keep this section light; implementation details move often. -- Harness-based coding agents live in the private `packages/harness` Node sidecar package rather than the browser app. Keep its session service backend-neutral, persist only opaque non-secret resume state, and expose host integration through its bounded JSONL protocol. Pi may use local `just-bash` for development, but that in-memory sandbox does not provide process-restart recovery. +- Harness-based coding agents live in the optional `@open-pencil/harness` Node companion rather than the browser app. Keep its session service backend-neutral, persist only opaque non-secret resume state, and expose host integration through its bounded JSONL protocol. Do not bundle a JavaScript runtime into Tauri; launch the separately installed `openpencil-harness` command. Pi may use local `just-bash`, but that in-memory sandbox does not provide process-restart recovery. - ACP UI/transport lives under `src/app/ai/acp/**`; provider definitions live in `packages/core/src/constants.ts`; app prompts live under `src/app/ai/**`. Direct model configuration lives under `src/app/ai/models/**`: reusable profiles reference provider connections, roles resolve to profiles, and runtime creation resolves credentials lazily. Keep model profiles, provider connections, and role assignments separate rather than returning to singleton provider/model settings. Public docs: `packages/docs/programmable/ai-chat.md` and `packages/docs/programmable/mcp-server.md`. - ACP transport uses Tauri shell permissions, so check `desktop/capabilities/**` when changing agent launch behavior. - Collaboration lives under `src/app/collab/**` and is documented in `packages/docs/programmable/collaboration.md`. It uses Trystero + Yjs + awareness; preserve crypto-safe room IDs and peer cleanup semantics when changing it. diff --git a/README.md b/README.md index c75e9d115..af82548f5 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,8 @@ Not every provider works in the browser, and not every model streams tool calls Use Claude Code, Codex, or Gemini CLI directly in the chat panel. The agent connects to the editor's MCP server and uses all 100+ design tools. Requires the desktop app and the agent CLI installed locally. +Pi is also available as an optional AI SDK Harness provider. Install its companion CLI with `npm install -g @open-pencil/harness`, then add a **Pi** model profile in **Settings → AI & agents**. The companion is installed separately so OpenPencil does not bundle a JavaScript runtime for users who do not enable Harness providers. + **Setup (Claude Code):** 1. Install the ACP adapter: `npm install -g @agentclientprotocol/claude-agent-acp` diff --git a/desktop/capabilities/default.json b/desktop/capabilities/default.json index ab8f43e17..109b7eeef 100644 --- a/desktop/capabilities/default.json +++ b/desktop/capabilities/default.json @@ -60,7 +60,7 @@ { "name": "codex-acp", "cmd": "codex-acp", "args": true }, { "name": "gemini", "cmd": "gemini", "args": true }, { "name": "openpencil-mcp-http", "cmd": "openpencil-mcp-http", "args": true }, - { "name": "binaries/openpencil-harness", "sidecar": true, "args": false }, + { "name": "openpencil-harness", "cmd": "openpencil-harness", "args": false }, { "name": "cmd", "cmd": "cmd", "args": true } ] }, diff --git a/desktop/tauri.conf.json b/desktop/tauri.conf.json index c8c8317e9..1497a4e28 100644 --- a/desktop/tauri.conf.json +++ b/desktop/tauri.conf.json @@ -4,9 +4,9 @@ "version": "0.14.0", "identifier": "net.dannote.open-pencil", "build": { - "beforeDevCommand": "bun run generate:tauri-menu && bun run build:harness-sidecar && bun run dev", + "beforeDevCommand": "bun run generate:tauri-menu && bun run dev", "devUrl": "http://localhost:1420", - "beforeBuildCommand": "bun run generate:tauri-menu && bun run build:harness-sidecar && bun run build", + "beforeBuildCommand": "bun run generate:tauri-menu && bun run build", "frontendDist": "../dist" }, "app": { @@ -26,7 +26,6 @@ "active": true, "targets": "all", "createUpdaterArtifacts": true, - "externalBin": ["binaries/openpencil-harness"], "icon": [ "icons/32x32.png", "icons/128x128.png", diff --git a/package.json b/package.json index ae97f3d4e..21ba731c8 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,6 @@ "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", "tauri": "tauri", - "build:harness-sidecar": "bun tools/harness-sidecar/src/build.ts", "build:native-test": "bun run build:packages && bun tauri build --debug --no-bundle --features native-test --config desktop/tauri.native-test.conf.json", "test:native": "bun run build:native-test && wdio run wdio.conf.ts", "lint": "bun run lint:structure && oxlint -c oxlint.json --type-aware --type-check src/ packages/scene-graph/src/ packages/core/src/ packages/vue/src/ packages/cli/src/ packages/mcp/src/ packages/harness/src/ packages/dom-css/src/ packages/pen/src/ packages/kiwi/src/ packages/fig/src/", diff --git a/packages/harness/README.md b/packages/harness/README.md index 16a52e901..b24e6cea3 100644 --- a/packages/harness/README.md +++ b/packages/harness/README.md @@ -1,6 +1,6 @@ # @open-pencil/harness -Private Node runtime for coding-agent harness sessions. It owns the backend-neutral session lifecycle, opaque resume-state persistence, and the JSONL sidecar protocol used by host applications. +Optional Node companion runtime for coding-agent harness sessions. It owns the backend-neutral session lifecycle, opaque resume-state persistence, and the JSONL sidecar protocol used by host applications. The first backend uses AI SDK `HarnessAgent`, Pi, and local `just-bash`. Pi runs in the Node host process; `just-bash` provides an isolated in-memory workspace and shell without requiring cloud infrastructure. @@ -11,7 +11,7 @@ The first backend uses AI SDK `HarnessAgent`, Pi, and local `just-bash`. Pi runs - JSONL stdio sidecar transport. - Pi + `just-bash` backend. -The package is not wired into the desktop application yet. Credentials are supplied to the sidecar process at runtime and are never written to resume-state storage. +The package is installed as an optional companion CLI for the desktop application. It is not bundled into every Tauri build; install `@open-pencil/harness` globally to make the `openpencil-harness` command available. Credentials are supplied to the companion process at runtime and are never written to resume-state storage. ## Local sandbox limitation diff --git a/packages/harness/package.json b/packages/harness/package.json index 00d7e2c60..a457de024 100644 --- a/packages/harness/package.json +++ b/packages/harness/package.json @@ -1,7 +1,9 @@ { "name": "@open-pencil/harness", - "version": "0.0.0", - "private": true, + "version": "0.14.0", + "private": false, + "license": "MIT", + "description": "Optional OpenPencil companion CLI for coding-agent Harness sessions", "type": "module", "exports": { ".": { @@ -20,6 +22,16 @@ "test": "bun test tests", "typecheck": "bunx tsc --noEmit -p tsconfig.json" }, + "repository": { + "type": "git", + "url": "git+https://github.com/open-pencil/open-pencil.git", + "directory": "packages/harness" + }, + "publishConfig": { + "access": "public", + "provenance": true + }, + "files": ["dist", "README.md"], "dependencies": { "@ai-sdk/harness": "1.0.74", "@ai-sdk/harness-pi": "1.0.76", diff --git a/src/app/ai/harness/process.ts b/src/app/ai/harness/process.ts index 662d950db..11821bf0a 100644 --- a/src/app/ai/harness/process.ts +++ b/src/app/ai/harness/process.ts @@ -1,5 +1,7 @@ import type { HarnessSidecarMessage } from '@open-pencil/harness' +import { resolvePlatformCommand } from '@/app/tauri/command' + export type HarnessChild = { write(data: number[]): Promise kill(): Promise @@ -16,7 +18,8 @@ export async function spawnHarnessProcess(options: { onUnexpectedClose: () => void }): Promise { const { Command } = await import('@tauri-apps/plugin-shell') - const command = Command.sidecar('binaries/openpencil-harness', [], { + const resolved = resolvePlatformCommand('openpencil-harness') + const command = Command.create(resolved.command, resolved.args, { encoding: 'raw', env: options.environment }) diff --git a/tests/engine/tauri/harness-process.test.ts b/tests/engine/tauri/harness-process.test.ts index 0c55cee8e..d48d195de 100644 --- a/tests/engine/tauri/harness-process.test.ts +++ b/tests/engine/tauri/harness-process.test.ts @@ -9,14 +9,14 @@ afterEach(async () => { }) describe('Harness sidecar process', () => { - test('spawns the bundled sidecar and keeps credentials in process environment', async () => { + test('spawns the optional companion and keeps credentials in process environment', async () => { let onEvent: ((event: unknown) => void) | undefined const calls: Array<{ cmd: string; args: unknown }> = [] await mockTauriIPC((cmd, args) => { calls.push({ cmd, args }) if (cmd === 'plugin:shell|spawn') { expect(args).toMatchObject({ - program: expect.stringContaining('openpencil-harness'), + program: 'openpencil-harness', args: [], options: { encoding: 'raw', @@ -51,4 +51,33 @@ describe('Harness sidecar process', () => { JSON.stringify(calls.find((call) => call.cmd === 'plugin:shell|stdin_write')) ).not.toContain('secret') }) + + test('routes the npm launcher through cmd on Windows', async () => { + const originalNavigator = Object.getOwnPropertyDescriptor(globalThis, 'navigator') + try { + Object.defineProperty(globalThis, 'navigator', { + configurable: true, + value: { userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' } + }) + await mockTauriIPC((cmd, args) => { + if (cmd === 'plugin:shell|spawn') { + expect(args).toMatchObject({ + program: 'cmd', + args: ['/c', 'openpencil-harness'], + options: { encoding: 'raw', env: {} } + }) + return 46 + } + return null + }) + const process = await spawnHarnessProcess({ + environment: {}, + onUnexpectedClose: () => undefined + }) + await process.child.kill() + } finally { + if (originalNavigator) Object.defineProperty(globalThis, 'navigator', originalNavigator) + else Reflect.deleteProperty(globalThis, 'navigator') + } + }) }) diff --git a/tools/harness-sidecar/package.json b/tools/harness-sidecar/package.json deleted file mode 100644 index 226628e67..000000000 --- a/tools/harness-sidecar/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "@open-pencil/harness-sidecar-tools", - "private": true, - "type": "module" -} diff --git a/tools/harness-sidecar/src/build.ts b/tools/harness-sidecar/src/build.ts deleted file mode 100644 index 5c8a94456..000000000 --- a/tools/harness-sidecar/src/build.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { execFileSync } from 'node:child_process' -import { chmod, mkdir } from 'node:fs/promises' -import { join } from 'node:path' - -const hostTarget = (): string => { - if (process.platform === 'darwin') - return process.arch === 'arm64' ? 'aarch64-apple-darwin' : 'x86_64-apple-darwin' - if (process.platform === 'linux' && process.arch === 'x64') return 'x86_64-unknown-linux-gnu' - if (process.platform === 'win32') { - return process.arch === 'arm64' ? 'aarch64-pc-windows-msvc' : 'x86_64-pc-windows-msvc' - } - throw new Error(`Unsupported Harness sidecar host: ${process.platform}/${process.arch}`) -} - -const target = process.argv[2] ?? process.env.TAURI_ENV_TARGET_TRIPLE ?? hostTarget() -const bunTargetByRustTarget: Record = { - 'aarch64-apple-darwin': 'bun-darwin-arm64', - 'x86_64-apple-darwin': 'bun-darwin-x64', - 'x86_64-unknown-linux-gnu': 'bun-linux-x64', - 'x86_64-pc-windows-msvc': 'bun-windows-x64', - 'aarch64-pc-windows-msvc': 'bun-windows-arm64' -} -const bunTarget = bunTargetByRustTarget[target] -if (!bunTarget) throw new Error(`Unsupported Harness sidecar target: ${target}`) - -const extension = target.includes('windows') ? '.exe' : '' -const output = join('desktop', 'binaries', `openpencil-harness-${target}${extension}`) -await mkdir(join('desktop', 'binaries'), { recursive: true }) -execFileSync( - 'bun', - [ - 'build', - '--compile', - `--target=${bunTarget}`, - 'packages/harness/src/stdio.ts', - '--outfile', - output - ], - { stdio: 'inherit' } -) -if (!extension) await chmod(output, 0o755) diff --git a/tools/release-packages/src/publish-dirs.ts b/tools/release-packages/src/publish-dirs.ts index 74d2f5688..5d2996da8 100644 --- a/tools/release-packages/src/publish-dirs.ts +++ b/tools/release-packages/src/publish-dirs.ts @@ -35,6 +35,7 @@ export const DEFAULT_PACKAGES: PackagePublishConfig[] = [ { dir: 'packages/dom-css', include: ['dist'], extraFiles: ['README.md'] }, { dir: 'packages/cli', include: ['bin', 'dist'], extraFiles: [] }, { dir: 'packages/mcp', include: ['dist'], extraFiles: [] }, + { dir: 'packages/harness', include: ['dist'], extraFiles: ['README.md'] }, { dir: 'packages/vue', include: ['dist'], extraFiles: ['README.md'] } ] diff --git a/tools/release-packages/tests/publish-dirs.test.ts b/tools/release-packages/tests/publish-dirs.test.ts index 2d8f69cf7..ccf314a33 100644 --- a/tools/release-packages/tests/publish-dirs.test.ts +++ b/tools/release-packages/tests/publish-dirs.test.ts @@ -63,6 +63,7 @@ describe('DEFAULT_PACKAGES', () => { 'packages/dom-css', 'packages/cli', 'packages/mcp', + 'packages/harness', 'packages/vue' ]) })