Fix life cycle ACP client connection #37

Description

@wilcorrea

Context

The ACP (Agent Communication Protocol) connection lifecycle in Arandu has several UX and reliability issues:

  1. 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.
  2. Brainstorm card overflow: The initial prompt card in TerminalChat uses w-full + mx-4, causing horizontal overflow and an unwanted scrollbar.
  3. Shallow heartbeat: The current 15s heartbeat only checks if the child process is alive (try_wait()), but doesn't verify JSON-RPC responsiveness.
  4. No connection diagnostics: Errors are volatile state — no persistent log to inspect connection history.
  5. Missing tests: No tests for useAcpConnection hook 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-stretch makes children fill width naturally; mx-4 on the card provides margins
    • Remove pt-2 since py-4 on scroll container (line 47) already provides 16px top padding
  • Line 51: Remove w-full from "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.tsx
  • apps/tauri/src/components/DirectoryWorkspace.tsx
  • apps/tauri/src/components/TerminalChat.tsx
  • apps/tauri/src/locales/en.json
  • apps/tauri/src/locales/pt-BR.json

Changes:

ActiveSessionView.tsx — Make the existing Connect button a toggle (lines 218-234):

  • When disconnected: Show Plug icon + "Connect" (current behavior)
  • When connected: Show Unplug icon + "Disconnect" (new — replaces hiding the button)
  • When connecting: Show Loader2 spinner + "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.rs
  • apps/tauri/src-tauri/src/acp/types.rs

Changes to connection.rs:

  1. Change next_id from AtomicU64 to Arc<AtomicU64> so the heartbeat task can share the ID generator
  2. Refactor heartbeat_task signature to receive writer_tx, pending, and next_id clones
  3. 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
  4. Update spawn() to pass the additional args to the heartbeat task

Changes to types.rs — Add HeartbeatEvent:

