Skip to content

Followup: Web Interface UX Parity#696

Merged
pedramamini merged 4 commits intoRunMaestro:rcfrom
chr1syy:feat/feat-web-ux-parity
Apr 1, 2026
Merged

Followup: Web Interface UX Parity#696
pedramamini merged 4 commits intoRunMaestro:rcfrom
chr1syy:feat/feat-web-ux-parity

Conversation

@chr1syy
Copy link
Copy Markdown
Contributor

@chr1syy chr1syy commented Mar 31, 2026

Summary

  • xterm.js terminal: Full PTY-backed terminal mode for web clients with dedicated process spawning, resize handling, and raw data forwarding
  • Swipeable panels: Left (agent) and right (files/history/auto run) panels now slide in/out with touch gestures on tablet, matching native drawer UX
  • Notifications dropdown: Completed-agent notifications accessible from header
  • Tab enhancements: Multi-tab support, tab search, tab bar improvements
  • Header cleanup: Removed redundant Auto Run play button (accessible via Right Panel and Cmd+K)
  • Resizable panels: Desktop-width screens get draggable resize handles on left/right panels
  • New components: LeftPanel, RightPanel (inline), RightDrawer (overlay), WebTerminal, CommandInputButtons, useResizableWebPanel, useIsMobile

Test plan

  • Verify terminal mode works on web (mode toggle, typing, output rendering, resize)
  • Test left panel swipe-open from left edge and swipe-left-to-close on mobile/tablet
  • Test right drawer swipe-open from right edge and swipe-right-to-close
  • Verify Auto Run play button is gone from header; Auto Run still accessible via right panel tab
  • Test notifications dropdown shows completed agents
  • Test tab bar with multiple AI tabs (select, create, close)
  • Verify desktop layout with resizable side panels

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Terminal mode with full WebSocket controls (spawn, write, resize, kill) and terminal-ready/data events
    • Web-based terminal UI (xterm-powered) for mobile/web
    • Tool event logging and broadcasting to connected web clients; unread/tool indicators and notifications for completed agents
    • File-tree browser accessible via WebSocket
    • Mobile improvements: resizable left/right panels, thinking-mode toggle, updated tab/terminal UX
  • Bug Fixes
    • PTY/terminal output now correctly broadcasts to web clients instead of being skipped

chr1syy and others added 3 commits March 31, 2026 08:19
…wn, tab enhancements

- Integrate xterm.js for full terminal emulation in web interface (replaces
  chat-message rendering). Backend broadcasts raw PTY data via terminal_data
  messages; frontend renders with full ANSI color, cursor, and resize support.
- Add agent completion notifications dropdown — bell icon now shows a list of
  completed/errored agents with direct switch-to-agent on click.
- Fix Auto Run "Configure & Launch" button not working from inline RightPanel
  (was gated on showAutoRunPanel which is only true for the old bottom sheet).
- Replace hamburger menu icon with robot head agent icon in mobile header.
- Add plus (+) button menu with "New AI Chat" and "New Terminal" options.
- Add close button to terminal tab to switch back to AI mode.
- Add left/right resizable panels, mobile viewport detection, and various
  UI improvements across the web interface.

Session: e1e5a5c3-bbca-495d-adab-7d358766737c

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…r tablet

