Skip to content

feat(acp): Session persistence, connection health and UI polish#45

Merged
wilcorrea merged 15 commits into
mainfrom
feat/acp-session-persistence
Mar 5, 2026
Merged

feat(acp): Session persistence, connection health and UI polish#45
wilcorrea merged 15 commits into
mainfrom
feat/acp-session-persistence

Conversation

@wilcorrea

@wilcorrea wilcorrea commented Mar 5, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Session state persistence: ACP session messages, mode, and streaming state are cached in a centralized store (session-cache.ts) with pub/sub, surviving minimize/restore and workspace switches without losing chat history
  • Connection health monitoring: Heartbeat system with periodic pings, automatic dead connection cleanup, and a connection logs panel for diagnostics
  • Session naming: Prepend session name to initial prompt so Copilot CLI derives meaningful session titles
  • Prompt timeout fix: session/prompt JSON-RPC requests now use a 10-minute timeout instead of 30 seconds, preventing false timeout errors during long agent operations
  • UI improvements: Collapsible user messages in chat, trash icon for session deletion, review panel button repositioned, terminal message styling refinements

Changes

Backend (Rust)

  • connection.rs: Add send_request_with_timeout for configurable JSON-RPC timeouts; heartbeat with 3-strike disconnect; connection logging events
  • commands.rs: acp_send_prompt uses 600s timeout; new session commands
  • sessions.rs: New session_update_chat_panel_size command for layout persistence
  • types.rs: New event types for connection status, heartbeat, and logs

Frontend (TypeScript/React)

  • session-cache.ts: New centralized session state store with pub/sub pattern
  • useAcpSession.ts: Refactored to use session-cache store instead of local state
  • useAcpConnection.ts: Heartbeat monitoring and status events
  • useAcpLogs.ts: New hook for connection log collection
  • ConnectionLogs.tsx: New diagnostics panel component
  • TerminalMessage.tsx: Collapsible long user messages with <details> toggle
  • ActiveSessionView.tsx: Session name prepended to initial prompt; layout persistence
  • SessionCard.tsx: Trash icon replaces X for delete action
  • MarkdownViewer.tsx: Review panel button repositioned

Tests

  • useAcpConnection.test.ts: 10 tests for connection lifecycle
  • useAcpLogs.test.ts: 7 tests for log collection and filtering

Test plan

  • npm test — 86 tests passing (15 test files)
  • cargo check — Rust compilation clean
  • Manual: Create new session → verify Copilot CLI shows session name as title
  • Manual: Minimize/restore workspace → verify chat history preserved
  • Manual: Long-running agent prompt → no timeout error
  • Manual: Collapse/expand long user messages in chat

Summary by CodeRabbit

  • New Features

    • Connection heartbeat with status events, structured connection logs (view, copy, clear), and persisted per-workspace log aggregation.
    • Persistent chat-panel sizing with UI controls and session persistence/recovery.
    • Session cache for faster session state sync and new command to update chat panel size.
  • Bug Fixes

    • Deterministic cleanup of stale connections and extended request timeouts for improved reliability.
  • UI/UX Improvements

    • Collapsible terminal messages, unified connection controls, log badges/overlays.
  • Tests

    • Added tests for connection and log hooks.

- Add Rust-side heartbeat task with 15s ping interval and 3-strike disconnect
- Clean up dead connections in `acp_connect` and `acp_check_health`
- Emit structured connection log events via `acp:log`
- Add `HeartbeatEvent`, `ConnectionLogEntry` and `ConnectionConfig` types
- Add `chat_panel_size` field to session persistence
…istory

- Add in-memory session cache with global event listener independent of React lifecycle
- Keep DirectoryWorkspace mounted via CSS hidden when minimized
- Add `persistedWorkspaceId` to AppContext for mount/visibility separation
- Always call `acp_load_session` to replay full conversation from ACP backend
- Restore cached messages on "already loaded" fallback
- Add periodic 30s heartbeat in `useAcpConnection` for proactive disconnect detection
- Remove auto-disconnect on unmount; move disconnect to explicit close/forget
- Add connection logs panel with `useAcpLogs` hook and `ConnectionLogs` component
- Add i18n strings for connection logs and ACP status
- Test connect/disconnect lifecycle, status events, workspace isolation
- Test cached state restoration and health check on mount
- Verify disconnect is not called on unmount
- Test log capture, filtering and clear behavior
@coderabbitai

coderabbitai Bot commented Mar 5, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@wilcorrea has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 10 minutes and 26 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cc099416-501a-421e-aca1-b03d1a51afb0

📥 Commits

Reviewing files that changed from the base of the PR and between ee78841 and 1da95a8.

📒 Files selected for processing (1)
  • apps/tauri/src-tauri/src/acp/commands.rs
📝 Walkthrough

Walkthrough

Backend: Hardened ACP connection lifecycle with heartbeat, timed JSON-RPC requests, structured connection logs, and deterministic cleanup. Frontend: session-cache stores and subscriptions, connection log aggregation + UI, connect/disconnect flow, chat-panel size persistence, new hooks/tests, and added types/locales.

Changes

Cohort / File(s) Summary
ACP Commands & Connection
apps/tauri/src-tauri/src/acp/commands.rs, apps/tauri/src-tauri/src/acp/connection.rs
Deterministic cleanup on connect/check paths; shared next_id via Arc<AtomicU64>; heartbeat loop with ping+timeout, failure counting and termination; send_request_with_timeout and 600s prompt timeout; emit_log helpers and structured lifecycle logging.
ACP Types (Rust & TS)
apps/tauri/src-tauri/src/acp/types.rs, apps/tauri/src/types/acp.ts
Add heartbeat and connection-log event types (Rust structs and TS interfaces) for emitting status, latency, and structured logs.
Session Cache & Stores
apps/tauri/src/lib/session-cache.ts
New in-memory stores (connection/session/ui), subscriber model, subscribeSession/updateSessionEntry/addUserMessage, processSessionUpdate merging many ACP update types, global listener, and clearWorkspaceCaches.
Frontend Hooks
apps/tauri/src/hooks/useAcpConnection.ts, apps/tauri/src/hooks/useAcpLogs.ts
useAcpConnection: cache hydration/persistence, periodic (30s) and visibility-triggered health checks, status persistence and event handling. useAcpLogs: acp:log listener, 200-entry ring buffer, clearLogs, and hasRecentErrors.
Connection Logging UI
apps/tauri/src/components/ConnectionLogs.tsx
New component: dialog log viewer with timestamped, level-colored entries, copy and clear actions, and unread/error badge.
Active Session & Workspace UI
apps/tauri/src/components/ActiveSessionView.tsx, apps/tauri/src/components/DirectoryWorkspace.tsx
Replace onReconnect with onDisconnect/onConnect; integrate logs; auto-mount cached session and persist mounted id; persist chat panel size via session_update_chat_panel_size; adjust initial ACP startup for new sessions.
Terminal Messages & Styles
apps/tauri/src/components/TerminalChat.tsx, apps/tauri/src/components/TerminalMessage.tsx, apps/tauri/src/index.css, apps/tauri/src/lib/format-path.ts
Remove reconnect UI; add collapsible user/tool/thinking message rendering and markdown wrapper; CSS for collapsible terminal styles; add shortenPaths utility.
Session Persistence & Context
apps/tauri/src/contexts/AppContext.tsx, apps/tauri/src/App.tsx
Add persistedWorkspaceId to context and provider; use persisted id for DirectoryWorkspace rendering; call clearWorkspaceCaches and disconnect on close/forget flows.
Database & Tauri Command
apps/tauri/src-tauri/src/sessions.rs, apps/tauri/src-tauri/src/lib.rs
Add chat_panel_size column migration; SessionRecord.chat_panel_size; update_chat_panel_size DB function and session_update_chat_panel_size command exported to frontend.
Tests
apps/tauri/src/__tests__/hooks/useAcpConnection.test.ts, apps/tauri/src/__tests__/hooks/useAcpLogs.test.ts
Add tests covering connection lifecycle, debounce, event handling, localStorage integration, and log accumulation/eviction, workspace filtering, and cleanup.
Types & Small UI Changes
apps/tauri/src/types/index.ts, apps/tauri/src/components/SessionCard.tsx, apps/tauri/src/components/ErrorConsole.tsx, apps/tauri/src/components/MarkdownViewer.tsx
Add chat_panel_size to SessionRecord; swap delete icon; minor styling/localization tweaks and small button position adjustment.
Localization
apps/tauri/src/locales/en.json, apps/tauri/src/locales/pt-BR.json
Add keys for dismiss, ACP disconnect/reconnect and connection logs (labels, empty state, clear, copy), plus terminal.done.

Sequence Diagram(s)

sequenceDiagram
    participant FE as Frontend<br/>(useAcpConnection / UI)
    participant Cmd as Rust Command<br/>(acp_connect / acp_check_health)
    participant Conn as AcpConnection<br/>(heartbeat task)
    participant Proc as Child Process<br/>(Agent)
    participant EVT as Event Emitter<br/>(acp:connection-status / acp:heartbeat / acp:log)

    FE->>Cmd: acp_connect(workspaceId, ...)
    Cmd->>Cmd: remove existing connection from map
    alt existing alive
        Cmd-->>FE: reinsert & return (reuse)
    else dead or missing
        Cmd->>Proc: spawn agent
        Cmd->>Conn: start reader + heartbeat
        Cmd->>EVT: emit_log("info","connect","started")
    end

    loop heartbeat (every 30s or on visibility)
        Conn->>Proc: JSON-RPC "ping" with timeout
        alt response within timeout
            Proc-->>Conn: pong
            Conn->>EVT: emit heartbeat("healthy", latency)
        else timeout/failure
            Conn->>Conn: increment fail count
            Conn->>EVT: emit heartbeat("degraded"/"disconnected")
            alt fail_count >= 3
                Conn->>Proc: terminate
                Conn->>EVT: emit_log("error","heartbeat","disconnected")
            end
        end
    end

    FE->>EVT: listen events
    EVT-->>FE: update UI, logs, cache
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • Fix life cycle ACP client connection #37: Modifies the same ACP connection lifecycle code (heartbeat, send_request_with_timeout, emit_log) and related frontend hooks/UI, indicating it is the same or directly related workstream.

