-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Context
The ACP (Agent Communication Protocol) connection lifecycle in Arandu has several UX and reliability issues:
- No disconnect control: The session header has a "Connect" button that only shows when disconnected, but no way to disconnect without ending the session (X button). Users need the Connect button to toggle into a Disconnect button when connected.
- Brainstorm card overflow: The initial prompt card in
TerminalChatusesw-full+mx-4, causing horizontal overflow and an unwanted scrollbar. - Shallow heartbeat: The current 15s heartbeat only checks if the child process is alive (
try_wait()), but doesn't verify JSON-RPC responsiveness. - No connection diagnostics: Errors are volatile state — no persistent log to inspect connection history.
- Missing tests: No tests for
useAcpConnectionhook or connection UI state transitions.
Architecture
DirectoryWorkspace
├─ useAcpConnection(workspaceId) → connection state (connect/disconnect/status)
├─ useLocalSessions(workspacePath) → session CRUD (SQLite persistence)
│
├─ browsing=true → SessionCard[] (click → mount session)
└─ browsing=false → ActiveSessionView
├─ props: session, isConnected, onConnect, onDisconnect (NEW), onEnd
├─ useAcpSession → ACP message streaming, startSession()
├─ usePlanWorkflow → phase transitions, plan file
├─ useAcpLogs (NEW) → connection diagnostics
│
├─ Header: [Name] [#hash] [Phase] [Connect/Disconnect toggle]
│ [chat] [plan] [logs] [minimize] [X end]
├─ TerminalChat (messages, input, brainstorm card)
└─ MarkdownViewer (plan file)
Key data flow: Session ↔ SQLite (persisted: acp_session_id, phase, plan_file_path).
Connection ↔ Rust process (transient: copilot stdin/stdout). Disconnect only kills
the process; session data survives. Reconnect reloads via acp_load_session(acp_session_id).
Plan
1. Brainstorm Card Layout Fix
File: apps/tauri/src/components/TerminalChat.tsx
- Line 49: Change
"flex flex-col items-start justify-start gap-3 pt-2"→"flex flex-col items-stretch gap-3"items-stretchmakes children fill width naturally;mx-4on the card provides margins- Remove
pt-2sincepy-4on scroll container (line 47) already provides 16px top padding
- Line 51: Remove
w-fullfrom"w-full px-4 py-3 mx-4 ..."→"px-4 py-3 mx-4 ..."w-full+mx-4= overflow;items-stretch+mx-4= proper sizing
Visual result:
┌─ Chat Panel ─────────────────────┐
│ 16px padding (py-4) │
│ ┌─ Brainstorm Card ──────────┐ │
│ │ 16px margin (mx-4) │ │
│ │ Brainstorm │ │
│ │ Tem algum tempo que... │ │
│ └────────────────────────────┘ │
│ │
│ (messages area) │
│ │
│ ┌─ Input ────────────────────┐ │
│ │ Type a message... │ │
│ │ [Send] │ │
│ └────────────────────────────┘ │
└──────────────────────────────────┘
2. Connect/Disconnect Toggle Button
Files to modify:
apps/tauri/src/components/ActiveSessionView.tsxapps/tauri/src/components/DirectoryWorkspace.tsxapps/tauri/src/components/TerminalChat.tsxapps/tauri/src/locales/en.jsonapps/tauri/src/locales/pt-BR.json
Changes:
ActiveSessionView.tsx — Make the existing Connect button a toggle (lines 218-234):
- When disconnected: Show
Plugicon + "Connect" (current behavior) - When connected: Show
Unplugicon + "Disconnect" (new — replaces hiding the button) - When connecting: Show
Loader2spinner + "Connecting..." (current behavior) - The button calls
onConnect()when disconnected,onDisconnect()when connected - Keep the X (end session) button exactly as it is (lines 269-300)
New prop: Add onDisconnect?: () => Promise<void> to ActiveSessionViewProps
DirectoryWorkspace.tsx — Pass onDisconnect={connection.disconnect} to ActiveSessionView
TerminalChat.tsx — Remove onReconnect prop and the Reconnect button (lines 17, 30, 60-65) since the header toggle now handles reconnection
i18n keys to add:
acp.disconnect: "Disconnect" / "Desconectar"
Visual result:
Disconnected:
┌─ Session Header ────────────────────────────────────────────┐
│ Revisar o README #hash ● Executing 🔌 Connect │
│ 💬 📄 │ ⤢ ✕ │
└─────────────────────────────────────────────────────────────┘
Connected:
┌─ Session Header ────────────────────────────────────────────┐
│ Revisar o README #hash ● Executing ⚡ Disconnect │
│ 💬 📄 │ ⤢ ✕ │
└─────────────────────────────────────────────────────────────┘
3. ACP Heartbeat Enhancement (JSON-RPC Ping)
Files to modify:
apps/tauri/src-tauri/src/acp/connection.rsapps/tauri/src-tauri/src/acp/types.rs
Changes to connection.rs:
- Change
next_idfromAtomicU64toArc<AtomicU64>so the heartbeat task can share the ID generator - Refactor
heartbeat_tasksignature to receivewriter_tx,pending, andnext_idclones - Add JSON-RPC ping logic inside the heartbeat loop:
- After the existing
try_wait()process check, send a lightweight JSON-RPC request ("ping"method) - Use a 5s timeout for the ping response
- Treat any response (including JSON-RPC error) as "alive" — only timeout/write-failure counts as failure
- Track consecutive failures; after 3 consecutive failures, emit
"disconnected"and return - On success, reset failure counter and emit
"acp:heartbeat"event with"healthy"status - On timeout, emit
"acp:heartbeat"event with"degraded"status
- After the existing
- Update
spawn()to pass the additional args to the heartbeat task
Changes to types.rs — Add HeartbeatEvent:
pub struct HeartbeatEvent {
pub workspace_id: String,
pub status: String, // "healthy" | "degraded" | "disconnected"
pub latency_ms: Option<u64>,
pub timestamp: String,
}Data flow:
┌─ heartbeat_task (Rust, every 15s) ────────────────────┐
│ │
│ 1. try_wait() → process alive? │
│ └─ exited → emit "disconnected", return │
│ │
│ 2. Send JSON-RPC "ping" request │
│ ├─ any response (ok/error) → healthy │
│ │ └─ emit "acp:heartbeat" {status:"healthy"} │
│ │ └─ consecutive_failures = 0 │
│ └─ timeout (5s) → degraded │
│ └─ emit "acp:heartbeat" {status:"degraded"} │
│ └─ consecutive_failures += 1 │
│ │
│ 3. consecutive_failures >= 3? │
│ └─ emit "disconnected", return │
│ │
│ 4. emit "acp:log" entry │
└────────────────────────────────────────────────────────┘
4. Connection Diagnostics & Logs
Architecture: Emit "acp:log" events from Rust → collect in frontend hook. This avoids threading AcpState through connection internals.
Files to create:
apps/tauri/src/hooks/useAcpLogs.tsapps/tauri/src/components/ConnectionLogs.tsx
Files to modify:
apps/tauri/src-tauri/src/acp/connection.rs— addemit_log()helper, instrumentspawn,reader_task,heartbeat_task,shutdownapps/tauri/src-tauri/src/acp/commands.rs— instrumentacp_connect,acp_disconnectapps/tauri/src-tauri/src/acp/types.rs— addConnectionLogEntrytypeapps/tauri/src/types/acp.ts— add TS typesapps/tauri/src/components/ActiveSessionView.tsx— add log viewer accessapps/tauri/src/locales/{en,pt-BR}.json— add log-related i18n keys
Rust side — ConnectionLogEntry type:
pub struct ConnectionLogEntry {
pub timestamp: String,
pub level: String, // "info" | "warn" | "error"
pub event: String, // "connect" | "disconnect" | "heartbeat_ok" | "ping_timeout" | "error"
pub message: String,
pub workspace_id: String,
}Rust side — emit_log() helper in connection.rs:
- Called at key lifecycle points: spawn success, reader exit, heartbeat failure, shutdown
- Also called from
commands.rson connect/disconnect
Frontend — useAcpLogs hook:
- Listens to
"acp:log"event, filters byworkspaceId - Stores up to 200 entries in state (ring buffer behavior: drop oldest when full)
- Exposes
logs,clearLogs
Frontend — ConnectionLogs component:
- Dialog accessible from a small
Activityicon button in the session header right side (next to toggle chat/plan icons) - Icon shows a subtle warning indicator (orange dot) when there are recent errors
- Scrollable list of timestamped log entries with level indicators (info=blue, warn=yellow, error=red)
- "Clear" and "Copy to clipboard" buttons
- Uses
ScrollAreafrom shadcn/ui
Data flow:
useAcpConnection Rust (connection.rs)
┌──────────────┐ ┌──────────────────────┐
│ │──invoke──────────→│ acp_connect │
│ connect() │ "acp_connect" │ ├─ spawn copilot │
│ │ │ ├─ initialize RPC │
│ │ │ └─ emit "connected" │
│ │ │ │
│ disconnect()│──invoke──────────→│ acp_disconnect │
│ │ "acp_disconnect" │ └─ shutdown + kill │
│ │ │ │
│ │←─event────────────│ Heartbeat (15s) │
│ status ←────│ "acp:connection │ ├─ try_wait() │
│ │ -status" │ ├─ JSON-RPC ping │
│ │ │ └─ emit status │
│ │ │ │
│ │←─event────────────│ emit_log() │
│ (useAcpLogs)│ "acp:log" │ ├─ connect events │
│ logs[] ←────│ │ ├─ heartbeat events │
│ │ │ └─ error events │
└──────────────┘ └──────────────────────┘
5. Tests
Files to create:
apps/tauri/src/__tests__/hooks/useAcpConnection.test.tsapps/tauri/src/__tests__/hooks/useAcpLogs.test.ts
useAcpConnection.test.ts — test cases:
- Starts with
idlestatus,isConnected=false,isConnecting=false, no error connect()callsacp_connect→ transitions toconnecting→connectedconnect()failure → setsconnectionError, statusdisconnecteddisconnect()→ callsacp_disconnect, resets toidle- Ignores
connect()when alreadyconnecting - Resets state when
workspaceIdchanges - Responds to
"acp:connection-status"events from Rust - Ignores events for different workspaceId
- Calls
acp_disconnecton unmount cleanup - Reads
localStorageforarandu-copilot-pathandarandu-gh-token
useAcpLogs.test.ts — test cases:
- Starts with empty logs
- Accumulates log entries from
"acp:log"events - Caps at 200 entries (drops oldest)
- Filters by workspaceId
clearLogs()empties the array- Cleans up listener on unmount
Implementation Sequence
- Brainstorm card fix (CSS only, quick win)
- Connect/Disconnect toggle (frontend, small change)
- Heartbeat enhancement (Rust backend)
- Diagnostics types + log emission (Rust backend)
- useAcpLogs hook + ConnectionLogs component (frontend)
- i18n keys (both locales)
- Tests (hooks)
Verification
npm test— run Vitest suitenpm run build— verify TypeScript compilescargo check— verify Rust compilesmake dev— manual verification:- Open a workspace, create a session
- Verify brainstorm card has no horizontal scrollbar and uniform padding
- Verify Connect/Disconnect toggle works (button text/icon changes on connect/disconnect)
- Verify X (end session) button still works with confirmation dialog
- Verify connection logs accessible from Activity icon, entries appear on connect/disconnect
- Verify heartbeat detects unresponsive agent (kill copilot process, observe UI reaction)