- Remove the Auto Run play button from the header (accessible via Right Panel's Auto Run tab and Cmd+K)
- Add left-edge swipe gesture to open the agent panel (mirrors existing right-edge swipe for right drawer)
- Add slide-in/out animation and swipe-left-to-close to LeftPanel in mobile/full-screen mode
- LeftPanel now renders as a 85vw sliding drawer (max 400px) with animated backdrop, matching RightDrawer UX

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add dedicated PTY spawning for web terminal sessions (spawn on mode switch, kill on exit)
- Support web terminal session ID format ({sessionId}-terminal) in PtySpawner
- Add spawnTerminalForWeb/killTerminalForWeb callbacks through WebServer and message handlers
- Expand WebTerminal component with full xterm.js integration, fit addon, and resize handling
- Wire up terminal resize messages through WebSocket
- Include session CWD in detail responses for terminal working directory

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 31, 2026

📝 Walkthrough

Walkthrough

Adds end-to-end terminal control and tool-event broadcasting: PTY spawn/resize/write/kill callbacks, broadcasting of terminal and tool events to web clients, mobile terminal UI and integrations, resizable mobile panels, file-tree WebSocket handler, and related type/handler expansions across backend, web-server, and mobile UI.

Changes

Cohort / File(s) Summary
WebServer surface & factory
src/main/web-server/WebServer.ts, src/main/web-server/web-server-factory.ts
Added terminal-control callbacks (write/resize/spawn/kill), setters to register them, and broadcastToolEvent. Factory now registers terminal callbacks and resolves shell/session-id logic for {sessionId}-terminal.
Broadcasting & forwarding
src/main/process-listeners/forwarding-listeners.ts, src/main/web-server/services/broadcastService.ts, src/main/process-listeners/data-listener.ts
Extended forwarding to accept getWebServer/patterns, derive base session/tab IDs, broadcast tool events to web clients, and changed terminal-data handling to broadcast -terminal session output to web. Added BroadcastService.broadcastToolEvent.
PTY handling
src/main/process-manager/spawners/PtySpawner.ts
Expanded terminal-tab detection: sessions that end with -terminal are treated as terminal tabs and bypass control-sequence stripping to emit raw PTY data.
WebSocket handlers / message types
src/main/web-server/handlers/messageHandlers.ts, src/web/hooks/useWebSocket.ts
Added get_file_tree handler with recursive file-tree building and depth limits. Introduced terminal WebSocket messages (terminal_data, terminal_ready) and tool_event type; added terminal control message handlers and spawn/resize/write flows.
Mobile session & websocket integration
src/web/hooks/useMobileSessionManagement.ts, src/web/hooks/useWebSocket.ts
Mobile session logic now records tool events, marks unread tabs on background output, supports cancellable log fetch, filters local echo, and exposes handlers for tool/terminal events. Added hasUnread flag on AI tabs.
Mobile terminal & UI panels
src/web/mobile/WebTerminal.tsx, src/web/mobile/LeftPanel.tsx, src/web/mobile/RightPanel.tsx, src/web/mobile/App.tsx
Added xterm-based WebTerminal with imperative API, link/search/key handling, and resize reporting. Introduced resizable LeftPanel (agents) and RightPanel (files/history/auto-run/git), integrated panels into MobileApp, added thinking-mode UI, unread indicators, completed-agent notifications, and terminal-mode routing.
Files tab & file-tree UI
src/web/mobile/RightDrawer.tsx (FilesTabContent), src/main/web-server/handlers/messageHandlers.ts
FilesTabContent upgraded to interactive file tree that fetches via get_file_tree WS request; RightDrawer/RightPanel wired to use sendRequest/projectPath.
Mobile input & controls
src/web/mobile/CommandInputBar.tsx, src/web/mobile/CommandInputButtons.tsx, src/web/mobile/TabBar.tsx, src/web/mobile/CommandInputButtons.tsx
Added ThinkingToggleButton and thinking-mode props to CommandInputBar; removed mode toggle prop. TabBar supports terminal tab, New dropdown (New AI / New Terminal), and terminal close control.
Message history rendering
src/web/mobile/MessageHistory.tsx
Extended LogEntry to include thinking/tool sources and metadata.toolState; MessageHistory filters and renders thinking/tool entries and summarizes tool input/status.
Hooks & utilities
src/web/hooks/useIsMobile.ts, src/web/hooks/useResizableWebPanel.ts
Added useIsMobile (debounced resize breakpoint) and useResizableWebPanel (drag-to-resize, localStorage persistence, clamp/commit semantics).
Tests updated
src/__tests__/**/*.test.ts{,x}, src/main/process-listeners/__tests__/*.test.ts
Updated many tests to account for new deps, mocks, terminal broadcasting, changed UI components (LeftPanel/RightPanel/WebTerminal), added ResizeObserver mock, and updated WebServer mock setters. Mostly test-only changes.
Renderer AutoRun docs
src/renderer/App.tsx
Changed getAutoRunDocs remote handler to return structured docs with task counts via readDoc per-file.

Sequence Diagram(s)

sequenceDiagram
    participant ProcessManager
    participant ForwardingListener
    participant WebServer
    participant BroadcastService
    participant WebClient

    ProcessManager->>ForwardingListener: emit tool-execution event
    ForwardingListener->>ForwardingListener: derive baseSessionId, tabId, toolLog
    ForwardingListener->>WebServer: broadcastToolEvent(baseSessionId, tabId, toolLog)
    WebServer->>BroadcastService: broadcastToolEvent(sessionId, tabId, toolLog)
    BroadcastService->>WebClient: send 'tool_event' to subscribed clients
    WebClient->>MobileApp: deliver ToolEventMessage
Loading
sequenceDiagram
    participant MobileApp
    participant WebSocket
    participant WebServer
    participant PTYSpawner
    participant WebTerminal

    MobileApp->>WebSocket: request switch to 'terminal'
    WebSocket->>WebServer: switch_mode -> spawnTerminalForWeb
    WebServer->>PTYSpawner: spawn/reuse PTY for "{sessionId}-terminal"
    PTYSpawner-->>WebServer: return { success, pid }
    WebServer->>WebSocket: send 'terminal_ready'
    WebSocket->>MobileApp: receive terminal_ready
    MobileApp->>WebTerminal: render terminal and send initial resize
    PTYSpawner->>WebServer: onData -> WebServer broadcasts 'terminal_data'
    WebSocket->>WebTerminal: receive terminal_data -> write to xterm
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

🐰 Soft paws tap keys—terminals hum, logs take flight,

Tool events skip like fireflies through the night.
Panels stretch and slide, terminals glow,
A rabbit cheers: "Broadcasting wherever you go!" 🥕✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 78.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Followup: Web Interface UX Parity' is vague and generic, using non-descriptive terms that don't convey meaningful information about the changeset's scope or primary focus. Consider using a more specific title that highlights the main feature or change, such as 'Add terminal mode, swipeable panels, and resizable web UI components' or 'Implement web interface UX parity: terminal, panels, and notifications'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 31, 2026

Greptile Summary

This PR delivers a large set of web interface UX improvements to bring the web client to parity with the native desktop experience. The changes span the full stack: a new PTY-backed xterm.js terminal (WebTerminal), swipeable LeftPanel (agents) and RightPanel (files/history/auto run/git), draggable resize handles for desktop, a notifications dropdown for completed agents, multi-tab support with tab search in TabBar, and real-time tool-execution streaming to web clients.

Key changes include:

  • Backend: data-listener.ts now broadcasts raw PTY data as terminal_data for xterm.js; forwarding-listeners.ts relays tool-execution events to web clients; messageHandlers.ts adds get_file_tree, terminal_write, and terminal_resize handlers plus PTY spawn/kill on mode switch; web-server-factory.ts removes the thinking/tool log filter for full UX parity.
  • Frontend: New WebTerminal (xterm.js with FitAddon, SearchAddon, custom key handling, link detection), LeftPanel (swipe-to-close on mobile, group collapse, unread dots), RightPanel (inline desktop / full-screen mobile), useResizableWebPanel, and useIsMobile hooks. useMobileSessionManagement gains tool-event handling, unread tab tracking, and an AbortController for fetch cleanup.
  • One P1 bug found: useResizableWebPanel adds mousemove/mouseup listeners to document during drag but never removes them if the panel unmounts before the drag ends, leaving a stale overlay div that blocks all pointer events.
  • Two P2 hardening gaps: handleGetFileTree does not cap maxDepth server-side and does not validate dirPath is within the session's project root. The onActiveSessionChanged echo-suppression window is unconditional and could suppress legitimate desktop-initiated switches.

Confidence Score: 4/5

Safe to merge after fixing the drag-listener leak in useResizableWebPanel which can block all mouse input after closing a panel mid-drag.

One P1 bug: drag event listeners and the resize overlay div are orphaned when a panel unmounts during an active resize, which blocks all page mouse input. The two P2 findings (uncapped maxDepth, unrestricted dirPath) are low-severity given the security-token-gated WebSocket, and the echo-suppression heuristic is a quality issue rather than a correctness one. All other changes are well-structured, properly cleaned up, and consistent with existing patterns.

src/web/hooks/useResizableWebPanel.ts — drag cleanup on unmount; src/main/web-server/handlers/messageHandlers.ts — maxDepth cap and path validation in handleGetFileTree.

Important Files Changed

Filename Overview
src/web/hooks/useResizableWebPanel.ts New hook for draggable panel resize — drag event listeners and overlay div are not cleaned up if the component unmounts during an active drag, causing a P1 memory/input-blocking bug.
src/main/web-server/handlers/messageHandlers.ts Adds get_file_tree, terminal_write, terminal_resize handlers and PTY spawn logic on mode switch; dirPath and maxDepth lack server-side caps (P2 hardening gaps).
src/web/mobile/WebTerminal.tsx New full xterm.js terminal component with FitAddon, SearchAddon, Unicode11, custom key handling, link detection, and theme sync — well-structured with proper cleanup.
src/web/mobile/LeftPanel.tsx New agent-list sidebar with swipe-to-close (mobile), group collapsing, worktree nesting, unread badges, and desktop resize handle — well-implemented.
src/web/mobile/RightPanel.tsx New inline right panel (Files/History/AutoRun/Git tabs) replacing the overlay RightDrawer on desktop; full-screen mobile mode relies on backdrop tap to close rather than swipe.
src/web/mobile/App.tsx Major layout overhaul: replaces SessionPillBar with LeftPanel+TabBar, adds RightPanel in flex row, integrates WebTerminal, notifications dropdown, thinking mode toggle, and per-edge swipe gestures.
src/web/hooks/useMobileSessionManagement.ts Adds tool-event handler, unread tab tracking, AbortController for fetch cleanup, and a 2 s echo-suppression guard for server session changes; the suppression window is applied unconditionally (P2).
src/main/web-server/web-server-factory.ts Wires up four new terminal callbacks (write, resize, spawn, kill) and removes the thinking/tool log filter so web clients receive full AI logs for UX parity.
src/main/process-listeners/data-listener.ts Changed from skipping PTY terminal output to broadcasting it as terminal_data messages so the xterm.js web client receives raw PTY data.
src/web/mobile/RightDrawer.tsx Upgraded FilesTabContent from a placeholder to a full file-tree explorer with filtering/refresh; exports tab content components for reuse in RightPanel.
src/web/mobile/TabBar.tsx Adds terminal tab, new-tab dropdown menu (AI Chat / New Terminal), and always shows tab bar even with one tab; active-state logic now accounts for inputMode.
src/web/hooks/useIsMobile.ts New hook with debounced resize listener to detect mobile viewport; correctly cleans up timer and listener on unmount.

Sequence Diagram

sequenceDiagram
    participant WC as Web Client (xterm.js)
    participant WS as WebSocket Server
    participant MH as MessageHandlers
    participant PM as ProcessManager (PTY)
    participant DL as DataListener

    WC->>WS: switch_mode {mode: "terminal"}
    WS->>MH: handleSwitchMode
    MH->>PM: spawnTerminalForWeb(sessionId-terminal, cwd)
    PM-->>MH: {success, pid}
    MH-->>WC: terminal_ready {sessionId}
    MH-->>WC: mode_switch_result {success}

    Note over WC: WebTerminal mounts, 100ms timer fires
    WC->>WS: terminal_resize {cols, rows}
    WS->>PM: resize(sessionId-terminal, cols, rows)

    WC->>WS: terminal_write {data}
    WS->>PM: write(sessionId-terminal, data)
    PM->>DL: onData(sessionId-terminal, rawData)
    DL->>WS: broadcastToSessionClients(sessionId, terminal_data)
    WS-->>WC: terminal_data {data}
    Note over WC: xterm.js renders ANSI output
Loading

Comments Outside Diff (1)

  1. src/web/hooks/useMobileSessionManagement.ts, line 910-913 (link)

    P2 Fixed 2 s window silently drops legitimate server-initiated session switches

    The guard ignores any active_session_changed broadcast from the server for 2 seconds after a local selection — including intentional desktop-side switches that target a different session. A tighter check that only suppresses echoes of the same session the user just selected would avoid this:

    // Only skip if the server is echoing back the exact session we just selected
    if (timeSinceLocalSelect < 2000 && sessionId === activeSessionIdRef.current) {
      // ignore echo
      return;
    }

Reviews (1): Last reviewed commit: "feat: web terminal PTY support and termi..." | Re-trigger Greptile

Comment on lines +51 to +88
const onResizeStart = useCallback(
(e: React.MouseEvent) => {
e.preventDefault();
isResizing.current = true;
startX.current = e.clientX;
startWidth.current = width;

// Add a full-screen overlay to capture mouse events during drag
const overlay = document.createElement('div');
overlay.id = 'resize-overlay';
overlay.style.cssText = 'position:fixed;inset:0;z-index:9999;cursor:col-resize;';
document.body.appendChild(overlay);

const onMouseMove = (ev: MouseEvent) => {
if (!isResizing.current || !panelRef.current) return;
const delta = side === 'left' ? ev.clientX - startX.current : startX.current - ev.clientX;
const newWidth = Math.max(minWidth, Math.min(maxWidth, startWidth.current + delta));
// Direct DOM manipulation for performance (no React re-renders during drag)
panelRef.current.style.width = `${newWidth}px`;
};

const onMouseUp = (ev: MouseEvent) => {
isResizing.current = false;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
const el = document.getElementById('resize-overlay');
if (el) el.remove();

// Commit final width to React state + localStorage
const delta = side === 'left' ? ev.clientX - startX.current : startX.current - ev.clientX;
commitWidth(startWidth.current + delta);
};

document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
},
[side, width, minWidth, maxWidth, commitWidth]
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Drag listeners and overlay orphaned on mid-drag unmount

onMouseMove and onMouseUp are added to document inside onResizeStart, but they are only removed when mouseup fires. If the panel is closed while the user is actively dragging (via keyboard shortcut, mobile swipe, parent re-render, or setShowLeftPanel(false)) the two listeners leak permanently and the resize-overlay div remains in the DOM — blocking all pointer events across the entire page.

A useEffect cleanup that removes the listeners and overlay on unmount is needed:

const activeListenersRef = useRef<{
  move: (ev: MouseEvent) => void;
  up: (ev: MouseEvent) => void;
} | null>(null);

// in onResizeStart, assign before attaching:
activeListenersRef.current = { move: onMouseMove, up: onMouseUp };
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);

Then add a cleanup effect:

useEffect(() => {
  return () => {
    if (activeListenersRef.current) {
      document.removeEventListener('mousemove', activeListenersRef.current.move);
      document.removeEventListener('mouseup', activeListenersRef.current.up);
      document.getElementById('resize-overlay')?.remove();
      activeListenersRef.current = null;
    }
  };
}, []);

Comment on lines +989 to +997
private handleGetFileTree(client: WebClient, message: WebClientMessage): void {
const sessionId = message.sessionId as string;
const dirPath = message.path as string;
const maxDepth = (message.maxDepth as number) || 3;

if (!dirPath) {
this.sendError(client, 'Missing path for get_file_tree');
return;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 dirPath and maxDepth lack server-side validation

Two hardening gaps in handleGetFileTree:

  1. Unrestricted path: dirPath is taken directly from the client message without validating it is within an expected root (e.g., the session's cwd). Any authenticated web client can request a file tree rooted at /, ~/.ssh, /etc, etc.

  2. Uncapped maxDepth: The client sends maxDepth: 3 today, but there is no server-side cap. A client sending maxDepth: 100 triggers deeply recursive buildFileTree calls that could exhaust memory or the call stack on large directory trees.

const MAX_ALLOWED_DEPTH = 5;
const maxDepth = Math.min((message.maxDepth as number) || 3, MAX_ALLOWED_DEPTH);

const sessionDetail = this.callbacks.getSessionDetail?.(sessionId);
const allowedRoot = sessionDetail?.cwd;
if (allowedRoot && !dirPath.startsWith(allowedRoot)) {
  this.sendError(client, 'Path is outside allowed project directory');
  return;
}

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

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

⚠️ Outside diff range comments (4)
src/web/hooks/useMobileSessionManagement.ts (2)

282-315: ⚠️ Potential issue | 🟡 Minor

Don’t let an aborted fetch clear the current loading state.

When the session/tab changes quickly, the old request is aborted in cleanup but its finally still runs and flips isLoadingLogs to false while the next request is already in flight. That causes the loading spinner to flicker and the empty state can flash briefly.

💡 Suggested fix
	useEffect(() => {
		if (!activeSessionId || isOffline) {
			setSessionLogs({ aiLogs: [], shellLogs: [] });
+			setIsLoadingLogs(false);
			return;
		}

		const controller = new AbortController();
+		let isCurrent = true;

		const fetchSessionLogs = async () => {
			setIsLoadingLogs(true);
			try {
				// Pass tabId explicitly to avoid race conditions with activeTabId sync
				const tabParam = activeTabId ? `?tabId=${activeTabId}` : '';
				const apiUrl = buildApiUrl(`/session/${activeSessionId}${tabParam}`);
				const response = await fetch(apiUrl, { signal: controller.signal });
				if (response.ok) {
					const data = await response.json();
					const session = data.session;
					setSessionLogs({
						aiLogs: session?.aiLogs || [],
						shellLogs: session?.shellLogs || [],
					});
					webLogger.debug('Fetched session logs:', 'Mobile', {
						aiLogs: session?.aiLogs?.length || 0,
						shellLogs: session?.shellLogs?.length || 0,
						requestedTabId: activeTabId,
						returnedTabId: session?.activeTabId,
					});
				}
			} catch (err) {
				if ((err as Error).name === 'AbortError') return;
				webLogger.error('Failed to fetch session logs', 'Mobile', err);
			} finally {
-				setIsLoadingLogs(false);
+				if (isCurrent) {
+					setIsLoadingLogs(false);
+				}
			}
		};

		fetchSessionLogs();
-		return () => controller.abort();
+		return () => {
+			isCurrent = false;
+			controller.abort();
+		};
	}, [activeSessionId, activeTabId, isOffline]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/web/hooks/useMobileSessionManagement.ts` around lines 282 - 315, The
effect's fetchSessionLogs finally block always calls setIsLoadingLogs(false), so
when an in-flight request is aborted it clears loading even though a new request
may have started; update fetchSessionLogs (in useMobileSessionManagement.ts) to
skip clearing loading when the request was aborted: in the catch/ finally logic
check the AbortController signal (controller.signal.aborted) or the caught error
name === 'AbortError' and avoid calling setIsLoadingLogs(false) when aborted,
ensuring setIsLoadingLogs is only cleared for non-abort completions.

638-668: ⚠️ Potential issue | 🟠 Major

Use tabId to mark the emitting tab unread instead of every tab or none.

For another session, this marks all tabs unread even when only one AI tab produced output. For a background tab in the current session, the early return drops the chunk without marking that tab unread at all. That makes the new unread badges unreliable as soon as multiple AI tabs are active.

💡 Suggested fix
				if (currentActiveId !== sessionId) {
					webLogger.debug('Marking session as unread - not active session', 'Mobile', {
						sessionId,
						activeSessionId: currentActiveId,
					});
					setSessions((prev) =>
						prev.map((s) => {
							if (s.id !== sessionId) return s;
+							const unreadTabId = source === 'ai' ? tabId ?? s.activeTabId : s.activeTabId;
							return {
								...s,
-								aiTabs: s.aiTabs?.map((tab) => ({
-									...tab,
-									hasUnread: true,
-								})),
+								aiTabs: s.aiTabs?.map((tab) => ({
+									...tab,
+									hasUnread: unreadTabId ? tab.id === unreadTabId || tab.hasUnread : tab.hasUnread,
+								})),
							};
						})
					);
					return;
				}

				// For AI output with tabId, only update if this is the active tab
				// This prevents output from newly created tabs appearing in the wrong tab's logs
				if (source === 'ai' && tabId && currentActiveTabId && tabId !== currentActiveTabId) {
					webLogger.debug('Skipping output - not active tab', 'Mobile', {
						sessionId,
						outputTabId: tabId,
						activeTabId: currentActiveTabId,
					});
+					setSessions((prev) =>
+						prev.map((s) =>
+							s.id === sessionId
+								? {
+										...s,
+										aiTabs: s.aiTabs?.map((tab) => ({
+											...tab,
+											hasUnread: tab.id === tabId || tab.hasUnread,
+										})),
+									}
+								: s
+						)
+					);
					return;
				}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/web/hooks/useMobileSessionManagement.ts` around lines 638 - 668, The
handler incorrectly marks all aiTabs unread or none: when currentActiveId !==
sessionId you should mark only the emitting tab (use tabId) instead of setting
hasUnread on every tab, and when source === 'ai' and tabId exists but isn't the
active tab, instead of returning early and dropping the chunk you should set
that specific ai tab's hasUnread = true via setSessions (update s.aiTabs where
tab.id === tabId) and then return; update the setSessions call (and the
early-return branch) around currentActiveId, sessionId, setSessions, s.aiTabs,
tabId and currentActiveTabId to only touch the matching tab's hasUnread flag.
src/web/mobile/App.tsx (2)

2405-2425: ⚠️ Potential issue | 🟡 Minor

Reuse handleAutoRunOpenSetup() for the palette launch path.

This is the one path that opens AutoRunSetupSheet without calling loadAutoRunDocuments(activeSessionId), so opening Auto Run from Cmd+K can show stale or empty documents.

Also add handleAutoRunOpenSetup to the commandPaletteActions useMemo dependency list when you switch this action over.

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

In `@src/web/mobile/App.tsx` around lines 2405 - 2425, The palette action with id
'autorun-launch' currently calls setShowAutoRunSetup(true) directly and can open
AutoRunSetupSheet without loading documents; change its action to call the
existing handleAutoRunOpenSetup() so loadAutoRunDocuments(activeSessionId) runs
before showing the sheet, and update the commandPaletteActions useMemo
dependency array to include handleAutoRunOpenSetup so the memo updates correctly
when that handler changes. Ensure the action still respects available: () =>
!!activeSessionId && !currentAutoRunState?.isRunning and remove the direct
setShowAutoRunSetup usage here.

3078-3084: ⚠️ Potential issue | 🟠 Major

Tab search should switch back to AI mode before selecting an AI tab.

The direct tab-bar path already does this at Lines 3004-3010, but the search modal still calls handleSelectTab directly. From terminal mode, picking a tab here changes the active AI tab while leaving the UI in terminal mode.

💡 Suggested fix
				<TabSearchModal
					tabs={activeSession.aiTabs}
					activeTabId={activeSession.activeTabId}
-					onSelectTab={handleSelectTab}
+					onSelectTab={(tabId) => {
+						if (currentInputMode !== 'ai') {
+							handleModeToggle('ai');
+						}
+						handleSelectTab(tabId);
+					}}
					onClose={handleCloseTabSearch}
				/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/web/mobile/App.tsx` around lines 3078 - 3084, When wiring TabSearchModal,
ensure selecting a tab forces the UI back into AI mode before activating the
tab: change the onSelectTab passed to <TabSearchModal> so it wraps
handleSelectTab with the same mode-switch used by the direct tab-bar path (i.e.,
first set the UI/mode to "ai" using the same routine used earlier, then call
handleSelectTab(tabId) and finally call handleCloseTabSearch()); update the
onSelectTab prop to this wrapper so selecting from the search modal behaves
identical to the direct tab-bar selection.
🧹 Nitpick comments (7)
src/main/web-server/WebServer.ts (1)

842-859: Extract the shared toolLog contract.

This inline payload shape is now duplicated here and in src/main/web-server/services/broadcastService.ts. The next field change can easily update one side but not the other. A shared ToolLog/ToolEventPayload type would keep the websocket contract aligned.

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

In `@src/main/web-server/WebServer.ts` around lines 842 - 859, The inline toolLog
payload type used in broadcastToolEvent and the duplicate in
broadcastService.broadcastToolEvent should be extracted into a shared type
(e.g., ToolLog or ToolEventPayload); create that exported interface/type and use
it in the WebServer.broadcastToolEvent method signature and in
BroadcastService.broadcastToolEvent so both reference the same type, then update
any imports/exports to use the new shared type and remove the inline anonymous
shape from the method signatures to keep the websocket contract in sync.
src/web/mobile/RightDrawer.tsx (1)

317-327: Potential performance issue with recursive filter matching.

The matchesFilter function recursively traverses all children when checking if a folder matches. For large trees, this could cause re-renders to be slow since filterLower changes trigger useMemo recalculation.

Consider memoizing the filtered tree result instead of checking matchesFilter during render.

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

In `@src/web/mobile/RightDrawer.tsx` around lines 317 - 327, The recursive
matchesFilter callback (matchesFilter, created with useCallback and depending on
filterLower) causes repeated deep traversal of node.children on each render;
replace per-node recursion during render by computing and memoizing a filtered
tree once when filterLower changes (e.g., use useMemo to produce filteredRoot or
getFilteredTree from the original tree) and then render from that memoized
filtered tree instead of calling matchesFilter for each node; ensure the
memoization logic preserves folder structure and only recurses inside useMemo so
matchesFilter (if kept) no longer runs per-render over the whole tree.
src/main/web-server/web-server-factory.ts (1)

305-311: Returning pid: 0 when PTY already exists may cause confusion.

When a PTY already exists, returning { success: true, pid: 0 } makes it hard for callers to distinguish between "successfully reused existing PTY" vs "spawned new PTY with pid 0" (which would be unusual). Consider returning the actual PID of the existing process or a distinct response shape.

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

In `@src/main/web-server/web-server-factory.ts` around lines 305 - 311, The
handler currently returns { success: true, pid: 0 } when
processManager.get(terminalSessionId) is truthy, which is ambiguous; instead
retrieve the existing PTY's real PID from processManager (or its stored object)
and return it (e.g., { success: true, pid: existingPid }) or change the response
shape to explicitly indicate reuse (e.g., { success: true, reused: true, pid:
existingPid }). Update the block that checks
processManager.get(terminalSessionId) in web-server-factory.ts to fetch the PID
from the stored entry for terminalSessionId (use the same identifier names
processManager and terminalSessionId) and return the clarified response shape,
and update any callers to handle the new field if necessary.
src/web/hooks/useResizableWebPanel.ts (1)

90-90: Returning isResizing as a ref may confuse consumers.

The hook returns isResizing which is a RefObject<boolean>, but consumers might expect a boolean value. Consider returning isResizing.current wrapped in state if reactive updates are needed, or document the ref behavior clearly.

src/web/mobile/RightPanel.tsx (1)

18-36: Unused prop: onAutoRunOpenDocument is declared but never used.

The onAutoRunOpenDocument prop is declared in RightPanelProps (line 26) and send (line 29), but neither is destructured in the component function (lines 48-63) nor passed to any child component.

If these props are intended for future use, consider removing them from the interface to avoid confusion. If they should be wired up, the AutoRunTabContent might need them.

♻️ Proposed cleanup
 export interface RightPanelProps {
 	sessionId: string;
 	activeTab?: RightDrawerTab;
 	autoRunState: AutoRunState | null;
 	gitStatus: UseGitStatusReturn;
 	onClose: () => void;
 	onFileSelect?: (path: string) => void;
 	projectPath?: string;
-	onAutoRunOpenDocument?: (filename: string) => void;
 	onAutoRunOpenSetup?: () => void;
 	sendRequest: UseWebSocketReturn['sendRequest'];
-	send: UseWebSocketReturn['send'];
 	onViewDiff?: (filePath: string) => void;
 	panelRef?: React.RefObject<HTMLDivElement>;
 	width?: number;
 	onResizeStart?: (e: React.MouseEvent) => void;
 	isFullScreen?: boolean;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/web/mobile/RightPanel.tsx` around lines 18 - 36, RightPanelProps declares
onAutoRunOpenDocument (and send) but the RightPanel component doesn't
destructure or pass them through; either remove these unused props from
RightPanelProps to clean up the interface, or wire them up by adding
onAutoRunOpenDocument (and send if needed) to the RightPanel component's props
destructuring and forwarding onAutoRunOpenDocument to the AutoRunTabContent (or
the appropriate child that expects an "open document" callback). Update usages
of AutoRunTabContent to accept the forwarded prop name if necessary and remove
any dead declarations if you choose the cleanup route.
src/main/process-listeners/forwarding-listeners.ts (1)

42-42: Consider handling missing tabId more explicitly.

When REGEX_AI_TAB_ID doesn't match, tabId is set to an empty string. Per the broadcastToolEvent signature in WebServer.ts, tabId is a required string parameter. An empty string might cause issues with client-side routing.

Consider logging a warning when tabId extraction fails or ensuring the regex always matches for valid AI session IDs.

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

In `@src/main/process-listeners/forwarding-listeners.ts` at line 42, The code
currently sets tabId to '' when REGEX_AI_TAB_ID fails to match (const tabId =
tabIdMatch ? tabIdMatch[1] : ''), but broadcastToolEvent in WebServer.ts
requires a valid non-empty tabId; update the logic in forwarding-listeners.ts to
handle missing tabId explicitly by either logging a warning/error via the
existing logger and skipping/aborting the broadcast or by deriving a safe
fallback before calling broadcastToolEvent; reference REGEX_AI_TAB_ID,
tabIdMatch/tabId and the broadcastToolEvent call so you can add the warning and
early return (or a deterministic fallback) to avoid passing an empty string to
broadcastToolEvent.
src/main/process-listeners/__tests__/forwarding-listeners.test.ts (1)

23-30: Test dependencies updated correctly, but web broadcast path is untested.

The mock dependencies now match the updated function signature. However, since getWebServer returns null, the web broadcast code path in the tool-execution handler isn't covered by these tests.

Consider adding a separate test case with a mock web server to verify broadcastToolEvent is called correctly.

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

In `@src/main/process-listeners/__tests__/forwarding-listeners.test.ts` around
lines 23 - 30, Add a new test that supplies mockDeps.getWebServer returning a
fake web server with a broadcastToolEvent spy, then emit the "tool-execution"
message to exercise the web broadcast path and assert broadcastToolEvent was
called with the expected payload; specifically update the test suite to include
a case where mockDeps.getWebServer = () => ({ broadcastToolEvent: jest.fn() })
(or equivalent), trigger the handler that processes "tool-execution" and verify
the fake server's broadcastToolEvent was invoked, while keeping existing tests
that use getWebServer = () => null to cover the non-web path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/web-server/services/broadcastService.ts`:
- Around line 444-467: The broadcastToolEvent currently calls broadcastToSession
which also sends to clients with no subscribedSessionId and thus fans out
tool_event to dashboards; instead restrict delivery to only clients actively
viewing the given session/tab. Update broadcastToolEvent to send only to clients
whose subscribedSessionId === sessionId (and if you track tab subscriptions,
subscribedTabId === tabId) rather than using broadcastToSession, or add/use a
helper (e.g., broadcastToSubscribedClients/broadcastToSessionTabSubscribers)
that filters by subscribedSessionId/subscribedTabId; ensure the toolLog
(including toolLog.metadata.toolState.input) is only sent to those filtered
subscribers.

In `@src/web/hooks/useIsMobile.ts`:
- Around line 28-46: The effect in useIsMobile only updates isMobile on resize
so changing the breakpoint prop doesn't update the hook until a user resizes;
modify the effect in useIsMobile to also synchronize isMobile when breakpoint
changes by running the same check immediately (e.g. call
setIsMobile(window.innerWidth <= breakpoint) or invoke handleResize once) before
registering the resize listener and debounce timer, and keep existing cleanup of
timerRef and event listener in the returned function.

In `@src/web/hooks/useMobileSessionManagement.ts`:
- Around line 562-573: The current onActiveSessionChanged handler suppresses all
server-driven active-session updates for 2s after any local selection; change it
to only suppress echoes for the same session by tracking the last locally
selected session id and comparing it to the incoming sessionId. Add a new ref
(e.g. lastLocalSelectedSessionIdRef) and set it whenever the user locally
selects a session (alongside updating lastLocalSelectionRef), then update
onActiveSessionChanged to only early-return when timeSinceLocalSelect < 2000 AND
sessionId === lastLocalSelectedSessionIdRef.current; keep the existing
webLogger.debug message but include the session id check so legitimate
desktop-driven switches to different sessions are not dropped.

In `@src/web/hooks/useResizableWebPanel.ts`:
- Around line 51-88: The onResizeStart handler installs document-level listeners
and an overlay but never ensures they are removed if the component unmounts or a
new drag starts; update the hook to clean up those listeners and the overlay:
track the onMouseMove/onMouseUp handlers (and overlay id 'resize-overlay') in
refs or in a scoped variable so you can remove them later, add a useEffect
cleanup that checks isResizing/current handlers to call
document.removeEventListener('mousemove', onMouseMove) and
removeEventListener('mouseup', onMouseUp) and remove the overlay element, and
ensure onResizeStart also removes any existing handlers/overlay before attaching
new ones; reference functions/vars to update: onResizeStart, isResizing, startX,
startWidth, panelRef, commitWidth.

In `@src/web/mobile/App.tsx`:
- Around line 1402-1418: The terminal handlers currently act on every PTY event
regardless of which terminal is mounted; fix by tracking the currently mounted
session id in a ref (e.g., mountedSessionIdRef) and gating both onTerminalData
and onTerminalReady on that ref: use the sessionId parameter (rename _sessionId
to sessionId or read it) and only call webTerminalRef.current?.write(...) or
fitAndGetSize()/wsSendRef.current... when sessionId ===
mountedSessionIdRef.current; ensure mountedSessionIdRef is set when the
WebTerminal mounts and cleared on unmount so stray PTY messages are ignored.

In `@src/web/mobile/CommandInputBar.tsx`:
- Around line 40-46: CommandInputBar's terminal-mode branches are unreachable
because the mobile App still passes a hardcoded inputMode={'ai'} and no
mode-toggle callback after replacing the action row; update the mobile App usage
to pass the real inputMode state and a mode-toggle handler (the same props
CommandInputBar expects, e.g., inputMode and onToggleInputMode or equivalent) or
reintroduce a visible mode switch in the action row so the component's inputMode
=== 'terminal' branches can be reached; locate CallSite in
src/web/mobile/App.tsx where CommandInputBar is instantiated and change the prop
wiring to forward the parent state and toggle callback instead of the hardcoded
'ai' value (also fix the other occurrences mentioned: lines ~137-142, 171-173,
760-767).

In `@src/web/mobile/CommandInputButtons.tsx`:
- Around line 561-595: The button is a three-state toggle but only exposes a
generic role; update the JSX for the button in CommandInputButtons (the element
using props isOff and isSticky and handler handleClick) to include an
aria-pressed attribute set to convey mixed/on/off state (use
aria-pressed={isSticky ? 'mixed' : !isOff}) so screen readers announce
sticky/on/off correctly; keep existing aria-label/title and other props
unchanged.

In `@src/web/mobile/LeftPanel.tsx`:
- Around line 583-623: The expander is implemented as a <span role="button">
inside the session row <button>, creating nested interactive controls and
removing it from the tab order; change the expander to a native button
(type="button") with proper aria-expanded and aria-label, ensure it uses the
existing toggleWorktrees(session.id) onClick and reflects isWorktreeExpanded,
include the visual contents (GitBranchIcon, children.length and chevron SVG),
and move or render this button as a sibling (not a descendant) of the session
row button so it can receive keyboard focus and avoid nested button semantics.

In `@src/web/mobile/TabBar.tsx`:
- Around line 793-881: The close "x" inside the Terminal tab is currently a
non-focusable <span> nested within a <button>, which is invalid and not
keyboard-accessible; change this to a separate focusable button element (use
type="button") rendered alongside the main Terminal tab button like the existing
Tab + close-button pattern, wire its onClick to call e.stopPropagation(),
triggerHaptic(HAPTIC_PATTERNS.tap) and onCloseTerminal(), ensure it only renders
when onCloseTerminal && inputMode === 'terminal', add an accessible aria-label
(e.g., "Close terminal"), and copy the visual styles
(width/height/borderRadius/opacity) so it visually matches the previous span
while being keyboard-focusable.

---

Outside diff comments:
In `@src/web/hooks/useMobileSessionManagement.ts`:
- Around line 282-315: The effect's fetchSessionLogs finally block always calls
setIsLoadingLogs(false), so when an in-flight request is aborted it clears
loading even though a new request may have started; update fetchSessionLogs (in
useMobileSessionManagement.ts) to skip clearing loading when the request was
aborted: in the catch/ finally logic check the AbortController signal
(controller.signal.aborted) or the caught error name === 'AbortError' and avoid
calling setIsLoadingLogs(false) when aborted, ensuring setIsLoadingLogs is only
cleared for non-abort completions.
- Around line 638-668: The handler incorrectly marks all aiTabs unread or none:
when currentActiveId !== sessionId you should mark only the emitting tab (use
tabId) instead of setting hasUnread on every tab, and when source === 'ai' and
tabId exists but isn't the active tab, instead of returning early and dropping
the chunk you should set that specific ai tab's hasUnread = true via setSessions
(update s.aiTabs where tab.id === tabId) and then return; update the setSessions
call (and the early-return branch) around currentActiveId, sessionId,
setSessions, s.aiTabs, tabId and currentActiveTabId to only touch the matching
tab's hasUnread flag.

In `@src/web/mobile/App.tsx`:
- Around line 2405-2425: The palette action with id 'autorun-launch' currently
calls setShowAutoRunSetup(true) directly and can open AutoRunSetupSheet without
loading documents; change its action to call the existing
handleAutoRunOpenSetup() so loadAutoRunDocuments(activeSessionId) runs before
showing the sheet, and update the commandPaletteActions useMemo dependency array
to include handleAutoRunOpenSetup so the memo updates correctly when that
handler changes. Ensure the action still respects available: () =>
!!activeSessionId && !currentAutoRunState?.isRunning and remove the direct
setShowAutoRunSetup usage here.
- Around line 3078-3084: When wiring TabSearchModal, ensure selecting a tab
forces the UI back into AI mode before activating the tab: change the
onSelectTab passed to <TabSearchModal> so it wraps handleSelectTab with the same
mode-switch used by the direct tab-bar path (i.e., first set the UI/mode to "ai"
using the same routine used earlier, then call handleSelectTab(tabId) and
finally call handleCloseTabSearch()); update the onSelectTab prop to this
wrapper so selecting from the search modal behaves identical to the direct
tab-bar selection.

---

Nitpick comments:
In `@src/main/process-listeners/__tests__/forwarding-listeners.test.ts`:
- Around line 23-30: Add a new test that supplies mockDeps.getWebServer
returning a fake web server with a broadcastToolEvent spy, then emit the
"tool-execution" message to exercise the web broadcast path and assert
broadcastToolEvent was called with the expected payload; specifically update the
test suite to include a case where mockDeps.getWebServer = () => ({
broadcastToolEvent: jest.fn() }) (or equivalent), trigger the handler that
processes "tool-execution" and verify the fake server's broadcastToolEvent was
invoked, while keeping existing tests that use getWebServer = () => null to
cover the non-web path.

In `@src/main/process-listeners/forwarding-listeners.ts`:
- Line 42: The code currently sets tabId to '' when REGEX_AI_TAB_ID fails to
match (const tabId = tabIdMatch ? tabIdMatch[1] : ''), but broadcastToolEvent in
WebServer.ts requires a valid non-empty tabId; update the logic in
forwarding-listeners.ts to handle missing tabId explicitly by either logging a
warning/error via the existing logger and skipping/aborting the broadcast or by
deriving a safe fallback before calling broadcastToolEvent; reference
REGEX_AI_TAB_ID, tabIdMatch/tabId and the broadcastToolEvent call so you can add
the warning and early return (or a deterministic fallback) to avoid passing an
empty string to broadcastToolEvent.

In `@src/main/web-server/web-server-factory.ts`:
- Around line 305-311: The handler currently returns { success: true, pid: 0 }
when processManager.get(terminalSessionId) is truthy, which is ambiguous;
instead retrieve the existing PTY's real PID from processManager (or its stored
object) and return it (e.g., { success: true, pid: existingPid }) or change the
response shape to explicitly indicate reuse (e.g., { success: true, reused:
true, pid: existingPid }). Update the block that checks
processManager.get(terminalSessionId) in web-server-factory.ts to fetch the PID
from the stored entry for terminalSessionId (use the same identifier names
processManager and terminalSessionId) and return the clarified response shape,
and update any callers to handle the new field if necessary.

In `@src/main/web-server/WebServer.ts`:
- Around line 842-859: The inline toolLog payload type used in
broadcastToolEvent and the duplicate in broadcastService.broadcastToolEvent
should be extracted into a shared type (e.g., ToolLog or ToolEventPayload);
create that exported interface/type and use it in the
WebServer.broadcastToolEvent method signature and in
BroadcastService.broadcastToolEvent so both reference the same type, then update
any imports/exports to use the new shared type and remove the inline anonymous
shape from the method signatures to keep the websocket contract in sync.

In `@src/web/mobile/RightDrawer.tsx`:
- Around line 317-327: The recursive matchesFilter callback (matchesFilter,
created with useCallback and depending on filterLower) causes repeated deep
traversal of node.children on each render; replace per-node recursion during
render by computing and memoizing a filtered tree once when filterLower changes
(e.g., use useMemo to produce filteredRoot or getFilteredTree from the original
tree) and then render from that memoized filtered tree instead of calling
matchesFilter for each node; ensure the memoization logic preserves folder
structure and only recurses inside useMemo so matchesFilter (if kept) no longer
runs per-render over the whole tree.

In `@src/web/mobile/RightPanel.tsx`:
- Around line 18-36: RightPanelProps declares onAutoRunOpenDocument (and send)
but the RightPanel component doesn't destructure or pass them through; either
remove these unused props from RightPanelProps to clean up the interface, or
wire them up by adding onAutoRunOpenDocument (and send if needed) to the
RightPanel component's props destructuring and forwarding onAutoRunOpenDocument
to the AutoRunTabContent (or the appropriate child that expects an "open
document" callback). Update usages of AutoRunTabContent to accept the forwarded
prop name if necessary and remove any dead declarations if you choose the
cleanup route.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 14234a4a-98d7-47c6-a021-1b04bec14f52

📥 Commits

Reviewing files that changed from the base of the PR and between 88b7e1b and 963dfdb.

📒 Files selected for processing (21)
  • src/main/process-listeners/__tests__/forwarding-listeners.test.ts
  • src/main/process-listeners/data-listener.ts
  • src/main/process-listeners/forwarding-listeners.ts
  • src/main/process-manager/spawners/PtySpawner.ts
  • src/main/web-server/WebServer.ts
  • src/main/web-server/handlers/messageHandlers.ts
  • src/main/web-server/services/broadcastService.ts
  • src/main/web-server/web-server-factory.ts
  • src/web/hooks/useIsMobile.ts
  • src/web/hooks/useMobileSessionManagement.ts
  • src/web/hooks/useResizableWebPanel.ts
  • src/web/hooks/useWebSocket.ts
  • src/web/mobile/App.tsx
  • src/web/mobile/CommandInputBar.tsx
  • src/web/mobile/CommandInputButtons.tsx
  • src/web/mobile/LeftPanel.tsx
  • src/web/mobile/MessageHistory.tsx
  • src/web/mobile/RightDrawer.tsx
  • src/web/mobile/RightPanel.tsx
  • src/web/mobile/TabBar.tsx
  • src/web/mobile/WebTerminal.tsx

Comment on lines +793 to +881
{/* Terminal tab */}
{onSelectTerminal && (
<button
onClick={() => {
triggerHaptic(HAPTIC_PATTERNS.tap);
onSelectTerminal();
}}
style={{
display: 'flex',
alignItems: 'center',
gap: '6px',
padding: '6px 10px',
borderTopLeftRadius: '6px',
borderTopRightRadius: '6px',
borderTop:
inputMode === 'terminal' ? `1px solid ${colors.border}` : '1px solid transparent',
borderLeft:
inputMode === 'terminal' ? `1px solid ${colors.border}` : '1px solid transparent',
borderRight:
inputMode === 'terminal' ? `1px solid ${colors.border}` : '1px solid transparent',
borderBottom:
inputMode === 'terminal' ? `1px solid ${colors.bgMain}` : '1px solid transparent',
backgroundColor: inputMode === 'terminal' ? colors.bgMain : 'transparent',
color: inputMode === 'terminal' ? colors.textMain : colors.textDim,
fontSize: '12px',
fontWeight: inputMode === 'terminal' ? 600 : 400,
fontFamily: 'monospace',
cursor: 'pointer',
whiteSpace: 'nowrap',
transition: 'all 0.15s ease',
marginBottom: inputMode === 'terminal' ? '-1px' : '0',
zIndex: inputMode === 'terminal' ? 1 : 0,
touchAction: 'pan-x pan-y',
WebkitTapHighlightColor: 'transparent',
userSelect: 'none',
WebkitUserSelect: 'none',
flexShrink: 0,
}}
>
{/* Terminal icon */}
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="4 17 10 11 4 5" />
<line x1="12" y1="19" x2="20" y2="19" />
</svg>
Terminal
{onCloseTerminal && inputMode === 'terminal' && (
<span
onClick={(e) => {
e.stopPropagation();
triggerHaptic(HAPTIC_PATTERNS.tap);
onCloseTerminal();
}}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '16px',
height: '16px',
borderRadius: '4px',
marginLeft: '4px',
cursor: 'pointer',
opacity: 0.6,
}}
>
<svg
width="10"
height="10"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</span>
)}
</button>
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