Possibly related PRs

Poem

🐰
I hop and ping to keep the beat,
I log each breath and time each beat,
Three missed replies — I fold my paw,
Cache the tales so UIs saw,
A little rabbit, tidy and neat.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.65% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: session persistence, connection health monitoring, and UI polish updates across both backend and frontend.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/acp-session-persistence

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@wilcorrea wilcorrea changed the title feat(acp): Session persistence, heartbeat and conversation reload feat(acp): Session persistence, connection health and UI polish Mar 5, 2026
…und"

- Reset `initRef` when `isConnected` goes false so reconnect triggers
  `doInit` and reloads the session in the new copilot process
- Auto-recover in `sendPrompt`: on "Session not found" (-32602), try
  `acp_load_session` then retry the prompt before surfacing the error
If no streaming event arrives within 60s, `isStreaming` is reset to
false automatically. Prevents the Stop button from staying red
indefinitely when `end_turn` is never received.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/tauri/src-tauri/src/acp/connection.rs (1)

343-358: ⚠️ Potential issue | 🟠 Major

Remove pending request entries on all error exits.

send_request_with_timeout inserts into pending but does not clean up on writer send failure, timeout, or dropped response channel.

Suggested cleanup patch
         self.writer_tx
             .send(line)
             .await
-            .map_err(|_| "Writer channel closed".to_string())?;
+            .map_err(|_| {
+                // ensure no stale pending entry on send failure
+                // (remove in a fire-and-forget async block is not possible here,
+                // so do it synchronously below before returning)
+                "Writer channel closed".to_string()
+            })?;
 