pubstructHeartbeatEvent{pubworkspace_id:String,pubstatus:String,// "healthy" | "degraded" | "disconnected"publatency_ms:Option<u64>,pubtimestamp: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.ts
  • apps/tauri/src/components/ConnectionLogs.tsx

Files to modify:

  • apps/tauri/src-tauri/src/acp/connection.rs — add emit_log() helper, instrument spawn, reader_task, heartbeat_task, shutdown
  • apps/tauri/src-tauri/src/acp/commands.rs — instrument acp_connect, acp_disconnect
  • apps/tauri/src-tauri/src/acp/types.rs — add ConnectionLogEntry type
  • apps/tauri/src/types/acp.ts — add TS types
  • apps/tauri/src/components/ActiveSessionView.tsx — add log viewer access
  • apps/tauri/src/locales/{en,pt-BR}.json — add log-related i18n keys

Rust side — ConnectionLogEntry type:

pubstructConnectionLogEntry{pubtimestamp:String,publevel:String,// "info" | "warn" | "error"pubevent:String,// "connect" | "disconnect" | "heartbeat_ok" | "ping_timeout" | "error"pubmessage:String,pubworkspace_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.rs on connect/disconnect

Frontend — useAcpLogs hook:

  • Listens to "acp:log" event, filters by workspaceId
  • 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 Activity icon 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 ScrollArea from 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.ts
  • apps/tauri/src/__tests__/hooks/useAcpLogs.test.ts

useAcpConnection.test.ts — test cases:

  1. Starts with idle status, isConnected=false, isConnecting=false, no error
  2. connect() calls acp_connect → transitions to connectingconnected
  3. connect() failure → sets connectionError, status disconnected
  4. disconnect() → calls acp_disconnect, resets to idle
  5. Ignores connect() when already connecting
  6. Resets state when workspaceId changes
  7. Responds to "acp:connection-status" events from Rust
  8. Ignores events for different workspaceId
  9. Calls acp_disconnect on unmount cleanup
  10. Reads localStorage for arandu-copilot-path and arandu-gh-token

useAcpLogs.test.ts — test cases:

  1. Starts with empty logs
  2. Accumulates log entries from "acp:log" events
  3. Caps at 200 entries (drops oldest)
  4. Filters by workspaceId
  5. clearLogs() empties the array
  6. Cleans up listener on unmount

Implementation Sequence

  1. Brainstorm card fix (CSS only, quick win)
  2. Connect/Disconnect toggle (frontend, small change)
  3. Heartbeat enhancement (Rust backend)
  4. Diagnostics types + log emission (Rust backend)
  5. useAcpLogs hook + ConnectionLogs component (frontend)
  6. i18n keys (both locales)
  7. Tests (hooks)

Verification

  1. npm test — run Vitest suite
  2. npm run build — verify TypeScript compiles
  3. cargo check — verify Rust compiles
  4. make 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
       blocks
      (function() {
      function addCopyButtons() {
      document.querySelectorAll('pre code').forEach(function(codeBlock) {
      if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
      codeBlock.parentElement.setAttribute('data-copy-added', 'true');
      var btn = document.createElement('button');
      btn.textContent = 'Copy';
      btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
      btn.onmouseover = function() { this.style.opacity = '1'; };
      btn.onmouseout = function() { this.style.opacity = '0.7'; };
      btn.onclick = function() {
      navigator.clipboard.writeText(codeBlock.textContent).then(function() {
      btn.textContent = 'Copied!';
      setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
      });
      };
      codeBlock.parentElement.style.position = 'relative';
      codeBlock.parentElement.appendChild(btn);
      });
      }
      addCopyButtons();
      // Re-run on dynamic content
      var observer = new MutationObserver(addCopyButtons);
      observer.observe(document.body, { childList: true, subtree: true });
      })();
      }
      } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
      })();
      (function(){
      try {
      var __m = "github.com";
      var __re = new RegExp('^' + "github\\.com" + '
      
      Skip to content

      Fix life cycle ACP client connection #37

      Description

      @wilcorrea

      Context

      The ACP (Agent Communication Protocol) connection lifecycle in Arandu has several UX and reliability issues:

      1. 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.
      2. Brainstorm card overflow: The initial prompt card in TerminalChat uses w-full + mx-4, causing horizontal overflow and an unwanted scrollbar.
      3. Shallow heartbeat: The current 15s heartbeat only checks if the child process is alive (try_wait()), but doesn't verify JSON-RPC responsiveness.
      4. No connection diagnostics: Errors are volatile state — no persistent log to inspect connection history.
      5. Missing tests: No tests for useAcpConnection hook 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-stretch makes children fill width naturally; mx-4 on the card provides margins
        • Remove pt-2 since py-4 on scroll container (line 47) already provides 16px top padding
      • Line 51: Remove w-full from "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.tsx
      • apps/tauri/src/components/DirectoryWorkspace.tsx
      • apps/tauri/src/components/TerminalChat.tsx
      • apps/tauri/src/locales/en.json
      • apps/tauri/src/locales/pt-BR.json

      Changes:

      ActiveSessionView.tsx — Make the existing Connect button a toggle (lines 218-234):

      • When disconnected: Show Plug icon + "Connect" (current behavior)
      • When connected: Show Unplug icon + "Disconnect" (new — replaces hiding the button)
      • When connecting: Show Loader2 spinner + "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.rs
      • apps/tauri/src-tauri/src/acp/types.rs

      Changes to connection.rs:

      1. Change next_id from AtomicU64 to Arc<AtomicU64> so the heartbeat task can share the ID generator
      2. Refactor heartbeat_task signature to receive writer_tx, pending, and next_id clones
      3. 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
      4. Update spawn() to pass the additional args to the heartbeat task

      Changes to types.rs — Add HeartbeatEvent:

      pubstructHeartbeatEvent{pubworkspace_id:String,pubstatus:String,// "healthy" | "degraded" | "disconnected"publatency_ms:Option<u64>,pubtimestamp: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.ts
      • apps/tauri/src/components/ConnectionLogs.tsx

      Files to modify:

      • apps/tauri/src-tauri/src/acp/connection.rs — add emit_log() helper, instrument spawn, reader_task, heartbeat_task, shutdown
      • apps/tauri/src-tauri/src/acp/commands.rs — instrument acp_connect, acp_disconnect
      • apps/tauri/src-tauri/src/acp/types.rs — add ConnectionLogEntry type
      • apps/tauri/src/types/acp.ts — add TS types
      • apps/tauri/src/components/ActiveSessionView.tsx — add log viewer access
      • apps/tauri/src/locales/{en,pt-BR}.json — add log-related i18n keys

      Rust side — ConnectionLogEntry type:

      pubstructConnectionLogEntry{pubtimestamp:String,publevel:String,// "info" | "warn" | "error"pubevent:String,// "connect" | "disconnect" | "heartbeat_ok" | "ping_timeout" | "error"pubmessage:String,pubworkspace_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.rs on connect/disconnect

      Frontend — useAcpLogs hook:

      • Listens to "acp:log" event, filters by workspaceId
      • 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 Activity icon 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 ScrollArea from 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.ts
      • apps/tauri/src/__tests__/hooks/useAcpLogs.test.ts

      useAcpConnection.test.ts — test cases:

      1. Starts with idle status, isConnected=false, isConnecting=false, no error
      2. connect() calls acp_connect → transitions to connectingconnected
      3. connect() failure → sets connectionError, status disconnected
      4. disconnect() → calls acp_disconnect, resets to idle
      5. Ignores connect() when already connecting
      6. Resets state when workspaceId changes
      7. Responds to "acp:connection-status" events from Rust
      8. Ignores events for different workspaceId
      9. Calls acp_disconnect on unmount cleanup
      10. Reads localStorage for arandu-copilot-path and arandu-gh-token

      useAcpLogs.test.ts — test cases:

      1. Starts with empty logs
      2. Accumulates log entries from "acp:log" events
      3. Caps at 200 entries (drops oldest)
      4. Filters by workspaceId
      5. clearLogs() empties the array
      6. Cleans up listener on unmount

      Implementation Sequence

      1. Brainstorm card fix (CSS only, quick win)
      2. Connect/Disconnect toggle (frontend, small change)
      3. Heartbeat enhancement (Rust backend)
      4. Diagnostics types + log emission (Rust backend)
      5. useAcpLogs hook + ConnectionLogs component (frontend)
      6. i18n keys (both locales)
      7. Tests (hooks)

      Verification

      1. npm test — run Vitest suite
      2. npm run build — verify TypeScript compiles
      3. cargo check — verify Rust compiles
      4. make 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)

      Metadata

      Metadata

      Assignees

      No one assigned

        Labels

        No labels
        No labels

        Type

        No type

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
          Skip to content

          Fix life cycle ACP client connection #37

          Description

          @wilcorrea

          Context

          The ACP (Agent Communication Protocol) connection lifecycle in Arandu has several UX and reliability issues:

          1. 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.
          2. Brainstorm card overflow: The initial prompt card in TerminalChat uses w-full + mx-4, causing horizontal overflow and an unwanted scrollbar.
          3. Shallow heartbeat: The current 15s heartbeat only checks if the child process is alive (try_wait()), but doesn't verify JSON-RPC responsiveness.
          4. No connection diagnostics: Errors are volatile state — no persistent log to inspect connection history.
          5. Missing tests: No tests for useAcpConnection hook 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-stretch makes children fill width naturally; mx-4 on the card provides margins
            • Remove pt-2 since py-4 on scroll container (line 47) already provides 16px top padding
          • Line 51: Remove w-full from "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.tsx
          • apps/tauri/src/components/DirectoryWorkspace.tsx
          • apps/tauri/src/components/TerminalChat.tsx
          • apps/tauri/src/locales/en.json
          • apps/tauri/src/locales/pt-BR.json

          Changes:

          ActiveSessionView.tsx — Make the existing Connect button a toggle (lines 218-234):

          • When disconnected: Show Plug icon + "Connect" (current behavior)
          • When connected: Show Unplug icon + "Disconnect" (new — replaces hiding the button)
          • When connecting: Show Loader2 spinner + "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.rs
          • apps/tauri/src-tauri/src/acp/types.rs

          Changes to connection.rs:

          1. Change next_id from AtomicU64 to Arc<AtomicU64> so the heartbeat task can share the ID generator
          2. Refactor heartbeat_task signature to receive writer_tx, pending, and next_id clones
          3. 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
          4. Update spawn() to pass the additional args to the heartbeat task

          Changes to types.rs — Add HeartbeatEvent:

          pubstructHeartbeatEvent{pubworkspace_id:String,pubstatus:String,// "healthy" | "degraded" | "disconnected"publatency_ms:Option<u64>,pubtimestamp: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.ts
          • apps/tauri/src/components/ConnectionLogs.tsx

          Files to modify:

          • apps/tauri/src-tauri/src/acp/connection.rs — add emit_log() helper, instrument spawn, reader_task, heartbeat_task, shutdown
          • apps/tauri/src-tauri/src/acp/commands.rs — instrument acp_connect, acp_disconnect
          • apps/tauri/src-tauri/src/acp/types.rs — add ConnectionLogEntry type
          • apps/tauri/src/types/acp.ts — add TS types
          • apps/tauri/src/components/ActiveSessionView.tsx — add log viewer access
          • apps/tauri/src/locales/{en,pt-BR}.json — add log-related i18n keys

          Rust side — ConnectionLogEntry type:

          pubstructConnectionLogEntry{pubtimestamp:String,publevel:String,// "info" | "warn" | "error"pubevent:String,// "connect" | "disconnect" | "heartbeat_ok" | "ping_timeout" | "error"pubmessage:String,pubworkspace_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.rs on connect/disconnect

          Frontend — useAcpLogs hook:

          • Listens to "acp:log" event, filters by workspaceId
          • 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 Activity icon 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 ScrollArea from 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.ts
          • apps/tauri/src/__tests__/hooks/useAcpLogs.test.ts

          useAcpConnection.test.ts — test cases:

          1. Starts with idle status, isConnected=false, isConnecting=false, no error
          2. connect() calls acp_connect → transitions to connectingconnected
          3. connect() failure → sets connectionError, status disconnected
          4. disconnect() → calls acp_disconnect, resets to idle
          5. Ignores connect() when already connecting
          6. Resets state when workspaceId changes
          7. Responds to "acp:connection-status" events from Rust
          8. Ignores events for different workspaceId
          9. Calls acp_disconnect on unmount cleanup
          10. Reads localStorage for arandu-copilot-path and arandu-gh-token

          useAcpLogs.test.ts — test cases:

          1. Starts with empty logs
          2. Accumulates log entries from "acp:log" events
          3. Caps at 200 entries (drops oldest)
          4. Filters by workspaceId
          5. clearLogs() empties the array
          6. Cleans up listener on unmount

          Implementation Sequence

          1. Brainstorm card fix (CSS only, quick win)
          2. Connect/Disconnect toggle (frontend, small change)
          3. Heartbeat enhancement (Rust backend)
          4. Diagnostics types + log emission (Rust backend)
          5. useAcpLogs hook + ConnectionLogs component (frontend)
          6. i18n keys (both locales)
          7. Tests (hooks)

          Verification

          1. npm test — run Vitest suite
          2. npm run build — verify TypeScript compiles
          3. cargo check — verify Rust compiles
          4. make 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)

          Metadata

          Metadata

          Assignees

          No one assigned

            Labels

            No labels
            No labels

            Type

            No type

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              Fix life cycle ACP client connection #37

              Description

              @wilcorrea

              Context

              The ACP (Agent Communication Protocol) connection lifecycle in Arandu has several UX and reliability issues:

              1. 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.
              2. Brainstorm card overflow: The initial prompt card in TerminalChat uses w-full + mx-4, causing horizontal overflow and an unwanted scrollbar.
              3. Shallow heartbeat: The current 15s heartbeat only checks if the child process is alive (try_wait()), but doesn't verify JSON-RPC responsiveness.
              4. No connection diagnostics: Errors are volatile state — no persistent log to inspect connection history.
              5. Missing tests: No tests for useAcpConnection hook 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-stretch makes children fill width naturally; mx-4 on the card provides margins
                • Remove pt-2 since py-4 on scroll container (line 47) already provides 16px top padding
              • Line 51: Remove w-full from "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.tsx
              • apps/tauri/src/components/DirectoryWorkspace.tsx
              • apps/tauri/src/components/TerminalChat.tsx
              • apps/tauri/src/locales/en.json
              • apps/tauri/src/locales/pt-BR.json

              Changes:

              ActiveSessionView.tsx — Make the existing Connect button a toggle (lines 218-234):

              • When disconnected: Show Plug icon + "Connect" (current behavior)
              • When connected: Show Unplug icon + "Disconnect" (new — replaces hiding the button)
              • When connecting: Show Loader2 spinner + "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.rs
              • apps/tauri/src-tauri/src/acp/types.rs

              Changes to connection.rs:

              1. Change next_id from AtomicU64 to Arc<AtomicU64> so the heartbeat task can share the ID generator
              2. Refactor heartbeat_task signature to receive writer_tx, pending, and next_id clones
              3. 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
              4. Update spawn() to pass the additional args to the heartbeat task

              Changes to types.rs — Add HeartbeatEvent:

              pubstructHeartbeatEvent{pubworkspace_id:String,pubstatus:String,// "healthy" | "degraded" | "disconnected"publatency_ms:Option<u64>,pubtimestamp: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.ts
              • apps/tauri/src/components/ConnectionLogs.tsx

              Files to modify:

              • apps/tauri/src-tauri/src/acp/connection.rs — add emit_log() helper, instrument spawn, reader_task, heartbeat_task, shutdown
              • apps/tauri/src-tauri/src/acp/commands.rs — instrument acp_connect, acp_disconnect
              • apps/tauri/src-tauri/src/acp/types.rs — add ConnectionLogEntry type
              • apps/tauri/src/types/acp.ts — add TS types
              • apps/tauri/src/components/ActiveSessionView.tsx — add log viewer access
              • apps/tauri/src/locales/{en,pt-BR}.json — add log-related i18n keys

              Rust side — ConnectionLogEntry type:

              pubstructConnectionLogEntry{pubtimestamp:String,publevel:String,// "info" | "warn" | "error"pubevent:String,// "connect" | "disconnect" | "heartbeat_ok" | "ping_timeout" | "error"pubmessage:String,pubworkspace_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.rs on connect/disconnect

              Frontend — useAcpLogs hook:

              • Listens to "acp:log" event, filters by workspaceId
              • 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 Activity icon 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 ScrollArea from 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.ts
              • apps/tauri/src/__tests__/hooks/useAcpLogs.test.ts

              useAcpConnection.test.ts — test cases:

              1. Starts with idle status, isConnected=false, isConnecting=false, no error
              2. connect() calls acp_connect → transitions to connectingconnected
              3. connect() failure → sets connectionError, status disconnected
              4. disconnect() → calls acp_disconnect, resets to idle
              5. Ignores connect() when already connecting
              6. Resets state when workspaceId changes
              7. Responds to "acp:connection-status" events from Rust
              8. Ignores events for different workspaceId
              9. Calls acp_disconnect on unmount cleanup
              10. Reads localStorage for arandu-copilot-path and arandu-gh-token

              useAcpLogs.test.ts — test cases:

              1. Starts with empty logs
              2. Accumulates log entries from "acp:log" events
              3. Caps at 200 entries (drops oldest)
              4. Filters by workspaceId
              5. clearLogs() empties the array
              6. Cleans up listener on unmount

              Implementation Sequence

              1. Brainstorm card fix (CSS only, quick win)
              2. Connect/Disconnect toggle (frontend, small change)
              3. Heartbeat enhancement (Rust backend)
              4. Diagnostics types + log emission (Rust backend)
              5. useAcpLogs hook + ConnectionLogs component (frontend)
              6. i18n keys (both locales)
              7. Tests (hooks)

              Verification

              1. npm test — run Vitest suite
              2. npm run build — verify TypeScript compiles
              3. cargo check — verify Rust compiles
              4. make 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)

              Metadata

              Metadata

              Assignees

              No one assigned

                Labels

                No labels
                No labels

                Type

                No type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions

                  , 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
                  Skip to content

                  Fix life cycle ACP client connection #37

                  Description

                  @wilcorrea

                  Context

                  The ACP (Agent Communication Protocol) connection lifecycle in Arandu has several UX and reliability issues:

                  1. 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.
                  2. Brainstorm card overflow: The initial prompt card in TerminalChat uses w-full + mx-4, causing horizontal overflow and an unwanted scrollbar.
                  3. Shallow heartbeat: The current 15s heartbeat only checks if the child process is alive (try_wait()), but doesn't verify JSON-RPC responsiveness.
                  4. No connection diagnostics: Errors are volatile state — no persistent log to inspect connection history.
                  5. Missing tests: No tests for useAcpConnection hook 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-stretch makes children fill width naturally; mx-4 on the card provides margins
                    • Remove pt-2 since py-4 on scroll container (line 47) already provides 16px top padding
                  • Line 51: Remove w-full from "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.tsx
                  • apps/tauri/src/components/DirectoryWorkspace.tsx
                  • apps/tauri/src/components/TerminalChat.tsx
                  • apps/tauri/src/locales/en.json
                  • apps/tauri/src/locales/pt-BR.json

                  Changes:

                  ActiveSessionView.tsx — Make the existing Connect button a toggle (lines 218-234):

                  • When disconnected: Show Plug icon + "Connect" (current behavior)
                  • When connected: Show Unplug icon + "Disconnect" (new — replaces hiding the button)
                  • When connecting: Show Loader2 spinner + "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.rs
                  • apps/tauri/src-tauri/src/acp/types.rs

                  Changes to connection.rs:

                  1. Change next_id from AtomicU64 to Arc<AtomicU64> so the heartbeat task can share the ID generator
                  2. Refactor heartbeat_task signature to receive writer_tx, pending, and next_id clones
                  3. 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
                  4. Update spawn() to pass the additional args to the heartbeat task

                  Changes to types.rs — Add HeartbeatEvent:

                  pubstructHeartbeatEvent{pubworkspace_id:String,pubstatus:String,// "healthy" | "degraded" | "disconnected"publatency_ms:Option<u64>,pubtimestamp: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.ts
                  • apps/tauri/src/components/ConnectionLogs.tsx

                  Files to modify:

                  • apps/tauri/src-tauri/src/acp/connection.rs — add emit_log() helper, instrument spawn, reader_task, heartbeat_task, shutdown
                  • apps/tauri/src-tauri/src/acp/commands.rs — instrument acp_connect, acp_disconnect
                  • apps/tauri/src-tauri/src/acp/types.rs — add ConnectionLogEntry type
                  • apps/tauri/src/types/acp.ts — add TS types
                  • apps/tauri/src/components/ActiveSessionView.tsx — add log viewer access
                  • apps/tauri/src/locales/{en,pt-BR}.json — add log-related i18n keys

                  Rust side — ConnectionLogEntry type:

                  pubstructConnectionLogEntry{pubtimestamp:String,publevel:String,// "info" | "warn" | "error"pubevent:String,// "connect" | "disconnect" | "heartbeat_ok" | "ping_timeout" | "error"pubmessage:String,pubworkspace_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.rs on connect/disconnect

                  Frontend — useAcpLogs hook:

                  • Listens to "acp:log" event, filters by workspaceId
                  • 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 Activity icon 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 ScrollArea from 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.ts
                  • apps/tauri/src/__tests__/hooks/useAcpLogs.test.ts

                  useAcpConnection.test.ts — test cases:

                  1. Starts with idle status, isConnected=false, isConnecting=false, no error
                  2. connect() calls acp_connect → transitions to connectingconnected
                  3. connect() failure → sets connectionError, status disconnected
                  4. disconnect() → calls acp_disconnect, resets to idle
                  5. Ignores connect() when already connecting
                  6. Resets state when workspaceId changes
                  7. Responds to "acp:connection-status" events from Rust
                  8. Ignores events for different workspaceId
                  9. Calls acp_disconnect on unmount cleanup
                  10. Reads localStorage for arandu-copilot-path and arandu-gh-token

                  useAcpLogs.test.ts — test cases:

                  1. Starts with empty logs
                  2. Accumulates log entries from "acp:log" events
                  3. Caps at 200 entries (drops oldest)
                  4. Filters by workspaceId
                  5. clearLogs() empties the array
                  6. Cleans up listener on unmount

                  Implementation Sequence

                  1. Brainstorm card fix (CSS only, quick win)
                  2. Connect/Disconnect toggle (frontend, small change)
                  3. Heartbeat enhancement (Rust backend)
                  4. Diagnostics types + log emission (Rust backend)
                  5. useAcpLogs hook + ConnectionLogs component (frontend)
                  6. i18n keys (both locales)
                  7. Tests (hooks)

                  Verification

                  1. npm test — run Vitest suite
                  2. npm run build — verify TypeScript compiles
                  3. cargo check — verify Rust compiles
                  4. make 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)

                  Metadata

                  Metadata

                  Assignees

                  No one assigned

                    Labels

                    No labels
                    No labels

                    Type

                    No type

                    Projects

                    No projects

                      Milestone

                      No milestone

                      Relationships

                      None yet

                      Development

                      No branches or pull requests

                      Issue actions

                      , 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                      Skip to content

                      Fix life cycle ACP client connection #37

                      Description

                      @wilcorrea

                      Context

                      The ACP (Agent Communication Protocol) connection lifecycle in Arandu has several UX and reliability issues:

                      1. 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.
                      2. Brainstorm card overflow: The initial prompt card in TerminalChat uses w-full + mx-4, causing horizontal overflow and an unwanted scrollbar.
                      3. Shallow heartbeat: The current 15s heartbeat only checks if the child process is alive (try_wait()), but doesn't verify JSON-RPC responsiveness.
                      4. No connection diagnostics: Errors are volatile state — no persistent log to inspect connection history.
                      5. Missing tests: No tests for useAcpConnection hook 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-stretch makes children fill width naturally; mx-4 on the card provides margins
                        • Remove pt-2 since py-4 on scroll container (line 47) already provides 16px top padding
                      • Line 51: Remove w-full from "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.tsx
                      • apps/tauri/src/components/DirectoryWorkspace.tsx
                      • apps/tauri/src/components/TerminalChat.tsx
                      • apps/tauri/src/locales/en.json
                      • apps/tauri/src/locales/pt-BR.json

                      Changes:

                      ActiveSessionView.tsx — Make the existing Connect button a toggle (lines 218-234):

                      • When disconnected: Show Plug icon + "Connect" (current behavior)
                      • When connected: Show Unplug icon + "Disconnect" (new — replaces hiding the button)
                      • When connecting: Show Loader2 spinner + "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.rs
                      • apps/tauri/src-tauri/src/acp/types.rs

                      Changes to connection.rs:

                      1. Change next_id from AtomicU64 to Arc<AtomicU64> so the heartbeat task can share the ID generator
                      2. Refactor heartbeat_task signature to receive writer_tx, pending, and next_id clones
                      3. 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
                      4. Update spawn() to pass the additional args to the heartbeat task

                      Changes to types.rs — Add HeartbeatEvent:

                      pubstructHeartbeatEvent{pubworkspace_id:String,pubstatus:String,// "healthy" | "degraded" | "disconnected"publatency_ms:Option<u64>,pubtimestamp: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.ts
                      • apps/tauri/src/components/ConnectionLogs.tsx

                      Files to modify:

                      • apps/tauri/src-tauri/src/acp/connection.rs — add emit_log() helper, instrument spawn, reader_task, heartbeat_task, shutdown
                      • apps/tauri/src-tauri/src/acp/commands.rs — instrument acp_connect, acp_disconnect
                      • apps/tauri/src-tauri/src/acp/types.rs — add ConnectionLogEntry type
                      • apps/tauri/src/types/acp.ts — add TS types
                      • apps/tauri/src/components/ActiveSessionView.tsx — add log viewer access
                      • apps/tauri/src/locales/{en,pt-BR}.json — add log-related i18n keys

                      Rust side — ConnectionLogEntry type:

                      pubstructConnectionLogEntry{pubtimestamp:String,publevel:String,// "info" | "warn" | "error"pubevent:String,// "connect" | "disconnect" | "heartbeat_ok" | "ping_timeout" | "error"pubmessage:String,pubworkspace_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.rs on connect/disconnect

                      Frontend — useAcpLogs hook:

                      • Listens to "acp:log" event, filters by workspaceId
                      • 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 Activity icon 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 ScrollArea from 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.ts
                      • apps/tauri/src/__tests__/hooks/useAcpLogs.test.ts

                      useAcpConnection.test.ts — test cases:

                      1. Starts with idle status, isConnected=false, isConnecting=false, no error
                      2. connect() calls acp_connect → transitions to connectingconnected
                      3. connect() failure → sets connectionError, status disconnected
                      4. disconnect() → calls acp_disconnect, resets to idle
                      5. Ignores connect() when already connecting
                      6. Resets state when workspaceId changes
                      7. Responds to "acp:connection-status" events from Rust
                      8. Ignores events for different workspaceId
                      9. Calls acp_disconnect on unmount cleanup
                      10. Reads localStorage for arandu-copilot-path and arandu-gh-token

                      useAcpLogs.test.ts — test cases:

                      1. Starts with empty logs
                      2. Accumulates log entries from "acp:log" events
                      3. Caps at 200 entries (drops oldest)
                      4. Filters by workspaceId
                      5. clearLogs() empties the array
                      6. Cleans up listener on unmount

                      Implementation Sequence

                      1. Brainstorm card fix (CSS only, quick win)
                      2. Connect/Disconnect toggle (frontend, small change)
                      3. Heartbeat enhancement (Rust backend)
                      4. Diagnostics types + log emission (Rust backend)
                      5. useAcpLogs hook + ConnectionLogs component (frontend)
                      6. i18n keys (both locales)
                      7. Tests (hooks)

                      Verification

                      1. npm test — run Vitest suite
                      2. npm run build — verify TypeScript compiles
                      3. cargo check — verify Rust compiles
                      4. make 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)

                      Metadata

                      Metadata

                      Assignees

                      No one assigned

                        Labels

                        No labels
                        No labels

                        Type

                        No type

                        Projects

                        No projects

                          Milestone

                          No milestone

                          Relationships

                          None yet

                          Development

                          No branches or pull requests

                          Issue actions

                          , 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                          Skip to content

                          Fix life cycle ACP client connection #37

                          Description

                          @wilcorrea

                          Context

                          The ACP (Agent Communication Protocol) connection lifecycle in Arandu has several UX and reliability issues:

                          1. 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.
                          2. Brainstorm card overflow: The initial prompt card in TerminalChat uses w-full + mx-4, causing horizontal overflow and an unwanted scrollbar.
                          3. Shallow heartbeat: The current 15s heartbeat only checks if the child process is alive (try_wait()), but doesn't verify JSON-RPC responsiveness.
                          4. No connection diagnostics: Errors are volatile state — no persistent log to inspect connection history.
                          5. Missing tests: No tests for useAcpConnection hook 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-stretch makes children fill width naturally; mx-4 on the card provides margins
                            • Remove pt-2 since py-4 on scroll container (line 47) already provides 16px top padding
                          • Line 51: Remove w-full from "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.tsx
                          • apps/tauri/src/components/DirectoryWorkspace.tsx
                          • apps/tauri/src/components/TerminalChat.tsx
                          • apps/tauri/src/locales/en.json
                          • apps/tauri/src/locales/pt-BR.json

                          Changes:

                          ActiveSessionView.tsx — Make the existing Connect button a toggle (lines 218-234):

                          • When disconnected: Show Plug icon + "Connect" (current behavior)
                          • When connected: Show Unplug icon + "Disconnect" (new — replaces hiding the button)
                          • When connecting: Show Loader2 spinner + "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.rs
                          • apps/tauri/src-tauri/src/acp/types.rs

                          Changes to connection.rs:

                          1. Change next_id from AtomicU64 to Arc<AtomicU64> so the heartbeat task can share the ID generator
                          2. Refactor heartbeat_task signature to receive writer_tx, pending, and next_id clones
                          3. 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
                          4. Update spawn() to pass the additional args to the heartbeat task

                          Changes to types.rs — Add HeartbeatEvent:

                          pubstructHeartbeatEvent{pubworkspace_id:String,pubstatus:String,// "healthy" | "degraded" | "disconnected"publatency_ms:Option<u64>,pubtimestamp: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.ts
                          • apps/tauri/src/components/ConnectionLogs.tsx

                          Files to modify:

                          • apps/tauri/src-tauri/src/acp/connection.rs — add emit_log() helper, instrument spawn, reader_task, heartbeat_task, shutdown
                          • apps/tauri/src-tauri/src/acp/commands.rs — instrument acp_connect, acp_disconnect
                          • apps/tauri/src-tauri/src/acp/types.rs — add ConnectionLogEntry type
                          • apps/tauri/src/types/acp.ts — add TS types
                          • apps/tauri/src/components/ActiveSessionView.tsx — add log viewer access
                          • apps/tauri/src/locales/{en,pt-BR}.json — add log-related i18n keys

                          Rust side — ConnectionLogEntry type:

                          pubstructConnectionLogEntry{pubtimestamp:String,publevel:String,// "info" | "warn" | "error"pubevent:String,// "connect" | "disconnect" | "heartbeat_ok" | "ping_timeout" | "error"pubmessage:String,pubworkspace_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.rs on connect/disconnect

                          Frontend — useAcpLogs hook:

                          • Listens to "acp:log" event, filters by workspaceId
                          • 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 Activity icon 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 ScrollArea from 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.ts
                          • apps/tauri/src/__tests__/hooks/useAcpLogs.test.ts

                          useAcpConnection.test.ts — test cases:

                          1. Starts with idle status, isConnected=false, isConnecting=false, no error
                          2. connect() calls acp_connect → transitions to connectingconnected
                          3. connect() failure → sets connectionError, status disconnected
                          4. disconnect() → calls acp_disconnect, resets to idle
                          5. Ignores connect() when already connecting
                          6. Resets state when workspaceId changes
                          7. Responds to "acp:connection-status" events from Rust
                          8. Ignores events for different workspaceId
                          9. Calls acp_disconnect on unmount cleanup
                          10. Reads localStorage for arandu-copilot-path and arandu-gh-token

                          useAcpLogs.test.ts — test cases:

                          1. Starts with empty logs
                          2. Accumulates log entries from "acp:log" events
                          3. Caps at 200 entries (drops oldest)
                          4. Filters by workspaceId
                          5. clearLogs() empties the array
                          6. Cleans up listener on unmount

                          Implementation Sequence

                          1. Brainstorm card fix (CSS only, quick win)
                          2. Connect/Disconnect toggle (frontend, small change)
                          3. Heartbeat enhancement (Rust backend)
                          4. Diagnostics types + log emission (Rust backend)
                          5. useAcpLogs hook + ConnectionLogs component (frontend)
                          6. i18n keys (both locales)
                          7. Tests (hooks)

                          Verification

                          1. npm test — run Vitest suite
                          2. npm run build — verify TypeScript compiles
                          3. cargo check — verify Rust compiles
                          4. make 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)

                          Metadata

                          Metadata

                          Assignees

                          No one assigned

                            Labels

                            No labels
                            No labels

                            Type

                            No type

                            Projects

                            No projects

                              Milestone

                              No milestone

                              Relationships

                              None yet

                              Development

                              No branches or pull requests

                              Issue actions

                              , 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                              Skip to content

                              Fix life cycle ACP client connection #37

                              Description

                              @wilcorrea

                              Context

                              The ACP (Agent Communication Protocol) connection lifecycle in Arandu has several UX and reliability issues:

                              1. 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.
                              2. Brainstorm card overflow: The initial prompt card in TerminalChat uses w-full + mx-4, causing horizontal overflow and an unwanted scrollbar.
                              3. Shallow heartbeat: The current 15s heartbeat only checks if the child process is alive (try_wait()), but doesn't verify JSON-RPC responsiveness.
                              4. No connection diagnostics: Errors are volatile state — no persistent log to inspect connection history.
                              5. Missing tests: No tests for useAcpConnection hook 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-stretch makes children fill width naturally; mx-4 on the card provides margins
                                • Remove pt-2 since py-4 on scroll container (line 47) already provides 16px top padding
                              • Line 51: Remove w-full from "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.tsx
                              • apps/tauri/src/components/DirectoryWorkspace.tsx
                              • apps/tauri/src/components/TerminalChat.tsx
                              • apps/tauri/src/locales/en.json
                              • apps/tauri/src/locales/pt-BR.json

                              Changes:

                              ActiveSessionView.tsx — Make the existing Connect button a toggle (lines 218-234):

                              • When disconnected: Show Plug icon + "Connect" (current behavior)
                              • When connected: Show Unplug icon + "Disconnect" (new — replaces hiding the button)
                              • When connecting: Show Loader2 spinner + "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.rs
                              • apps/tauri/src-tauri/src/acp/types.rs

                              Changes to connection.rs:

                              1. Change next_id from AtomicU64 to Arc<AtomicU64> so the heartbeat task can share the ID generator
                              2. Refactor heartbeat_task signature to receive writer_tx, pending, and next_id clones
                              3. 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
                              4. Update spawn() to pass the additional args to the heartbeat task

                              Changes to types.rs — Add HeartbeatEvent:

                              pubstructHeartbeatEvent{pubworkspace_id:String,pubstatus:String,// "healthy" | "degraded" | "disconnected"publatency_ms:Option<u64>,pubtimestamp: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.ts
                              • apps/tauri/src/components/ConnectionLogs.tsx

                              Files to modify:

                              • apps/tauri/src-tauri/src/acp/connection.rs — add emit_log() helper, instrument spawn, reader_task, heartbeat_task, shutdown
                              • apps/tauri/src-tauri/src/acp/commands.rs — instrument acp_connect, acp_disconnect
                              • apps/tauri/src-tauri/src/acp/types.rs — add ConnectionLogEntry type
                              • apps/tauri/src/types/acp.ts — add TS types
                              • apps/tauri/src/components/ActiveSessionView.tsx — add log viewer access
                              • apps/tauri/src/locales/{en,pt-BR}.json — add log-related i18n keys

                              Rust side — ConnectionLogEntry type:

                              pubstructConnectionLogEntry{pubtimestamp:String,publevel:String,// "info" | "warn" | "error"pubevent:String,// "connect" | "disconnect" | "heartbeat_ok" | "ping_timeout" | "error"pubmessage:String,pubworkspace_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.rs on connect/disconnect

                              Frontend — useAcpLogs hook:

                              • Listens to "acp:log" event, filters by workspaceId
                              • 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 Activity icon 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 ScrollArea from 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.ts
                              • apps/tauri/src/__tests__/hooks/useAcpLogs.test.ts

                              useAcpConnection.test.ts — test cases:

                              1. Starts with idle status, isConnected=false, isConnecting=false, no error
                              2. connect() calls acp_connect → transitions to connectingconnected
                              3. connect() failure → sets connectionError, status disconnected
                              4. disconnect() → calls acp_disconnect, resets to idle
                              5. Ignores connect() when already connecting
                              6. Resets state when workspaceId changes
                              7. Responds to "acp:connection-status" events from Rust
                              8. Ignores events for different workspaceId
                              9. Calls acp_disconnect on unmount cleanup
                              10. Reads localStorage for arandu-copilot-path and arandu-gh-token

                              useAcpLogs.test.ts — test cases:

                              1. Starts with empty logs
                              2. Accumulates log entries from "acp:log" events
                              3. Caps at 200 entries (drops oldest)
                              4. Filters by workspaceId
                              5. clearLogs() empties the array
                              6. Cleans up listener on unmount

                              Implementation Sequence

                              1. Brainstorm card fix (CSS only, quick win)
                              2. Connect/Disconnect toggle (frontend, small change)
                              3. Heartbeat enhancement (Rust backend)
                              4. Diagnostics types + log emission (Rust backend)
                              5. useAcpLogs hook + ConnectionLogs component (frontend)
                              6. i18n keys (both locales)
                              7. Tests (hooks)

                              Verification

                              1. npm test — run Vitest suite
                              2. npm run build — verify TypeScript compiles
                              3. cargo check — verify Rust compiles
                              4. make 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)

                              Metadata

                              Metadata

                              Assignees

                              No one assigned

                                Labels

                                No labels
                                No labels

                                Type

                                No type

                                Projects

                                No projects

                                  Milestone

                                  No milestone

                                  Relationships

                                  None yet

                                  Development

                                  No branches or pull requests

                                  Issue actions