Make the terminal close affordance a real button.

The x is a clickable <span> nested inside the terminal tab <button>. It's not keyboard-focusable, and nested interactive content inside a button is invalid HTML, so keyboard users can't close the terminal tab from this UI. Please mirror the existing Tab wrapper + separate close-button pattern here.

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

In `@src/web/mobile/TabBar.tsx` around lines 793 - 881, The close "x" inside the
Terminal tab is currently a non-focusable <span> nested within a <button>, which
is invalid and not keyboard-accessible; change this to a separate focusable
button element (use type="button") rendered alongside the main Terminal tab
button like the existing Tab + close-button pattern, wire its onClick to call
e.stopPropagation(), triggerHaptic(HAPTIC_PATTERNS.tap) and onCloseTerminal(),
ensure it only renders when onCloseTerminal && inputMode === 'terminal', add an
accessible aria-label (e.g., "Close terminal"), and copy the visual styles
(width/height/borderRadius/opacity) so it visually matches the previous span
while being keyboard-focusable.

Greptile: fix resize listener leak on unmount, add file tree path/depth
validation. CodeRabbit: scope tool_event broadcasts to subscribed clients,
sync useIsMobile on breakpoint change, fix session echo suppression to
allow cross-session switches, guard terminal events by session ID, wire
real inputMode to CommandInputBar/MessageHistory, add aria-pressed to
toggle, fix nested interactive elements in worktree expander and terminal
close button. User issues: add swipe-to-close for right panel, transform
Auto Run file paths into documents with task counts, persist group
collapse state across sidebar open/close. Update all affected tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (5)
src/__tests__/main/web-server/web-server-factory.test.ts (1)