-        let result = tokio::time::timeout(timeout, rx)
-            .await
-            .map_err(|_| format!("Timeout waiting for response to {}", method))?
-            .map_err(|_| "Response channel dropped".to_string())?;
+        let result = match tokio::time::timeout(timeout, rx).await {
+            Ok(Ok(v)) => v,
+            Ok(Err(_)) => {
+                self.pending.lock().await.remove(&id);
+                return Err("Response channel dropped".to_string());
+            }
+            Err(_) => {
+                self.pending.lock().await.remove(&id);
+                return Err(format!("Timeout waiting for response to {}", method));
+            }
+        };
 
         result.map_err(|e| e.to_string())
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/tauri/src-tauri/src/acp/connection.rs` around lines 343 - 358,
send_request_with_timeout currently inserts (id, tx) into self.pending but
returns early on errors (writer_tx.send failure, timeout, or rx closed) without
removing that entry; update the function so that any error path removes the
pending entry for id before returning. Locate the block that creates
oneshot::channel and inserts into self.pending (use symbols id, tx, rx,
pending_guard, self.pending, writer_tx.send, tokio::time::timeout) and ensure
you remove the pending entry (e.g., lock self.pending and remove(id)) on all
failure branches (writer send Err, timeout Err, rx.map_err Err) — use an
explicit cleanup before each early return or add a small scope-guard helper to
automatically remove the entry on drop to avoid leaks.
🧹 Nitpick comments (8)
apps/tauri/src/components/ErrorConsole.tsx (1)

14-19: Keep error indicators on semantic destructive text tokens

Switching icon/message/count text to text-foreground/* weakens error-state semantics. For this error console, use destructive foreground tokens for status indicators.

♻️ Proposed patch
-      <AlertCircle className="h-3.5 w-3.5 text-foreground/60 mt-0.5 shrink-0" />
-      <p className="flex-1 font-mono text-xs text-foreground/70 break-words leading-relaxed">
+      <AlertCircle className="h-3.5 w-3.5 text-destructive mt-0.5 shrink-0" />
+      <p className="flex-1 font-mono text-xs text-destructive/90 break-words leading-relaxed">
         {lastError}
         {errors.length > 1 && (
-          <span className="ml-2 text-foreground/40">(+{errors.length - 1})</span>
+          <span className="ml-2 text-destructive/70">(+{errors.length - 1})</span>
         )}
       </p>

Based on learnings: “Use semantic SHADCN UI tokens for status indicators… use text-destructive (foreground variants) instead of non-semantic colors.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/tauri/src/components/ErrorConsole.tsx` around lines 14 - 19, The
icon/message/count currently use non-semantic foreground tokens; update the
ErrorConsole UI to use destructive foreground tokens for error semantics by
replacing text-foreground/* with text-destructive/* where the error is rendered
(in apps/tauri/src/components/ErrorConsole.tsx) — specifically adjust the
AlertCircle className, the <p> that renders {lastError}, and the <span> that
renders the count ( (+{errors.length - 1}) ) so they use text-destructive (and
appropriate opacity variants) instead of text-foreground to preserve semantic
error styling.
apps/tauri/src/components/DirectoryWorkspace.tsx (1)

31-39: Consider adding workspaceId to dependency array for cache invalidation.

The effect restores cachedMountedSessionId on mount, but if workspace.id changes while sessions are still loading, the stale cachedMountedSessionId from the previous workspace could be used since the variable is derived outside the effect.

Currently safe because cachedMountedSessionId is re-derived on each render from the current workspace, but explicitly listing workspace?.id in the dependency array would make the intent clearer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/tauri/src/components/DirectoryWorkspace.tsx` around lines 31 - 39, The
useEffect that restores cachedMountedSessionId should also depend on
workspace?.id to ensure the cached session from a previous workspace isn't
reused; update the dependency array of the useEffect containing
cachedMountedSessionId, local.sessions, and local.loading to include
workspace?.id so the effect re-runs when the workspace changes and correctly
validates mountedSessionRef/current and calls setBrowsing(false) only for the
current workspace.
apps/tauri/src/components/ConnectionLogs.tsx (3)

29-34: Consider adding user feedback after copy.

The copy action completes silently. A brief toast or visual confirmation would improve UX.

♻️ Example using sonner toast
+import { toast } from "sonner";
+
 const handleCopy = () => {
   const text = logs
     .map((l) => `[${l.timestamp}] [${l.level.toUpperCase()}] ${l.event}: ${l.message}`)
     .join("\n");
-  navigator.clipboard.writeText(text);
+  navigator.clipboard.writeText(text).then(() => {
+    toast.success(t("review.copied"));
+  });
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/tauri/src/components/ConnectionLogs.tsx` around lines 29 - 34,
handleCopy currently writes logs to the clipboard silently; update handleCopy to
await navigator.clipboard.writeText(text) and then show user feedback (success
or error) — e.g., call a toast or set a short-lived UI state — so users see
confirmation on success and an error message on failure; reference the
handleCopy function and the clipboard writeText call, and use your app's
existing toast utility (or add sonner's toast.success / toast.error) to display
the messages.

46-48: Consider using semantic token for error badge.

The error indicator badge uses hardcoded bg-orange-500. For theme consistency, consider bg-warning or bg-destructive.

♻️ Proposed fix
           {hasRecentErrors && (
-            <span className="absolute top-0.5 right-0.5 w-1.5 h-1.5 rounded-full bg-orange-500" />
+            <span className="absolute top-0.5 right-0.5 w-1.5 h-1.5 rounded-full bg-warning" />
           )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/tauri/src/components/ConnectionLogs.tsx` around lines 46 - 48, Replace
the hardcoded tailwind color on the error badge in the ConnectionLogs component:
locate the JSX that renders the small indicator when hasRecentErrors (the <span
className="... bg-orange-500" />) and swap the fixed color class for a semantic
token class (e.g., bg-warning or bg-destructive) so the badge follows theme
tokens; update the className on that span accordingly and run the UI to verify
the badge adopts the theme color.

14-18: Use semantic SHADCN UI tokens instead of hardcoded Tailwind colors.

Per coding guidelines, status indicators should use theme-aware semantic tokens rather than hardcoded colors for proper light/dark mode support.

♻️ Proposed fix using semantic tokens
 const LEVEL_STYLES: Record<string, string> = {
-  info: "text-blue-500",
-  warn: "text-yellow-500",
-  error: "text-red-500",
+  info: "text-muted-foreground",
+  warn: "text-warning",
+  error: "text-destructive",
 };

Based on learnings: "Use text-success, text-warning, and text-destructive tokens (foreground variants) instead of hardcoded colors."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/tauri/src/components/ConnectionLogs.tsx` around lines 14 - 18, Replace
the hardcoded Tailwind color classes in the LEVEL_STYLES constant with the
semantic shadcn UI foreground tokens so the UI respects light/dark themes;
update the LEVEL_STYLES mapping (const LEVEL_STYLES) to use "text-success",
"text-warning", and "text-destructive" (or the project’s equivalent foreground
variants) for "info", "warn", and "error" respectively, and keep all usages
(e.g., where LEVEL_STYLES[level] is applied in ConnectionLogs.tsx) unchanged so
styling becomes theme-aware.
apps/tauri/src/hooks/useAcpConnection.ts (1)

96-110: Potential duplicate status update on mount.

When a cached connection exists, this effect calls acp_check_health which both:

  1. Returns the status (handled here at lines 100-104)
  2. Emits acp:connection-status event (per context snippet from commands.rs line 279-280)

The event listener at line 50 will also update state, causing a double state update. This is harmless but slightly redundant.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/tauri/src/hooks/useAcpConnection.ts` around lines 96 - 110, The mount
effect calling acp_check_health can cause duplicate updates because the same
status is also emitted via the acp:connection-status event; to avoid redundant
state writes, update connectedRef, setConnectionStatus and setConnectionError
only if the returned status differs from current state: inside the .then handler
for acp_check_health, compare the resolved status (s) against
connectedRef.current or the current connectionStatus and only call
connectedRef.current = s === "connected", setConnectionStatus(s), or
setConnectionError(null) when there's an actual change; keep the event listener
logic (the handler registered elsewhere) unchanged.
apps/tauri/src/lib/session-cache.ts (2)

54-146: Consider extracting case handlers for improved testability.

The processSessionUpdate function handles multiple update types correctly, but its size (90+ lines) makes it harder to unit test individual handlers. Consider extracting each case into a named helper function.

♻️ Example refactor for one handler
function handleAgentMessageChunk(
  msgs: AcpMessage[],
  p: Record<string, unknown>,
  nextMsgId: () => string
): { msgs: AcpMessage[]; isStreaming: boolean } {
  const content = p.content as Record<string, unknown> | undefined;
  const text = content?.type === "text" ? (content.text as string) : "";
  if (!text) return { msgs, isStreaming: false };
  
  if (/^(Warning:|Info:|🔬|Experimental)/.test(text)) {
    msgs.push({ id: nextMsgId(), role: "assistant", type: "notice", content: text, timestamp: new Date() });
  } else {
    const last = msgs[msgs.length - 1];
    if (last?.role === "assistant" && !last.type) {
      msgs[msgs.length - 1] = { ...last, content: last.content + text };
    } else {
      msgs.push({ id: nextMsgId(), role: "assistant", content: text, timestamp: new Date() });
    }
  }
  return { msgs, isStreaming: true };
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/tauri/src/lib/session-cache.ts` around lines 54 - 146, The function
processSessionUpdate is large and hard to unit-test; extract each switch case
into small, named helper functions (e.g., handleAgentMessageChunk,
handleAgentThoughtChunk, handleToolCall, handleToolCallUpdate,
handleUserMessageChunk, handleCurrentModeUpdate) that accept the mutable pieces
(msgs, p, nextMsgId, entry fields) and return updated fragments (msgs,
isStreaming, agentPlanFilePath, currentMode) so processSessionUpdate becomes a
thin dispatcher that calls these helpers and composes their results; ensure each
helper is exported for unit tests and that existing symbols (nextMsgId, msgs
array mutation logic, msg properties like toolCallId/toolTitle/toolStatus) are
preserved exactly when refactoring.

158-174: Consider storing the unlisten function for proper cleanup.

The listen() call returns a Promise that resolves to an unlisten function, but it's currently discarded. While this singleton pattern is acceptable for production, it can cause issues during development with Hot Module Replacement (HMR), where the listener may accumulate across module reloads.

♻️ Proposed fix to store unlisten function
 let listenerSetup = false;
+let unlistenFn: (() => void) | null = null;
+
 function ensureGlobalListener() {
   if (listenerSetup) return;
   listenerSetup = true;
 
-  window.__TAURI__.event.listen<AcpSessionUpdate>("acp:session-update", (event: { payload: AcpSessionUpdate }) => {
+  window.__TAURI__.event.listen<AcpSessionUpdate>("acp:session-update", (event: { payload: AcpSessionUpdate }) => {
     const update = event.payload;
     const { workspaceId, sessionId } = update;
     const entry = sessions.get(workspaceId);
     if (!entry) return;
     if (entry.activeAcpSessionId && sessionId !== entry.activeAcpSessionId) return;
 
     const newEntry = processSessionUpdate(entry, update);
     sessions.set(workspaceId, newEntry);
     notifySubscribers(workspaceId, newEntry);
-  }).catch(console.error);
+  }).then((fn) => {
+    unlistenFn = fn;
+  }).catch(console.error);
 }
+
+// Optional: Export for cleanup during HMR or testing
+export function teardownGlobalListener() {
+  if (unlistenFn) {
+    unlistenFn();
+    unlistenFn = null;
+    listenerSetup = false;
+  }
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/tauri/src/lib/session-cache.ts` around lines 158 - 174, The
ensureGlobalListener currently discards the Promise returned by
window.__TAURI__.event.listen, causing orphaned listeners during HMR; capture
and store the unlisten function (e.g., in a module-level variable like
unlistenAcpSession) when listen() resolves, and before setting up a new listener
(or when resetting listenerSetup) call that stored unlisten function to remove
the previous listener; alternatively export a cleanup function (e.g.,
cleanupSessionCacheListeners) that calls the stored unlisten and resets
listenerSetup so tests/HMR can call it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/tauri/src-tauri/src/acp/commands.rs`:
- Around line 31-39: You're awaiting async operations while holding the
connections mutex; instead clone or take ownership of the connection under the
lock, drop the lock, then perform async checks/shutdowns. Concretely: when
handling state.connections.lock().await, if let Some(existing) =
connections.get(&workspace_id) { clone the connection (e.g., existing.clone() or
Arc::clone(existing)), drop the connections lock, await cloned.is_alive().await
and return if alive; if not alive, re-acquire the connections lock and remove
the entry (connections.remove(&workspace_id)), drop the lock, then await
dead.shutdown().await and finally lock state.configs to remove(&workspace_id).
Apply the same pattern to the other occurrence around lines 276-286.

In `@apps/tauri/src/components/ActiveSessionView.tsx`:
- Around line 127-136: The reconnect flow in handleReconnect currently aborts if
onDisconnect() rejects, so change handleReconnect to catch errors from
onDisconnect while still proceeding to call onConnect(); specifically, wrap the
await onDisconnect() call in a try/catch (or Promise.catch) and log or
setInitError with the disconnect error, but do not rethrow so that the
subsequent await onConnect() (and any initRef.current reset) always runs; update
references in handleReconnect to ensure onConnect is invoked regardless of
onDisconnect outcome and preserve existing state resets (initRef.current = false
and setInitError(null)).

In `@apps/tauri/src/components/ErrorConsole.tsx`:
- Around line 22-25: Replace the native <button> in ErrorConsole.tsx with the
shadcn/ui Button primitive (import Button from your app's Button/shadcn wrapper)
and replace the hardcoded aria-label "Dismiss" with a localized string from
useTranslation() (call useTranslation() in the ErrorConsole component and use
t('dismiss') or the appropriate key); keep the onClick handler (onClear) and
className/props behavior when migrating to Button so functionality and styles
remain the same.

In `@apps/tauri/src/components/TerminalMessage.tsx`:
- Line 73: Replace the hardcoded hex color class in the span inside
TerminalMessage.tsx (the element rendering the "done" status) with a semantic
SHADCN status token; for example swap text-[`#3dd68c`]/60 for a semantic class
such as text-success or text-success/60 (or text-warning/text-destructive where
appropriate) so the status color follows theme tokens and respects light/dark
styling.
- Around line 73-74: Import and use the useTranslation() hook in TerminalMessage
(e.g., const { t } = useTranslation()) and replace all hardcoded user-facing
strings ("done", "Thinking…", and the "{contentLines.length} lines..." message)
with translation keys using t('...') (add corresponding keys like terminal.done,
terminal.thinking, terminal.linesCount) and update the JSX spans where those
strings appear (including the occurrences around lines 80-83 and 100-103) to
call t and format the lines count (e.g., t('terminal.linesCount', { count:
contentLines.length })). Also replace the hardcoded tailwind color class
text-[`#3dd68c`]/60 with the semantic token text-success/60 to use theme tokens.

In `@apps/tauri/src/hooks/useAcpSession.ts`:
- Around line 73-82: The code only resets messages and isStreaming when loading
a cache entry via sessionStore.get(workspaceId), which can leave stale UI state
in currentMode, availableModes, activeAcpSessionId, agentPlanFilePath and
activeAcpSessionIdRef; create a single helper (e.g., resetAcpSessionState) that
clears/initializes all related state (setMessages, setIsStreaming,
setCurrentMode, setAvailableModes, setActiveAcpSessionId, setAgentPlanFilePath
and activeAcpSessionIdRef.current) and call it whenever an entry is missing or
you're replacing the cache entry (replace the current partial reset block around
sessionStore.get(workspaceId) and apply the same fix to the analogous blocks
around the other occurrences mentioned).

In `@apps/tauri/src/index.css`:
- Around line 599-605: Replace hardcoded hex colors in the CSS selectors (e.g.,
.terminal-markdown code and the other affected selectors around lines 721–730)
with theme-aware HSL CSS variables or Tailwind semantic tokens; update the color
declarations (currently using `#56d4dd` and `#3dd68c`) to reference your app's HSL
variables (for example --color-accent-h / --color-accent-s / --color-accent-l or
a single --color-accent) or Tailwind theme classes so they respect light/dark
modes, and keep other properties (background, border, padding, font-size)
unchanged; ensure you use the same variable names consistently across all
affected selectors to preserve theming.
- Around line 466-472: In .tool-content-inline replace the deprecated rule
word-break: break-word with overflow-wrap: anywhere and ensure color uses the
HSL theme variable (e.g., change color: hsl(var(--muted-foreground) / 0.7) if
not already); also find the places that use the hardcoded hex colors (`#56d4dd`
and `#3dd68c`) and replace them with the appropriate HSL CSS variables (e.g., use
hsl(var(--accent-foreground)) or another project-specific --* HSL variable) so
all color usages follow the HSL variable theming guideline.

---

Outside diff comments:
In `@apps/tauri/src-tauri/src/acp/connection.rs`:
- Around line 343-358: send_request_with_timeout currently inserts (id, tx) into
self.pending but returns early on errors (writer_tx.send failure, timeout, or rx
closed) without removing that entry; update the function so that any error path
removes the pending entry for id before returning. Locate the block that creates
oneshot::channel and inserts into self.pending (use symbols id, tx, rx,
pending_guard, self.pending, writer_tx.send, tokio::time::timeout) and ensure
you remove the pending entry (e.g., lock self.pending and remove(id)) on all
failure branches (writer send Err, timeout Err, rx.map_err Err) — use an
explicit cleanup before each early return or add a small scope-guard helper to
automatically remove the entry on drop to avoid leaks.

---

Nitpick comments:
In `@apps/tauri/src/components/ConnectionLogs.tsx`:
- Around line 29-34: handleCopy currently writes logs to the clipboard silently;
update handleCopy to await navigator.clipboard.writeText(text) and then show
user feedback (success or error) — e.g., call a toast or set a short-lived UI
state — so users see confirmation on success and an error message on failure;
reference the handleCopy function and the clipboard writeText call, and use your
app's existing toast utility (or add sonner's toast.success / toast.error) to
display the messages.
- Around line 46-48: Replace the hardcoded tailwind color on the error badge in
the ConnectionLogs component: locate the JSX that renders the small indicator
when hasRecentErrors (the <span className="... bg-orange-500" />) and swap the
fixed color class for a semantic token class (e.g., bg-warning or
bg-destructive) so the badge follows theme tokens; update the className on that
span accordingly and run the UI to verify the badge adopts the theme color.
- Around line 14-18: Replace the hardcoded Tailwind color classes in the
LEVEL_STYLES constant with the semantic shadcn UI foreground tokens so the UI
respects light/dark themes; update the LEVEL_STYLES mapping (const LEVEL_STYLES)
to use "text-success", "text-warning", and "text-destructive" (or the project’s
equivalent foreground variants) for "info", "warn", and "error" respectively,
and keep all usages (e.g., where LEVEL_STYLES[level] is applied in
ConnectionLogs.tsx) unchanged so styling becomes theme-aware.

In `@apps/tauri/src/components/DirectoryWorkspace.tsx`:
- Around line 31-39: The useEffect that restores cachedMountedSessionId should
also depend on workspace?.id to ensure the cached session from a previous
workspace isn't reused; update the dependency array of the useEffect containing
cachedMountedSessionId, local.sessions, and local.loading to include
workspace?.id so the effect re-runs when the workspace changes and correctly
validates mountedSessionRef/current and calls setBrowsing(false) only for the
current workspace.

In `@apps/tauri/src/components/ErrorConsole.tsx`:
- Around line 14-19: The icon/message/count currently use non-semantic
foreground tokens; update the ErrorConsole UI to use destructive foreground
tokens for error semantics by replacing text-foreground/* with
text-destructive/* where the error is rendered (in
apps/tauri/src/components/ErrorConsole.tsx) — specifically adjust the
AlertCircle className, the <p> that renders {lastError}, and the <span> that
renders the count ( (+{errors.length - 1}) ) so they use text-destructive (and
appropriate opacity variants) instead of text-foreground to preserve semantic
error styling.

In `@apps/tauri/src/hooks/useAcpConnection.ts`:
- Around line 96-110: The mount effect calling acp_check_health can cause
duplicate updates because the same status is also emitted via the
acp:connection-status event; to avoid redundant state writes, update
connectedRef, setConnectionStatus and setConnectionError only if the returned
status differs from current state: inside the .then handler for
acp_check_health, compare the resolved status (s) against connectedRef.current
or the current connectionStatus and only call connectedRef.current = s ===
"connected", setConnectionStatus(s), or setConnectionError(null) when there's an
actual change; keep the event listener logic (the handler registered elsewhere)
unchanged.

In `@apps/tauri/src/lib/session-cache.ts`:
- Around line 54-146: The function processSessionUpdate is large and hard to
unit-test; extract each switch case into small, named helper functions (e.g.,
handleAgentMessageChunk, handleAgentThoughtChunk, handleToolCall,
handleToolCallUpdate, handleUserMessageChunk, handleCurrentModeUpdate) that
accept the mutable pieces (msgs, p, nextMsgId, entry fields) and return updated
fragments (msgs, isStreaming, agentPlanFilePath, currentMode) so
processSessionUpdate becomes a thin dispatcher that calls these helpers and
composes their results; ensure each helper is exported for unit tests and that
existing symbols (nextMsgId, msgs array mutation logic, msg properties like
toolCallId/toolTitle/toolStatus) are preserved exactly when refactoring.
- Around line 158-174: The ensureGlobalListener currently discards the Promise
returned by window.__TAURI__.event.listen, causing orphaned listeners during
HMR; capture and store the unlisten function (e.g., in a module-level variable
like unlistenAcpSession) when listen() resolves, and before setting up a new
listener (or when resetting listenerSetup) call that stored unlisten function to
remove the previous listener; alternatively export a cleanup function (e.g.,
cleanupSessionCacheListeners) that calls the stored unlisten and resets
listenerSetup so tests/HMR can call it.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5d7e8c6e-d930-4b69-8093-826ad486150c

📥 Commits

Reviewing files that changed from the base of the PR and between 25da3bf and 989a1dc.

📒 Files selected for processing (26)
  • apps/tauri/src-tauri/src/acp/commands.rs
  • apps/tauri/src-tauri/src/acp/connection.rs
  • apps/tauri/src-tauri/src/acp/types.rs
  • apps/tauri/src-tauri/src/lib.rs
  • apps/tauri/src-tauri/src/sessions.rs
  • apps/tauri/src/App.tsx
  • apps/tauri/src/__tests__/hooks/useAcpConnection.test.ts
  • apps/tauri/src/__tests__/hooks/useAcpLogs.test.ts
  • apps/tauri/src/components/ActiveSessionView.tsx
  • apps/tauri/src/components/ConnectionLogs.tsx
  • apps/tauri/src/components/DirectoryWorkspace.tsx
  • apps/tauri/src/components/ErrorConsole.tsx
  • apps/tauri/src/components/MarkdownViewer.tsx
  • apps/tauri/src/components/TerminalChat.tsx
  • apps/tauri/src/components/TerminalMessage.tsx
  • apps/tauri/src/contexts/AppContext.tsx
  • apps/tauri/src/hooks/useAcpConnection.ts
  • apps/tauri/src/hooks/useAcpLogs.ts
  • apps/tauri/src/hooks/useAcpSession.ts
  • apps/tauri/src/index.css
  • apps/tauri/src/lib/format-path.ts
  • apps/tauri/src/lib/session-cache.ts
  • apps/tauri/src/locales/en.json
  • apps/tauri/src/locales/pt-BR.json
  • apps/tauri/src/types/acp.ts
  • apps/tauri/src/types/index.ts

Comment thread apps/tauri/src-tauri/src/acp/commands.rs Outdated
Comment thread apps/tauri/src/components/ActiveSessionView.tsx
Comment thread apps/tauri/src/components/ErrorConsole.tsx Outdated
Comment thread apps/tauri/src/components/TerminalMessage.tsx Outdated
Comment on lines 73 to 74
<span className="ml-1.5 text-[#3dd68c]/60 text-[10px]">done</span>
)}

@coderabbitai coderabbitai Bot Mar 5, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, check the file content around the mentioned lines
cat -n apps/tauri/src/components/TerminalMessage.tsx | sed -n '65,110p'

Repository: devitools/arandu

Length of output: 2116


🏁 Script executed:

# Check if useTranslation is imported in this file
rg "useTranslation|i18n" apps/tauri/src/components/TerminalMessage.tsx -A 2 -B 2

Repository: devitools/arandu

Length of output: 42


🏁 Script executed:

# Check existing translation files for these keys
find src/locales -name "*.json" -type f | head -5

Repository: devitools/arandu

Length of output: 106


🏁 Script executed:

# Check if these strings are already translated
rg "done|Thinking|lines" src/locales/ -t json

Repository: devitools/arandu

Length of output: 112


🏁 Script executed:

# Search for locale files
find . -name "*.json" -path "*locale*" -o -name "*.json" -path "*i18n*" | head -10

Repository: devitools/arandu

Length of output: 128


🏁 Script executed:

# Look for i18n configuration
rg "i18n|locales" -t json -t js -t ts . --max-count=5 | head -20

Repository: devitools/arandu

Length of output: 1591


🏁 Script executed:

# Check if there's an i18n config file
fd "i18n" . --type f

Repository: devitools/arandu

Length of output: 88


🏁 Script executed:

# Check if TerminalMessage.tsx has useTranslation imported
head -30 apps/tauri/src/components/TerminalMessage.tsx | grep -i "import.*useTranslation\|react-i18next"

Repository: devitools/arandu

Length of output: 42


🏁 Script executed:

# Check the entire locale file to see existing structure
cat apps/tauri/src/locales/en.json

Repository: devitools/arandu

Length of output: 8672


🏁 Script executed:

# Check theme/color configuration
cat apps/tauri/src/lib/utils.ts | head -50

Repository: devitools/arandu

Length of output: 228


🏁 Script executed:

# Check Tailwind theme or CSS variables
find . -name "tailwind.config.*" -o -name "globals.css" -o -name "*.css" | grep -E "(tailwind|global)" | head -5

Repository: devitools/arandu

Length of output: 91


🏁 Script executed:

# Check tailwind config for theme colors
cat apps/tauri/tailwind.config.ts

Repository: devitools/arandu

Length of output: 3924


🏁 Script executed:

# Check globals.css for color variables
find apps/tauri/src -name "globals.css" -o -name "*.css" | xargs cat 2>/dev/null | grep -E "color|--" | head -30

Repository: devitools/arandu

Length of output: 984


🏁 Script executed:

# Check if the file imports useTranslation
head -15 apps/tauri/src/components/TerminalMessage.tsx

Repository: devitools/arandu

Length of output: 467


Localize user-facing strings and use semantic color tokens.

The strings "done", "Thinking…", and {contentLines.length} lines... must be moved to translation keys. Additionally, replace the hardcoded color text-[#3dd68c]/60 with the semantic token text-success/60 to maintain theme consistency.

Per coding guidelines, use useTranslation() hook from react-i18next for all user-facing strings and TailwindCSS semantic color tokens instead of hardcoded hex values.

Also applies to: 80-83, 100-103

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/tauri/src/components/TerminalMessage.tsx` around lines 73 - 74, Import
and use the useTranslation() hook in TerminalMessage (e.g., const { t } =
useTranslation()) and replace all hardcoded user-facing strings ("done",
"Thinking…", and the "{contentLines.length} lines..." message) with translation
keys using t('...') (add corresponding keys like terminal.done,
terminal.thinking, terminal.linesCount) and update the JSX spans where those
strings appear (including the occurrences around lines 80-83 and 100-103) to
call t and format the lines count (e.g., t('terminal.linesCount', { count:
contentLines.length })). Also replace the hardcoded tailwind color class
text-[`#3dd68c`]/60 with the semantic token text-success/60 to use theme tokens.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Valid follow-up. Will also localize the hardcoded "done" label via useTranslation() along with the hex color fix from the previous comment.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@wilcorrea, sounds good! Here's a quick reference for the changes when you get to it:

Locale keys to add (in both en.json and pt-BR.json under a terminal section):

"terminal": {
  "done": "done",
  "thinking": "Thinking…",
  "linesCount": "{{count}} lines..."
}

Component update sketch:

import { useTranslation } from "react-i18next";

// inside the component:
const { t } = useTranslation();

// Replace hardcoded strings:
// "done"         → t("terminal.done")
// "Thinking…"    → t("terminal.thinking")
// `${contentLines.length} lines...` → t("terminal.linesCount", { count: contentLines.length })

// Replace color token:
// text-[`#3dd68c`]/60  →  text-success/60

Note that text-success is already defined in apps/tauri/src/index.css under both :root and .dark (as established in a previous PR), so the semantic token should be available directly.


🧠 Learnings used
Learnt from: CR
Repo: devitools/arandu PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-02T05:28:33.736Z
Learning: Applies to apps/tauri/src/**/*.{ts,tsx} : Use `useTranslation()` hook from react-i18next for all user-facing strings

