Skip to content
Open
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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,11 @@ playwright-report/
companions/*
!companions/.gitkeep

# Exception: companions/desktop is a real, committed source tree (the
# OpenAgentic Tauri desktop companion app), not transient vendor-staged docs
# source like the other companions/<name> dirs this block targets.
!companions/desktop/

# Local scratch / brainstorming artifacts
.superpowers/

Expand Down
10 changes: 10 additions & 0 deletions companions/desktop/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
node_modules/
dist/
src-tauri/target/
src-tauri/binaries/
src-tauri/cli-runtime/
src-tauri/gen/

# The repo-root .gitignore ignores *.png globally (screenshot dumps); the
# generated Tauri app icons under src-tauri/icons/ must be committed.
!src-tauri/icons/**
1 change: 1 addition & 0 deletions companions/desktop/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
shamefully-hoist=false
155 changes: 155 additions & 0 deletions companions/desktop/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# openagentic-desktop

The OpenAgentic desktop companion — a [Tauri](https://tauri.app) shell around
the chat, code, and flows surfaces. Standalone pnpm app; not part of the root
workspace (has its own `pnpm-lock.yaml`).

## Develop

```bash
pnpm install
pnpm tauri dev
```

## Build

```bash
# preflight: fails fast if the (gitignored) agenticode sidecar isn't staged —
# on a fresh clone run scripts/stage-agenticode.sh first. Not wired into
# tauri.conf's beforeBuildCommand on purpose (it would also gate `pnpm dev`).
./scripts/check-sidecar.sh

pnpm tauri build --bundles app
```

Produces `src-tauri/target/release/bundle/macos/OpenAgentic.app`.

## Test

```bash
pnpm test
```

## Layout

- `src/` — React frontend (Vite + Tailwind v4).
- `src/host/` — the Host seam (`getHost()`): one interface over everything
that differs between the Tauri webview and a plain browser tab (HTTP
transport, keychain, pickers, code child, workspace bridge). Tauri host
uses `@tauri-apps/plugin-http` fetch + the Rust `secret_*` keychain
commands; browser host uses `window.fetch` (dev proxy) + a localStorage
fallback (dev only).
- `src/stores/appStore.ts` — Zustand store driving the surface switch
(`chat | code | flows | settings`).
- `src/stores/authStore.ts` — auth state machine: `signIn` = login →
mint durable `oa_` API key → validate → persist key+profile via the host
keychain; `restore()` boots from the keychain; `signOut()` forgets it.
- `src/styles/tokens.css` — design tokens mirroring
`services/openagentic-ui/src/styles/theme.css`.
- `src-tauri/` — Rust shell (window, plugins, tray, vibrancy).
`src-tauri/src/keychain.rs` holds the `secret_get/set/delete` commands
(service name `io.openagentics.desktop`).

## Streaming path (Task 5, Step 5 verdict)

NDJSON response streaming inside the Tauri webview goes through
`@tauri-apps/plugin-http`'s `fetch` and was verified to arrive
**incrementally** (per-frame deltas of ~5–13 ms during token emission, with
a `ttft` marker mid-stream), not as one end-of-stream burst. The
`stream_fetch.rs` Rust fallback described in the task brief was therefore
**not needed** and is not implemented.

To re-verify unattended (signs in, streams one message, logs per-frame
arrival timings to the `tauri dev` terminal via plugin-log):

```bash
VITE_STREAM_PROBE=1 \
VITE_PROBE_URL=http://localhost:8000 \
VITE_PROBE_EMAIL=<admin email> \
VITE_PROBE_PASSWORD=<password> \
pnpm tauri dev
```

The same probe is available interactively under Settings → Diagnostics →
"Stream probe".

Two platform-side gotchas found during verification:

- **CORS**: the webview sends `Origin: http://localhost:1430` under
`tauri dev` (and `tauri://localhost` in production builds). The platform
api's default allowlist (`server.ts` `allowedOrigins`) does not include
these — set `ALLOWED_ORIGINS` on the api to include them.
- **Abort semantics**: plugin-http rejects an aborted request with a plain
`"Request cancelled"` error, not a DOMException named `AbortError` —
don't gate abort handling on `err.name === "AbortError"` alone.

## agenticode sidecar — staging + spawn (Task 9)

The code surface drives the **agenticode CLI** (openagentic edition) as a
Tauri **plugin-shell sidecar**. The CLI is a proprietary binary and is
**never committed** — both staging destinations are gitignored:
`src-tauri/binaries/` (the sidecar) and `src-tauri/cli-runtime/` (a portable
`rg`).

### Staging the binary

```bash
# needs an agenticode checkout (default ~/git/agenticode) + bun on PATH
AGENTICODE_DIR=~/git/agenticode ./scripts/stage-agenticode.sh
```

`stage-agenticode.sh` compiles the **openagentic edition** for the host triple
(`bun build --compile … --define process.env.AGENTICODE_EDITION="openagentic"`)
into `src-tauri/binaries/agenticode-cli-<triple>`, and stages a portable
ripgrep into `src-tauri/cli-runtime/rg`. It does **not** call agenticode's
`scripts/bundle.sh` — that script's positional arg is an OUTDIR (not a target)
and it builds all five OS/arch targets; we mirror the single-target
`bun build --compile` from the agenticode desktop README instead, adding the
edition `--define` and the externals bundle.sh skips. If the checkout is
missing the script exits 2 with instructions.

The prebuilt CLI checked into `agenticode/desktop/…/binaries` is the **default**
(multi-provider) edition — do not use it; the openagentic edition is
provider-locked to a local instance and must be built fresh.

### Spawn wiring

`tauriHost.spawnCode({ cwd, instanceUrl, apiKey })` launches the sidecar via
`Command.sidecar('binaries/agenticode-cli', args, { cwd, env, encoding: 'raw' })`:

- **args** (fixed): `--print --verbose --output-format stream-json
--input-format stream-json --include-partial-messages --replay-user-messages
--permission-prompt-tool stdio`.
- **env**: `AGENTICWORK_BASE_URL=<instanceUrl>/api/v1`,
`AGENTICWORK_API_KEY=<oa_ key>`, and `PATH=<cli-runtime>:<app PATH>`. The
rg dir is resolved with `resolveResource('cli-runtime')`; the base PATH comes
from the Rust `code_spawn_base_path` command (`src-tauri/src/code_spawn.rs`),
which returns the app's PATH with Homebrew/local-bin promoted (a
GUI-launched macOS app inherits a stripped launchd PATH). Pure env assembly +
the fixed args live in `src/host/codeSpawn.ts` (unit-tested).
- The bun-compiled CLI can't exec its **own** embedded ripgrep (it lives in
bunfs with no on-disk path), so a real `rg` on PATH is mandatory for the
Grep/Glob tools.
- `CodeChild.onLine` reassembles raw stdout (`encoding: 'raw'` → `Uint8Array`
chunks that are **not** line-aligned) into whole NDJSON frames via a
carry-buffer splitter (`createLineSplitter`). stderr is drained into a
bounded ring buffer exposed as `stderrTail()`.

Capabilities: `src-tauri/capabilities/default.json` grants `shell:allow-spawn`
scoped to the `binaries/agenticode-cli` sidecar with the exact arg vector, plus
`shell:allow-stdin-write` and `shell:allow-kill`.

### Live-spawn findings (openagentic edition ↔ this api)

Spawning the staged binary outside the app against the live api
(`http://localhost:8000`) confirmed the edition lock (a non-loopback
`AGENTICWORK_BASE_URL` is refused with the edition message), the base-URL
attach, and that the API key authenticates (models fetches return 200, no 401).
The CLI does **not** complete a headless turn against this api build, though:
its openagentic edition targets the compose UI shim (default
`http://localhost:8080/api/v1`) and its model-catalog fetch doesn't populate
from this api's `/api/v1/models` response shape (with base `…/api/v1` it even
requests `…/api/v1/v1/models` → 404), so no model resolves and the turn — and
thus the `system`/`init` frame — is skipped. Populating the catalog / selecting
a model is the code-surface UI's job (a later task); the spawn plumbing here is
edition-, auth-, and base-URL-correct up to that hand-off.
25 changes: 25 additions & 0 deletions companions/desktop/app-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions companions/desktop/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OpenAgentic</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
42 changes: 42 additions & 0 deletions companions/desktop/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "openagentic-desktop",
"private": true,
"version": "0.1.0",
"type": "module",
"packageManager": "pnpm@10.15.0",
"description": "OpenAgentic desktop companion — Tauri shell for chat, code, and flows.",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"test": "vitest run",
"tauri": "tauri"
},
"dependencies": {
"@tauri-apps/api": "^2.2.0",
"@tauri-apps/plugin-dialog": "^2.2.0",
"@tauri-apps/plugin-http": "^2.2.0",
"@tauri-apps/plugin-log": "^2.2.0",
"@tauri-apps/plugin-os": "^2.2.0",
"@tauri-apps/plugin-shell": "^2.2.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-markdown": "^10.1.0",
"rehype-highlight": "^7.0.2",
"remark-gfm": "^4.0.1",
"zustand": "^5.0.3"
},
"devDependencies": {
"@tailwindcss/vite": "^4.0.0",
"@tauri-apps/cli": "^2.2.7",
"@testing-library/react": "^16.1.0",
"@types/node": "^22.10.7",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@vitejs/plugin-react": "^4.3.4",
"happy-dom": "^16.5.3",
"tailwindcss": "^4.0.0",
"typescript": "^5.7.3",
"vite": "^6.0.7",
"vitest": "^3.0.4"
}
}
Loading
Loading