73-76: Add explicit registration assertions for the new terminal callbacks.

These mock additions are correct, but the suite still doesn’t verify that createWebServerFactory actually registers all four terminal setters. Please add expectations in the callback registrations block for:
setWriteToTerminalCallback, setResizeTerminalCallback, setSpawnTerminalForWebCallback, and setKillTerminalForWebCallback, to prevent silent regressions in PTY wiring.

Suggested test addition
 describe('callback registrations', () => {
@@
 	it('should register file and auto-run callbacks', () => {
 		expect(server.setOpenFileTabCallback).toHaveBeenCalled();
 		expect(server.setRefreshFileTreeCallback).toHaveBeenCalled();
 		expect(server.setRefreshAutoRunDocsCallback).toHaveBeenCalled();
 		expect(server.setConfigureAutoRunCallback).toHaveBeenCalled();
 	});
+
+	it('should register terminal callbacks', () => {
+		expect(server.setWriteToTerminalCallback).toHaveBeenCalled();
+		expect(server.setResizeTerminalCallback).toHaveBeenCalled();
+		expect(server.setSpawnTerminalForWebCallback).toHaveBeenCalled();
+		expect(server.setKillTerminalForWebCallback).toHaveBeenCalled();
+	});
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/main/web-server/web-server-factory.test.ts` around lines 73 -
76, The test currently defines mocks for setWriteToTerminalCallback,
setResizeTerminalCallback, setSpawnTerminalForWebCallback, and
setKillTerminalForWebCallback but doesn't assert they were registered; update
the "callback registrations" test block in the createWebServerFactory spec to
add expectations that each mock was invoked (e.g.
expect(setWriteToTerminalCallback).toHaveBeenCalled(),
expect(setResizeTerminalCallback).toHaveBeenCalled(),
expect(setSpawnTerminalForWebCallback).toHaveBeenCalled(),
expect(setKillTerminalForWebCallback).toHaveBeenCalled()) after calling
createWebServerFactory so the suite verifies the factory actually registers all
four terminal setters.
src/renderer/App.tsx (1)

2045-2047: Consider adding a debug log for per-file read failures.

The error isolation pattern is sound—one file failing shouldn't abort the entire docs fetch. However, the completely silent catch makes debugging harder when files unexpectedly fail to load. A minimal log would preserve the graceful degradation while aiding troubleshooting.

🔧 Suggested improvement
 					} catch {
-						// If reading fails, leave counts at 0
+						// Expected: file may be deleted, renamed, or inaccessible
+						console.debug('[Remote] getAutoRunDocs: skipped file (read failed)', filePath);
 					}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/App.tsx` around lines 2045 - 2047, The silent catch in
src/renderer/App.tsx (the per-file read try/catch that currently “leaves counts
at 0”) should log the failure for debugging: inside that catch block, capture
the error and the file identifier (e.g., filename or doc id from the surrounding
scope) and call the project's debug logger (or console.debug if no logger
available) with a concise message and the error object so per-file read failures
are visible without changing behavior. Ensure you reference the same variables
used in the try (filename/docId) to provide context.
src/__tests__/main/process-listeners/forwarding-listeners.test.ts (1)

77-90: Add one positive test for the web broadcast branch.

Line 25 stubs getWebServer to null for all cases, so the new web-path behavior for tool-execution isn’t asserted here. A single test with a non-null web server mock would close that gap and protect this new dependency surface.

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

In `@src/__tests__/main/process-listeners/forwarding-listeners.test.ts` around
lines 77 - 90, Add a new test that covers the web broadcast branch by stubbing
mockDeps.getWebServer to return a non-null mock web server with a jest.fn()
broadcast method, then call setupForwardingListeners(mockProcessManager,
mockDeps), retrieve the 'tool-execution' handler from eventHandlers and invoke
it with a test session id and payload, and finally assert that the mock web
server's broadcast was called with the expected channel/payload (and optionally
that mockSafeSend was still called if both should run); reference
setupForwardingListeners, eventHandlers, mockDeps.getWebServer, and the mock web
server's broadcast in the test.
src/__tests__/web/mobile/TabBar.test.tsx (1)