Learnt from: wilcorrea
Repo: devitools/arandu PR: 41
File: apps/tauri/src/components/settings/DiagnosticsSettings.tsx:0-0
Timestamp: 2026-03-02T22:46:53.564Z
Learning: Enforce using semantic SHADCN UI tokens for status indicators in TSX components under apps/tauri/src. Use text-success, text-warning, and text-destructive tokens (foreground variants) instead of hardcoded colors. These tokens are defined in apps/tauri/src/index.css under both :root (light) and .dark (dark mode) and registered in tailwind.config.ts. Apply this pattern broadly to TSX files in apps/tauri/src (not just DiagnosticsSettings.tsx) to ensure theme-aware color usage for status icons and indicators.

Learnt from: wilcorrea
Repo: devitools/arandu PR: 41
File: apps/tauri/src/components/settings/DiagnosticsSettings.tsx:26-27
Timestamp: 2026-03-02T22:40:16.330Z
Learning: The shadcn/ui design system in this project currently only defines `text-destructive` as a semantic color token. Semantic tokens for success (`text-success`) and warning (`text-warning`) states need to be added to `apps/tauri/src/index.css` under both `:root` (light) and `.dark` (dark mode) sections before status icons can use theme-aware colors throughout the app.

Learnt from: CR
Repo: devitools/arandu PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-02T05:29:04.553Z
Learning: Applies to **/*.{tsx,ts} : Use `useTranslation()` hook for all user-facing text in React components

Learnt from: CR
Repo: devitools/arandu PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-02T05:28:33.736Z
Learning: Applies to apps/tauri/src/**/*.{css,tsx} : Use TailwindCSS with HSL CSS variables for theming instead of hardcoded color values

