Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 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
103 changes: 103 additions & 0 deletions ui/goose2/acp-plus-migration-plan/00-overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# ACP-Plus Migration Plan: Overview

## Goal

Move all ACP protocol handling from the Rust Tauri backend into the TypeScript/WebView layer, so the frontend communicates directly with `goose serve` over WebSocket. The Rust layer shrinks to a thin native shell responsible only for:

1. Spawning and managing the `goose serve` child process
2. Providing the server URL to the frontend
3. Window management / OS integration

Long-term, config, personas, skills, projects, git, doctor, and all other native operations will also move behind `goose serve` ACP extension methods — eliminating the Rust middleware entirely.

## Current Architecture

```
Frontend (TS)
→ invoke("acp_send_message") [Tauri IPC]
→ GooseAcpManager [Rust singleton, dedicated thread]
→ ClientSideConnection [Rust ACP client over WebSocket]
→ goose serve ws://127.0.0.1:<port>/acp [child process]
← SessionNotification [ACP callback in Rust]
← TauriMessageWriter [emits Tauri events]
← listen("acp:text", ...) [Tauri event bus]
→ Zustand store updates
```

## Target Architecture (Phase A)

```
Frontend (TS)
→ GooseClient (WebSocket)
→ goose serve ws://127.0.0.1:<port>/acp [child process]
← Client callbacks → direct Zustand store updates

Tauri Rust shell:
- Spawn goose serve, expose URL
- Config/personas/skills/projects/git/doctor (temporary — Phase B removes these)
- Window management
```

## Target Architecture (Phase B — Long-Term)

```
Frontend (TS)
→ GooseClient (WebSocket)
→ goose serve ws://127.0.0.1:<port>/acp
← Client callbacks → direct Zustand store updates

Tauri Rust shell (~200 lines):
- Spawn goose serve, expose URL
- Window management
```

## Steps

| Step | File | Summary |
|------|------|---------|
| 01 | `01-expose-goose-serve-url.md` | Add Tauri command to expose the `goose serve` WebSocket URL to the frontend |
| 02 | `02-add-acp-npm-dependencies.md` | Add `@aaif/goose-acp` and `@agentclientprotocol/sdk` to goose2 |
| 03 | `03-create-ts-acp-connection.md` | Create the singleton TypeScript ACP connection manager (WebSocket transport), reconnection logic, and feature flag |
| 04 | `04-create-ts-notification-handler.md` | Port the Rust `SessionEventDispatcher` to TypeScript |
| 05 | `05-create-ts-session-manager.md` | Port session state management and ACP operations to TypeScript |
| 06 | `06-port-session-search.md` | Port session content search from Rust to TypeScript |
| 07 | `07-rewire-shared-api-acp.md` | Replace `invoke()` wrappers in `src/shared/api/acp.ts` with direct TS ACP calls |
| 08 | `08-rewire-hooks.md` | Remove `useAcpStream`, update `useChat`, `useAppStartup`, `AppShell` |
| 09 | `09-delete-rust-acp-code.md` | Delete the Rust ACP middleware and unused dependencies |
| 10 | `10-phase-b-future-native-migration.md` | Plan for moving config/personas/skills/projects/git/doctor to `goose serve` |

## Ordering & Dependencies

```
01 ──┐
├──→ 03 ──→ 04 ──→ 05 ──→ 07 ──→ 08 ──→ 09
02 ──┘ │
└──→ 06 ──→ 07
```

- Steps 01 and 02 are independent and can be done in parallel.
- Steps 03–06 build on each other, though 06 can proceed in parallel with 04/05.
- Step 07 wires everything together.
- Step 08 removes the old Tauri event listeners.
- Step 09 is cleanup — only after everything works.
- Step 10 is the Phase B roadmap.

## Key Decisions

1. **WebSocket transport.** `goose serve` exposes a WebSocket endpoint at `/acp`. Each WS text frame is a single JSON-RPC message. This is the same transport the Rust layer already uses — we are moving the WebSocket client from Rust to TypeScript. WebSocket provides true bidirectional streaming with lower overhead than HTTP+SSE.

2. **Direct store updates over event bus.** The notification handler calls Zustand store methods directly instead of emitting Tauri events. This eliminates a layer of indirection and the `useAcpStream` hook.