106-129: Assert real tab-bar chrome here.

container.firstChild only proves that React rendered something; an empty wrapper or injected <style> would still pass. Assert one stable piece of chrome instead, such as the “New Tab” button or the tablist, so these cases keep catching regressions.

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

In `@src/__tests__/web/mobile/TabBar.test.tsx` around lines 106 - 129, Replace the
weak assertion that merely checks container.firstChild with a stable DOM
assertion that verifies a specific piece of chrome from the TabBar component
(e.g., assert the presence of the "New Tab" button or the tablist). In the tests
that render <TabBar ... tabs={[]} /> and <TabBar ... tabs={[defaultTab]} />, use
testing-library queries such as getByRole('tablist') or getByText('New Tab')
against the rendered output (the same render result used now) and assert those
elements are present so the tests will fail if the actual chrome is removed or
altered.
src/__tests__/web/mobile/App.test.tsx (1)

195-220: Consider using stricter typing for the sessions parameter.

The mock correctly implements the new LeftPanel interface. However, using any for the session type in the map callback loses type safety.

🔧 Suggested improvement for type safety
 vi.mock('../../../web/mobile/LeftPanel', () => ({
 	LeftPanel: ({
 		sessions,
 		activeSessionId,
 		onSelectSession,
 		onClose,
 	}: {
 		sessions: unknown[];
 		activeSessionId: string | null;
 		onSelectSession: (id: string) => void;
 		onClose: () => void;
 		collapsedGroups: Set<string>;
 		setCollapsedGroups: React.Dispatch<React.SetStateAction<Set<string>>>;
 	}) => (
 		<div data-testid="left-panel">
-			{sessions.map((s: any) => (
+			{(sessions as { id: string; name: string }[]).map((s) => (
 				<button key={s.id} data-testid={`session-${s.id}`} onClick={() => onSelectSession(s.id)}>
 					{s.name}
 				</button>
 			))}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/web/mobile/App.test.tsx` around lines 195 - 220, The mock
LeftPanel uses sessions.map((s: any) => ...) which loses type safety; replace
the any with a concrete session type (e.g., { id: string; name: string } or the
existing Session type if available) by updating the mock prop signature for
sessions and the map callback parameter in the LeftPanel mock so the compiler
knows sessions items have id and name; adjust the sessions parameter type in the
mocked LeftPanel declaration (or import/alias the real Session type) and use
that type instead of any in the map.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/__tests__/main/process-listeners/forwarding-listeners.test.ts`:
- Around line 23-30: The test fixture's mockDeps uses patterns.REGEX_AI_SUFFIX
and patterns.REGEX_AI_TAB_ID with [^-]+ which fails to match hyphenated
UUID-style tab IDs; update those two regexes in mockDeps to allow hyphens (e.g.,
use .+ or a character class that includes hyphen/digits/letters such as
[A-Za-z0-9-]+) so the mock mirrors real suffix/tab-id shapes and will catch
parsing regressions in the forwarding listeners tests (refer to mockDeps,
patterns.REGEX_AI_SUFFIX, and patterns.REGEX_AI_TAB_ID).

In `@src/web/hooks/useMobileSessionManagement.ts`:
- Around line 305-310: The spinner is being cleared in the finally block even
when the request was aborted; update the fetch flow in
useMobileSessionManagement (the function that calls setIsLoadingLogs) to capture
a per-request identifier or the AbortSignal for the specific fetch (e.g.,
localFetchId or currentSignal = controller.signal) and in the finally block only
call setIsLoadingLogs(false) if that request was not aborted and is still the
active one (check !currentSignal.aborted or matching fetchId); keep the existing
catch that returns on AbortError but prevent finally from hiding the spinner for
aborted/stale requests.
- Around line 638-655: The current code in the setSessions updater marks every
aiTab.hasUnread = true for the sessionId, which incorrectly flags all tabs;
change the logic in the setSessions mapping (used with currentActiveId,
sessionId, webLogger.debug) to: when a background output includes a tabId only
set hasUnread = true for the aiTab whose id matches that tabId and leave other
tabs unchanged, and if there is no tabId set a session-level unread flag (e.g.,
session.hasUnread or session.unreadCount) on the session object instead of
toggling all aiTabs; update the aiTabs mapping in the function that handles the
output to check tab.id === tabId before setting hasUnread and preserve existing
per-tab flags otherwise.

In `@src/web/mobile/App.tsx`:
- Around line 1484-1490: The Action that opens the Auto Run setup sheet can
bypass the new preload logic because some callers (the "Launch Auto Run"
command) call setShowAutoRunSetup(true) directly; ensure documents are always
loaded when the sheet opens by either funneling all openers to the existing
handleAutoRunOpenSetup helper or moving the load logic into a useEffect that
watches showAutoRunSetup === true and activeSessionId, calling
loadAutoRunDocuments(activeSessionId) when the sheet becomes visible; update
references to setShowAutoRunSetup so they use handleAutoRunOpenSetup or add the
useEffect to guarantee fresh documents regardless of which opener is used.

In `@src/web/mobile/LeftPanel.tsx`:
- Around line 231-237: handleSelect in LeftPanel is causing a duplicate
vibration because it calls triggerHaptic(HAPTIC_PATTERNS.tap) while the caller
useMobileSessionManagement.handleSelectSession already triggers haptics; remove
the redundant haptic call from handleSelect so it only calls
onSelectSession(sessionId) (keep useCallback and [onSelectSession] dependency
intact) and rely on useMobileSessionManagement.handleSelectSession to perform
the vibration.

In `@src/web/mobile/RightPanel.tsx`:
- Around line 66-67: The component initializes currentTab from the activeTab
prop but never updates it when activeTab changes; add a useEffect that watches
activeTab and calls setCurrentTab(activeTab) so the UI stays in sync when a
different entry point requests a new tab (referencing currentTab, setCurrentTab
and activeTab in the RightPanel component).

---

Nitpick comments:
In `@src/__tests__/main/process-listeners/forwarding-listeners.test.ts`:
- Around line 77-90: Add a new test that covers the web broadcast branch by
stubbing mockDeps.getWebServer to return a non-null mock web server with a
jest.fn() broadcast method, then call
setupForwardingListeners(mockProcessManager, mockDeps), retrieve the
'tool-execution' handler from eventHandlers and invoke it with a test session id
and payload, and finally assert that the mock web server's broadcast was called
with the expected channel/payload (and optionally that mockSafeSend was still
called if both should run); reference setupForwardingListeners, eventHandlers,
mockDeps.getWebServer, and the mock web server's broadcast in the test.

In `@src/__tests__/main/web-server/web-server-factory.test.ts`:
- Around line 73-76: The test currently defines mocks for
setWriteToTerminalCallback, setResizeTerminalCallback,
setSpawnTerminalForWebCallback, and setKillTerminalForWebCallback but doesn't
assert they were registered; update the "callback registrations" test block in
the createWebServerFactory spec to add expectations that each mock was invoked
(e.g. expect(setWriteToTerminalCallback).toHaveBeenCalled(),
expect(setResizeTerminalCallback).toHaveBeenCalled(),
expect(setSpawnTerminalForWebCallback).toHaveBeenCalled(),
expect(setKillTerminalForWebCallback).toHaveBeenCalled()) after calling
createWebServerFactory so the suite verifies the factory actually registers all
four terminal setters.

In `@src/__tests__/web/mobile/App.test.tsx`:
- Around line 195-220: The mock LeftPanel uses sessions.map((s: any) => ...)
which loses type safety; replace the any with a concrete session type (e.g., {
id: string; name: string } or the existing Session type if available) by
updating the mock prop signature for sessions and the map callback parameter in
the LeftPanel mock so the compiler knows sessions items have id and name; adjust
the sessions parameter type in the mocked LeftPanel declaration (or import/alias
the real Session type) and use that type instead of any in the map.

In `@src/__tests__/web/mobile/TabBar.test.tsx`:
- Around line 106-129: Replace the weak assertion that merely checks
container.firstChild with a stable DOM assertion that verifies a specific piece
of chrome from the TabBar component (e.g., assert the presence of the "New Tab"
button or the tablist). In the tests that render <TabBar ... tabs={[]} /> and
<TabBar ... tabs={[defaultTab]} />, use testing-library queries such as
getByRole('tablist') or getByText('New Tab') against the rendered output (the
same render result used now) and assert those elements are present so the tests
will fail if the actual chrome is removed or altered.

In `@src/renderer/App.tsx`:
- Around line 2045-2047: The silent catch in src/renderer/App.tsx (the per-file
read try/catch that currently “leaves counts at 0”) should log the failure for
debugging: inside that catch block, capture the error and the file identifier
(e.g., filename or doc id from the surrounding scope) and call the project's
debug logger (or console.debug if no logger available) with a concise message
and the error object so per-file read failures are visible without changing
behavior. Ensure you reference the same variables used in the try
(filename/docId) to provide context.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fdd25780-27d1-4141-ac1a-8d06e2eaa88a

📥 Commits

Reviewing files that changed from the base of the PR and between 963dfdb and f707dcb.

📒 Files selected for processing (19)
  • src/__tests__/main/process-listeners/data-listener.test.ts
  • src/__tests__/main/process-listeners/forwarding-listeners.test.ts
  • src/__tests__/main/web-server/web-server-factory.test.ts
  • src/__tests__/web/mobile/App.test.tsx
  • src/__tests__/web/mobile/CommandInputBar.test.tsx
  • src/__tests__/web/mobile/TabBar.test.tsx
  • src/main/process-listeners/__tests__/data-listener.test.ts
  • src/main/process-listeners/__tests__/forwarding-listeners.test.ts
  • src/main/web-server/handlers/messageHandlers.ts
  • src/main/web-server/services/broadcastService.ts
  • src/renderer/App.tsx
  • src/web/hooks/useIsMobile.ts
  • src/web/hooks/useMobileSessionManagement.ts
  • src/web/hooks/useResizableWebPanel.ts
  • src/web/mobile/App.tsx
  • src/web/mobile/CommandInputButtons.tsx
  • src/web/mobile/LeftPanel.tsx
  • src/web/mobile/RightPanel.tsx
  • src/web/mobile/TabBar.tsx
✅ Files skipped from review due to trivial changes (1)
  • src/web/hooks/useIsMobile.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/main/process-listeners/tests/forwarding-listeners.test.ts
  • src/main/web-server/services/broadcastService.ts
  • src/web/mobile/CommandInputButtons.tsx
  • src/web/mobile/TabBar.tsx
  • src/web/hooks/useResizableWebPanel.ts
  • src/main/web-server/handlers/messageHandlers.ts

Comment on lines +23 to +30
mockDeps = {
safeSend: mockSafeSend,
getWebServer: () => null,
patterns: {
REGEX_AI_SUFFIX: /-ai-[^-]+$/,
REGEX_AI_TAB_ID: /-ai-([^-]+)$/,
},
};
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

Mocked AI tab regex is too restrictive for UUID-style tab IDs.

Line 27 and Line 28 use [^-]+, which won’t capture full hyphenated UUID tab IDs and can mask parsing regressions in listener behavior. Consider matching the full suffix/tab segment shape used elsewhere.

Proposed test-fixture fix
 		mockDeps = {
 			safeSend: mockSafeSend,
 			getWebServer: () => null,
 			patterns: {
-				REGEX_AI_SUFFIX: /-ai-[^-]+$/,
-				REGEX_AI_TAB_ID: /-ai-([^-]+)$/,
+				REGEX_AI_SUFFIX: /-ai-.+$/,
+				REGEX_AI_TAB_ID: /-ai-(.+)$/,
 			},
 		};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/main/process-listeners/forwarding-listeners.test.ts` around
lines 23 - 30, The test fixture's mockDeps uses patterns.REGEX_AI_SUFFIX and
patterns.REGEX_AI_TAB_ID with [^-]+ which fails to match hyphenated UUID-style
tab IDs; update those two regexes in mockDeps to allow hyphens (e.g., use .+ or
a character class that includes hyphen/digits/letters such as [A-Za-z0-9-]+) so
the mock mirrors real suffix/tab-id shapes and will catch parsing regressions in
the forwarding listeners tests (refer to mockDeps, patterns.REGEX_AI_SUFFIX, and
patterns.REGEX_AI_TAB_ID).

Comment on lines 305 to 310
} catch (err) {
if ((err as Error).name === 'AbortError') return;
webLogger.error('Failed to fetch session logs', 'Mobile', err);
} finally {
setIsLoadingLogs(false);
}
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

Aborted log loads still clear the spinner.

When the active session or tab changes, the old request is aborted, but its finally still runs and calls setIsLoadingLogs(false). A quick switch can therefore hide the loading state while the replacement fetch is still in flight.

💡 Suggested fix
 			} catch (err) {
 				if ((err as Error).name === 'AbortError') return;
 				webLogger.error('Failed to fetch session logs', 'Mobile', err);
 			} finally {
-				setIsLoadingLogs(false);
+				if (!controller.signal.aborted) {
+					setIsLoadingLogs(false);
+				}
 			}
📝 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
} catch (err) {
if ((err as Error).name === 'AbortError') return;
webLogger.error('Failed to fetch session logs', 'Mobile', err);
} finally {
setIsLoadingLogs(false);
}
} catch (err) {
if ((err as Error).name === 'AbortError') return;
webLogger.error('Failed to fetch session logs', 'Mobile', err);
} finally {
if (!controller.signal.aborted) {
setIsLoadingLogs(false);
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/web/hooks/useMobileSessionManagement.ts` around lines 305 - 310, The
spinner is being cleared in the finally block even when the request was aborted;
update the fetch flow in useMobileSessionManagement (the function that calls
setIsLoadingLogs) to capture a per-request identifier or the AbortSignal for the
specific fetch (e.g., localFetchId or currentSignal = controller.signal) and in
the finally block only call setIsLoadingLogs(false) if that request was not
aborted and is still the active one (check !currentSignal.aborted or matching
fetchId); keep the existing catch that returns on AbortError but prevent finally
from hiding the spinner for aborted/stale requests.

Comment on lines +638 to +655
// Mark as unread if output is for a non-active session
if (currentActiveId !== sessionId) {
webLogger.debug('Skipping output - not active session', 'Mobile', {
webLogger.debug('Marking session as unread - not active session', 'Mobile', {
sessionId,
activeSessionId: currentActiveId,
});
setSessions((prev) =>
prev.map((s) => {
if (s.id !== sessionId) return s;
return {
...s,
aiTabs: s.aiTabs?.map((tab) => ({
...tab,
hasUnread: true,
})),
};
})
);
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

Don't fan one unread event out to every AI tab.

This maps every aiTab to hasUnread: true whenever a background session emits output. In a multi-tab session the unread badge stops telling the user which conversation actually changed. Use tabId when it exists, and keep any session-level unread state separate from per-tab flags.

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

In `@src/web/hooks/useMobileSessionManagement.ts` around lines 638 - 655, The
current code in the setSessions updater marks every aiTab.hasUnread = true for
the sessionId, which incorrectly flags all tabs; change the logic in the
setSessions mapping (used with currentActiveId, sessionId, webLogger.debug) to:
when a background output includes a tabId only set hasUnread = true for the
aiTab whose id matches that tabId and leave other tabs unchanged, and if there
is no tabId set a session-level unread flag (e.g., session.hasUnread or
session.unreadCount) on the session object instead of toggling all aiTabs;
update the aiTabs mapping in the function that handles the output to check
tab.id === tabId before setting hasUnread and preserve existing per-tab flags
otherwise.

Comment on lines 1484 to +1490
const handleAutoRunOpenSetup = useCallback(() => {
setShowAutoRunSetup(true);
}, []);
// Load documents when setup sheet opens so it has the latest list
if (activeSessionId) {
loadAutoRunDocuments(activeSessionId);
}
}, [activeSessionId, loadAutoRunDocuments]);
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

This only refreshes Auto Run documents for one open path.

handleAutoRunOpenSetup now preloads documents, but the command-palette “Launch Auto Run” action in this file still opens the sheet with setShowAutoRunSetup(true) directly. That route can still render stale or empty documents. Load on showAutoRunSetup === true, or funnel every opener through the same helper.

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

In `@src/web/mobile/App.tsx` around lines 1484 - 1490, The Action that opens the
Auto Run setup sheet can bypass the new preload logic because some callers (the
"Launch Auto Run" command) call setShowAutoRunSetup(true) directly; ensure
documents are always loaded when the sheet opens by either funneling all openers
to the existing handleAutoRunOpenSetup helper or moving the load logic into a
useEffect that watches showAutoRunSetup === true and activeSessionId, calling
loadAutoRunDocuments(activeSessionId) when the sheet becomes visible; update
references to setShowAutoRunSetup so they use handleAutoRunOpenSetup or add the
useEffect to guarantee fresh documents regardless of which opener is used.

Comment on lines +231 to +237
const handleSelect = useCallback(
(sessionId: string) => {
triggerHaptic(HAPTIC_PATTERNS.tap);
onSelectSession(sessionId);
},
[onSelectSession]
);
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

Session selection currently vibrates twice.

This callback triggers HAPTIC_PATTERNS.tap before delegating, and the current caller (useMobileSessionManagement.handleSelectSession) already does the same. Every tap from the left panel will therefore buzz twice.

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

In `@src/web/mobile/LeftPanel.tsx` around lines 231 - 237, handleSelect in
LeftPanel is causing a duplicate vibration because it calls
triggerHaptic(HAPTIC_PATTERNS.tap) while the caller
useMobileSessionManagement.handleSelectSession already triggers haptics; remove
the redundant haptic call from handleSelect so it only calls
onSelectSession(sessionId) (keep useCallback and [onSelectSession] dependency
intact) and rely on useMobileSessionManagement.handleSelectSession to perform
the vibration.

Comment on lines +66 to +67
const [currentTab, setCurrentTab] = useState<RightDrawerTab>(activeTab);

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

Keep currentTab in sync with activeTab.

activeTab is only used for the initial state. If the panel is already open and another entry point asks for a different tab, the prop changes but the UI stays on the old tab.

💡 Suggested fix
 	const [currentTab, setCurrentTab] = useState<RightDrawerTab>(activeTab);
+
+	useEffect(() => {
+		setCurrentTab(activeTab);
+	}, [activeTab]);
📝 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
const [currentTab, setCurrentTab] = useState<RightDrawerTab>(activeTab);
const [currentTab, setCurrentTab] = useState<RightDrawerTab>(activeTab);
useEffect(() => {
setCurrentTab(activeTab);
}, [activeTab]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/web/mobile/RightPanel.tsx` around lines 66 - 67, The component
initializes currentTab from the activeTab prop but never updates it when
activeTab changes; add a useEffect that watches activeTab and calls
setCurrentTab(activeTab) so the UI stays in sync when a different entry point
requests a new tab (referencing currentTab, setCurrentTab and activeTab in the
RightPanel component).

@pedramamini pedramamini merged commit c10413c into RunMaestro:rc Apr 1, 2026
3 checks passed
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.

2 participants