Learnt from: CR
Repo: devitools/arandu PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-02T05:29:04.553Z
Learning: Applies to src/lib/tray-sync.ts : For tray menu translations, update both the locale files and the `update_tray_labels` Rust command in `lib/tray-sync.ts`

Learnt from: CR
Repo: devitools/arandu PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-02T05:28:33.736Z
Learning: Applies to apps/tauri/src/**/*.{ts,tsx} : Use the `cn()` utility from `lib/utils.ts` for merging Tailwind CSS classes

Learnt from: CR
Repo: devitools/arandu PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-02T05:29:04.553Z
Learning: Applies to src/lib/i18n.ts : Synchronize i18n across layers using `i18n.on('languageChanged')` for React → localStorage, `storage` event listener for localStorage → React, and `updateTrayLabels()` command for React → Rust

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 2778aaf. Also localized the hardcoded "done" label via useTranslation() with key terminal.done.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Rate Limit Exceeded

@wilcorrea have exceeded the limit for the number of chat messages per hour. Please wait 0 minutes and 4 seconds before sending another message.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 2778aaf. Also localized the hardcoded "done" label via useTranslation() with key terminal.done.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
git show 2778aaf -- apps/tauri/src/components/TerminalMessage.tsx 2>/dev/null || echo "Commit not found, checking recent commits..."
git log --oneline -10

Length of output: 824


🏁 Script executed:

#!/bin/bash
# Check all branches for the commit
git fetch --all 2>/dev/null
git log --all --oneline | grep -i "2778aaf" || echo "Commit 2778aaf not found in any branch"

# Check if it exists in remote refs
git branch -a | head -20

# Also check the latest state of TerminalMessage.tsx to see if translation was applied
cat apps/tauri/src/components/TerminalMessage.tsx

Length of output: 5098