3. **Reuse `@aaif/goose-acp`.** Already used by `ui/desktop` (Electron) and `ui/text` (Ink TUI). Provides `GooseClient`, generated types, and Zod validators. A `createWebSocketStream` helper will be added (either in `@aaif/goose-acp` or locally in goose2) since the package currently only ships `createHttpStream`.

4. **Auto-approve permissions.** Same as the current Rust implementation — accept the first option on all `request_permission` callbacks.

## Risks & Mitigations

| Risk | Mitigation |
|------|------------|
| Tauri CSP blocks localhost WebSocket | CSP is already `null` (disabled) in `tauri.conf.json` |
| `goose serve` not ready when frontend initializes | Rust still does a readiness check; the URL command only resolves after the server is confirmed ready |
| WebSocket disconnection / reconnection | Implement reconnection logic in the connection manager; `GooseClient.closed` signals when the connection drops |
| Replay timing (notifications arriving after `loadSession` resolves) | Port the drain/stabilization logic from Rust, or rely on the `replay_complete` signal from the backend |
| Session state consistency during migration | Feature flag (`useDirectAcp` in `acpFeatureFlag.ts`) routes between old Tauri IPC and new WebSocket path. Default off, flip per-user to test, flip default to on after validation, remove in Step 09 |
87 changes: 87 additions & 0 deletions ui/goose2/acp-plus-migration-plan/01-expose-goose-serve-url.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Step 01: Expose the `goose serve` URL to the Frontend

## Objective

Add a Tauri command that returns the WebSocket URL of the running `goose serve` process so the frontend can connect directly via WebSocket.

## Why

The Rust layer currently connects to `goose serve` over WebSocket internally and proxies everything. The frontend never knows the server URL. Exposing it lets the TypeScript ACP client connect directly.

## Changes

### 1. Re-export `GooseServeProcess`

**File:** `src-tauri/src/services/acp/mod.rs`

Add a re-export so the command layer can reference the struct:

```rust
pub(crate) use goose_serve::GooseServeProcess;
```

No changes to `GooseServeProcess` itself — the existing `ws_url()` method already returns `ws://127.0.0.1:<port>/acp`.

### 2. Add the Tauri command

**File:** `src-tauri/src/commands/acp.rs`

Add this command alongside the existing ones:

```rust
use crate::services::acp::goose_serve::GooseServeProcess;

/// Return the WebSocket URL of the running goose serve process.
///
/// This command blocks until the server is confirmed ready. The frontend
/// uses this URL to establish a direct WebSocket ACP connection.
#[tauri::command]
pub async fn get_goose_serve_url() -> Result<String, String> {
GooseServeProcess::start().await?;
let process = GooseServeProcess::get()?;
Ok(process.ws_url())
}
```

### 3. Register the command

**File:** `src-tauri/src/lib.rs`

Add the new command to the `invoke_handler` macro near the other `commands::acp::*` entries:

```rust
commands::acp::get_goose_serve_url,
```

### 4. CSP — no changes needed

**File:** `src-tauri/tauri.conf.json`

CSP is currently disabled (`"csp": null`), so the frontend can open WebSocket connections to `ws://127.0.0.1:*` without restriction.

If CSP is ever re-enabled, add:
```
connect-src 'self' ws://127.0.0.1:*
```

## Verification

1. `cargo check` in `src-tauri/` — confirms compilation.
2. `cargo clippy --all-targets -- -D warnings` in `src-tauri/`.
3. `cargo fmt` in `src-tauri/`.
4. Add a temporary `console.log(await invoke("get_goose_serve_url"))` in the frontend startup — it should print something like `ws://127.0.0.1:54321/acp`.

## Files Modified

| File | Change |
|------|--------|
| `src-tauri/src/services/acp/mod.rs` | Add `pub(crate) use goose_serve::GooseServeProcess` |
| `src-tauri/src/commands/acp.rs` | Add `get_goose_serve_url` command |
| `src-tauri/src/lib.rs` | Register `get_goose_serve_url` in invoke_handler |

## Notes

- The existing ACP commands remain functional during migration. They are removed in Step 09.
- `GooseServeProcess::start()` is idempotent — the first call spawns the process; subsequent calls return immediately.
- The readiness check (`wait_for_server_ready`) ensures the URL is only returned after the server is accepting connections.
- The URL includes the `/acp` path — the same WebSocket endpoint the Rust layer currently uses in `thread.rs`.
115 changes: 115 additions & 0 deletions ui/goose2/acp-plus-migration-plan/02-add-acp-npm-dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Step 02: Add ACP NPM Dependencies to goose2

## Objective

Add `@aaif/goose-acp` and `@agentclientprotocol/sdk` as dependencies of the goose2 frontend so we can use the TypeScript ACP client.

## Why

The `@aaif/goose-acp` package (located at `ui/acp/` in the monorepo) already provides:

- **`GooseClient`** — a full TypeScript ACP client wrapping `ClientSideConnection`
- **`GooseExtClient`** — generated typed client for Goose extension methods (`goose/providers/list`, `goose/session/export`, etc.)
- **`createHttpStream`** — an HTTP+SSE transport (we won't use this — we'll use WebSocket instead, see Step 03)
- **Generated types + Zod validators** for all Goose ACP extension method request/response shapes

This package is already used by `ui/desktop` (Electron) and `ui/text` (Ink TUI). goose2 currently does NOT depend on it.

## Changes

### 1. Add dependencies

**File:** `ui/goose2/package.json`

goose2 has its own `pnpm-lock.yaml` and is not part of the `ui/pnpm-workspace.yaml` workspace. Use the published npm packages:

```bash
cd ui/goose2
pnpm add @aaif/goose-acp @agentclientprotocol/sdk@^0.14.1
```

The `@aaif/goose-acp` package declares `@agentclientprotocol/sdk` as a peer dependency (`"*"`). Pin to `^0.14.1` to match the version used by `ui/acp/package.json`.

### 2. Verify the dependency resolves

After installation, verify the imports work:

```bash
cd ui/goose2
pnpm typecheck
```

Create a temporary test file to confirm imports resolve:

```typescript
// src/shared/api/_test_acp_import.ts (DELETE AFTER VERIFICATION)
import { GooseClient } from "@aaif/goose-acp";
import type { Client, SessionNotification } from "@agentclientprotocol/sdk";

console.log("GooseClient:", GooseClient);
```

Run `pnpm typecheck` to confirm no type errors. Then delete the test file.

### 3. Verify key exports are available

The following imports must resolve — these are what Steps 03–06 will use:

From `@aaif/goose-acp`:
```typescript
import { GooseClient } from "@aaif/goose-acp";
```

From `@agentclientprotocol/sdk`:
```typescript
import type {
Client,
SessionNotification,
SessionUpdate,
ContentBlock,
ToolCallContent,
RequestPermissionRequest,
RequestPermissionResponse,
NewSessionRequest,
NewSessionResponse,
LoadSessionRequest,
LoadSessionResponse,
PromptRequest,
PromptResponse,
CancelNotification,
SetSessionConfigOptionRequest,
SetSessionConfigOptionResponse,
ForkSessionRequest,
ForkSessionResponse,
ListSessionsRequest,
ListSessionsResponse,
InitializeRequest,
ProtocolVersion,
Implementation,
SessionModelState,
SessionInfoUpdate,
SessionConfigOption,
SessionConfigKind,
SessionConfigSelectOptions,
SessionConfigOptionCategory,
} from "@agentclientprotocol/sdk";
```

## Verification

1. `pnpm typecheck` passes with no errors related to the new dependencies.
2. `pnpm check` (Biome lint + file sizes) passes.
3. `pnpm test` still passes (no existing tests should break).

## Files Modified

| File | Change |
|------|--------|
| `package.json` | Add `@aaif/goose-acp` and `@agentclientprotocol/sdk` to dependencies |
| `pnpm-lock.yaml` | Auto-updated by pnpm |

## Notes

- `GooseClient` wraps `ClientSideConnection` from `@agentclientprotocol/sdk` and adds Goose-specific extension methods via `GooseExtClient`.
- The package ships `createHttpStream` (HTTP+SSE transport), but we will use **WebSocket** transport instead. `GooseClient` accepts any `Stream` (a `{ readable, writable }` pair of `ReadableStream<AnyMessage>` and `WritableStream<AnyMessage>`). In Step 03 we'll create a `createWebSocketStream` helper.
- The `goose serve` WebSocket endpoint at `/acp` uses simple framing: each WS text frame is a single JSON-RPC message (no newline delimiters needed). This is the same transport the Rust Tauri backend already uses in `thread.rs`.
Loading
Loading