Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
104 changes: 104 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,104 @@
# 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 HTTP+SSE. 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 (HTTP+SSE)
→ goose serve http://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 (HTTP+SSE)
→ goose serve http://127.0.0.1:<port>/acp
← Client callbacks → direct Zustand store updates

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

## Steps

| Step | File | Summary |
|------|------|---------|
| 01 | `01-expose-goose-serve-url.md` | Add Tauri command to expose the `goose serve` HTTP 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 |
| 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 but 06 can be done 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. **HTTP+SSE over WebSocket**: `goose serve` supports both. HTTP+SSE is already battle-tested by `ui/acp`'s `createHttpStream`. Visible in browser DevTools. Can switch to WS later if needed.

2. **Direct store updates over event bus**: The notification handler calls Zustand store methods directly instead of emitting Tauri events. 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`, `createHttpStream`, generated types, and Zod validators.

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 fetch | CSP is already `null` (disabled) in `tauri.conf.json` |
| `goose serve` not ready when frontend initializes | Rust still does readiness check; URL command only resolves after server is confirmed ready |
| HTTP+SSE performance vs WebSocket | Both transports are supported; can switch later. SSE has keep-alive. |
| 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 | Can keep old Rust path behind a flag initially; remove after validation |
117 changes: 117 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,117 @@
# Step 01: Expose the `goose serve` URL to the Frontend

## Objective

Add a single new Tauri command that returns the HTTP base URL of the running `goose serve` process. The frontend will use this URL to connect directly via HTTP+SSE.
Comment thread
jamadeo marked this conversation as resolved.
Outdated

## Why

Currently the Rust layer connects to `goose serve` over WebSocket internally and proxies everything. The frontend never knows the server URL. We need to expose it so the TypeScript ACP client can connect directly.

## Changes

### 1. Make `port` accessible on `GooseServeProcess`

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

Add a public getter for the port:

```rust
impl GooseServeProcess {
/// Return the HTTP base URL for this server (used by the frontend).
pub fn http_url(&self) -> String {
format!("http://{LOCALHOST}:{}", self.port)
}

// ... existing methods unchanged ...
}
```

The `port` field is already stored on the struct but is only used internally via `ws_url()`. The new `http_url()` method provides the HTTP variant.

### 2. Add the Tauri command

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

Add this command alongside the existing ones (it will coexist during migration):

```rust
/// Return the HTTP base 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 HTTP+SSE ACP connection.
#[tauri::command]
pub async fn get_goose_serve_url() -> Result<String, String> {
// GooseServeProcess::start() is idempotent — it returns immediately
// if the process is already running.
GooseServeProcess::start().await?;
let process = GooseServeProcess::get()?;
Ok(process.http_url())
}
```

Add the necessary import at the top of the file if not already present:

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

Note: `GooseServeProcess` is currently re-exported from `services::acp` as `resolve_goose_binary` but the struct itself is used via `goose_serve::GooseServeProcess` internally. You may need to add a `pub use` in `services/acp/mod.rs`:

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

### 3. Register the command

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

Add the new command to the `invoke_handler` macro:

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

Place it near the other `commands::acp::*` entries.

### 4. Verify CSP allows localhost fetch

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

Check the `security.csp` field. Currently it is:

```json
"security": {
"csp": null,
...
}
```

`null` means CSP is disabled — **no changes needed**. The frontend can freely fetch from `http://127.0.0.1:*`.

If CSP were ever re-enabled, you'd need to add:
```
connect-src 'self' http://127.0.0.1:* ws://127.0.0.1:*
```

## Verification

1. Run `just tauri-check` (or `cargo check` in `src-tauri/`) to confirm the Rust compiles.
2. Run `cargo clippy --all-targets -- -D warnings` in `src-tauri/`.
3. Run `cargo fmt` in `src-tauri/`.
4. Manually test by adding a temporary `console.log(await invoke("get_goose_serve_url"))` in the frontend startup — it should print something like `http://127.0.0.1:54321`.

## Files Modified

| File | Change |
|------|--------|
| `src-tauri/src/services/acp/goose_serve.rs` | Add `http_url()` method |
| `src-tauri/src/services/acp/mod.rs` | Add `pub(crate) use goose_serve::GooseServeProcess` if needed |
| `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 will be removed in Step 09.
- `GooseServeProcess::start()` is idempotent — calling it multiple times is safe. The first call spawns the process; subsequent calls return immediately.
- The readiness check (WebSocket probe loop) ensures the URL is only returned after the server is accepting connections.
150 changes: 150 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,150 @@
# 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`
- **`createHttpStream`** — an HTTP+SSE transport that speaks the same protocol as `goose serve`
- **`GooseExtClient`** — generated typed client for Goose extension methods (`goose/providers/list`, `goose/session/export`, etc.)
- **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`

Run from the `ui/goose2/` directory:

```bash
source ./bin/activate-hermit
pnpm add @aaif/goose-acp @agentclientprotocol/sdk
```

If the packages are not published to npm yet or you want to use the local workspace version, use the workspace protocol instead. Check `ui/pnpm-workspace.yaml` to see if `ui/acp` is included in the workspace. If it is:

```bash
pnpm add @aaif/goose-acp@workspace:* @agentclientprotocol/sdk
```

If `ui/acp` is NOT in the pnpm workspace (goose2 has its own `pnpm-lock.yaml`), you have two options:

**Option A — Link locally during development:**
```bash
cd ui/acp
npm run build
npm link

cd ui/goose2
pnpm link @aaif/goose-acp
pnpm add @agentclientprotocol/sdk
```

**Option B — Use the published npm package:**
```bash
cd ui/goose2
pnpm add @aaif/goose-acp @agentclientprotocol/sdk
```

### 2. Verify the dependency resolves

After installation, verify the imports work:

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

Create a temporary test file to confirm imports resolve:

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

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

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, createHttpStream } 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";
```

### 4. Check `@agentclientprotocol/sdk` version compatibility

The `@aaif/goose-acp` package declares `@agentclientprotocol/sdk` as a **peer dependency** (`"*"`). The Rust backend currently uses `agent-client-protocol = "0.10.4"`. The TypeScript SDK should be at a compatible version.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is interesting. i thought i did a refactor such that it fully wraps the @agentclientprotocol/sdk interactions all inside GooseClient

maybe I/we mistakenly left it as a peer dep?


Check `ui/acp/package.json` for the devDependency version — it currently shows `"@agentclientprotocol/sdk": "^0.14.1"`. Install the same or newer version:

```bash
pnpm add @agentclientprotocol/sdk@^0.14.1
```

## 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

- The `@aaif/goose-acp` package exports `GooseClient` which wraps `ClientSideConnection` from `@agentclientprotocol/sdk`. It adds Goose-specific extension methods via `GooseExtClient`.
- The `createHttpStream` function creates a `Stream` (a `{ readable, writable }` pair of `ReadableStream<AnyMessage>` and `WritableStream<AnyMessage>`) that communicates with `goose serve` over HTTP POST + SSE.
- The HTTP+SSE transport works by: (1) POSTing JSON-RPC messages to `/acp`, (2) receiving responses and notifications via Server-Sent Events on the same connection. The first POST (initialize) establishes the SSE stream; subsequent POSTs use the `Acp-Session-Id` header.
Loading
Loading