@wilcorrea, it looks like commit 2778aaf hasn't been pushed to the PR branch yet — it's not visible in the remote history and the current TerminalMessage.tsx still contains the unfixed items:

  • text-[#3dd68c]/60 hardcoded hex color (Line 73)
  • Hardcoded "done" string (Line 73)
  • Hardcoded "Thinking…" string (Line 100)
  • Hardcoded `${contentLines.length} lines...` string (Line 84)
  • No useTranslation import

Could you push the commit to the branch? I'll re-verify once it's available.


🧠 Learnings used
Learnt from: CR
Repo: devitools/arandu PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-02T05:28:33.736Z
Learning: Applies to apps/tauri/src/**/*.{ts,tsx} : Use `useTranslation()` hook from react-i18next for all user-facing strings

Learnt from: wilcorrea
Repo: devitools/arandu PR: 41
File: apps/tauri/src/components/settings/DiagnosticsSettings.tsx:0-0
Timestamp: 2026-03-02T22:46:53.564Z
Learning: Enforce using semantic SHADCN UI tokens for status indicators in TSX components under apps/tauri/src. Use text-success, text-warning, and text-destructive tokens (foreground variants) instead of hardcoded colors. These tokens are defined in apps/tauri/src/index.css under both :root (light) and .dark (dark mode) and registered in tailwind.config.ts. Apply this pattern broadly to TSX files in apps/tauri/src (not just DiagnosticsSettings.tsx) to ensure theme-aware color usage for status icons and indicators.

Learnt from: wilcorrea
Repo: devitools/arandu PR: 41
File: apps/tauri/src/components/settings/DiagnosticsSettings.tsx:26-27
Timestamp: 2026-03-02T22:40:16.330Z
Learning: The shadcn/ui design system in this project currently only defines `text-destructive` as a semantic color token. Semantic tokens for success (`text-success`) and warning (`text-warning`) states need to be added to `apps/tauri/src/index.css` under both `:root` (light) and `.dark` (dark mode) sections before status icons can use theme-aware colors throughout the app.

Learnt from: CR
Repo: devitools/arandu PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-02T05:29:04.553Z
Learning: Applies to **/*.{tsx,ts} : Use `useTranslation()` hook for all user-facing text in React components

Learnt from: CR
Repo: devitools/arandu PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-02T05:29:04.553Z
Learning: Applies to src/lib/tray-sync.ts : For tray menu translations, update both the locale files and the `update_tray_labels` Rust command in `lib/tray-sync.ts`

Learnt from: CR
Repo: devitools/arandu PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-02T05:29:04.553Z
Learning: Applies to src/lib/i18n.ts : Synchronize i18n across layers using `i18n.on('languageChanged')` for React → localStorage, `storage` event listener for localStorage → React, and `updateTrayLabels()` command for React → Rust

Comment thread apps/tauri/src/hooks/useAcpSession.ts
Comment thread apps/tauri/src/index.css
Comment thread apps/tauri/src/index.css

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

♻️ Duplicate comments (2)
apps/tauri/src/components/ActiveSessionView.tsx (1)

131-140: ⚠️ Potential issue | 🟠 Major

Don’t let onDisconnect() failure block reconnect.

At Line 135, a rejection prevents Line 138 from running, so reconnect can get stuck in error state.

🔧 Suggested resilient reconnect flow
   const handleReconnect = useCallback(async () => {
     initRef.current = false;
     setInitError(null);
-    if (onDisconnect) {
-      await onDisconnect();
-    }
+    try {
+      if (onDisconnect) await onDisconnect();
+    } catch (e) {
+      console.warn("[ActiveSessionView] disconnect during reconnect failed:", e);
+    }
     if (onConnect) {
       await onConnect();
     }
   }, [onDisconnect, onConnect]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/tauri/src/components/ActiveSessionView.tsx` around lines 131 - 140, The
reconnect handler handleReconnect currently awaits onDisconnect() which, if it
rejects, prevents the subsequent onConnect() call; change handleReconnect so
initRef.current = false and setInitError(null) remain, but call onDisconnect()
inside a try/catch (or Promise.catch) and swallow or log the error so it doesn't
rethrow, then always proceed to await onConnect(); reference handleReconnect,
onDisconnect, onConnect, initRef, and setInitError when making the change.
apps/tauri/src/hooks/useAcpSession.ts (1)

73-82: ⚠️ Potential issue | 🟠 Major

Reset the full session state when entry is missing/replaced.

Current logic resets only messages/isStreaming in refresh paths and does nothing when a workspace has no cache entry, so stale values can leak (currentMode, availableModes, activeAcpSessionId, agentPlanFilePath, and ref state).

🔧 Suggested consolidation helper
+  const applyEntryToState = useCallback((entry: SessionEntry) => {
+    setMessages(entry.messages);
+    setIsStreaming(entry.isStreaming);
+    setCurrentMode(entry.currentMode);
+    setAvailableModes(entry.availableModes);
+    setActiveAcpSessionId(entry.activeAcpSessionId);
+    setAgentPlanFilePath(entry.agentPlanFilePath);
+    activeAcpSessionIdRef.current = entry.activeAcpSessionId;
+  }, []);
+
   useEffect(() => {
     const entry = sessionStore.get(workspaceId);
-    if (entry) {
-      setMessages(entry.messages);
-      setIsStreaming(entry.isStreaming);
-      setCurrentMode(entry.currentMode);
-      setAvailableModes(entry.availableModes);
-      setActiveAcpSessionId(entry.activeAcpSessionId);
-      setAgentPlanFilePath(entry.agentPlanFilePath);
-      activeAcpSessionIdRef.current = entry.activeAcpSessionId;
-    }
+    applyEntryToState(entry ?? EMPTY_SESSION);
@@
-        sessionStore.set(workspaceId, fresh);
-        setMessages([]);
-        setIsStreaming(false);
+        sessionStore.set(workspaceId, fresh);
+        applyEntryToState(fresh);
@@
-        sessionStore.set(workspaceId, fresh);
-        setMessages([]);
-        setIsStreaming(false);
+        sessionStore.set(workspaceId, fresh);
+        applyEntryToState(fresh);

Also applies to: 121-128, 150-154

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/tauri/src/hooks/useAcpSession.ts` around lines 73 - 82, When
sessionStore.get(workspaceId) returns no entry (or when an entry is replaced)
the code only updates messages/isStreaming and leaves stale values in
currentMode, availableModes, activeAcpSessionId, agentPlanFilePath and
activeAcpSessionIdRef; update the logic in the blocks that call
sessionStore.get(...) and then set* (e.g., the block using setMessages,
setIsStreaming, setCurrentMode, setAvailableModes, setActiveAcpSessionId,
setAgentPlanFilePath and activeAcpSessionIdRef.current) to reset all session
state to explicit defaults (clear messages, set isStreaming false, set
currentMode/availableModes/agentPlanFilePath to null/empty, set
activeAcpSessionId to null and activeAcpSessionIdRef.current = null) whenever
entry is missing or replaced; apply the same full-reset fix to the other similar
sections that update state after fetching from sessionStore.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/tauri/src/components/ActiveSessionView.tsx`:
- Line 12: Replace the relative import for the ConnectionLogs component with the
path-alias import using `@/` so it resolves from src; update the import statement
that references ConnectionLogs in ActiveSessionView.tsx to use the alias (e.g.,
import ConnectionLogs via "@/components/ConnectionLogs") so it follows the
project's "@/ -> ./src/" convention and aligns with other component imports.
- Around line 121-129: The effect watching isConnected should also reset/init
when the session identity changes: update the useEffect that references
isConnected and initRef to include session.id and session.acp_session_id (or
otherwise detect session identity changes) in its dependency check and set
initRef.current = false when those values change while connected so doInit (the
useCallback depending on session.id and session.acp_session_id) will run for the
new session; specifically adjust the useEffect containing initRef and doInit to
account for session.id and session.acp_session_id so initialization is
retriggered on session identity changes.

In `@apps/tauri/src/hooks/useAcpSession.ts`:
- Around line 102-103: The effect in useAcpSession currently only calls
unsubscribe on cleanup and leaves the idle timer running, which can later set
isStreaming to false for a new workspace; update the effect cleanup to also
clear the idle timer (the timeout/timer variable used to reset isStreaming—e.g.,
idleTimer, idleTimeout, or timerId) before or after calling unsubscribe so no
pending timeout survives a workspaceId change and incorrectly flips isStreaming
for the new session.
- Around line 130-133: Replace the imported invoke usage in the useAcpSession
hook with the global Tauri API: change all calls like
invoke<...>("acp_load_session", {...}) (and the six other command invocations in
the same file) to window.__TAURI__.core.invoke<...>(...) and remove the
now-unused import from `@tauri-apps/api/core`; ensure each call preserves its
original generic type and argument object (e.g., the acp_load_session call and
the other six commands referenced in the review) so only the call site is
switched to window.__TAURI__.core.invoke().

In `@apps/tauri/src/lib/session-cache.ts`:
- Around line 231-241: addUserMessage sets isStreaming: true but never calls
resetStreamingTimer(workspaceId), so the session can stay stuck in streaming if
no backend events arrive; after creating and sessions.set(workspaceId, newEntry)
call resetStreamingTimer(workspaceId) (ensure resetStreamingTimer is in
scope/imported) and then call notifySubscribers(workspaceId, newEntry) so the
streaming timeout is started whenever a user message begins streaming.
- Around line 185-207: The listenerSetup flag is being set before the async
listen promise resolves, preventing retries on failure; update
ensureGlobalListener so it does not mark listenerSetup true until the listen
promise fulfills: call window.__TAURI__.event.listen<AcpSessionUpdate>(...) and
in its .then() set listenerSetup = true (or set a separate pending flag while
registering and flip to true on success), keep the existing
.catch(console.error) so failures leave listenerSetup false to allow retries,
and preserve the existing handler logic that calls processSessionUpdate,
sessions.set, notifySubscribers, resetStreamingTimer/clearStreamingTimer.

---

Duplicate comments:
In `@apps/tauri/src/components/ActiveSessionView.tsx`:
- Around line 131-140: The reconnect handler handleReconnect currently awaits
onDisconnect() which, if it rejects, prevents the subsequent onConnect() call;
change handleReconnect so initRef.current = false and setInitError(null) remain,
but call onDisconnect() inside a try/catch (or Promise.catch) and swallow or log
the error so it doesn't rethrow, then always proceed to await onConnect();
reference handleReconnect, onDisconnect, onConnect, initRef, and setInitError
when making the change.

In `@apps/tauri/src/hooks/useAcpSession.ts`:
- Around line 73-82: When sessionStore.get(workspaceId) returns no entry (or
when an entry is replaced) the code only updates messages/isStreaming and leaves
stale values in currentMode, availableModes, activeAcpSessionId,
agentPlanFilePath and activeAcpSessionIdRef; update the logic in the blocks that
call sessionStore.get(...) and then set* (e.g., the block using setMessages,
setIsStreaming, setCurrentMode, setAvailableModes, setActiveAcpSessionId,
setAgentPlanFilePath and activeAcpSessionIdRef.current) to reset all session
state to explicit defaults (clear messages, set isStreaming false, set
currentMode/availableModes/agentPlanFilePath to null/empty, set
activeAcpSessionId to null and activeAcpSessionIdRef.current = null) whenever
entry is missing or replaced; apply the same full-reset fix to the other similar
sections that update state after fetching from sessionStore.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 324e50ae-ce5c-441b-afa2-5cee504c5197

📥 Commits

Reviewing files that changed from the base of the PR and between 989a1dc and 3b2afc6.

📒 Files selected for processing (4)
  • apps/tauri/src/components/ActiveSessionView.tsx
  • apps/tauri/src/components/SessionCard.tsx
  • apps/tauri/src/hooks/useAcpSession.ts
  • apps/tauri/src/lib/session-cache.ts

Comment thread apps/tauri/src/components/ActiveSessionView.tsx Outdated
Comment thread apps/tauri/src/components/ActiveSessionView.tsx Outdated
Comment thread apps/tauri/src/hooks/useAcpSession.ts Outdated
Comment on lines 130 to 133
const info = await invoke<AcpSessionInfo>("acp_load_session", {
workspaceId,
sessionId: existingAcpSessionId,
cwd: workspacePath,

@coderabbitai coderabbitai Bot Mar 5, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n apps/tauri/src/hooks/useAcpSession.ts | head -50

Repository: devitools/arandu

Length of output: 1616


🏁 Script executed:

rg "import.*invoke" apps/tauri/src/hooks/useAcpSession.ts

Repository: devitools/arandu

Length of output: 106


🏁 Script executed:

cat -n apps/tauri/src/hooks/useAcpSession.ts | sed -n '125,140p'

Repository: devitools/arandu

Length of output: 747


🏁 Script executed:

cat -n apps/tauri/src/hooks/useAcpSession.ts | sed -n '150,160p'

Repository: devitools/arandu

Length of output: 514


🏁 Script executed:

cat -n apps/tauri/src/hooks/useAcpSession.ts | sed -n '175,200p'

Repository: devitools/arandu

Length of output: 906


🏁 Script executed:

cat -n apps/tauri/src/hooks/useAcpSession.ts | sed -n '210,240p'

Repository: devitools/arandu

Length of output: 1061


🏁 Script executed:

fd "vite.config" apps/tauri/

Repository: devitools/arandu

Length of output: 85


🏁 Script executed:

cat apps/tauri/vite.config.ts 2>/dev/null | head -80

Repository: devitools/arandu

Length of output: 2244


🏁 Script executed:

fd "tauri.conf" . --type f

Repository: devitools/arandu

Length of output: 98


🏁 Script executed:

cat apps/tauri/vite.config.ts | tail -30

Repository: devitools/arandu

Length of output: 759


🏁 Script executed:

cat apps/tauri/src-tauri/tauri.conf.json | head -50

Repository: devitools/arandu

Length of output: 1364


Replace imported invoke() with window.__TAURI__.core.invoke() to align with configured Tauri API style.

The hook currently imports and uses invoke from @tauri-apps/api/core, but the project is configured with withGlobalTauri: true in tauri.conf.json, which requires using window.__TAURI__.core.invoke() for all Rust backend function calls.

Update all 7 command invocations at lines 130, 155, 180, 189 (2 calls), 194, 216, and 233 to use window.__TAURI__.core.invoke() instead of the imported invoke function.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/tauri/src/hooks/useAcpSession.ts` around lines 130 - 133, Replace the
imported invoke usage in the useAcpSession hook with the global Tauri API:
change all calls like invoke<...>("acp_load_session", {...}) (and the six other
command invocations in the same file) to window.__TAURI__.core.invoke<...>(...)
and remove the now-unused import from `@tauri-apps/api/core`; ensure each call
preserves its original generic type and argument object (e.g., the
acp_load_session call and the other six commands referenced in the review) so
only the call site is switched to window.__TAURI__.core.invoke().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Acknowledged as follow-up analysis supporting the idle timer cleanup fix above.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@wilcorrea, understood! The concern about using window.__TAURI__.core.invoke() instead of the imported invoke from @tauri-apps/api/core remains valid and should be addressed in a follow-up. Would you like me to open a GitHub issue to track this so it doesn't get lost?


🧠 Learnings used
Learnt from: CR
Repo: devitools/arandu PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-02T05:29:04.553Z
Learning: Applies to src/hooks/*.ts : Custom hooks calling Tauri commands should use `window.__TAURI__.core.invoke()` and hooks listening to Tauri events should use `window.__TAURI__.event.listen()` with cleanup in `useEffect` return

Learnt from: CR
Repo: devitools/arandu PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-02T05:28:33.736Z
Learning: Applies to apps/tauri/src/**/*.{ts,tsx} : Use `window.__TAURI__.core.invoke()` for calling Rust backend functions (`withGlobalTauri: true` in config)

Learnt from: CR
Repo: devitools/arandu PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-02T05:29:04.553Z
Learning: Applies to **/*.{ts,tsx} : Call Tauri commands from frontend using `window.__TAURI__.core.invoke('command_name', { args })`

Learnt from: CR
Repo: devitools/arandu PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-02T05:29:04.553Z
Learning: Applies to src/__tests__/setup.ts : Mock `window.__TAURI__` in test setup file with implementations for `core.invoke`, `window.getCurrentWindow`, `dialog.open`, and `event.listen`

Learnt from: CR
Repo: devitools/arandu PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-02T05:28:33.736Z
Learning: Applies to apps/tauri/src/hooks/**/*.ts : New custom React hooks should follow the naming convention `useXxx()` and leverage Tauri commands and event listeners for backend communication

Learnt from: CR
Repo: devitools/arandu PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-02T05:29:04.553Z
Learning: Applies to src/hooks/*.ts : Use ref-based callbacks (`sendPromptRef`, `setModeRef`) to avoid dependency array churn in workflow state machines

Learnt from: CR
Repo: devitools/arandu PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-02T05:28:33.736Z
Learning: Applies to apps/tauri/src-tauri/src/**/*.rs : Define new Tauri commands as functions, register them in `invoke_handler`, and add permissions to `capabilities/default.json`

Learnt from: CR
Repo: devitools/arandu PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-02T05:28:33.736Z
Learning: Applies to apps/tauri/src/**/*.{ts,tsx} : Use localStorage for workspace persistence and language preference with `storage` event listeners for cross-window state sync

Comment thread apps/tauri/src/lib/session-cache.ts
Comment on lines +231 to +241
export function addUserMessage(workspaceId: string, text: string) {
const entry = sessions.get(workspaceId);
if (!entry) return;
const newEntry = {
...entry,
messages: [...entry.messages, { id: nextMsgId(), role: "user" as const, content: text, timestamp: new Date() }],
isStreaming: true,
};
sessions.set(workspaceId, newEntry);
notifySubscribers(workspaceId, newEntry);
}

@coderabbitai coderabbitai Bot Mar 5, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Start streaming timeout when addUserMessage sets streaming true.

This path sets isStreaming: true but doesn’t call resetStreamingTimer(workspaceId). If backend events never arrive afterward, cache state can remain stuck as streaming.

🔧 Suggested fix
 export function addUserMessage(workspaceId: string, text: string) {
@@
   sessions.set(workspaceId, newEntry);
   notifySubscribers(workspaceId, newEntry);
+  resetStreamingTimer(workspaceId);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function addUserMessage(workspaceId: string, text: string) {
const entry = sessions.get(workspaceId);
if (!entry) return;
const newEntry = {
...entry,
messages: [...entry.messages, { id: nextMsgId(), role: "user" as const, content: text, timestamp: new Date() }],
isStreaming: true,
};
sessions.set(workspaceId, newEntry);
notifySubscribers(workspaceId, newEntry);
}
export function addUserMessage(workspaceId: string, text: string) {
const entry = sessions.get(workspaceId);
if (!entry) return;
const newEntry = {
...entry,
messages: [...entry.messages, { id: nextMsgId(), role: "user" as const, content: text, timestamp: new Date() }],
isStreaming: true,
};
sessions.set(workspaceId, newEntry);
notifySubscribers(workspaceId, newEntry);
resetStreamingTimer(workspaceId);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/tauri/src/lib/session-cache.ts` around lines 231 - 241, addUserMessage
sets isStreaming: true but never calls resetStreamingTimer(workspaceId), so the
session can stay stuck in streaming if no backend events arrive; after creating
and sessions.set(workspaceId, newEntry) call resetStreamingTimer(workspaceId)
(ensure resetStreamingTimer is in scope/imported) and then call
notifySubscribers(workspaceId, newEntry) so the streaming timeout is started
whenever a user message begins streaming.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Valid. addUserMessage sets isStreaming: true but never calls resetStreamingTimer(workspaceId). If no backend events follow, the cache stays permanently stuck in streaming state. Will add the resetStreamingTimer call.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Rate Limit Exceeded

@wilcorrea have exceeded the limit for the number of chat messages per hour. Please wait 1 minutes and 50 seconds before sending another message.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 6ff7259. Added resetStreamingTimer(workspaceId) call in addUserMessage so the streaming state is always auto-cleared if no backend events follow.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Rate Limit Exceeded

@wilcorrea have exceeded the limit for the number of chat messages per hour. Please wait 0 minutes and 23 seconds before sending another message.

wilcorrea and others added 3 commits March 5, 2026 04:21
… in ErrorConsole

- Use Button (variant=ghost, size=icon) from shadcn/ui
- Add useTranslation() and use t('common.dismiss') for aria-label
- Add common.dismiss key to pt-BR.json ("Dispensar") and en.json ("Dismiss")

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
In `acp_connect` and `acp_check_health`, `is_alive().await` and
`shutdown().await` were called while the connections mutex was held,
potentially stalling other ACP commands behind long waits.

Refactored both functions to remove the entry under the lock, drop the
lock, perform async checks/shutdown outside it, then re-acquire the
lock only to re-insert (if still alive) or finalize cleanup.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
In `handleReconnect`, if `onDisconnect()` rejected the promise chain
would abort before calling `onConnect()`, leaving the UI stuck in an
error state. Wrapped `onDisconnect` in try/catch so `onConnect`
always executes regardless of disconnect errors.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/tauri/src-tauri/src/acp/commands.rs (1)

306-313: ⚠️ Potential issue | 🟡 Minor

Avoid awaiting multiple shutdown() calls while holding connections lock.

The lock is held during all shutdown().await calls in the loop. While this is likely only called during app shutdown, it's inconsistent with the fixes applied elsewhere and could block if any shutdown hangs.

🔧 Suggested fix
 pub async fn disconnect_all(state: &AcpState) {
     state.configs.lock().await.clear();
-    let mut connections = state.connections.lock().await;
-    for (id, conn) in connections.drain() {
+    let drained: Vec<_> = {
+        let mut connections = state.connections.lock().await;
+        connections.drain().collect()
+    };
+    for (id, conn) in drained {
         eprintln!("[acp] Disconnecting workspace {}", id);
         conn.shutdown().await;
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/tauri/src-tauri/src/acp/commands.rs` around lines 306 - 313,
disconnect_all currently holds the state.connections lock across each
conn.shutdown().await call; to fix, acquire the lock once, drain or collect the
connections into a local Vec (e.g., let conns =
state.connections.lock().await.drain().collect::<Vec<_>>()), then drop the lock
before iterating over that Vec and calling conn.shutdown().await for each entry;
keep the existing configs.clear() behavior but ensure the shutdown awaits happen
after releasing the connections lock (referencing disconnect_all,
state.connections, connections.lock(), and conn.shutdown()).
apps/tauri/src/components/ActiveSessionView.tsx (1)

103-106: ⚠️ Potential issue | 🟡 Minor

Use global Tauri invoke API instead of @tauri-apps/api/core import.

Lines 103-106 and 147 call backend commands through imported invoke; this file should use window.__TAURI__.core.invoke(...) per project convention with withGlobalTauri: true configured.

Suggested change
-import { invoke } from "@tauri-apps/api/core";
@@
-        await invoke("session_update_acp_id", {
+        await window.__TAURI__.core.invoke("session_update_acp_id", {
           id: session.id,
           acpSessionId: acpId,
         });
@@
-      invoke("session_update_chat_panel_size", { id: session.id, size: sizes[0] }).catch(console.error);
+      window.__TAURI__.core
+        .invoke("session_update_chat_panel_size", { id: session.id, size: sizes[0] })
+        .catch(console.error);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/tauri/src/components/ActiveSessionView.tsx` around lines 103 - 106, The
code is calling backend commands using the imported invoke from
`@tauri-apps/api/core` (e.g., the session_update_acp_id call that passes { id:
session.id, acpSessionId: acpId } and other invoke usages around the file), but
the project uses the global Tauri API; replace those imports/uses with the
global call window.__TAURI__.core.invoke(...) (ensure the argument shape stays
the same) and remove any direct import of invoke from `@tauri-apps/api/core` so
the component relies on the configured withGlobalTauri: true global entrypoint.
♻️ Duplicate comments (2)
apps/tauri/src/components/ActiveSessionView.tsx (2)

12-12: ⚠️ Potential issue | 🟡 Minor

Use @/ alias for ConnectionLogs import.

Line 12 regresses to a relative import; please switch to @/components/ConnectionLogs for consistency with repo import rules.

Suggested change
-import { ConnectionLogs } from "./ConnectionLogs";
+import { ConnectionLogs } from "@/components/ConnectionLogs";

As per coding guidelines: Use path alias @/ which maps to ./src/ for imports.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/tauri/src/components/ActiveSessionView.tsx` at line 12, The import in
ActiveSessionView.tsx uses a relative path for ConnectionLogs; update the import
to use the project alias by replacing the relative import with
"@/components/ConnectionLogs" so the ConnectionLogs component is imported via
the repository's `@/` (./src) alias to remain consistent with other imports and
linting rules.

121-129: ⚠️ Potential issue | 🟠 Major

Re-run initialization when session identity changes while connected.

At Line 129, the effect depends only on isConnected. If session.id/session.acp_session_id changes without disconnecting, initRef.current can stay true and skip doInit for the new session.

Suggested change
+  useEffect(() => {
+    initRef.current = false;
+    setInitError(null);
+  }, [session.id, session.acp_session_id]);
+
   useEffect(() => {
     if (!isConnected) {
       initRef.current = false;
       return;
     }
     if (initRef.current) return;
     initRef.current = true;
-    doInit();
-  }, [isConnected]);
+    void doInit();
+  }, [isConnected, doInit, session.id, session.acp_session_id]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/tauri/src/components/ActiveSessionView.tsx` around lines 121 - 129, The
effect only depends on isConnected so when a new session identity arrives while
connected initRef.current stays true and doInit is skipped; update the useEffect
to also depend on the session identity (e.g., include session.id and
session.acp_session_id or a single derived sessionKey) and reset initRef.current
to false when the identity changes while connected so doInit runs for the new
session; specifically modify the useEffect signature that references
isConnected/initRef/doInit to include the session identity deps and add logic to
set initRef.current = false when the session id/acp_session_id changes and
isConnected is true.
🧹 Nitpick comments (1)
apps/tauri/src/components/ErrorConsole.tsx (1)

25-33: Minor indentation inconsistency inside the Button element.

The Button's children and closing tag are indented with 8 spaces instead of 6, creating misalignment with sibling elements.

✨ Suggested fix for consistent indentation
       <Button
-          variant="ghost"
-          size="icon"
-          onClick={onClear}
-          className="h-6 w-6 shrink-0 text-muted-foreground/40 hover:text-foreground"
-          aria-label={t("common.dismiss")}
-        >
-          <X className="h-3.5 w-3.5" />
-        </Button>
+        variant="ghost"
+        size="icon"
+        onClick={onClear}
+        className="h-6 w-6 shrink-0 text-muted-foreground/40 hover:text-foreground"
+        aria-label={t("common.dismiss")}
+      >
+        <X className="h-3.5 w-3.5" />
+      </Button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/tauri/src/components/ErrorConsole.tsx` around lines 25 - 33, The Button
JSX in ErrorConsole.tsx has its child <X .../> and closing </Button> indented 8
spaces instead of matching the 6-space indentation used for the Button
attributes; adjust the indentation of the child element and the closing
</Button> so they align with the Button opening tag/props (i.e., same
indentation level as the other Button attributes) to restore consistent
formatting in the Button element.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/tauri/src-tauri/src/acp/commands.rs`:
- Around line 96-100: The code currently holds the state.connections mutex while
awaiting conn.shutdown().await; change the flow so you remove the connection
from the lock (using state.connections.lock().await and
connections.remove(&workspace_id)), then drop the mutex guard before awaiting
shutdown — e.g., store the returned conn in a local variable, let the
connections guard go out of scope (or call drop(connections)) and only then call
conn.emit_log("info", "disconnect", "Disconnected by user") and await
conn.shutdown().await; ensure you reference state.connections,
connections.remove, conn.emit_log, and conn.shutdown().await when making the
change.

---

Outside diff comments:
In `@apps/tauri/src-tauri/src/acp/commands.rs`:
- Around line 306-313: disconnect_all currently holds the state.connections lock
across each conn.shutdown().await call; to fix, acquire the lock once, drain or
collect the connections into a local Vec (e.g., let conns =
state.connections.lock().await.drain().collect::<Vec<_>>()), then drop the lock
before iterating over that Vec and calling conn.shutdown().await for each entry;
keep the existing configs.clear() behavior but ensure the shutdown awaits happen
after releasing the connections lock (referencing disconnect_all,
state.connections, connections.lock(), and conn.shutdown()).

In `@apps/tauri/src/components/ActiveSessionView.tsx`:
- Around line 103-106: The code is calling backend commands using the imported
invoke from `@tauri-apps/api/core` (e.g., the session_update_acp_id call that
passes { id: session.id, acpSessionId: acpId } and other invoke usages around
the file), but the project uses the global Tauri API; replace those imports/uses
with the global call window.__TAURI__.core.invoke(...) (ensure the argument
shape stays the same) and remove any direct import of invoke from
`@tauri-apps/api/core` so the component relies on the configured withGlobalTauri:
true global entrypoint.

---

Duplicate comments:
In `@apps/tauri/src/components/ActiveSessionView.tsx`:
- Line 12: The import in ActiveSessionView.tsx uses a relative path for
ConnectionLogs; update the import to use the project alias by replacing the
relative import with "@/components/ConnectionLogs" so the ConnectionLogs
component is imported via the repository's `@/` (./src) alias to remain
consistent with other imports and linting rules.
- Around line 121-129: The effect only depends on isConnected so when a new
session identity arrives while connected initRef.current stays true and doInit
is skipped; update the useEffect to also depend on the session identity (e.g.,
include session.id and session.acp_session_id or a single derived sessionKey)
and reset initRef.current to false when the identity changes while connected so
doInit runs for the new session; specifically modify the useEffect signature
that references isConnected/initRef/doInit to include the session identity deps
and add logic to set initRef.current = false when the session id/acp_session_id
changes and isConnected is true.

---

Nitpick comments:
In `@apps/tauri/src/components/ErrorConsole.tsx`:
- Around line 25-33: The Button JSX in ErrorConsole.tsx has its child <X .../>
and closing </Button> indented 8 spaces instead of matching the 6-space
indentation used for the Button attributes; adjust the indentation of the child
element and the closing </Button> so they align with the Button opening
tag/props (i.e., same indentation level as the other Button attributes) to
restore consistent formatting in the Button element.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bd83e20a-c5c8-4a43-a119-31601e2bef53

📥 Commits

Reviewing files that changed from the base of the PR and between 3b2afc6 and 0ab279c.

📒 Files selected for processing (5)
  • apps/tauri/src-tauri/src/acp/commands.rs
  • apps/tauri/src/components/ActiveSessionView.tsx
  • apps/tauri/src/components/ErrorConsole.tsx
  • apps/tauri/src/locales/en.json
  • apps/tauri/src/locales/pt-BR.json

Comment thread apps/tauri/src-tauri/src/acp/commands.rs Outdated
wilcorrea and others added 6 commits March 5, 2026 04:36
Flip listenerSetup flag only after the Tauri event listener resolves,
allowing retries if setup fails. Also call resetStreamingTimer() in
addUserMessage so the isStreaming flag is always auto-cleared if no
backend events follow.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
….css

Add --terminal-code CSS variable and replace all #3dd68c and #56d4dd
hex values with hsl(var(--success)) and hsl(var(--terminal-code))
respectively, ensuring colors respect the app theme system.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace hex color class with text-success/60 semantic token and
localize the hardcoded "done" label via useTranslation().

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rkspace change

Reset all local state fields (currentMode, availableModes,
activeAcpSessionId, agentPlanFilePath) when loading an existing session
to prevent stale UI state. Also clear idleTimerRef in the workspaceId
effect cleanup so timers don't fire on the wrong workspace.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…on change

Replace relative import with @/ path alias. Add a dedicated effect to
reset initRef when session identity changes so doInit() reruns when a
different session is mounted on the same connected workspace.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…onnect

Drop the connections lock before calling conn.shutdown().await,
consistent with the same fix applied to acp_connect and acp_check_health.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@wilcorrea wilcorrea merged commit b171f29 into main Mar 5, 2026
@wilcorrea wilcorrea deleted the feat/acp-session-persistence branch March 5, 2026 07:47
wilcorrea added a commit that referenced this pull request Mar 6, 2026
* feat(acp): Add heartbeat, dead connection cleanup and connection logging

- Add Rust-side heartbeat task with 15s ping interval and 3-strike disconnect
- Clean up dead connections in `acp_connect` and `acp_check_health`
- Emit structured connection log events via `acp:log`
- Add `HeartbeatEvent`, `ConnectionLogEntry` and `ConnectionConfig` types
- Add `chat_panel_size` field to session persistence

* feat(acp): Persist session state across minimize/restore and reload history

- Add in-memory session cache with global event listener independent of React lifecycle
- Keep DirectoryWorkspace mounted via CSS hidden when minimized
- Add `persistedWorkspaceId` to AppContext for mount/visibility separation
- Always call `acp_load_session` to replay full conversation from ACP backend
- Restore cached messages on "already loaded" fallback
- Add periodic 30s heartbeat in `useAcpConnection` for proactive disconnect detection
- Remove auto-disconnect on unmount; move disconnect to explicit close/forget
- Add connection logs panel with `useAcpLogs` hook and `ConnectionLogs` component
- Add i18n strings for connection logs and ACP status

* test(acp): Add tests for connection hook and logs hook

- Test connect/disconnect lifecycle, status events, workspace isolation
- Test cached state restoration and health check on mount
- Verify disconnect is not called on unmount
- Test log capture, filtering and clear behavior

* style(ui): Replace X icon with Trash2 on session delete button

* fix(acp): Reset session init on disconnect and auto-reload on "not found"

- Reset `initRef` when `isConnected` goes false so reconnect triggers
  `doInit` and reloads the session in the new copilot process
- Auto-recover in `sendPrompt`: on "Session not found" (-32602), try
  `acp_load_session` then retry the prompt before surfacing the error

* fix(acp): Add 60s streaming timeout to auto-clear stale Stop button

If no streaming event arrives within 60s, `isStreaming` is reset to
false automatically. Prevents the Stop button from staying red
indefinitely when `end_turn` is never received.

* fix: replace native button with shadcn Button and localize aria-label in ErrorConsole

- Use Button (variant=ghost, size=icon) from shadcn/ui
- Add useTranslation() and use t('common.dismiss') for aria-label
- Add common.dismiss key to pt-BR.json ("Dispensar") and en.json ("Dismiss")

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(acp): Avoid awaiting async ops while holding connections mutex

In `acp_connect` and `acp_check_health`, `is_alive().await` and
`shutdown().await` were called while the connections mutex was held,
potentially stalling other ACP commands behind long waits.

Refactored both functions to remove the entry under the lock, drop the
lock, perform async checks/shutdown outside it, then re-acquire the
lock only to re-insert (if still alive) or finalize cleanup.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(acp): Ensure `onConnect` runs even if `onDisconnect` rejects

In `handleReconnect`, if `onDisconnect()` rejected the promise chain
would abort before calling `onConnect()`, leaving the UI stuck in an
error state. Wrapped `onDisconnect` in try/catch so `onConnect`
always executes regardless of disconnect errors.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(acp): Fix listener guard and add streaming timer in session-cache

Flip listenerSetup flag only after the Tauri event listener resolves,
allowing retries if setup fails. Also call resetStreamingTimer() in
addUserMessage so the isStreaming flag is always auto-cleared if no
backend events follow.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(ui): Replace hardcoded hex colors with HSL CSS variables in index.css

Add --terminal-code CSS variable and replace all #3dd68c and #56d4dd
hex values with hsl(var(--success)) and hsl(var(--terminal-code))
respectively, ensuring colors respect the app theme system.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(ui): Use semantic token and localize done label in TerminalMessage

Replace hex color class with text-success/60 semantic token and
localize the hardcoded "done" label via useTranslation().

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(acp): Full state reset on session load and clear idle timer on workspace change

Reset all local state fields (currentMode, availableModes,
activeAcpSessionId, agentPlanFilePath) when loading an existing session
to prevent stale UI state. Also clear idleTimerRef in the workspaceId
effect cleanup so timers don't fire on the wrong workspace.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(ui): Use @/ alias for ConnectionLogs and re-trigger init on session change

Replace relative import with @/ path alias. Add a dedicated effect to
reset initRef when session identity changes so doInit() reruns when a
different session is mounted on the same connected workspace.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(acp): Avoid holding connections mutex during shutdown in acp_disconnect

Drop the connections lock before calling conn.shutdown().await,
consistent with the same fix applied to acp_connect and acp_check_health.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant