Skip to content

feat: Encore System, Tab Description, Unified Inbox, Fuzzy Search#476

Open
felipeggv wants to merge 47 commits intoRunMaestro:mainfrom
felipeggv:feat/encore-inbox-tabdesc-fuzzy
Open

feat: Encore System, Tab Description, Unified Inbox, Fuzzy Search#476
felipeggv wants to merge 47 commits intoRunMaestro:mainfrom
felipeggv:feat/encore-inbox-tabdesc-fuzzy

Conversation

@felipeggv
Copy link

@felipeggv felipeggv commented Feb 27, 2026

Summary

  • Encore Feature Flags: Added unifiedInbox and tabDescription flags to EncoreFeatureFlags, gated keyboard shortcuts and modal rendering behind feature toggles
  • Tab Description: Inline description editing in tab hover overlay with draft persistence, focus management, and 3-line truncation
  • Unified Inbox: Full Agent Inbox modal with Focus Mode, filters (all/unread/starred/needs-input), keyboard navigation (j/k/Enter/Escape), command palette integration
  • Fuzzy Search: Ranked scoring engine with fast-paths for exact/prefix matches, applied to session filter and slash command autocomplete

Details

Rebuilt from clean main baseline using 4 parallel worktrees, each developed independently then merged via integration branch. 29 files changed, +5,090 lines.

New Files

  • src/renderer/components/AgentInbox/ (index, InboxListView, FocusModeView)
  • src/renderer/hooks/useAgentInbox.ts
  • src/renderer/types/agent-inbox.ts
  • src/renderer/utils/tabDisplayName.ts
  • Tests for AgentInbox, useAgentInbox, TabBar, useTabHandlers, search utils

Modified Files

  • App.tsx — Inbox modal integration + encore gating
  • SettingsModal.tsx — Encore toggle cards for new features
  • TabBar.tsx — Description display/edit UI
  • modalStore.ts — updateModalData behavior
  • settingsStore.ts — New encore flag defaults
  • shortcuts.ts + useMainKeyboardHandler.ts — Option+I shortcut

Test plan

  • Verify Encore Features toggle in Settings enables/disables each feature independently
  • Test Tab Description: hover tab → edit description → save → verify persistence
  • Test Unified Inbox: Option+I opens modal, filters work, j/k navigation, Focus Mode reply
  • Test Fuzzy Search: type partial match in Left Bar filter, verify ranked results
  • Run npm run lint — clean
  • Run npm run test — 21,081 passing (3 pre-existing CodexSessionStorage timeouts)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Unified Inbox: centralized inbox with list & focus views, filtering, sorting, quick-reply, keyboard navigation, resizable sidebar, and per-item focus mode.
    • Tab Descriptions: editable per-tab descriptions surfaced in tab overlays with a Settings toggle.
  • Keyboard Shortcuts

    • Added Unified Inbox (mac: Opt+I, Win/Linux: Alt+I) and Director's Notes (mac: Cmd+Shift+O, Win/Linux: Ctrl+Shift+O).
  • Settings / Documentation

    • Added toggles and docs for Encore features and shortcuts.
  • Improvements

    • Smarter slash-command autocomplete using fuzzy matching.
  • Tests

    • Expanded test coverage across inbox, focus mode, tab descriptions, hooks, handlers, and search.

Felipe Gobbi and others added 26 commits February 27, 2026 10:51
Added optional `description?: string` to AITab for user-defined tab
context annotations. This is the data model foundation for the tab
description feature.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extends the Encore Feature system with two new feature flags for upcoming
Unified Inbox and Tab Description features. tabDescription defaults to
true (enabled by default), unifiedInbox defaults to false.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add missing tests for scoring hierarchy (exact > substring > scattered),
case-insensitive scored matching, special characters in query (regex
metacharacters, unicode, emoji), and deterministic scoring verification.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…Handlers

Adds a new handler for updating AI tab descriptions with trim and
empty-string-to-undefined normalization. Follows existing immutable
state update pattern via useSessionStore. Includes 4 tests covering
set, trim, empty, and whitespace-only cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add early returns in fuzzyMatch and fuzzyMatchWithScore for:
- Empty text (can't match anything)
- Query longer than text (impossible to match all chars)

Empty query already had early return. All 5 consumers already
skip fuzzy matching on empty input. No consumer changes needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…gs Encore tab

- Import Inbox and FileText icons from lucide-react
- Change Encore tab icon from FlaskConical to Sparkles per spec
- Add Unified Inbox feature card with toggle, Beta badge, left-border accent
- Add Tab Descriptions feature card with toggle, left-border accent
- Both cards follow existing toggle pattern (spread-copy + flip flag)
- All 113 SettingsModal tests pass, lint clean

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… TabBar

Thread the tab description handler from useTabHandlers through
useMainPanelProps → App.tsx → MainPanel → TabBar. Gate with
encoreFeatures.tabDescription flag (default: false).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…es encore features

Add agentInbox shortcut (Alt+I) with encoreFeatures.unifiedInbox guard.
Director's Notes shortcut already had its guard. Register agentInbox modal
in the modal store and wire through App.tsx keyboard handler context.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a description section to the AI tab hover overlay menu with two modes:
- Display mode: FileText icon + description text (2-line clamp) or italic placeholder
- Edit mode: auto-focus textarea with Enter to save, Shift+Enter for newline, Escape to cancel

Feature-gated behind onUpdateTabDescription prop. Only calls handler when value actually changed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…edit

Add cleanup logic so that if the overlay closes while the user is editing
a tab description, the current draft is saved rather than discarded. Uses
a useRef to track the latest draft value, avoiding stale closure issues.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…n tab overlay

Adds requestAnimationFrame-based focus management so that when
the user saves or cancels description editing, focus returns to
the description display button for keyboard accessibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Port 6 inbox files from feat/unified-inbox-encore (which already incorporates
all 8 rounds of CodeRabbit/Greptile review fixes) and integrate into the
existing codebase with full App.tsx handler wiring.

CRITICAL fixes included:
- Race condition: effect-driven pendingInboxQuickReply replaces setTimeout
- Memory leak: resizeCleanupRef + useEffect unmount guard for drag listeners
- Group collision: sessionId as group key, sessionName as display-only label
- Ref binding: inboxProcessInputRef.current = processInput after useInputHandlers
- Duplicate log: quick reply delegates to processInput (no manual log push)
- AI mode: force inputMode='ai' before sending inbox replies
- Modal data: openModal preserves data, updateModalData handles undefined base

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…nd unifiedInbox encore feature

- Add onOpenAgentInbox gated prop (App.tsx → AppModals → QuickActionsModal)
- Add AgentInbox rendering placeholder in App.tsx (commented, component TBD)
- Add Unified Inbox entry to QuickActionsModal command palette
- DirectorNotesModal gating was already in place from prior work

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds 2 missing useTabHandlers tests (non-active session isolation,
other tabs isolation) and 7 TabBar description UI tests covering
rendering, edit mode, Enter/Escape behavior, and feature gating.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…on consistency

The tabDescription encore feature was defaulting to true, inconsistent
with the encore pattern (all features disabled by default). No handler
or UI exists yet for tab descriptions, so this is purely a default fix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The reply input auto-focus in FocusModeView used a setTimeout(60ms) fallback
alongside requestAnimationFrame. Replaced with double requestAnimationFrame
which reliably fires after browser paint without arbitrary timing delays.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ection

Adds role="switch" and aria-checked for accessibility compliance.
Follows existing Director's Notes toggle pattern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…gating

Add onOpenAgentInbox prop through App.tsx → AppModals → QuickActionsModal
chain, gated behind encoreFeatures.unifiedInbox. The action calls the same
setAgentInboxOpen(true) used by the Alt+I keyboard shortcut, ensuring all
entry points share the same open/close state from modalStore.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…stamp)

26 tests covering:
- Filter logic: all/unread/read/starred modes
- Sort logic: newest/oldest/grouped/byAgent modes
- Summary truncation at MAX_MESSAGE_LENGTH (90 chars)
- Timestamp fallback chain: log → createdAt → Date.now()
- Exported utilities: truncate() and generateSmartSummary()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ocus mode)

18 tests across 6 describe blocks covering:
- List mode rendering with ARIA attributes
- Filter segmented control (All/Unread/Read/Starred)
- Keyboard navigation (ArrowUp/Down, Enter to navigate)
- Escape/close behavior (overlay click, close button)
- Focus mode entry (F key, double-click) and exit (Escape, Backspace)
- Reply textarea presence in focus mode

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
updateModalData now stores data even on unopened modals (needed for
AgentInbox pattern). Updated test expectation accordingly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…egration

# Conflicts:
#	src/renderer/stores/settingsStore.ts
#	src/renderer/types/index.ts
…ration

# Conflicts:
#	src/renderer/App.tsx
#	src/renderer/components/AppModals.tsx
#	src/renderer/components/QuickActionsModal.tsx
#	src/renderer/components/SettingsModal.tsx
#	src/renderer/hooks/keyboard/useMainKeyboardHandler.ts
#	src/renderer/stores/modalStore.ts
#	src/renderer/stores/settingsStore.ts
#	src/renderer/types/index.ts
Added Alt+I (Unified Inbox) and Cmd+Shift+O (Director's Notes) to the
Global Shortcuts table in keyboard-shortcuts.md with footnote noting
Encore Feature requirement. Also added Unified Inbox to the Available
Features table in encore-features.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…zzy Search

Merges 4 feature branches rebuilt from clean main baseline:
- feat/rebuild-encore-system: Encore feature flags (unifiedInbox, tabDescription)
- feat/rebuild-tab-description: Inline tab description editing in hover overlay
- feat/rebuild-unified-inbox: Agent Inbox modal with Focus Mode, filters, keyboard nav
- Fuzzy search engine with ranked scoring and fast-paths

25 commits across 4 features, all lint-clean and tested.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Feb 27, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a Unified Inbox (Agent Inbox) feature with list and focus UIs, a new useAgentInbox hook and types, modal/shortcut/store wiring, tab description editing (handler + UI), settings feature flags, fuzzy command/search updates, and many new tests.

Changes

Cohort / File(s) Summary
Docs
docs/encore-features.md, docs/keyboard-shortcuts.md
Updated Available Features table and keyboard shortcuts to include Unified Inbox and Director's Notes; added feature-gating note.
Agent Inbox UI
src/renderer/components/AgentInbox/index.tsx, src/renderer/components/AgentInbox/InboxListView.tsx, src/renderer/components/AgentInbox/FocusModeView.tsx, src/renderer/components/AgentInbox/*
New AgentInbox component with List + Focus views, virtualized list, keyboard navigation, resizable sidebar, quick-reply/open-and-reply flows, and exported helper resolveContextUsageColor.
Agent Inbox Hook & Types
src/renderer/hooks/useAgentInbox.ts, src/renderer/types/agent-inbox.ts, src/renderer/types/index.ts
New useAgentInbox hook with truncate/generateSmartSummary/deriveTimestamp/sortItems utilities; new Inbox types and constants; AITab gains optional description; encore feature flags extended.
App Integration & Modals
src/renderer/App.tsx, src/renderer/components/AppModals.tsx, src/renderer/components/AppModals.tsx
Lazy-loads AgentInbox, wires onOpenAgentInbox through modal props, exposes modal open state/actions, and injects AgentInbox into unified modals area when enabled.
Modal Store & Priorities
src/renderer/stores/modalStore.ts, src/renderer/constants/modalPriorities.ts
Registered agentInbox modal id/data, added modal actions/selectors (agentInboxOpen, setAgentInboxOpen, updateAgentInboxData), and new modal priority AGENT_INBOX.
Settings & Feature Flags
src/renderer/components/SettingsModal.tsx, src/renderer/stores/settingsStore.ts
Added unifiedInbox and tabDescription flags (default false) and feature cards/toggles in Settings UI.
Tab Description Feature
src/renderer/components/TabBar.tsx, src/renderer/hooks/tabs/useTabHandlers.ts, src/renderer/components/MainPanel.tsx, src/renderer/hooks/props/useMainPanelProps.ts
Threaded onUpdateTabDescription prop and handleUpdateTabDescription handler through tab UI; added description edit UI in tab overlays; handler trims/normalizes and updates active session tab description.
Shortcuts & Keyboard
src/renderer/constants/shortcuts.ts, src/renderer/hooks/keyboard/useMainKeyboardHandler.ts, src/renderer/components/SessionList.tsx
Added agentInbox shortcut and handler to open the inbox when feature enabled; added Unified Inbox entry to hamburger menu and wired menu action to modal.
Search / Slash Command Changes
src/renderer/utils/search.ts, src/renderer/components/InputArea.tsx, src/renderer/hooks/input/useInputKeyDown.ts
Added guards to fuzzyMatch/fuzzyMatchWithScore and switched slash-command filtering to fuzzyMatchWithScore with score-based sorting; updated related input handling and tests.
Utilities
src/renderer/utils/tabDisplayName.ts
New getTabDisplayName utility deriving display names from tab name or agentSessionId for multiple ID formats.
Stores & Settings (persist/defaults)
src/renderer/stores/settingsStore.ts, src/renderer/stores/modalStore.ts
Persisted new encore defaults and Agent Inbox modal data schema; adjusted modal open/update behavior to merge and store data for unopened modals.
Tests
src/__tests__/renderer/components/AgentInbox/*, src/__tests__/renderer/hooks/*, src/__tests__/renderer/utils/search.test.ts, src/__tests__/renderer/components/TabBar.test.tsx, src/__tests__/renderer/components/InputArea.test.tsx
Large new and extended test suites covering AgentInbox UI and hook behaviors, tab description handler tests, modal store changes, fuzzy search/command ordering adjustments, and keyboard/navigation flows.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant App as App
    participant ModalStore as ModalStore
    participant AgentInbox as AgentInbox
    participant InboxListView as InboxListView
    participant FocusModeView as FocusModeView
    participant Session as Session

    User->>App: Press Unified Inbox shortcut (Alt/Opt+I)
    App->>ModalStore: setAgentInboxOpen(true)
    App->>AgentInbox: Mount (lazy)
    AgentInbox->>AgentInbox: useAgentInbox(sessions, groups) -> items
    AgentInbox->>InboxListView: Render list (filters/sort)
    User->>InboxListView: Select item / Enter
    InboxListView->>AgentInbox: onEnterFocus(index)
    AgentInbox->>FocusModeView: Render focused item
    FocusModeView->>Session: Read session.aiLogs
    User->>FocusModeView: Type reply + Enter
    FocusModeView->>AgentInbox: onQuickReply(sessionId, tabId, text)
    AgentInbox->>App: call processInput / focus session
Loading
sequenceDiagram
    participant User as User
    participant TabBar as TabBar
    participant Tab as Tab
    participant useTabHandlers as useTabHandlers
    participant Session as Session

    User->>TabBar: Open tab overlay
    TabBar->>Tab: Show description UI (if prop provided)
    User->>Tab: Edit description and press Enter
    Tab->>useTabHandlers: handleUpdateTabDescription(tabId, trimmedText)
    useTabHandlers->>Session: Update active session.aiTabs.description
    Session->>Tab: Re-render with updated description
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.64% 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 features introduced: Encore System, Tab Description, Unified Inbox, and Fuzzy Search. It is concise, specific, and directly relates to the primary changes in the changeset.

✏️ 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

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

@greptile-apps
Copy link

greptile-apps bot commented Feb 27, 2026

Greptile Summary

Introduces three Encore features (Unified Inbox, Tab Descriptions, Fuzzy Search) with comprehensive testing and feature flag gating.

Key changes:

  • Unified Inbox (Option+I): Full cross-agent inbox modal with Focus Mode, filters (all/unread/starred/needs-input), keyboard navigation (j/k/Enter/Escape), and quick-reply functionality that switches sessions, sends input, and optionally restores previous context
  • Tab Descriptions: Inline description editing in tab hover overlay with draft persistence, auto-save on blur, and 2-line truncation display
  • Fuzzy Search: Scored ranking engine with bonuses for consecutive matches, word boundaries, exact matches, and case sensitivity applied to session filtering
  • Modal Store: Modified updateModalData to allow persisting state even when modal is closed (enables inbox filter/sort persistence)

Architecture:

  • 3 new AgentInbox components (shell, list view, focus view) with virtualized scrolling and complex focus management
  • Quick-reply orchestration uses pending state pattern with ref-based processInput binding to handle async session switching
  • Tab description auto-saves on blur and overlay close with draft state tracking via refs to avoid stale closures

Testing:
Comprehensive test coverage (21,081 passing) including unit tests for inbox hook, fuzzy search, and component integration tests.

Confidence Score: 4/5

  • This PR is safe to merge with minimal risk - well-tested features behind feature flags
  • Large but well-structured PR with comprehensive testing (21,081 passing tests), proper feature flag gating for gradual rollout, and adherence to existing codebase patterns. The quick-reply state management in App.tsx is complex but correctly handles async session switching. Minor complexity concerns in focus restoration and pending state orchestration, but code review shows correct implementation with proper cleanup and ref usage.
  • src/renderer/App.tsx has complex quick-reply orchestration - verify the pending state cleanup works correctly in edge cases (rapid session switches, modal close during pending state)

Important Files Changed

Filename Overview
src/renderer/hooks/useAgentInbox.ts Added comprehensive inbox data aggregation hook with filtering, sorting, and smart message summarization
src/renderer/components/AgentInbox/index.tsx Main inbox modal shell with view mode switching, frozen snapshots for focus mode, and keyboard navigation
src/renderer/components/AgentInbox/InboxListView.tsx Virtualized inbox list with filtering, sorting controls, group headers, and keyboard navigation (j/k/Enter)
src/renderer/components/AgentInbox/FocusModeView.tsx Full-screen focus mode with conversation replay, inline reply textarea, and prev/next navigation
src/renderer/stores/modalStore.ts Modified updateModalData to allow updates even when modal isn't open (for persisting inbox state)
src/renderer/App.tsx Integrated inbox modal with complex quick-reply orchestration, session switching, and pending state management
src/renderer/components/TabBar.tsx Added inline tab description editing with draft persistence, auto-save on blur, and 2-line truncation display
src/renderer/utils/search.ts Added fuzzyMatchWithScore for ranked search with bonuses for consecutive matches, word boundaries, and exact matches

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User presses Option+I] --> B[useMainKeyboardHandler]
    B --> C[setAgentInboxOpen true]
    C --> D[AgentInbox Modal Opens]
    
    D --> E[useAgentInbox Hook]
    E --> F[Filter sessions by mode]
    F --> G[Sort items by mode]
    G --> H[Generate smart summaries]
    
    D --> I[InboxListView]
    I --> J[Virtual scrolling]
    J --> K[j/k navigation]
    K --> L[Enter to open or F for Focus]
    
    D --> M{User Action?}
    M -->|Press F| N[FocusModeView]
    M -->|Double-click| O[Navigate to session]
    M -->|Enter| O
    
    N --> P[Show full conversation]
    P --> Q[Inline reply textarea]
    Q --> R{Reply Type?}
    R -->|Quick Reply| S[Send & return to previous]
    R -->|Open & Reply| T[Navigate to session]
    
    S --> U[handleAgentInboxQuickReply]
    U --> V[Set pending state]
    V --> W[Switch active session]
    W --> X[Process input via ref]
    X --> Y[Restore previous session]
    
    D --> Z[TabBar with descriptions]
    Z --> AA[Hover shows overlay]
    AA --> AB[Edit description inline]
    AB --> AC[Auto-save on blur]
    
    style D fill:#e1f5ff
    style N fill:#fff4e6
    style Z fill:#f3e5f5
Loading

Last reviewed commit: e50f079

Copy link

@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: 13

🧹 Nitpick comments (3)
src/__tests__/renderer/components/TabBar.test.tsx (1)

5841-5872: Add a regression test for empty-description normalization.

This block validates trimming on save, but it doesn’t explicitly assert the empty-after-trim path (' ' -> undefined), which is core behavior for this feature.

🧪 Suggested test addition
+	it('pressing Enter with whitespace-only description saves undefined', () => {
+		const tabs = [
+			createTab({
+				id: 'tab-1',
+				name: 'Tab 1',
+				agentSessionId: 'session-1',
+			}),
+		];
+
+		render(
+			<TabBar
+				tabs={tabs}
+				activeTabId="tab-1"
+				theme={mockThemeDesc}
+				onTabSelect={mockOnTabSelect}
+				onTabClose={mockOnTabClose}
+				onNewTab={mockOnNewTab}
+				onUpdateTabDescription={mockOnUpdateTabDescription}
+			/>
+		);
+
+		openOverlay('Tab 1');
+		fireEvent.click(screen.getByLabelText('Add tab description'));
+		const textarea = screen.getByLabelText('Tab description');
+		fireEvent.change(textarea, { target: { value: '   ' } });
+		fireEvent.keyDown(textarea, { key: 'Enter' });
+
+		expect(mockOnUpdateTabDescription).toHaveBeenCalledWith('tab-1', undefined);
+	});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/renderer/components/TabBar.test.tsx` around lines 5841 - 5872,
The test covers trimming non-empty descriptions but misses the empty-after-trim
case; add a new assertion in the same test (or an adjacent test) that opens the
overlay for "Tab 1", clicks the description button, sets the textarea value to a
whitespace-only string (e.g. '   '), simulates pressing Enter (same sequence
using getByLabelText('Tab description') and fireEvent.keyDown), and then asserts
mockOnUpdateTabDescription was called with the tab id ('tab-1') and undefined to
verify empty descriptions are normalized to undefined; keep using the existing
TabBar render and mockOnUpdateTabDescription setup.
src/renderer/types/agent-inbox.ts (1)

1-1: Narrow toolType to the existing union type.

Line 10 uses string, which allows invalid tool identifiers to flow through inbox items. Prefer ToolType for stronger type safety.

Suggested fix
-import type { SessionState } from './index';
+import type { SessionState, ToolType } from './index';
@@
-	toolType: string;
+	toolType: ToolType;

Also applies to: 10-10

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

In `@src/renderer/types/agent-inbox.ts` at line 1, Change the toolType property
from a plain string to the existing union type ToolType to prevent invalid
identifiers from being used; update the import statement to include ToolType
(e.g., import type { SessionState, ToolType } from './index') and replace any
occurrences/annotations of toolType: string with toolType: ToolType in the inbox
item types/interfaces so the compiler enforces only valid tool identifiers.
src/renderer/stores/modalStore.ts (1)

145-147: Tighten Agent Inbox modal typing end-to-end.

Line 145-147 and Line 777 currently accept broad string/Record<string, unknown>, which allows invalid filter/sort payloads to be persisted.

Suggested fix
 export interface AgentInboxModalData {
-	filterMode?: string;
-	sortMode?: string;
+	filterMode?: 'all' | 'unread' | 'read' | 'starred';
+	sortMode?: 'newest' | 'oldest' | 'grouped' | 'byAgent';
 	isExpanded?: boolean;
 }
@@
-		updateAgentInboxData: (data: Record<string, unknown>) => updateModalData('agentInbox', data),
+		updateAgentInboxData: (data: Partial<AgentInboxModalData>) =>
+			updateModalData('agentInbox', data),

Also applies to: 777-777

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

In `@src/renderer/stores/modalStore.ts` around lines 145 - 147, Tighten the modal
typing by replacing the broad string/Record types with explicit types/enums and
a typed payload interface: change filterMode?: string and sortMode?: string to
concrete union types (e.g., type AgentInboxFilter = 'all' | 'unread' |
'assigned'; type AgentInboxSort = 'newest' | 'oldest' | 'priority') and replace
any Record<string, unknown> modal payload with a dedicated interface (e.g.,
AgentInboxModalState { filterMode?: AgentInboxFilter; sortMode?: AgentInboxSort;
isExpanded?: boolean; ... }). Update modalStore's modal state type, and update
any setters/accessors (e.g., functions that read/write modal state such as
setModal, updateModal, persistModalState or similar usages around the previous
line ~777) to use the new types so only valid filter/sort values are accepted
and persisted.
🤖 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__/renderer/components/TabBar.test.tsx`:
- Around line 5784-5791: The test fixtures use unnecessary "as any" casts when
calling createTab (e.g., createTab({ id: 'tab-1', name: 'Tab 1', agentSessionId:
'session-1', description: 'My existing description' } as any)); remove the "as
any" casts and pass the object directly since description is already an optional
string on the AITab interface; update any other similar occurrences in
TabBar.test.tsx (e.g., around lines where createTab is invoked with description)
to restore proper type checking without changing the fixture shape.

In `@src/renderer/App.tsx`:
- Around line 4265-4275: The AgentInbox invocation is missing the enterToSendAI
prop so quick-reply behavior won't respect user settings; update the AgentInbox
JSX (the component instance named AgentInbox) to include
enterToSendAI={enterToSendAI} (or the appropriate local variable) so the prop is
forwarded into AgentInbox (which in turn forwards it to Focus Mode) and keeps
quick-reply/Enter-to-send behavior consistent.

In `@src/renderer/components/AgentInbox/FocusModeView.tsx`:
- Around line 751-753: The effect that clears reply text (useEffect that calls
setReplyText('') tied to [item.sessionId, item.tabId]) only empties content but
doesn't reset the textarea height; update that effect to also reset the textarea
DOM height (e.g., replyTextareaRef.current.style.height = 'auto' or '' if a ref
is used) so the input collapses when switching items. Do the same reset in the
send handler (the function around lines 1099-1104, e.g., handleSend/sendReply)
after clearing text so the textarea height is restored when a message is sent;
reference the textarea ref (replyTextareaRef or replyInputRef) and ensure you
guard for its existence before manipulating style.
- Line 595: The onMarkAsRead handler passed as onMarkAsRead/_onMarkAsRead is
never invoked by the reply flow, so replied items remain unread; update the
explicit reply handlers (the functions that send replies—e.g., the local reply
submit/handler methods used around lines ~755-770 and the handler referenced
near 786-787) to call _onMarkAsRead (or props.onMarkAsRead) after a successful
reply send: locate the reply submit functions (e.g., onReplySubmit,
handleReply/submitReply or similar) and, after awaiting the send/submit result
and confirming success, invoke _onMarkAsRead() (with a null-check or optional
chaining) so the unread state is cleared, and ensure this call is executed only
on success and does not break existing error paths.
- Around line 427-438: The sidebar renders option-like children without a
listbox container and marks collapsible headers as role="option"; update
FocusModeView so the scrollable container (the element with className "flex-1
overflow-y-auto py-1") has role="listbox" and manage focus/aria-activedescendant
for options, and change the collapsible header elements (currently using
role="option") to an appropriate semantic role such as role="group" or
role="heading" with a nested toggle as role="button" plus
aria-expanded/aria-controls; apply the same change to the other header block
around the 491-497 area so headers are not role="option" while list items remain
role="option" inside the listbox.
- Around line 387-388: The grouping currently uses groupName which can merge
distinct groups that share a display name; change the grouping key logic in
FocusModeView so groupKey prefers itm.groupId when present (falling back to
itm.groupName and then 'Ungrouped') while leaving groupName as the
human-readable label (itm.groupName ?? 'Ungrouped' or itm.sessionName). Update
the expression that defines groupKey (and any consumers that track collapse
state) to use itm.groupId ?? itm.groupName ?? 'Ungrouped' when useGroupName is
true, keeping sessionId as the fallback when useGroupName is false, so collapse
buckets are stable and unique per actual group identity.
- Around line 228-233: The toggle button in FocusModeView (the element with
onToggleRaw and using showRawMarkdown for state) is visually hidden via opacity
until hover and lacks an accessible name; update the button so it’s keyboard and
screen-reader discoverable by adding an explicit accessible label (aria-label or
visually hidden text that reflects showRawMarkdown state), ensure it remains
visible/focusable when keyboard-focused (remove or override the opacity:0 rule
for :focus or add a focus-visible style), and keep the existing title for mouse
users; this preserves onToggleRaw behavior while making the control accessible.
- Around line 988-1005: The resize handle is mouse-only; make it
keyboard-accessible by adding tabIndex={0}, appropriate ARIA (e.g.,
role="separator" and aria-orientation="vertical") and a onKeyDown handler that
implements keyboard resize (handle ArrowLeft/ArrowRight, Home/End) and reuses or
delegates to the same resize logic used by handleResizeStart; also add onFocus
and onBlur to mirror the current onMouseEnter/onMouseLeave visual feedback (use
isResizingRef and theme.colors to set backgroundColor) so keyboard focus shows
the same highlight, and ensure the element keeps cursor styling and flex
behavior.

In `@src/renderer/components/AgentInbox/InboxListView.tsx`:
- Around line 1269-1271: The current click handlers on the row call
handleNavigate on single-click and onEnterFocus on double-click, but
handleNavigate closes the modal so onDoubleClick never fires; choose one
interaction model and update the handlers accordingly: either make onClick a
non-navigating selection action (e.g., setSelected / highlight only) and keep
onDoubleClick calling onEnterFocus for entry (ensure handleNavigate is not
invoked on single click), or remove onDoubleClick and move the onEnterFocus
behavior into handleNavigate/onClick so a single click performs navigation and
closes the modal; update the handlers referenced (handleNavigate, onEnterFocus)
in InboxListView to match the chosen model and adjust any modal-closing logic so
double-click remains reachable if you keep it.
- Around line 1059-1074: The focusable list container (the div with
role="listbox" and handlers like onKeyDown={handleKeyDown}) is not wired to the
existing containerRef, so calls to containerRef.current?.focus() do nothing;
attach the ref by adding ref={containerRef} to that div (the element using
aria-activedescendant and onFocus/onBlur) so containerRef points to the listbox
and keyboard focus returns work as intended.
- Around line 808-811: getHeaderFocusables currently includes disabled controls,
which can cause focus to land on non-focusable elements; update the
getHeaderFocusables function to exclude disabled/aria-disabled and non-focusable
elements by changing the query and/or filtering the result (for example use
headerRef.current.querySelectorAll<HTMLElement>('button:not([disabled]):not([aria-disabled="true"]),
[tabindex="0"]:not([aria-disabled="true"])') and then further filter the
NodeList to only include elements that are not hidden and have a non-negative
tabIndex (e.g. filter by !el.hasAttribute('disabled') &&
el.getAttribute('aria-disabled') !== 'true' && el.tabIndex >= 0 && !el.hidden)
so that only truly focusable header controls are returned; apply the same
filtering logic where getHeaderFocusables is used to prevent trapping focus.

In `@src/renderer/components/SettingsModal.tsx`:
- Around line 3687-3709: The icon-only switch buttons in SettingsModal (the
button controlling encoreFeatures.unifiedInbox using setEncoreFeatures and the
similar switch around the other block) have role="switch" but no accessible
name; add an accessible name by supplying either aria-label or aria-labelledby
on each button (for example aria-label="Enable unified inbox" for the
unifiedInbox switch) and ensure aria-checked remains a boolean, and mark the
decorative inner <span> as aria-hidden="true" so screen readers only announce
the labeled switch state.

In `@src/renderer/components/TabBar.tsx`:
- Around line 482-505: The blur handler handleDescriptionBlur is saving the
draft even when handleDescriptionCancel was invoked; change the flow to set a
transient "cancelled" flag (e.g., cancelledRef.current = true) inside
handleDescriptionCancel before moving focus, then in handleDescriptionBlur check
that flag and skip calling handleDescriptionSave if cancelled, and finally clear
the flag (via requestAnimationFrame or setTimeout 0) so subsequent blurs behave
normally; update references in handleDescriptionKeyDown (Escape path) to use
handleDescriptionCancel so the flag prevents the blur-save from persisting
cancelled edits.

---

Nitpick comments:
In `@src/__tests__/renderer/components/TabBar.test.tsx`:
- Around line 5841-5872: The test covers trimming non-empty descriptions but
misses the empty-after-trim case; add a new assertion in the same test (or an
adjacent test) that opens the overlay for "Tab 1", clicks the description
button, sets the textarea value to a whitespace-only string (e.g. '   '),
simulates pressing Enter (same sequence using getByLabelText('Tab description')
and fireEvent.keyDown), and then asserts mockOnUpdateTabDescription was called
with the tab id ('tab-1') and undefined to verify empty descriptions are
normalized to undefined; keep using the existing TabBar render and
mockOnUpdateTabDescription setup.

In `@src/renderer/stores/modalStore.ts`:
- Around line 145-147: Tighten the modal typing by replacing the broad
string/Record types with explicit types/enums and a typed payload interface:
change filterMode?: string and sortMode?: string to concrete union types (e.g.,
type AgentInboxFilter = 'all' | 'unread' | 'assigned'; type AgentInboxSort =
'newest' | 'oldest' | 'priority') and replace any Record<string, unknown> modal
payload with a dedicated interface (e.g., AgentInboxModalState { filterMode?:
AgentInboxFilter; sortMode?: AgentInboxSort; isExpanded?: boolean; ... }).
Update modalStore's modal state type, and update any setters/accessors (e.g.,
functions that read/write modal state such as setModal, updateModal,
persistModalState or similar usages around the previous line ~777) to use the
new types so only valid filter/sort values are accepted and persisted.

In `@src/renderer/types/agent-inbox.ts`:
- Line 1: Change the toolType property from a plain string to the existing union
type ToolType to prevent invalid identifiers from being used; update the import
statement to include ToolType (e.g., import type { SessionState, ToolType } from
'./index') and replace any occurrences/annotations of toolType: string with
toolType: ToolType in the inbox item types/interfaces so the compiler enforces
only valid tool identifiers.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9d612b6 and e50f079.

📒 Files selected for processing (29)
  • docs/encore-features.md
  • docs/keyboard-shortcuts.md
  • src/__tests__/renderer/components/AgentInbox/AgentInbox.test.tsx
  • src/__tests__/renderer/components/TabBar.test.tsx
  • src/__tests__/renderer/hooks/useAgentInbox.test.ts
  • src/__tests__/renderer/hooks/useTabHandlers.test.ts
  • src/__tests__/renderer/stores/modalStore.test.ts
  • src/__tests__/renderer/utils/search.test.ts
  • src/renderer/App.tsx
  • src/renderer/components/AgentInbox/FocusModeView.tsx
  • src/renderer/components/AgentInbox/InboxListView.tsx
  • src/renderer/components/AgentInbox/index.tsx
  • src/renderer/components/AppModals.tsx
  • src/renderer/components/MainPanel.tsx
  • src/renderer/components/QuickActionsModal.tsx
  • src/renderer/components/SettingsModal.tsx
  • src/renderer/components/TabBar.tsx
  • src/renderer/constants/modalPriorities.ts
  • src/renderer/constants/shortcuts.ts
  • src/renderer/hooks/keyboard/useMainKeyboardHandler.ts
  • src/renderer/hooks/props/useMainPanelProps.ts
  • src/renderer/hooks/tabs/useTabHandlers.ts
  • src/renderer/hooks/useAgentInbox.ts
  • src/renderer/stores/modalStore.ts
  • src/renderer/stores/settingsStore.ts
  • src/renderer/types/agent-inbox.ts
  • src/renderer/types/index.ts
  • src/renderer/utils/search.ts
  • src/renderer/utils/tabDisplayName.ts

- Fix resize handler stale closure via ref instead of sidebarWidth dep
- Simplify double rAF focus to single rAF
- Add runtime validation for filterMode/sortMode instead of unsafe cast
- Add aria-labels to nav buttons, close button, toggle controls, switches
- Call onMarkAsRead after quick-reply and open-and-reply actions
- Use groupId for sidebar grouping to prevent name-collision merges
- Fix sidebar ARIA: role=listbox container, role=separator for headers
- Make raw/markdown toggle keyboard-focusable with focus:opacity
- Exclude disabled buttons from Tab focus cycling in InboxListView
- Fix TabBar description Escape cancel being overridden by blur-save
- Reset textarea height on item switch in FocusModeView
- Guard against concurrent quick-reply race condition in App.tsx
- Pass enterToSendAI prop to AgentInbox for consistent reply behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@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.

🧹 Nitpick comments (1)
src/renderer/components/AgentInbox/InboxListView.tsx (1)

809-814: Consider filtering [tabindex="0"] elements that are disabled.

The current selector 'button:not(:disabled), [tabindex="0"]' excludes disabled buttons but doesn't filter [tabindex="0"] elements that might be disabled via aria-disabled. While this may not be an issue currently, it could lead to focus landing on non-interactable elements if such patterns are introduced later.

♻️ Suggested defensive improvement
 	const getHeaderFocusables = useCallback((): HTMLElement[] => {
 		if (!headerRef.current) return [];
 		return Array.from(
 			headerRef.current.querySelectorAll<HTMLElement>('button:not(:disabled), [tabindex="0"]')
-		);
+		).filter((el) => el.getAttribute('aria-disabled') !== 'true');
 	}, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/AgentInbox/InboxListView.tsx` around lines 809 - 814,
The getHeaderFocusables function's selector includes [tabindex="0"] but doesn't
exclude elements that are aria-disabled, so focus could land on non-interactable
elements; update the query in getHeaderFocusables (using headerRef) to exclude
aria-disabled elements (e.g. add :not([aria-disabled="true"]) for the
[tabindex="0"] part) or post-filter the NodeList to remove elements where
element.getAttribute('aria-disabled') === 'true', ensuring returned HTMLElements
are actionable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/renderer/components/AgentInbox/InboxListView.tsx`:
- Around line 809-814: The getHeaderFocusables function's selector includes
[tabindex="0"] but doesn't exclude elements that are aria-disabled, so focus
could land on non-interactable elements; update the query in getHeaderFocusables
(using headerRef) to exclude aria-disabled elements (e.g. add
:not([aria-disabled="true"]) for the [tabindex="0"] part) or post-filter the
NodeList to remove elements where element.getAttribute('aria-disabled') ===
'true', ensuring returned HTMLElements are actionable.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9100dca and a86b2e1.

📒 Files selected for processing (4)
  • src/renderer/components/AgentInbox/FocusModeView.tsx
  • src/renderer/components/AgentInbox/InboxListView.tsx
  • src/renderer/components/AgentInbox/index.tsx
  • src/renderer/components/TabBar.tsx

Felipe Gobbi and others added 10 commits February 27, 2026 16:21
…nsion

Long descriptions no longer push the overlay wider. The overlay is now
fixed at 220px width with word-break forcing text to wrap within bounds.
The existing 3-line clamp with ellipsis handles vertical truncation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…y width

Reverted overlay width back to minWidth: 220px (production default).
Added overflow: hidden to description container so long descriptions
wrap within the overlay's natural width instead of expanding it.
The 3-line clamp with word-break handles vertical truncation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Locked overlay to exactly 220px (minWidth + maxWidth) so long
descriptions cannot expand it horizontally. Added overflow-hidden
to description container and word-break for proper text wrapping.
The existing 3-line clamp with ellipsis handles vertical truncation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…normalization tests

Addresses remaining CodeRabbit findings from PR RunMaestro#476:
- agent-inbox.ts: toolType changed from `string` to `ToolType` for stronger type safety
- TabBar.test.tsx: added regression tests for whitespace-only description normalization

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Collapse two-bar header (header + subheader) into single h-16 px-6 bar
- Adopt app title bar pipe-separated pattern (Group | Name | Tab)
- Move metadata badges (git branch, context %, status, thinking) to right side
- Widen list-mode dialog from 780px to 900px
- Add backdrop-filter blur(6px) to modal overlay for focus effect

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…o triage

Replace numeric focusIndex with identity-based focusId tracking so that
items re-entering the filtered list (e.g. agent responds → hasUnread flips
back to true) no longer shift the focused item or steal textarea focus.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…idebar

- Embed ⌘[ and ⌘] kbd badges inside Prev/Next buttons
- Simplify center hint to just "Esc Back"
- Widen sidebar default from 220px to 300px, resize range 200-440px
- Correct shortcut tooltips from ⌘←/→ to ⌘[/]

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…400px cap

Replaces the fixed expandedWidth calculation (Math.min(window.innerWidth * 0.92, 1400))
with a simple 75vw viewport-relative width. This scales naturally across all screen sizes
(~1440px on 4K/1920px effective) while the existing maxWidth: 95vw CSS acts as safety net.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@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: 1

🧹 Nitpick comments (6)
src/renderer/types/agent-inbox.ts (1)

52-58: Simplify the type guards by removing broad string-array casts.

Current casts work, but they widen intent unnecessarily. You can keep stricter typing with a narrower cast on value.

♻️ Proposed cleanup
 export function isValidFilterMode(value: unknown): value is InboxFilterMode {
-	return typeof value === 'string' && (VALID_FILTER_MODES as readonly string[]).includes(value);
+	return typeof value === 'string' && VALID_FILTER_MODES.includes(value as InboxFilterMode);
 }
 
 export function isValidSortMode(value: unknown): value is InboxSortMode {
-	return typeof value === 'string' && (VALID_SORT_MODES as readonly string[]).includes(value);
+	return typeof value === 'string' && VALID_SORT_MODES.includes(value as InboxSortMode);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/types/agent-inbox.ts` around lines 52 - 58, The type guards
isValidFilterMode and isValidSortMode should avoid broad array casts; instead
use a narrow cast on the checked value. Replace the checks that use
(VALID_FILTER_MODES as readonly string[]).includes(value) and (VALID_SORT_MODES
as readonly string[]).includes(value) with (VALID_FILTER_MODES as readonly
InboxFilterMode[]).includes(value as InboxFilterMode) and (VALID_SORT_MODES as
readonly InboxSortMode[]).includes(value as InboxSortMode) respectively (or cast
only value: includes(value as InboxFilterMode / value as InboxSortMode)), so the
intent is preserved and TypeScript understands the specific union types without
widening the arrays to string[].
src/renderer/components/AgentInbox/index.tsx (2)

113-119: Simplify the fallback index calculation.

Line 118's Math.min(items.length - 1, 0) always returns 0 (or -1 when items is empty, but that case is guarded by line 114). The expression is correct but unnecessarily complex.

♻️ Simplify fallback to index 0
 	const safeFocusIndex = useMemo(() => {
 		if (!focusId || items.length === 0) return 0;
 		const idx = items.findIndex(
 			(i) => i.sessionId === focusId.sessionId && i.tabId === focusId.tabId
 		);
-		return idx >= 0 ? idx : Math.min(items.length - 1, 0);
+		return idx >= 0 ? idx : 0;
 	}, [focusId, items]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/AgentInbox/index.tsx` around lines 113 - 119, The
fallback calculation in the safeFocusIndex useMemo is overly complex: when items
is non-empty the expression Math.min(items.length - 1, 0) always yields 0;
update the return so that after computing idx you return idx >= 0 ? idx : 0
instead (modify the safeFocusIndex useMemo that references focusId and items).

314-315: Remove empty event handlers.

The onFocus and onBlur handlers are empty no-ops. If they're not needed, removing them improves readability.

♻️ Remove empty handlers
 				onKeyDownCapture={handleCaptureKeyDown}
 				onKeyDown={handleShellKeyDown}
-				onFocus={() => {}}
-				onBlur={() => {}}
 			>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/AgentInbox/index.tsx` around lines 314 - 315, Remove
the no-op onFocus and onBlur props from the JSX in the AgentInbox
component—delete the empty arrow handlers (onFocus={() => {}} and onBlur={() =>
{}}) in src/renderer/components/AgentInbox/index.tsx so the markup is cleaner;
before removing, quickly verify there are no external expectations for those
handlers (e.g., passed-in props or tests referencing AgentInbox.onFocus/onBlur)
and, if any logic is required, replace the empty handlers with the intended
function references.
src/renderer/components/AgentInbox/FocusModeView.tsx (3)

869-905: Consider adding aria-pressed for the thinking toggle.

The thinking toggle button cycles through three states but doesn't announce the current state to screen readers beyond the title tooltip.

♻️ Optional: Add aria-pressed for better screen reader support
 				<button
 					onClick={cycleThinking}
+					aria-pressed={showThinking !== 'off'}
+					aria-label={`Thinking: ${showThinking}`}
 					className={`flex items-center gap-1 text-[10px] px-2 py-1 rounded-full cursor-pointer transition-all ${
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/AgentInbox/FocusModeView.tsx` around lines 869 - 905,
The thinking toggle button (in FocusModeView, using showThinking and
cycleThinking) should expose its current state to assistive tech by adding
aria-pressed; set aria-pressed to true when showThinking is 'on' or 'sticky' and
false when 'off' (e.g., aria-pressed={showThinking !== 'off'}) so screen readers
know whether the toggle is active, keeping the existing title and onClick
behavior unchanged.

204-206: Consider logging clipboard failures for debugging.

The empty catch block silently swallows clipboard errors. While this prevents UI disruption, it may complicate debugging clipboard issues in certain environments.

♻️ Optional: Add minimal error logging
 	const handleCopy = (text: string) => {
-		navigator.clipboard.writeText(text).catch(() => {});
+		navigator.clipboard.writeText(text).catch((err) => {
+			console.debug('Clipboard write failed:', err);
+		});
 	};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/AgentInbox/FocusModeView.tsx` around lines 204 - 206,
The empty catch in handleCopy swallowing clipboard errors makes debugging hard;
update the handleCopy function (the navigator.clipboard.writeText(...) call) to
log errors in the catch handler—e.g., call console.error (or the app logger if
available) with a descriptive message, the text attempted to copy, and the
caught error so failures are visible for troubleshooting.

1133-1166: Consider adding focus styling for nav buttons.

The Prev/Next buttons have hover effects via onMouseEnter/onMouseLeave but lack corresponding focus styling. Keyboard users won't see the visual feedback that mouse users get.

♻️ Optional: Add focus styling matching hover
 				onMouseLeave={(e) => {
 					e.currentTarget.style.backgroundColor = 'transparent';
 				}}
+				onFocus={(e) => {
+					if (items.length > 1)
+						e.currentTarget.style.backgroundColor = `${theme.colors.accent}10`;
+				}}
+				onBlur={(e) => {
+					e.currentTarget.style.backgroundColor = 'transparent';
+				}}
 				title="Previous item (⌘[)"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/AgentInbox/FocusModeView.tsx` around lines 1133 -
1166, The Prev/Next navigation buttons in FocusModeView lack keyboard focus
styling, so add focus handling that mirrors the hover effect: for the button
elements (the ones calling onNavigateItem and rendering
ChevronLeft/ChevronRight), apply the same visual change used onMouseEnter
(backgroundColor = `${theme.colors.accent}10`) when the element receives
keyboard focus and revert it on blur; ensure this only happens when items.length
> 1 (respect disabled/aria-disabled), and also consider adding a visible outline
or box-shadow on focus for stronger accessibility. Use onFocus/onBlur handlers
(or a focus-visible CSS class) on the same buttons to implement this
consistently for both Prev and Next.
🤖 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/renderer/components/TabBar.tsx`:
- Line 775: The inline callback ref ref={(el) => el?.focus()} on the textarea in
TabBar causes focus to be called on every render/keystroke; replace it with a
persistent ref created via useRef<HTMLTextAreaElement | null>() (e.g., const
editTextRef = useRef(null)) and attach it to the textarea (ref={editTextRef}),
then use useEffect that watches the edit mode flag (e.g., isEditing or editMode)
to call editTextRef.current?.focus() only when entering edit mode; remove the
inline callback ref to avoid repeatedly resetting focus/caret during onChange
updates.

---

Nitpick comments:
In `@src/renderer/components/AgentInbox/FocusModeView.tsx`:
- Around line 869-905: The thinking toggle button (in FocusModeView, using
showThinking and cycleThinking) should expose its current state to assistive
tech by adding aria-pressed; set aria-pressed to true when showThinking is 'on'
or 'sticky' and false when 'off' (e.g., aria-pressed={showThinking !== 'off'})
so screen readers know whether the toggle is active, keeping the existing title
and onClick behavior unchanged.
- Around line 204-206: The empty catch in handleCopy swallowing clipboard errors
makes debugging hard; update the handleCopy function (the
navigator.clipboard.writeText(...) call) to log errors in the catch
handler—e.g., call console.error (or the app logger if available) with a
descriptive message, the text attempted to copy, and the caught error so
failures are visible for troubleshooting.
- Around line 1133-1166: The Prev/Next navigation buttons in FocusModeView lack
keyboard focus styling, so add focus handling that mirrors the hover effect: for
the button elements (the ones calling onNavigateItem and rendering
ChevronLeft/ChevronRight), apply the same visual change used onMouseEnter
(backgroundColor = `${theme.colors.accent}10`) when the element receives
keyboard focus and revert it on blur; ensure this only happens when items.length
> 1 (respect disabled/aria-disabled), and also consider adding a visible outline
or box-shadow on focus for stronger accessibility. Use onFocus/onBlur handlers
(or a focus-visible CSS class) on the same buttons to implement this
consistently for both Prev and Next.

In `@src/renderer/components/AgentInbox/index.tsx`:
- Around line 113-119: The fallback calculation in the safeFocusIndex useMemo is
overly complex: when items is non-empty the expression Math.min(items.length -
1, 0) always yields 0; update the return so that after computing idx you return
idx >= 0 ? idx : 0 instead (modify the safeFocusIndex useMemo that references
focusId and items).
- Around line 314-315: Remove the no-op onFocus and onBlur props from the JSX in
the AgentInbox component—delete the empty arrow handlers (onFocus={() => {}} and
onBlur={() => {}}) in src/renderer/components/AgentInbox/index.tsx so the markup
is cleaner; before removing, quickly verify there are no external expectations
for those handlers (e.g., passed-in props or tests referencing
AgentInbox.onFocus/onBlur) and, if any logic is required, replace the empty
handlers with the intended function references.

In `@src/renderer/types/agent-inbox.ts`:
- Around line 52-58: The type guards isValidFilterMode and isValidSortMode
should avoid broad array casts; instead use a narrow cast on the checked value.
Replace the checks that use (VALID_FILTER_MODES as readonly
string[]).includes(value) and (VALID_SORT_MODES as readonly
string[]).includes(value) with (VALID_FILTER_MODES as readonly
InboxFilterMode[]).includes(value as InboxFilterMode) and (VALID_SORT_MODES as
readonly InboxSortMode[]).includes(value as InboxSortMode) respectively (or cast
only value: includes(value as InboxFilterMode / value as InboxSortMode)), so the
intent is preserved and TypeScript understands the specific union types without
widening the arrays to string[].

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cbf5ccc and 6a28299.

📒 Files selected for processing (5)
  • src/__tests__/renderer/components/TabBar.test.tsx
  • src/renderer/components/AgentInbox/FocusModeView.tsx
  • src/renderer/components/AgentInbox/index.tsx
  • src/renderer/components/TabBar.tsx
  • src/renderer/types/agent-inbox.ts

>
{isEditingDescription ? (
<textarea
ref={(el) => el?.focus()}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's view the file around line 775 to understand the context
sed -n '700,830p' src/renderer/components/TabBar.tsx | cat -n

Repository: RunMaestro/Maestro

Length of output: 5101


🏁 Script executed:

# Also check the state variables and hooks being used
sed -n '230,250p' src/renderer/components/TabBar.tsx | cat -n

Repository: RunMaestro/Maestro

Length of output: 991


🏁 Script executed:

# Check if there are any other ref-related or focus-related code patterns
rg -n "ref.*focus|useRef|focus\(\)" src/renderer/components/TabBar.tsx -A 2 -B 2

Repository: RunMaestro/Maestro

Length of output: 2365


🏁 Script executed:

# Search for useEffect blocks and see which ones involve isEditingDescription
rg -n "useEffect\|isEditingDescription" src/renderer/components/TabBar.tsx | head -40

Repository: RunMaestro/Maestro

Length of output: 44


🏁 Script executed:

# Look at the keydown and blur handlers for description
sed -n '500,560p' src/renderer/components/TabBar.tsx | cat -n

Repository: RunMaestro/Maestro

Length of output: 2156


Use useRef and useEffect to focus the textarea only when entering edit mode, not on every keystroke.

The inline callback ref at line 775 (ref={(el) => el?.focus()}) is recreated on each render. Since the onChange handler updates state on every keystroke, this causes focus to be called repeatedly during typing, potentially interrupting user input and resetting the caret position.

♻️ Proposed fix
	const descriptionButtonRef = useRef<HTMLButtonElement>(null);
+	const descriptionTextareaRef = useRef<HTMLTextAreaElement>(null);
	const skipBlurSaveRef = useRef(false);

	const handleDescriptionSave = useCallback(
	// Sync draft with tab.description when it changes externally
	useEffect(() => {
		if (!isEditingDescription) {
			setDescriptionDraft(tab.description ?? '');
		}
	}, [tab.description, isEditingDescription]);

+	// Focus textarea once when entering edit mode
+	useEffect(() => {
+		if (isEditingDescription) {
+			descriptionTextareaRef.current?.focus();
+		}
+	}, [isEditingDescription]);

	// Keep ref in sync with latest draft value to avoid stale closures in cleanup
	useEffect(() => {
		descriptionDraftRef.current = descriptionDraft;
	}, [descriptionDraft]);
 									<textarea
-										ref={(el) => el?.focus()}
+										ref={descriptionTextareaRef}
 										value={descriptionDraft}
 										onChange={(e) => setDescriptionDraft(e.target.value)}
 										onKeyDown={handleDescriptionKeyDown}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/TabBar.tsx` at line 775, The inline callback ref
ref={(el) => el?.focus()} on the textarea in TabBar causes focus to be called on
every render/keystroke; replace it with a persistent ref created via
useRef<HTMLTextAreaElement | null>() (e.g., const editTextRef = useRef(null))
and attach it to the textarea (ref={editTextRef}), then use useEffect that
watches the edit mode flag (e.g., isEditing or editMode) to call
editTextRef.current?.focus() only when entering edit mode; remove the inline
callback ref to avoid repeatedly resetting focus/caret during onChange updates.

Felipe Gobbi and others added 2 commits February 28, 2026 03:42
Slash command filtering used startsWith() while the rest of the app
(File Search, Tab Switcher, @mentions) used fuzzyMatchWithScore.
Now typing partial/out-of-order characters like /cmpct finds /compact.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Gated by encoreFeatures.unifiedInbox flag, with Inbox icon,
shortcut badge, and setAgentInboxOpen handler.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@felipeggv
Copy link
Author

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Feb 28, 2026

✅ Actions performed

Full review triggered.

Copy link

@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: 4

♻️ Duplicate comments (2)
src/renderer/components/TabBar.tsx (1)

775-775: ⚠️ Potential issue | 🟠 Major

Avoid focusing the textarea via an inline callback ref.

ref={(el) => el?.focus()} is recreated every render; while typing, this can repeatedly refocus and disturb caret behavior.

Run this to verify the pattern is still present:

#!/bin/bash
rg -n "ref=\\{\\(el\\) => el\\?\\.focus\\(\\)\\}" src/renderer/components/TabBar.tsx -C 2
♻️ Proposed fix
 	const descriptionButtonRef = useRef<HTMLButtonElement>(null);
+	const descriptionTextareaRef = useRef<HTMLTextAreaElement>(null);
 	const skipBlurSaveRef = useRef(false);

+	useEffect(() => {
+		if (isEditingDescription) {
+			descriptionTextareaRef.current?.focus();
+		}
+	}, [isEditingDescription]);
+
 	// Sync draft with tab.description when it changes externally
 	useEffect(() => {
 		if (!isEditingDescription) {
 			setDescriptionDraft(tab.description ?? '');
@@
 									{isEditingDescription ? (
 										<textarea
-											ref={(el) => el?.focus()}
+											ref={descriptionTextareaRef}
 											value={descriptionDraft}
 											onChange={(e) => setDescriptionDraft(e.target.value)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/TabBar.tsx` at line 775, The inline callback ref
ref={(el) => el?.focus()} in the TabBar component causes a new function each
render and repeatedly refocuses the textarea; replace it with a stable ref and
effect or a memoized callback: create a textareaRef via
useRef<HTMLTextAreaElement | null>(null) (or a useCallback setTextareaRef) and
assign that to the textarea's ref, then call textareaRef.current.focus() inside
a useEffect that runs only when the desired focus condition changes (or on
mount), so caret/focus isn't disturbed during typing. Ensure you update the
textarea ref usage in TabBar.tsx (the textarea element) and remove the inline
arrow ref.
src/renderer/components/AgentInbox/FocusModeView.tsx (1)

752-760: ⚠️ Potential issue | 🟡 Minor

Reset textarea height after quick-reply send.

After clearing replyText, the textarea can remain visually expanded from prior auto-resize.

🔧 Proposed fix
 	const handleQuickReply = useCallback(() => {
 		const text = replyText.trim();
 		if (!text || !onQuickReply) return;
 		onQuickReply(item.sessionId, item.tabId, text);
 		onMarkAsRead?.(item.sessionId, item.tabId);
 		setReplyText('');
+		if (replyInputRef.current) {
+			replyInputRef.current.style.height = 'auto';
+		}
 	}, [replyText, item, onQuickReply, onMarkAsRead]);

Also applies to: 1081-1086

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

In `@src/renderer/components/AgentInbox/FocusModeView.tsx` around lines 752 - 760,
handleQuickReply clears replyText but doesn't reset the textarea's auto-resized
height, leaving it visually expanded; update handleQuickReply (and the analogous
handler at 1081-1086) to reset the textarea height after setReplyText('') by
invoking the existing auto-resize helper (e.g.,
adjustTextareaHeight/autoResizeTextarea) or, if no helper exists, set the
textarea ref's style height back to its default (via
textareaRef.current.style.height = 'auto' or '') to collapse the field;
reference the handleQuickReply function, replyText state and setReplyText call
when making this change.
🧹 Nitpick comments (3)
src/renderer/hooks/tabs/useTabHandlers.ts (1)

970-984: Avoid no-op session writes in description updates.

Line 973 currently rebuilds active-session state even when nothing changes. Add a guard for “tab not found” or unchanged normalized value to reduce unnecessary re-renders.

♻️ Proposed refactor
 const handleUpdateTabDescription = useCallback((tabId: string, description: string) => {
-	const trimmed = description.trim();
+	const normalized = description.trim() || undefined;
 	const { setSessions, activeSessionId } = useSessionStore.getState();
 	setSessions((prev: Session[]) =>
 		prev.map((s) => {
 			if (s.id !== activeSessionId) return s;
+			const tabIndex = s.aiTabs.findIndex((tab) => tab.id === tabId);
+			if (tabIndex === -1) return s;
+			if (s.aiTabs[tabIndex].description === normalized) return s;
 			return {
 				...s,
 				aiTabs: s.aiTabs.map((tab) =>
-					tab.id === tabId ? { ...tab, description: trimmed || undefined } : tab
+					tab.id === tabId ? { ...tab, description: normalized } : tab
 				),
 			};
 		})
 	);
 }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/tabs/useTabHandlers.ts` around lines 970 - 984,
handleUpdateTabDescription currently always calls setSessions which rebuilds the
active session even if the description is unchanged or the tab isn't found;
modify the function to early-return instead of calling setSessions when no
change is needed. Specifically, inside useTabHandlers.handleUpdateTabDescription
(using useSessionStore.getState(), setSessions and activeSessionId), read the
active session and locate the aiTabs entry with matching tabId, compute the
normalized value (trimmed or undefined), and if the tab is missing or the
normalized value equals the existing tab.description, do nothing; only call
setSessions to map/update sessions when the description actually changes.
src/__tests__/renderer/components/TabBar.test.tsx (1)

5657-5691: Reduce duplicated local test fixture/setup in this suite.

mockThemeDesc and the timer/clipboard setup are redefined here despite existing equivalents in the file. Reusing shared fixtures/helpers will reduce maintenance drift.

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

In `@src/__tests__/renderer/components/TabBar.test.tsx` around lines 5657 - 5691,
Replace the duplicated local test fixture and setup by reusing the existing
shared fixtures/helpers: remove the local mockThemeDesc and the
beforeEach/afterEach setup in this test and reference the shared theme fixture
(mockThemeDesc or sharedTheme) and common setup helper (e.g.,
setupTestEnvironment or global beforeEach in the file) instead; ensure the tests
still call Element.prototype.scrollTo and Element.prototype.scrollIntoView mocks
and navigator.clipboard.writeText mock from the shared setup so TabBar.test.tsx
no longer redeclares mockThemeDesc, the vi.useFakeTimers()/vi.useRealTimers()
blocks, or the clipboard/element prototype mocks but uses the central
definitions and teardown.
src/renderer/utils/tabDisplayName.ts (1)

13-15: Handle whitespace-only names to avoid blank tab labels.

If tab.name is ' ', this currently returns an empty-looking title. Trim before the name check so fallback naming still works.

♻️ Proposed tweak
 export function getTabDisplayName(tab: Pick<AITab, 'name' | 'agentSessionId'>): string {
-	if (tab.name) {
-		return tab.name;
+	const normalizedName = tab.name?.trim();
+	if (normalizedName) {
+		return normalizedName;
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/utils/tabDisplayName.ts` around lines 13 - 15, The check that
returns tab.name should ignore whitespace-only names — trim tab.name before
testing so a string like "   " falls back to the default; in the tabDisplayName
utility (where you reference tab.name) call .trim() (or store trimmedName) and
use that trimmed value for the truthy check and return so fallback naming still
applies for whitespace-only titles.
🤖 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/renderer/components/AgentInbox/FocusModeView.tsx`:
- Around line 992-999: The reply composer remains interactive even when
sessionExists is false; update FocusModeView to fully disable the composer and
actions whenever sessionExists is false by (1) rendering the non-interactive
placeholder instead of the interactive ReplyComposer/textarea and action
buttons, or by passing disabled props to the ReplyComposer, textarea and
buttons, (2) short-circuiting submit/handleSubmit, onChange and send handlers to
no-op when sessionExists is false, and (3) ensuring any keyboard shortcuts or
form submit handlers in FocusModeView are ignored when sessionExists is false so
the UI shows "Agent no longer available" and is not actionable.
- Around line 752-769: The handlers handleQuickReply and handleOpenAndReply
currently call onMarkAsRead regardless of whether the reply action actually ran;
change them so onMarkAsRead(item.sessionId, item.tabId) is only invoked after
the corresponding action has executed (i.e., inside the conditional that checks
onQuickReply or onOpenAndReply and after calling it) and keep the existing
reply-text clearing (setReplyText('')) only for the quick-reply path that
succeeded; update handleQuickReply, handleOpenAndReply to return early when no
handler exists so unread state isn't cleared erroneously.

In `@src/renderer/components/AgentInbox/InboxListView.tsx`:
- Around line 63-64: The grouping key derivation is inconsistent: buildRows uses
item.groupId ?? item.groupName ?? 'Ungrouped' but other places use only
groupName, causing header toggle state to drift from item visibility. Replace
the disparate uses with a single shared derived key (e.g., compute const
groupKey = sortMode === 'byAgent' ? item.sessionId : (item.groupId ??
item.groupName ?? 'Ungrouped')) and use that groupKey everywhere in this
component — in buildRows, in the collapse/filtering logic, and in the item
click/toggle handler that toggles group visibility — so header collapse state
and item visibility always reference the exact same identifier.

In `@src/renderer/hooks/input/useInputKeyDown.ts`:
- Around line 209-213: The key handler in useInputKeyDown.ts currently builds
filteredCommands from allSlashCommands using fuzzyMatch and mode checks, but the
UI highlights a separately computed score-ranked list, causing mismatches;
change the handler to compute the same score-ranked list the renderer uses
(apply the same scoring/fuzzy function and mode filters to allSlashCommands, map
each cmd to {cmd, score}, filter out non-matches, sort by score descending) and
then use that ranked list for Enter/Tab resolution (and for any index-based
selection), referencing symbols: allSlashCommands, inputValue, isTerminalMode,
fuzzyMatch/score function, and filteredCommands so both input handler and UI use
identical ranked output.

---

Duplicate comments:
In `@src/renderer/components/AgentInbox/FocusModeView.tsx`:
- Around line 752-760: handleQuickReply clears replyText but doesn't reset the
textarea's auto-resized height, leaving it visually expanded; update
handleQuickReply (and the analogous handler at 1081-1086) to reset the textarea
height after setReplyText('') by invoking the existing auto-resize helper (e.g.,
adjustTextareaHeight/autoResizeTextarea) or, if no helper exists, set the
textarea ref's style height back to its default (via
textareaRef.current.style.height = 'auto' or '') to collapse the field;
reference the handleQuickReply function, replyText state and setReplyText call
when making this change.

In `@src/renderer/components/TabBar.tsx`:
- Line 775: The inline callback ref ref={(el) => el?.focus()} in the TabBar
component causes a new function each render and repeatedly refocuses the
textarea; replace it with a stable ref and effect or a memoized callback: create
a textareaRef via useRef<HTMLTextAreaElement | null>(null) (or a useCallback
setTextareaRef) and assign that to the textarea's ref, then call
textareaRef.current.focus() inside a useEffect that runs only when the desired
focus condition changes (or on mount), so caret/focus isn't disturbed during
typing. Ensure you update the textarea ref usage in TabBar.tsx (the textarea
element) and remove the inline arrow ref.

---

Nitpick comments:
In `@src/__tests__/renderer/components/TabBar.test.tsx`:
- Around line 5657-5691: Replace the duplicated local test fixture and setup by
reusing the existing shared fixtures/helpers: remove the local mockThemeDesc and
the beforeEach/afterEach setup in this test and reference the shared theme
fixture (mockThemeDesc or sharedTheme) and common setup helper (e.g.,
setupTestEnvironment or global beforeEach in the file) instead; ensure the tests
still call Element.prototype.scrollTo and Element.prototype.scrollIntoView mocks
and navigator.clipboard.writeText mock from the shared setup so TabBar.test.tsx
no longer redeclares mockThemeDesc, the vi.useFakeTimers()/vi.useRealTimers()
blocks, or the clipboard/element prototype mocks but uses the central
definitions and teardown.

In `@src/renderer/hooks/tabs/useTabHandlers.ts`:
- Around line 970-984: handleUpdateTabDescription currently always calls
setSessions which rebuilds the active session even if the description is
unchanged or the tab isn't found; modify the function to early-return instead of
calling setSessions when no change is needed. Specifically, inside
useTabHandlers.handleUpdateTabDescription (using useSessionStore.getState(),
setSessions and activeSessionId), read the active session and locate the aiTabs
entry with matching tabId, compute the normalized value (trimmed or undefined),
and if the tab is missing or the normalized value equals the existing
tab.description, do nothing; only call setSessions to map/update sessions when
the description actually changes.

In `@src/renderer/utils/tabDisplayName.ts`:
- Around line 13-15: The check that returns tab.name should ignore
whitespace-only names — trim tab.name before testing so a string like "   "
falls back to the default; in the tabDisplayName utility (where you reference
tab.name) call .trim() (or store trimmedName) and use that trimmed value for the
truthy check and return so fallback naming still applies for whitespace-only
titles.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9d612b6 and c0a2758.

📒 Files selected for processing (34)
  • docs/encore-features.md
  • docs/keyboard-shortcuts.md
  • src/__tests__/renderer/components/AgentInbox/AgentInbox.test.tsx
  • src/__tests__/renderer/components/InputArea.test.tsx
  • src/__tests__/renderer/components/TabBar.test.tsx
  • src/__tests__/renderer/hooks/useAgentInbox.test.ts
  • src/__tests__/renderer/hooks/useInputKeyDown.test.ts
  • src/__tests__/renderer/hooks/useTabHandlers.test.ts
  • src/__tests__/renderer/stores/modalStore.test.ts
  • src/__tests__/renderer/utils/search.test.ts
  • src/renderer/App.tsx
  • src/renderer/components/AgentInbox/FocusModeView.tsx
  • src/renderer/components/AgentInbox/InboxListView.tsx
  • src/renderer/components/AgentInbox/index.tsx
  • src/renderer/components/AppModals.tsx
  • src/renderer/components/InputArea.tsx
  • src/renderer/components/MainPanel.tsx
  • src/renderer/components/QuickActionsModal.tsx
  • src/renderer/components/SessionList.tsx
  • src/renderer/components/SettingsModal.tsx
  • src/renderer/components/TabBar.tsx
  • src/renderer/constants/modalPriorities.ts
  • src/renderer/constants/shortcuts.ts
  • src/renderer/hooks/input/useInputKeyDown.ts
  • src/renderer/hooks/keyboard/useMainKeyboardHandler.ts
  • src/renderer/hooks/props/useMainPanelProps.ts
  • src/renderer/hooks/tabs/useTabHandlers.ts
  • src/renderer/hooks/useAgentInbox.ts
  • src/renderer/stores/modalStore.ts
  • src/renderer/stores/settingsStore.ts
  • src/renderer/types/agent-inbox.ts
  • src/renderer/types/index.ts
  • src/renderer/utils/search.ts
  • src/renderer/utils/tabDisplayName.ts

@felipeggv
Copy link
Author

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Feb 28, 2026

✅ Actions performed

Full review triggered.

1 similar comment
@coderabbitai
Copy link

coderabbitai bot commented Feb 28, 2026

✅ Actions performed

Full review triggered.

Copy link

@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: 4

♻️ Duplicate comments (2)
src/renderer/components/TabBar.tsx (1)

774-776: ⚠️ Potential issue | 🟠 Major

Replace inline focus callback ref to avoid repeated refocus on every rerender.

ref={(el) => el?.focus()} re-runs during typing rerenders, which can reset caret position and interrupt edits.

♻️ Proposed fix
-	const descriptionButtonRef = useRef<HTMLButtonElement>(null);
+	const descriptionButtonRef = useRef<HTMLButtonElement>(null);
+	const descriptionTextareaRef = useRef<HTMLTextAreaElement>(null);
 	const skipBlurSaveRef = useRef(false);
+	useEffect(() => {
+		if (isEditingDescription) {
+			descriptionTextareaRef.current?.focus();
+		}
+	}, [isEditingDescription]);
-										<textarea
-											ref={(el) => el?.focus()}
+										<textarea
+											ref={descriptionTextareaRef}
#!/bin/bash
# Verify the inline callback ref focus pattern still exists in TabBar
rg -n "ref=\{\(el\)\s*=>\s*el\?\.focus\(\)\}" src/renderer/components/TabBar.tsx -C 2
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/TabBar.tsx` around lines 774 - 776, The inline ref on
the textarea (ref={(el) => el?.focus()}) in TabBar.tsx causes repeated
refocusing during rerenders and resets the caret; replace this pattern by
creating a stable ref (e.g., descriptionRef via useRef) and use an effect or a
specific lifecycle/event handler to call descriptionRef.current.focus() only
when you need to set initial focus (such as on mount or when entering edit
mode), and then assign ref={descriptionRef} to the textarea to prevent caret
jumps while typing.
src/renderer/components/AgentInbox/FocusModeView.tsx (1)

752-758: ⚠️ Potential issue | 🟡 Minor

Reset composer height after successful quick reply.

Line 757 clears text, but the auto-resized textarea height can stay expanded after send.

♻️ Suggested fix
 	const handleQuickReply = useCallback(() => {
 		const text = replyText.trim();
 		if (!text || !onQuickReply) return;
 		onQuickReply(item.sessionId, item.tabId, text);
 		onMarkAsRead?.(item.sessionId, item.tabId);
 		setReplyText('');
+		if (replyInputRef.current) {
+			replyInputRef.current.style.height = 'auto';
+		}
 	}, [replyText, item, onQuickReply, onMarkAsRead]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/AgentInbox/FocusModeView.tsx` around lines 752 - 758,
When clearing the composer in handleQuickReply, also reset the auto-resized
textarea height so the composer collapses back to its default size: after
setReplyText('') call the composer textarea ref (e.g. replyTextareaRef or
composerRef) and set its style.height to '' or 'auto' (or call the existing
adjust/reset function if one exists), and add that ref/function to the
handleQuickReply dependency array so the linter doesn't complain.
🧹 Nitpick comments (3)
src/__tests__/renderer/components/TabBar.test.tsx (1)

5649-5978: LGTM — comprehensive test coverage for the new tab description feature.

The test suite provides solid coverage for:

  • Conditional rendering based on onUpdateTabDescription prop
  • Placeholder vs. existing description display
  • Edit mode entry/exit (click, Enter, Escape)
  • Input trimming and whitespace handling

One optional refinement: mockThemeDesc (lines 5657-5675) is identical to mockTheme (lines 117-135). Consider reusing the existing mock to reduce duplication.

,

♻️ Optional: Reuse existing mockTheme
 describe('TabBar description section', () => {
 	const mockOnTabSelect = vi.fn();
 	const mockOnTabClose = vi.fn();
 	const mockOnNewTab = vi.fn();
 	const mockOnUpdateTabDescription = vi.fn();
 	const mockOnTabStar = vi.fn();
 	const mockOnRequestRename = vi.fn();
-
-	const mockThemeDesc: Theme = {
-		id: 'test-theme',
-		name: 'Test Theme',
-		mode: 'dark',
-		colors: {
-			bgMain: '#1a1a1a',
-			bgSidebar: '#2a2a2a',
-			bgActivity: '#3a3a3a',
-			textMain: '#ffffff',
-			textDim: '#888888',
-			accent: '#007acc',
-			border: '#444444',
-			error: '#ff4444',
-			success: '#44ff44',
-			warning: '#ffaa00',
-			vibe: '#ff00ff',
-			agentStatus: '#00ff00',
-		},
-	};
+	// Reuse mockTheme from outer scope

Then replace all mockThemeDesc references with mockTheme in the tests below.

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

In `@src/__tests__/renderer/components/TabBar.test.tsx` around lines 5649 - 5978,
Duplicate theme mock: mockThemeDesc duplicates mockTheme; replace usages of
mockThemeDesc with the existing mockTheme to remove duplication. Locate the mock
definition named mockThemeDesc in the TabBar.test.tsx tests and change the tests
to pass mockTheme (the already defined Theme mock) to TabBar in the
describe('TabBar description section') cases; remove or delete the mockThemeDesc
declaration if no longer referenced. Ensure calls that reference mockThemeDesc
(in render(...) prop theme={mockThemeDesc}) are updated to theme={mockTheme} and
run tests to confirm no other references remain.
src/__tests__/renderer/hooks/useAgentInbox.test.ts (1)

16-67: Test factories duplicate those in AgentInbox.test.tsx.

The makeTab, makeSession, and makeGroup factories are nearly identical to those in AgentInbox.test.tsx. Consider extracting these to a shared test utilities file (e.g., src/__tests__/renderer/factories/) to maintain consistency and reduce duplication.

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

In `@src/__tests__/renderer/hooks/useAgentInbox.test.ts` around lines 16 - 67, The
test file duplicates factory functions makeTab, makeSession, and makeGroup;
extract these factories into a shared test utilities module (e.g., a new
src/__tests__/renderer/factories/ file) and import them into both
useAgentInbox.test.ts and AgentInbox.test.tsx, updating references to use the
shared exports (makeTab, makeSession, makeGroup) so both tests reuse the single
canonical implementations and remove the duplicated definitions from the test
files.
src/__tests__/renderer/components/AgentInbox/AgentInbox.test.tsx (1)

111-139: Consider extracting session factory to a shared test utility.

The makeSession factory uses as unknown as Session type assertion. While acceptable for testing, this pattern is duplicated in useAgentInbox.test.ts (lines 31-59). Consider extracting shared test factories to a common test utility file to reduce duplication and ensure consistency.

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

In `@src/__tests__/renderer/components/AgentInbox/AgentInbox.test.tsx` around
lines 111 - 139, The makeSession test factory is duplicated (see makeSession in
AgentInbox.test.tsx and similar factory in useAgentInbox.test.ts) and uses the
ad-hoc "as unknown as Session" assertion; extract this factory into a shared
test utility (e.g., tests/utils/testFactories.ts) and export makeSession so both
AgentInbox.test.tsx and useAgentInbox.test.ts can import it; update the tests to
import makeSession from the new module and remove the local duplicate factories,
keeping the same shape and keeping the "as unknown as Session" cast only in the
shared factory.
🤖 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/renderer/components/AgentInbox/FocusModeView.tsx`:
- Around line 1086-1111: The Quick Reply and Open & Reply buttons currently only
check sessionExists and replyText but not whether their handlers exist, so they
can appear enabled but do nothing; update the disabled checks and UI state for
the button using the handler presence (handleQuickReply relies on onQuickReply,
handleOpenAndReply relies on onOpenAndReply) by including a guard like
(!onQuickReply || !sessionExists || !replyText.trim()) and (!onOpenAndReply ||
!sessionExists || !replyText.trim()) respectively, and propagate that same
condition to cursor, opacity, title, and any other interactive styling so the
buttons are truly disabled when the callbacks are not provided.

In `@src/renderer/components/AgentInbox/InboxListView.tsx`:
- Around line 830-845: The Shift+Tab handling traps when focusIdx === -1 because
the shift branch always focuses the container; update the keyboard handler in
InboxListView (the block using focusIdx, focusables, containerRef) so that when
e.shiftKey and focusIdx === -1 you move focus to the last focusable item
(focusables[focusables.length - 1].focus()) instead of re-focusing
containerRef.current; ensure you still call e.preventDefault() and preserve
existing behaviors for other focusIdx cases.

In `@src/renderer/stores/modalStore.ts`:
- Around line 774-777: The Agent Inbox's persisted view state is being cleared
because setAgentInboxOpen(false) calls closeModal('agentInbox'), and closeModal
clears modal data; to fix, avoid clearing data for the 'agentInbox' modal by
either (A) adding an optional flag to closeModal (e.g., closeModal(id, {
preserveData: true })) and calling closeModal('agentInbox', { preserveData: true
}) from setAgentInboxOpen, or (B) add a new helper like
closeModalPreserveData('agentInbox') that performs the same close flow but does
not clear data; update setAgentInboxOpen to use the new behavior and ensure
updateModalData('agentInbox', ...) remains unchanged so filters/sort/expanded
state persist across closes/opens.

In `@src/renderer/utils/tabDisplayName.ts`:
- Around line 13-15: Normalize tab.name by trimming whitespace before returning:
instead of returning tab.name blindly, compute a trimmedName = tab.name?.trim()
and if trimmedName is non-empty return it, otherwise fall back to the existing
ID/default logic (use tab.id or whatever default the current function uses).
Update the code in the function that reads tab.name (the tab.name check in
tabDisplayName.ts) to use trimmedName so whitespace-only names like "   " do not
produce blank-looking titles.

---

Duplicate comments:
In `@src/renderer/components/AgentInbox/FocusModeView.tsx`:
- Around line 752-758: When clearing the composer in handleQuickReply, also
reset the auto-resized textarea height so the composer collapses back to its
default size: after setReplyText('') call the composer textarea ref (e.g.
replyTextareaRef or composerRef) and set its style.height to '' or 'auto' (or
call the existing adjust/reset function if one exists), and add that
ref/function to the handleQuickReply dependency array so the linter doesn't
complain.

In `@src/renderer/components/TabBar.tsx`:
- Around line 774-776: The inline ref on the textarea (ref={(el) =>
el?.focus()}) in TabBar.tsx causes repeated refocusing during rerenders and
resets the caret; replace this pattern by creating a stable ref (e.g.,
descriptionRef via useRef) and use an effect or a specific lifecycle/event
handler to call descriptionRef.current.focus() only when you need to set initial
focus (such as on mount or when entering edit mode), and then assign
ref={descriptionRef} to the textarea to prevent caret jumps while typing.

---

Nitpick comments:
In `@src/__tests__/renderer/components/AgentInbox/AgentInbox.test.tsx`:
- Around line 111-139: The makeSession test factory is duplicated (see
makeSession in AgentInbox.test.tsx and similar factory in useAgentInbox.test.ts)
and uses the ad-hoc "as unknown as Session" assertion; extract this factory into
a shared test utility (e.g., tests/utils/testFactories.ts) and export
makeSession so both AgentInbox.test.tsx and useAgentInbox.test.ts can import it;
update the tests to import makeSession from the new module and remove the local
duplicate factories, keeping the same shape and keeping the "as unknown as
Session" cast only in the shared factory.

In `@src/__tests__/renderer/components/TabBar.test.tsx`:
- Around line 5649-5978: Duplicate theme mock: mockThemeDesc duplicates
mockTheme; replace usages of mockThemeDesc with the existing mockTheme to remove
duplication. Locate the mock definition named mockThemeDesc in the
TabBar.test.tsx tests and change the tests to pass mockTheme (the already
defined Theme mock) to TabBar in the describe('TabBar description section')
cases; remove or delete the mockThemeDesc declaration if no longer referenced.
Ensure calls that reference mockThemeDesc (in render(...) prop
theme={mockThemeDesc}) are updated to theme={mockTheme} and run tests to confirm
no other references remain.

In `@src/__tests__/renderer/hooks/useAgentInbox.test.ts`:
- Around line 16-67: The test file duplicates factory functions makeTab,
makeSession, and makeGroup; extract these factories into a shared test utilities
module (e.g., a new src/__tests__/renderer/factories/ file) and import them into
both useAgentInbox.test.ts and AgentInbox.test.tsx, updating references to use
the shared exports (makeTab, makeSession, makeGroup) so both tests reuse the
single canonical implementations and remove the duplicated definitions from the
test files.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9d612b6 and 826ca20.

📒 Files selected for processing (34)
  • docs/encore-features.md
  • docs/keyboard-shortcuts.md
  • src/__tests__/renderer/components/AgentInbox/AgentInbox.test.tsx
  • src/__tests__/renderer/components/InputArea.test.tsx
  • src/__tests__/renderer/components/TabBar.test.tsx
  • src/__tests__/renderer/hooks/useAgentInbox.test.ts
  • src/__tests__/renderer/hooks/useInputKeyDown.test.ts
  • src/__tests__/renderer/hooks/useTabHandlers.test.ts
  • src/__tests__/renderer/stores/modalStore.test.ts
  • src/__tests__/renderer/utils/search.test.ts
  • src/renderer/App.tsx
  • src/renderer/components/AgentInbox/FocusModeView.tsx
  • src/renderer/components/AgentInbox/InboxListView.tsx
  • src/renderer/components/AgentInbox/index.tsx
  • src/renderer/components/AppModals.tsx
  • src/renderer/components/InputArea.tsx
  • src/renderer/components/MainPanel.tsx
  • src/renderer/components/QuickActionsModal.tsx
  • src/renderer/components/SessionList.tsx
  • src/renderer/components/SettingsModal.tsx
  • src/renderer/components/TabBar.tsx
  • src/renderer/constants/modalPriorities.ts
  • src/renderer/constants/shortcuts.ts
  • src/renderer/hooks/input/useInputKeyDown.ts
  • src/renderer/hooks/keyboard/useMainKeyboardHandler.ts
  • src/renderer/hooks/props/useMainPanelProps.ts
  • src/renderer/hooks/tabs/useTabHandlers.ts
  • src/renderer/hooks/useAgentInbox.ts
  • src/renderer/stores/modalStore.ts
  • src/renderer/stores/settingsStore.ts
  • src/renderer/types/agent-inbox.ts
  • src/renderer/types/index.ts
  • src/renderer/utils/search.ts
  • src/renderer/utils/tabDisplayName.ts

Comment on lines +1086 to +1111
<button
onClick={handleQuickReply}
disabled={!sessionExists || !replyText.trim()}
className="p-2 rounded-lg transition-colors flex-shrink-0"
style={{
backgroundColor: replyText.trim()
? theme.colors.accent
: `${theme.colors.accent}30`,
color: replyText.trim() ? theme.colors.accentForeground : theme.colors.textDim,
cursor: replyText.trim() ? 'pointer' : 'default',
}}
title={enterToSendAI ? 'Quick reply (Enter)' : 'Quick reply (⌘Enter)'}
>
<ArrowUp className="w-4 h-4" />
</button>
{/* Open & Reply button (secondary) */}
<button
onClick={handleOpenAndReply}
disabled={!sessionExists || !replyText.trim()}
className="p-1.5 rounded-lg transition-colors flex-shrink-0 text-xs"
style={{
border: `1px solid ${theme.colors.border}`,
color: replyText.trim() ? theme.colors.textMain : theme.colors.textDim,
backgroundColor: 'transparent',
cursor: replyText.trim() ? 'pointer' : 'default',
opacity: replyText.trim() ? 1 : 0.5,
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Disable no-op reply actions when handlers are unavailable.

onQuickReply and onOpenAndReply are optional, but Line 1088 and Line 1104 only gate on session/text state. This can render enabled actions that do nothing.

♻️ Suggested fix
+	const canQuickReply = sessionExists && !!replyText.trim() && !!onQuickReply;
+	const canOpenAndReply = sessionExists && !!replyText.trim() && !!onOpenAndReply;
...
 						<button
 							onClick={handleQuickReply}
-							disabled={!sessionExists || !replyText.trim()}
+							disabled={!canQuickReply}
 							className="p-2 rounded-lg transition-colors flex-shrink-0"
 							style={{
-								backgroundColor: replyText.trim()
+								backgroundColor: canQuickReply
 									? theme.colors.accent
 									: `${theme.colors.accent}30`,
-								color: replyText.trim() ? theme.colors.accentForeground : theme.colors.textDim,
-								cursor: replyText.trim() ? 'pointer' : 'default',
+								color: canQuickReply ? theme.colors.accentForeground : theme.colors.textDim,
+								cursor: canQuickReply ? 'pointer' : 'default',
 							}}
...
 						<button
 							onClick={handleOpenAndReply}
-							disabled={!sessionExists || !replyText.trim()}
+							disabled={!canOpenAndReply}
 							className="p-1.5 rounded-lg transition-colors flex-shrink-0 text-xs"
 							style={{
 								border: `1px solid ${theme.colors.border}`,
-								color: replyText.trim() ? theme.colors.textMain : theme.colors.textDim,
+								color: canOpenAndReply ? theme.colors.textMain : theme.colors.textDim,
 								backgroundColor: 'transparent',
-								cursor: replyText.trim() ? 'pointer' : 'default',
-								opacity: replyText.trim() ? 1 : 0.5,
+								cursor: canOpenAndReply ? 'pointer' : 'default',
+								opacity: canOpenAndReply ? 1 : 0.5,
 							}}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/AgentInbox/FocusModeView.tsx` around lines 1086 -
1111, The Quick Reply and Open & Reply buttons currently only check
sessionExists and replyText but not whether their handlers exist, so they can
appear enabled but do nothing; update the disabled checks and UI state for the
button using the handler presence (handleQuickReply relies on onQuickReply,
handleOpenAndReply relies on onOpenAndReply) by including a guard like
(!onQuickReply || !sessionExists || !replyText.trim()) and (!onOpenAndReply ||
!sessionExists || !replyText.trim()) respectively, and propagate that same
condition to cursor, opacity, title, and any other interactive styling so the
buttons are truly disabled when the callbacks are not provided.

Comment on lines +830 to +845
if (e.shiftKey) {
if (focusIdx <= 0) {
e.preventDefault();
containerRef.current?.focus();
} else {
e.preventDefault();
focusables[focusIdx - 1].focus();
}
} else {
if (focusIdx === -1) {
e.preventDefault();
focusables[0].focus();
} else if (focusIdx >= focusables.length - 1) {
e.preventDefault();
containerRef.current?.focus();
} else {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix Shift+Tab trapping on the list container.

When focus is on the listbox (focusIdx === -1), Shift+Tab currently re-focuses the same element (Line 833), so backward keyboard navigation stalls.

♻️ Suggested fix
 				if (e.shiftKey) {
-					if (focusIdx <= 0) {
+					if (focusIdx === -1) {
+						e.preventDefault();
+						focusables[focusables.length - 1].focus();
+					} else if (focusIdx === 0) {
 						e.preventDefault();
 						containerRef.current?.focus();
 					} else {
 						e.preventDefault();
 						focusables[focusIdx - 1].focus();
 					}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/AgentInbox/InboxListView.tsx` around lines 830 - 845,
The Shift+Tab handling traps when focusIdx === -1 because the shift branch
always focuses the container; update the keyboard handler in InboxListView (the
block using focusIdx, focusables, containerRef) so that when e.shiftKey and
focusIdx === -1 you move focus to the last focusable item
(focusables[focusables.length - 1].focus()) instead of re-focusing
containerRef.current; ensure you still call e.preventDefault() and preserve
existing behaviors for other focusIdx cases.

Comment on lines 774 to 777
// Agent Inbox Modal (Unified Inbox)
setAgentInboxOpen: (open: boolean) =>
open ? openModal('agentInbox') : closeModal('agentInbox'),
updateAgentInboxData: (data: Record<string, unknown>) => updateModalData('agentInbox', data),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Closing Agent Inbox currently drops its persisted view state.

setAgentInboxOpen(false) calls closeModal('agentInbox'), and closeModal clears data (Line 348). That conflicts with the persisted-state contract on Line 143 and resets filter/sort/expanded preferences on reopen.

💡 Proposed fix (preserve data when closing Agent Inbox)
 		// Agent Inbox Modal (Unified Inbox)
-		setAgentInboxOpen: (open: boolean) =>
-			open ? openModal('agentInbox') : closeModal('agentInbox'),
+		setAgentInboxOpen: (open: boolean) => {
+			if (open) {
+				openModal('agentInbox');
+				return;
+			}
+
+			useModalStore.setState((state) => {
+				const current = state.modals.get('agentInbox');
+				if (!current?.open) return state;
+				const newModals = new Map(state.modals);
+				newModals.set('agentInbox', { open: false, data: current.data });
+				return { modals: newModals };
+			});
+		},
📝 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
// Agent Inbox Modal (Unified Inbox)
setAgentInboxOpen: (open: boolean) =>
open ? openModal('agentInbox') : closeModal('agentInbox'),
updateAgentInboxData: (data: Record<string, unknown>) => updateModalData('agentInbox', data),
// Agent Inbox Modal (Unified Inbox)
setAgentInboxOpen: (open: boolean) => {
if (open) {
openModal('agentInbox');
return;
}
useModalStore.setState((state) => {
const current = state.modals.get('agentInbox');
if (!current?.open) return state;
const newModals = new Map(state.modals);
newModals.set('agentInbox', { open: false, data: current.data });
return { modals: newModals };
});
},
updateAgentInboxData: (data: Record<string, unknown>) => updateModalData('agentInbox', data),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/stores/modalStore.ts` around lines 774 - 777, The Agent Inbox's
persisted view state is being cleared because setAgentInboxOpen(false) calls
closeModal('agentInbox'), and closeModal clears modal data; to fix, avoid
clearing data for the 'agentInbox' modal by either (A) adding an optional flag
to closeModal (e.g., closeModal(id, { preserveData: true })) and calling
closeModal('agentInbox', { preserveData: true }) from setAgentInboxOpen, or (B)
add a new helper like closeModalPreserveData('agentInbox') that performs the
same close flow but does not clear data; update setAgentInboxOpen to use the new
behavior and ensure updateModalData('agentInbox', ...) remains unchanged so
filters/sort/expanded state persist across closes/opens.

Comment on lines 13 to 15
if (tab.name) {
return tab.name;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Normalize whitespace-only tab names before returning.

Line 13 currently treats " " as a valid name, which can produce blank-looking tab titles instead of falling back to ID/default naming.

Proposed fix
 export function getTabDisplayName(tab: Pick<AITab, 'name' | 'agentSessionId'>): string {
-	if (tab.name) {
-		return tab.name;
+	const normalizedName = tab.name?.trim();
+	if (normalizedName) {
+		return normalizedName;
 	}
 	if (tab.agentSessionId) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/utils/tabDisplayName.ts` around lines 13 - 15, Normalize
tab.name by trimming whitespace before returning: instead of returning tab.name
blindly, compute a trimmedName = tab.name?.trim() and if trimmedName is
non-empty return it, otherwise fall back to the existing ID/default logic (use
tab.id or whatever default the current function uses). Update the code in the
function that reads tab.name (the tab.name check in tabDisplayName.ts) to use
trimmedName so whitespace-only names like "   " do not produce blank-looking
titles.

Copy link

@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: 3

♻️ Duplicate comments (4)
src/renderer/utils/tabDisplayName.ts (1)

13-15: ⚠️ Potential issue | 🟡 Minor

Normalize whitespace-only tab names before returning.

Line 13 returns tab.name directly if truthy, but " " (whitespace-only string) is truthy and would produce a blank-looking tab title instead of falling back to ID-based naming.

Proposed fix
 export function getTabDisplayName(tab: Pick<AITab, 'name' | 'agentSessionId'>): string {
-	if (tab.name) {
-		return tab.name;
+	const trimmedName = tab.name?.trim();
+	if (trimmedName) {
+		return trimmedName;
 	}
 	if (tab.agentSessionId) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/utils/tabDisplayName.ts` around lines 13 - 15, The current early
return in tabDisplayName returns tab.name when truthy, but fails to normalize
whitespace-only names; update the logic in tabDisplayName to trim tab.name
(e.g., const name = tab.name?.trim()) and only return it if the trimmed value is
non-empty, otherwise fall back to the existing ID-based naming path (use tab.id
or the existing fallback logic) so whitespace-only names do not produce blank
titles.
src/renderer/components/AgentInbox/InboxListView.tsx (1)

830-850: ⚠️ Potential issue | 🟠 Major

Shift+Tab still traps when focus is on the list container.

When the list container itself has focus (focusIdx === -1) and Shift+Tab is pressed, the code focuses the container again (Line 833), creating a keyboard trap. The condition should distinguish between "focus is on container" vs "focus is on first header element".

🔧 Proposed fix
 			if (e.shiftKey) {
-				if (focusIdx <= 0) {
+				if (focusIdx === -1) {
+					// Focus is on container itself, cycle to last header focusable
+					e.preventDefault();
+					if (focusables.length > 0) {
+						focusables[focusables.length - 1].focus();
+					}
+				} else if (focusIdx === 0) {
+					// Focus is on first header element, return to container
 					e.preventDefault();
 					containerRef.current?.focus();
 				} else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/AgentInbox/InboxListView.tsx` around lines 830 - 850,
The Shift+Tab branch is treating container focus (focusIdx === -1) the same as
the first header and re-focusing the container, causing a keyboard trap; update
the key handling in InboxListView.tsx so that when e.shiftKey is true you only
call e.preventDefault() and move focus to containerRef.current when focusIdx ===
0 (the first header), but if focusIdx === -1 simply return without
preventDefault (allow the browser to move focus out); reference the focusIdx,
focusables and containerRef symbols when making this change.
src/renderer/components/AgentInbox/FocusModeView.tsx (2)

752-758: ⚠️ Potential issue | 🟡 Minor

Reset composer height after successful quick reply.

setReplyText('') clears content, but the textarea can remain expanded after send.

🔧 Suggested patch
 	const handleQuickReply = useCallback(() => {
 		const text = replyText.trim();
 		if (!text || !onQuickReply) return;
 		onQuickReply(item.sessionId, item.tabId, text);
 		onMarkAsRead?.(item.sessionId, item.tabId);
 		setReplyText('');
+		if (replyInputRef.current) {
+			replyInputRef.current.style.height = 'auto';
+		}
 	}, [replyText, item, onQuickReply, onMarkAsRead]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/AgentInbox/FocusModeView.tsx` around lines 752 - 758,
In handleQuickReply, after calling setReplyText(''), also reset the composer
textarea height so it doesn't remain expanded; locate the handleQuickReply
function and either call an existing reset function (e.g., resetComposerHeight
or setComposerHeight(DEFAULT_HEIGHT)) or, if no helper exists, access the
textarea ref (e.g., composerRef or textareaRef) and set its style.height = ''
(or call the existing adjustTextareaHeight/compressTextarea helper) guarded by a
null check so the composer returns to its initial height after a successful
quick reply.

1086-1114: ⚠️ Potential issue | 🟡 Minor

Disable reply actions when callbacks are unavailable.

Buttons can appear enabled even when onQuickReply/onOpenAndReply are undefined, resulting in no-op interactions.

🔧 Suggested patch
+	const canQuickReply = sessionExists && !!replyText.trim() && !!onQuickReply;
+	const canOpenAndReply = sessionExists && !!replyText.trim() && !!onOpenAndReply;
+
 						<button
 							onClick={handleQuickReply}
-							disabled={!sessionExists || !replyText.trim()}
+							disabled={!canQuickReply}
@@
-								backgroundColor: replyText.trim()
+								backgroundColor: canQuickReply
 									? theme.colors.accent
 									: `${theme.colors.accent}30`,
-								color: replyText.trim() ? theme.colors.accentForeground : theme.colors.textDim,
-								cursor: replyText.trim() ? 'pointer' : 'default',
+								color: canQuickReply ? theme.colors.accentForeground : theme.colors.textDim,
+								cursor: canQuickReply ? 'pointer' : 'default',
 							}}
@@
 						<button
 							onClick={handleOpenAndReply}
-							disabled={!sessionExists || !replyText.trim()}
+							disabled={!canOpenAndReply}
@@
-								color: replyText.trim() ? theme.colors.textMain : theme.colors.textDim,
+								color: canOpenAndReply ? theme.colors.textMain : theme.colors.textDim,
 								backgroundColor: 'transparent',
-								cursor: replyText.trim() ? 'pointer' : 'default',
-								opacity: replyText.trim() ? 1 : 0.5,
+								cursor: canOpenAndReply ? 'pointer' : 'default',
+								opacity: canOpenAndReply ? 1 : 0.5,
 							}}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/AgentInbox/FocusModeView.tsx` around lines 1086 -
1114, The reply buttons can be clickable even if the callbacks are not provided;
update the two buttons and their handlers so they are truly disabled when
callbacks are missing: in the button JSX for the ArrowUp action and the "Open &
Reply" secondary action include the callback existence in the disabled condition
(e.g. disabled={!sessionExists || !replyText.trim() || !onQuickReply} and
disabled={!sessionExists || !replyText.trim() || !onOpenAndReply}) and adjust
styles/titles accordingly, and also add early-return guards inside
handleQuickReply and handleOpenAndReply to no-op if their respective
onQuickReply/onOpenAndReply props are undefined to prevent no-op interactions.
🧹 Nitpick comments (6)
src/renderer/utils/tabDisplayName.ts (1)

39-39: Consider using "New Agent" instead of "New Session" for user-facing text.

Per coding guidelines, user-facing language should use "agent" rather than "session" (which is reserved for provider-level conversation contexts).

Proposed fix
-	return 'New Session';
+	return 'New Agent';

As per coding guidelines: "Use the Session interface in code to represent agents (due to historical naming), but use 'agent' in user-facing language."

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

In `@src/renderer/utils/tabDisplayName.ts` at line 39, Update the user-facing
label returned from the function in src/renderer/utils/tabDisplayName.ts so it
uses "New Agent" instead of "New Session"; locate the return statement (the one
returning 'New Session') inside the tab display name helper (e.g.,
getTabDisplayName or default export in tabDisplayName.ts) and change the string
to "New Agent" to follow the guideline that UI text should use "agent" while
keeping the existing Session interface names in code.
src/renderer/components/SessionList.tsx (1)

737-739: Prefer agent-centric copy in the subtitle.

Consider changing Unified tabs inbox to Unified agent inbox for consistent user-facing terminology.

As per coding guidelines, "Use the Session interface in code to represent agents (due to historical naming), but use 'agent' in user-facing language and 'session' for provider-level conversation contexts".

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

In `@src/renderer/components/SessionList.tsx` around lines 737 - 739, In the
SessionList component update the user-facing subtitle string from "Unified tabs
inbox" to "Unified agent inbox" to follow agent-centric wording; locate the div
with className "text-xs" (inside SessionList) that currently renders "Unified
tabs inbox" and replace its text content with "Unified agent inbox" so UI copy
aligns with the guideline that uses "agent" in user-facing language while
keeping the Session interface name in code.
src/renderer/components/SettingsModal.tsx (1)

3687-3710: Add tabIndex={0} to the switch buttons for keyboard focus consistency.

Other switch buttons in this file (e.g., lines 2221, 2627, 2660) include tabIndex={0} and outline-none for keyboard navigation. The new Unified Inbox and Tab Descriptions switches are missing tabIndex, which may cause inconsistent keyboard navigation.

♻️ Proposed fix
 									<button
 										role="switch"
 										aria-label="Toggle Unified Inbox"
 										aria-checked={encoreFeatures.unifiedInbox}
 										onClick={(e) => {
 											e.stopPropagation();
 											setEncoreFeatures({
 												...encoreFeatures,
 												unifiedInbox: !encoreFeatures.unifiedInbox,
 											});
 										}}
-										className="relative w-10 h-5 rounded-full transition-colors flex-shrink-0"
+										className="relative w-10 h-5 rounded-full transition-colors flex-shrink-0 outline-none"
+										tabIndex={0}
 										style={{

Apply the same change to the Tab Descriptions switch button.

Also applies to: 3745-3768

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

In `@src/renderer/components/SettingsModal.tsx` around lines 3687 - 3710, The
switch button for the Unified Inbox (the element with role="switch" and
aria-checked tied to encoreFeatures.unifiedInbox, using setEncoreFeatures to
toggle) is missing tabIndex={0} and the keyboard outline class; update that
button (and the analogous Tab Descriptions switch around the same area) to
include tabIndex={0} and the same focus/outline styling (e.g., include
outline-none or the project's focus class) so it matches other switches and
supports keyboard navigation consistently.
src/renderer/hooks/tabs/useTabHandlers.ts (1)

970-984: Skip no-op state writes in description updates.

Line 973 currently remaps sessions/aiTabs even when the target tab is missing or the normalized description is unchanged, causing avoidable store updates.

♻️ Proposed refactor
 const handleUpdateTabDescription = useCallback((tabId: string, description: string) => {
-	const trimmed = description.trim();
+	const trimmed = description.trim();
+	const nextDescription = trimmed || undefined;
 	const { setSessions, activeSessionId } = useSessionStore.getState();
 	setSessions((prev: Session[]) =>
 		prev.map((s) => {
 			if (s.id !== activeSessionId) return s;
+			let changed = false;
+			const aiTabs = s.aiTabs.map((tab) => {
+				if (tab.id !== tabId) return tab;
+				if (tab.description === nextDescription) return tab;
+				changed = true;
+				return { ...tab, description: nextDescription };
+			});
 			return {
-				...s,
-				aiTabs: s.aiTabs.map((tab) =>
-					tab.id === tabId ? { ...tab, description: trimmed || undefined } : tab
-				),
+				...(changed ? { ...s, aiTabs } : s),
 			};
 		})
 	);
 }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/tabs/useTabHandlers.ts` around lines 970 - 984, The
handler handleUpdateTabDescription currently remaps sessions/aiTabs even when
the target tab doesn't exist or the normalized description hasn't changed;
update it to first locate the active session via
useSessionStore.getState().activeSessionId and the specific tab by tabId,
compute the newDesc = trimmed || undefined, and if the tab is missing or its
existing description === newDesc return prev unchanged to avoid a no-op set;
otherwise call setSessions with a new sessions array where only that session's
aiTabs replace the specific tab's description. Ensure you reference setSessions,
activeSessionId, aiTabs, tabId and description when implementing the conditional
early return.
src/renderer/components/AgentInbox/index.tsx (1)

314-315: Consider removing empty event handlers.

The onFocus and onBlur handlers are no-ops. If they're placeholders for future functionality, consider adding a brief comment. Otherwise, removing them would clean up the code.

♻️ Optional cleanup
 				onKeyDownCapture={handleCaptureKeyDown}
 				onKeyDown={handleShellKeyDown}
-				onFocus={() => {}}
-				onBlur={() => {}}
 			>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/AgentInbox/index.tsx` around lines 314 - 315, Remove
the no-op handlers on the input/component in AgentInbox (the onFocus and onBlur
props) or replace them with a short comment if they’re intentional placeholders;
locate the JSX in AgentInbox/index.tsx where onFocus={() => {}} and onBlur={()
=> {}} are attached and either delete those props or add a comment like //
placeholder for future focus/blur handling to make intent explicit.
src/renderer/hooks/useAgentInbox.ts (1)

14-32: Unused sessionState parameter.

The sessionState parameter is passed to matchesFilter but never used in the function body. If this is reserved for a future filter mode (e.g., "needs-input"), consider adding a comment. Otherwise, it can be removed to reduce confusion.

♻️ Option A: Remove if not needed
 function matchesFilter(
-	sessionState: Session['state'],
 	hasUnread: boolean,
 	filterMode: InboxFilterMode,
 	isStarred: boolean
 ): boolean {

And update the call site at line 209:

-			if (!matchesFilter(session.state, hasUnread, filterMode, tab.starred === true)) continue;
+			if (!matchesFilter(hasUnread, filterMode, tab.starred === true)) continue;
♻️ Option B: Document intent for future use
 function matchesFilter(
+	// Reserved for future 'needs-input' filter mode
 	sessionState: Session['state'],
 	hasUnread: boolean,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/useAgentInbox.ts` around lines 14 - 32, The parameter
sessionState on matchesFilter is unused; remove it from the function signature
in useAgentInbox.ts (function matchesFilter) and update all call sites to stop
passing a Session['state'] argument, adjusting any type annotations accordingly;
if the parameter is intentionally reserved for a future "needs-input" filter,
instead keep the parameter but add a clear comment above matchesFilter
explaining it's reserved for future use and why so it doesn't appear unused.
🤖 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/renderer/App.tsx`:
- Around line 1408-1425: The effect that flushes pendingInboxQuickReply should
also clear it when the target session no longer exists; update the useEffect
handling pendingInboxQuickReply (which currently checks activeSession?.id !==
pendingInboxQuickReply.targetSessionId) to detect when the targetSessionId
cannot be found (or the session list no longer contains it) and call
setPendingInboxQuickReply(null) in that branch so stale pendingInboxQuickReply
is cleared and doesn't block future quick replies handled by
handleAgentInboxQuickReply; keep the existing logic that processes the reply
when the activeSession matches and the microtask that restores
previousActiveSessionId.

In `@src/renderer/components/AgentInbox/FocusModeView.tsx`:
- Around line 433-509: The list container currently uses role="listbox" while
rendering interactive header divs with role="button" inside rows.map in
FocusModeView.tsx, creating an invalid ARIA hierarchy; change the container role
from "listbox" to "list" and mark each rendered row (both header and item) as
role="listitem" (or wrap header contents in a real <button> while keeping its
listitem wrapper) so headers remain interactive but the ARIA tree is valid;
update the div that currently has role="button" (the header rendering block) to
be a listitem wrapper with the interactive element inside (or keep tabIndex and
onKeyDown on a child button) and ensure aria-expanded is moved to the
interactive control while the outer element stays role="listitem".

In `@src/renderer/components/AgentInbox/InboxListView.tsx`:
- Around line 984-1024: The Focus button (uses items, selectedIndex,
onEnterFocus) and the Expand button (uses onToggleExpanded, isExpanded) need
keyboard-focus styling like the Close button: add tabIndex={0} and
onFocus/onBlur handlers to both buttons that apply and remove the same visible
styles used for the Close button (e.g., set backgroundColor and
outline/boxShadow consistent with theme.colors.accent variants), ensuring the
handlers reference the same theme values and element via e.currentTarget so
keyboard users see the focus indicator; keep existing onMouseEnter/onMouseLeave
logic intact and implement focus handlers analogously for both elements.

---

Duplicate comments:
In `@src/renderer/components/AgentInbox/FocusModeView.tsx`:
- Around line 752-758: In handleQuickReply, after calling setReplyText(''), also
reset the composer textarea height so it doesn't remain expanded; locate the
handleQuickReply function and either call an existing reset function (e.g.,
resetComposerHeight or setComposerHeight(DEFAULT_HEIGHT)) or, if no helper
exists, access the textarea ref (e.g., composerRef or textareaRef) and set its
style.height = '' (or call the existing adjustTextareaHeight/compressTextarea
helper) guarded by a null check so the composer returns to its initial height
after a successful quick reply.
- Around line 1086-1114: The reply buttons can be clickable even if the
callbacks are not provided; update the two buttons and their handlers so they
are truly disabled when callbacks are missing: in the button JSX for the ArrowUp
action and the "Open & Reply" secondary action include the callback existence in
the disabled condition (e.g. disabled={!sessionExists || !replyText.trim() ||
!onQuickReply} and disabled={!sessionExists || !replyText.trim() ||
!onOpenAndReply}) and adjust styles/titles accordingly, and also add
early-return guards inside handleQuickReply and handleOpenAndReply to no-op if
their respective onQuickReply/onOpenAndReply props are undefined to prevent
no-op interactions.

In `@src/renderer/components/AgentInbox/InboxListView.tsx`:
- Around line 830-850: The Shift+Tab branch is treating container focus
(focusIdx === -1) the same as the first header and re-focusing the container,
causing a keyboard trap; update the key handling in InboxListView.tsx so that
when e.shiftKey is true you only call e.preventDefault() and move focus to
containerRef.current when focusIdx === 0 (the first header), but if focusIdx ===
-1 simply return without preventDefault (allow the browser to move focus out);
reference the focusIdx, focusables and containerRef symbols when making this
change.

In `@src/renderer/utils/tabDisplayName.ts`:
- Around line 13-15: The current early return in tabDisplayName returns tab.name
when truthy, but fails to normalize whitespace-only names; update the logic in
tabDisplayName to trim tab.name (e.g., const name = tab.name?.trim()) and only
return it if the trimmed value is non-empty, otherwise fall back to the existing
ID-based naming path (use tab.id or the existing fallback logic) so
whitespace-only names do not produce blank titles.

---

Nitpick comments:
In `@src/renderer/components/AgentInbox/index.tsx`:
- Around line 314-315: Remove the no-op handlers on the input/component in
AgentInbox (the onFocus and onBlur props) or replace them with a short comment
if they’re intentional placeholders; locate the JSX in AgentInbox/index.tsx
where onFocus={() => {}} and onBlur={() => {}} are attached and either delete
those props or add a comment like // placeholder for future focus/blur handling
to make intent explicit.

In `@src/renderer/components/SessionList.tsx`:
- Around line 737-739: In the SessionList component update the user-facing
subtitle string from "Unified tabs inbox" to "Unified agent inbox" to follow
agent-centric wording; locate the div with className "text-xs" (inside
SessionList) that currently renders "Unified tabs inbox" and replace its text
content with "Unified agent inbox" so UI copy aligns with the guideline that
uses "agent" in user-facing language while keeping the Session interface name in
code.

In `@src/renderer/components/SettingsModal.tsx`:
- Around line 3687-3710: The switch button for the Unified Inbox (the element
with role="switch" and aria-checked tied to encoreFeatures.unifiedInbox, using
setEncoreFeatures to toggle) is missing tabIndex={0} and the keyboard outline
class; update that button (and the analogous Tab Descriptions switch around the
same area) to include tabIndex={0} and the same focus/outline styling (e.g.,
include outline-none or the project's focus class) so it matches other switches
and supports keyboard navigation consistently.

In `@src/renderer/hooks/tabs/useTabHandlers.ts`:
- Around line 970-984: The handler handleUpdateTabDescription currently remaps
sessions/aiTabs even when the target tab doesn't exist or the normalized
description hasn't changed; update it to first locate the active session via
useSessionStore.getState().activeSessionId and the specific tab by tabId,
compute the newDesc = trimmed || undefined, and if the tab is missing or its
existing description === newDesc return prev unchanged to avoid a no-op set;
otherwise call setSessions with a new sessions array where only that session's
aiTabs replace the specific tab's description. Ensure you reference setSessions,
activeSessionId, aiTabs, tabId and description when implementing the conditional
early return.

In `@src/renderer/hooks/useAgentInbox.ts`:
- Around line 14-32: The parameter sessionState on matchesFilter is unused;
remove it from the function signature in useAgentInbox.ts (function
matchesFilter) and update all call sites to stop passing a Session['state']
argument, adjusting any type annotations accordingly; if the parameter is
intentionally reserved for a future "needs-input" filter, instead keep the
parameter but add a clear comment above matchesFilter explaining it's reserved
for future use and why so it doesn't appear unused.

In `@src/renderer/utils/tabDisplayName.ts`:
- Line 39: Update the user-facing label returned from the function in
src/renderer/utils/tabDisplayName.ts so it uses "New Agent" instead of "New
Session"; locate the return statement (the one returning 'New Session') inside
the tab display name helper (e.g., getTabDisplayName or default export in
tabDisplayName.ts) and change the string to "New Agent" to follow the guideline
that UI text should use "agent" while keeping the existing Session interface
names in code.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9d612b6 and 826ca20.

📒 Files selected for processing (34)
  • docs/encore-features.md
  • docs/keyboard-shortcuts.md
  • src/__tests__/renderer/components/AgentInbox/AgentInbox.test.tsx
  • src/__tests__/renderer/components/InputArea.test.tsx
  • src/__tests__/renderer/components/TabBar.test.tsx
  • src/__tests__/renderer/hooks/useAgentInbox.test.ts
  • src/__tests__/renderer/hooks/useInputKeyDown.test.ts
  • src/__tests__/renderer/hooks/useTabHandlers.test.ts
  • src/__tests__/renderer/stores/modalStore.test.ts
  • src/__tests__/renderer/utils/search.test.ts
  • src/renderer/App.tsx
  • src/renderer/components/AgentInbox/FocusModeView.tsx
  • src/renderer/components/AgentInbox/InboxListView.tsx
  • src/renderer/components/AgentInbox/index.tsx
  • src/renderer/components/AppModals.tsx
  • src/renderer/components/InputArea.tsx
  • src/renderer/components/MainPanel.tsx
  • src/renderer/components/QuickActionsModal.tsx
  • src/renderer/components/SessionList.tsx
  • src/renderer/components/SettingsModal.tsx
  • src/renderer/components/TabBar.tsx
  • src/renderer/constants/modalPriorities.ts
  • src/renderer/constants/shortcuts.ts
  • src/renderer/hooks/input/useInputKeyDown.ts
  • src/renderer/hooks/keyboard/useMainKeyboardHandler.ts
  • src/renderer/hooks/props/useMainPanelProps.ts
  • src/renderer/hooks/tabs/useTabHandlers.ts
  • src/renderer/hooks/useAgentInbox.ts
  • src/renderer/stores/modalStore.ts
  • src/renderer/stores/settingsStore.ts
  • src/renderer/types/agent-inbox.ts
  • src/renderer/types/index.ts
  • src/renderer/utils/search.ts
  • src/renderer/utils/tabDisplayName.ts

Comment on lines 1408 to 1425
// Flush pending quick-reply once target session is active (deterministic, no RAF timing chain).
useEffect(() => {
if (!pendingInboxQuickReply) return;
if (activeSession?.id !== pendingInboxQuickReply.targetSessionId) return;

inboxProcessInputRef.current(pendingInboxQuickReply.text);
const previousActiveSessionId = pendingInboxQuickReply.previousActiveSessionId;
setPendingInboxQuickReply(null);

if (
previousActiveSessionId &&
previousActiveSessionId !== pendingInboxQuickReply.targetSessionId
) {
queueMicrotask(() => {
setActiveSessionId(previousActiveSessionId);
});
}
}, [pendingInboxQuickReply, activeSession?.id, setActiveSessionId]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Clear stale pendingInboxQuickReply when the target session no longer exists.

If the target session is removed before activation, this effect never clears pendingInboxQuickReply, and later quick replies get blocked by the guard in handleAgentInboxQuickReply.

🔧 Suggested patch
 	useEffect(() => {
 		if (!pendingInboxQuickReply) return;
+		const targetExists = sessions.some((s) => s.id === pendingInboxQuickReply.targetSessionId);
+		if (!targetExists) {
+			setPendingInboxQuickReply(null);
+			return;
+		}
 		if (activeSession?.id !== pendingInboxQuickReply.targetSessionId) return;
 
 		inboxProcessInputRef.current(pendingInboxQuickReply.text);
 		const previousActiveSessionId = pendingInboxQuickReply.previousActiveSessionId;
 		setPendingInboxQuickReply(null);
@@
-	}, [pendingInboxQuickReply, activeSession?.id, setActiveSessionId]);
+	}, [pendingInboxQuickReply, activeSession?.id, sessions, setActiveSessionId]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/App.tsx` around lines 1408 - 1425, The effect that flushes
pendingInboxQuickReply should also clear it when the target session no longer
exists; update the useEffect handling pendingInboxQuickReply (which currently
checks activeSession?.id !== pendingInboxQuickReply.targetSessionId) to detect
when the targetSessionId cannot be found (or the session list no longer contains
it) and call setPendingInboxQuickReply(null) in that branch so stale
pendingInboxQuickReply is cleared and doesn't block future quick replies handled
by handleAgentInboxQuickReply; keep the existing logic that processes the reply
when the activeSession matches and the microtask that restores
previousActiveSessionId.

Comment on lines 433 to 509
<div role="listbox" aria-label="Inbox items" className="flex-1 overflow-y-auto py-1">
{(() => {
let activeGroup: string | null = null;
return rows.map((row, rowIdx) => {
if (row.type === 'header') {
activeGroup = row.groupKey;
return (
<div
key={`header-${row.groupKey}-${rowIdx}`}
tabIndex={0}
role="button"
aria-expanded={!collapsedGroups.has(row.groupKey)}
onClick={() => {
setCollapsedGroups((prev) => {
const next = new Set(prev);
if (next.has(row.groupKey)) next.delete(row.groupKey);
else next.add(row.groupKey);
return next;
});
}}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setCollapsedGroups((prev) => {
const next = new Set(prev);
if (next.has(row.groupKey)) next.delete(row.groupKey);
else next.add(row.groupKey);
return next;
});
}
}}
className="flex items-center gap-1.5 px-3 py-1.5 text-[10px] uppercase tracking-wider cursor-pointer"
onFocus={(e) => {
e.currentTarget.style.boxShadow = `inset 0 0 0 2px ${theme.colors.accent}`;
}}
onBlur={(e) => {
e.currentTarget.style.boxShadow = 'none';
}}
style={{
color: theme.colors.textDim,
fontWeight: 600,
backgroundColor: theme.colors.bgSidebar,
outline: 'none',
}}
>
{collapsedGroups.has(row.groupKey) ? (
<ChevronRight className="w-3 h-3" />
) : (
<ChevronDown className="w-3 h-3" />
)}
{row.groupName}
<span style={{ color: theme.colors.textDim, opacity: 0.5, marginLeft: 'auto' }}>
{row.type === 'header' ? row.count : 0}
</span>
</div>
);
}

// Skip items in collapsed groups
if (activeGroup && collapsedGroups.has(activeGroup)) return null;

const itm = row.item;
const idx = row.index;
const isCurrent = idx === currentIndex;
const statusColor = resolveStatusColor(itm.state, theme);

const previewText = itm.lastMessage
? itm.lastMessage.replace(/[#*`>]/g, '').slice(0, 60)
: '';

return (
<div
key={`${itm.sessionId}-${itm.tabId}`}
ref={isCurrent ? currentRowRef : undefined}
tabIndex={0}
role="option"
aria-selected={isCurrent}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix ARIA role hierarchy in the sidebar list.

role="listbox" expects option/group semantics, but headers are interactive role="button" siblings. This creates inconsistent screen-reader navigation.

♿ Suggested patch
-			<div role="listbox" aria-label="Inbox items" className="flex-1 overflow-y-auto py-1">
+			<div role="list" aria-label="Inbox items" className="flex-1 overflow-y-auto py-1">
@@
-								role="option"
-								aria-selected={isCurrent}
+								role="listitem"
+								aria-current={isCurrent ? 'true' : undefined}
📝 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
<div role="listbox" aria-label="Inbox items" className="flex-1 overflow-y-auto py-1">
{(() => {
let activeGroup: string | null = null;
return rows.map((row, rowIdx) => {
if (row.type === 'header') {
activeGroup = row.groupKey;
return (
<div
key={`header-${row.groupKey}-${rowIdx}`}
tabIndex={0}
role="button"
aria-expanded={!collapsedGroups.has(row.groupKey)}
onClick={() => {
setCollapsedGroups((prev) => {
const next = new Set(prev);
if (next.has(row.groupKey)) next.delete(row.groupKey);
else next.add(row.groupKey);
return next;
});
}}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setCollapsedGroups((prev) => {
const next = new Set(prev);
if (next.has(row.groupKey)) next.delete(row.groupKey);
else next.add(row.groupKey);
return next;
});
}
}}
className="flex items-center gap-1.5 px-3 py-1.5 text-[10px] uppercase tracking-wider cursor-pointer"
onFocus={(e) => {
e.currentTarget.style.boxShadow = `inset 0 0 0 2px ${theme.colors.accent}`;
}}
onBlur={(e) => {
e.currentTarget.style.boxShadow = 'none';
}}
style={{
color: theme.colors.textDim,
fontWeight: 600,
backgroundColor: theme.colors.bgSidebar,
outline: 'none',
}}
>
{collapsedGroups.has(row.groupKey) ? (
<ChevronRight className="w-3 h-3" />
) : (
<ChevronDown className="w-3 h-3" />
)}
{row.groupName}
<span style={{ color: theme.colors.textDim, opacity: 0.5, marginLeft: 'auto' }}>
{row.type === 'header' ? row.count : 0}
</span>
</div>
);
}
// Skip items in collapsed groups
if (activeGroup && collapsedGroups.has(activeGroup)) return null;
const itm = row.item;
const idx = row.index;
const isCurrent = idx === currentIndex;
const statusColor = resolveStatusColor(itm.state, theme);
const previewText = itm.lastMessage
? itm.lastMessage.replace(/[#*`>]/g, '').slice(0, 60)
: '';
return (
<div
key={`${itm.sessionId}-${itm.tabId}`}
ref={isCurrent ? currentRowRef : undefined}
tabIndex={0}
role="option"
aria-selected={isCurrent}
<div role="list" aria-label="Inbox items" className="flex-1 overflow-y-auto py-1">
{(() => {
let activeGroup: string | null = null;
return rows.map((row, rowIdx) => {
if (row.type === 'header') {
activeGroup = row.groupKey;
return (
<div
key={`header-${row.groupKey}-${rowIdx}`}
tabIndex={0}
role="button"
aria-expanded={!collapsedGroups.has(row.groupKey)}
onClick={() => {
setCollapsedGroups((prev) => {
const next = new Set(prev);
if (next.has(row.groupKey)) next.delete(row.groupKey);
else next.add(row.groupKey);
return next;
});
}}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setCollapsedGroups((prev) => {
const next = new Set(prev);
if (next.has(row.groupKey)) next.delete(row.groupKey);
else next.add(row.groupKey);
return next;
});
}
}}
className="flex items-center gap-1.5 px-3 py-1.5 text-[10px] uppercase tracking-wider cursor-pointer"
onFocus={(e) => {
e.currentTarget.style.boxShadow = `inset 0 0 0 2px ${theme.colors.accent}`;
}}
onBlur={(e) => {
e.currentTarget.style.boxShadow = 'none';
}}
style={{
color: theme.colors.textDim,
fontWeight: 600,
backgroundColor: theme.colors.bgSidebar,
outline: 'none',
}}
>
{collapsedGroups.has(row.groupKey) ? (
<ChevronRight className="w-3 h-3" />
) : (
<ChevronDown className="w-3 h-3" />
)}
{row.groupName}
<span style={{ color: theme.colors.textDim, opacity: 0.5, marginLeft: 'auto' }}>
{row.type === 'header' ? row.count : 0}
</span>
</div>
);
}
// Skip items in collapsed groups
if (activeGroup && collapsedGroups.has(activeGroup)) return null;
const itm = row.item;
const idx = row.index;
const isCurrent = idx === currentIndex;
const statusColor = resolveStatusColor(itm.state, theme);
const previewText = itm.lastMessage
? itm.lastMessage.replace(/[#*`>]/g, '').slice(0, 60)
: '';
return (
<div
key={`${itm.sessionId}-${itm.tabId}`}
ref={isCurrent ? currentRowRef : undefined}
tabIndex={0}
role="listitem"
aria-current={isCurrent ? 'true' : undefined}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/AgentInbox/FocusModeView.tsx` around lines 433 - 509,
The list container currently uses role="listbox" while rendering interactive
header divs with role="button" inside rows.map in FocusModeView.tsx, creating an
invalid ARIA hierarchy; change the container role from "listbox" to "list" and
mark each rendered row (both header and item) as role="listitem" (or wrap header
contents in a real <button> while keeping its listitem wrapper) so headers
remain interactive but the ARIA tree is valid; update the div that currently has
role="button" (the header rendering block) to be a listitem wrapper with the
interactive element inside (or keep tabIndex and onKeyDown on a child button)
and ensure aria-expanded is moved to the interactive control while the outer
element stays role="listitem".

Comment on lines 984 to 1024
<button
onClick={() => {
if (items.length > 0 && items[selectedIndex]) {
onEnterFocus(items[selectedIndex]);
}
}}
disabled={items.length === 0}
className="text-xs px-2.5 py-1 rounded transition-colors"
style={{
backgroundColor: items.length > 0 ? `${theme.colors.accent}15` : 'transparent',
color: items.length > 0 ? theme.colors.accent : theme.colors.textDim,
cursor: items.length > 0 ? 'pointer' : 'default',
opacity: items.length === 0 ? 0.5 : 1,
}}
onMouseEnter={(e) => {
if (items.length > 0) {
e.currentTarget.style.backgroundColor = `${theme.colors.accent}25`;
}
}}
onMouseLeave={(e) => {
if (items.length > 0) {
e.currentTarget.style.backgroundColor = `${theme.colors.accent}15`;
}
}}
title="Enter Focus Mode (F)"
>
Focus ▶
</button>
<button
onClick={() => onToggleExpanded((prev) => !prev)}
className="p-1.5 rounded"
style={{ color: theme.colors.textDim }}
onMouseEnter={(e) =>
(e.currentTarget.style.backgroundColor = `${theme.colors.accent}20`)
}
onMouseLeave={(e) => (e.currentTarget.style.backgroundColor = 'transparent')}
title={isExpanded ? 'Collapse' : 'Expand'}
aria-label={isExpanded ? 'Collapse modal' : 'Expand modal'}
>
{isExpanded ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
</button>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add focus styling to Focus and Expand buttons for keyboard accessibility.

The close button (Lines 1033-1038) has onFocus/onBlur handlers for visible focus, but the Focus button (Lines 984-1011) and Expand button (Lines 1012-1024) lack these handlers. Keyboard users navigating through header controls won't see focus indicators on these buttons.

♿ Proposed fix for Focus button
 						<button
 							onClick={() => {
 								if (items.length > 0 && items[selectedIndex]) {
 									onEnterFocus(items[selectedIndex]);
 								}
 							}}
 							disabled={items.length === 0}
 							className="text-xs px-2.5 py-1 rounded transition-colors"
 							style={{
 								backgroundColor: items.length > 0 ? `${theme.colors.accent}15` : 'transparent',
 								color: items.length > 0 ? theme.colors.accent : theme.colors.textDim,
 								cursor: items.length > 0 ? 'pointer' : 'default',
 								opacity: items.length === 0 ? 0.5 : 1,
 							}}
 							onMouseEnter={(e) => {
 								if (items.length > 0) {
 									e.currentTarget.style.backgroundColor = `${theme.colors.accent}25`;
 								}
 							}}
 							onMouseLeave={(e) => {
 								if (items.length > 0) {
 									e.currentTarget.style.backgroundColor = `${theme.colors.accent}15`;
 								}
 							}}
+							onFocus={(e) => {
+								e.currentTarget.style.outline = `2px solid ${theme.colors.accent}`;
+								e.currentTarget.style.outlineOffset = '-2px';
+							}}
+							onBlur={(e) => {
+								e.currentTarget.style.outline = 'none';
+							}}
 							title="Enter Focus Mode (F)"
 						>

As per coding guidelines, "Add tabIndex attribute and focus event handlers when implementing components that need keyboard focus."

📝 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
<button
onClick={() => {
if (items.length > 0 && items[selectedIndex]) {
onEnterFocus(items[selectedIndex]);
}
}}
disabled={items.length === 0}
className="text-xs px-2.5 py-1 rounded transition-colors"
style={{
backgroundColor: items.length > 0 ? `${theme.colors.accent}15` : 'transparent',
color: items.length > 0 ? theme.colors.accent : theme.colors.textDim,
cursor: items.length > 0 ? 'pointer' : 'default',
opacity: items.length === 0 ? 0.5 : 1,
}}
onMouseEnter={(e) => {
if (items.length > 0) {
e.currentTarget.style.backgroundColor = `${theme.colors.accent}25`;
}
}}
onMouseLeave={(e) => {
if (items.length > 0) {
e.currentTarget.style.backgroundColor = `${theme.colors.accent}15`;
}
}}
title="Enter Focus Mode (F)"
>
Focus
</button>
<button
onClick={() => onToggleExpanded((prev) => !prev)}
className="p-1.5 rounded"
style={{ color: theme.colors.textDim }}
onMouseEnter={(e) =>
(e.currentTarget.style.backgroundColor = `${theme.colors.accent}20`)
}
onMouseLeave={(e) => (e.currentTarget.style.backgroundColor = 'transparent')}
title={isExpanded ? 'Collapse' : 'Expand'}
aria-label={isExpanded ? 'Collapse modal' : 'Expand modal'}
>
{isExpanded ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
</button>
<button
onClick={() => {
if (items.length > 0 && items[selectedIndex]) {
onEnterFocus(items[selectedIndex]);
}
}}
disabled={items.length === 0}
className="text-xs px-2.5 py-1 rounded transition-colors"
style={{
backgroundColor: items.length > 0 ? `${theme.colors.accent}15` : 'transparent',
color: items.length > 0 ? theme.colors.accent : theme.colors.textDim,
cursor: items.length > 0 ? 'pointer' : 'default',
opacity: items.length === 0 ? 0.5 : 1,
}}
onMouseEnter={(e) => {
if (items.length > 0) {
e.currentTarget.style.backgroundColor = `${theme.colors.accent}25`;
}
}}
onMouseLeave={(e) => {
if (items.length > 0) {
e.currentTarget.style.backgroundColor = `${theme.colors.accent}15`;
}
}}
onFocus={(e) => {
e.currentTarget.style.outline = `2px solid ${theme.colors.accent}`;
e.currentTarget.style.outlineOffset = '-2px';
}}
onBlur={(e) => {
e.currentTarget.style.outline = 'none';
}}
title="Enter Focus Mode (F)"
>
Focus
</button>
<button
onClick={() => onToggleExpanded((prev) => !prev)}
className="p-1.5 rounded"
style={{ color: theme.colors.textDim }}
onMouseEnter={(e) =>
(e.currentTarget.style.backgroundColor = `${theme.colors.accent}20`)
}
onMouseLeave={(e) => (e.currentTarget.style.backgroundColor = 'transparent')}
title={isExpanded ? 'Collapse' : 'Expand'}
aria-label={isExpanded ? 'Collapse modal' : 'Expand modal'}
>
{isExpanded ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
</button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/AgentInbox/InboxListView.tsx` around lines 984 -
1024, The Focus button (uses items, selectedIndex, onEnterFocus) and the Expand
button (uses onToggleExpanded, isExpanded) need keyboard-focus styling like the
Close button: add tabIndex={0} and onFocus/onBlur handlers to both buttons that
apply and remove the same visible styles used for the Close button (e.g., set
backgroundColor and outline/boxShadow consistent with theme.colors.accent
variants), ensuring the handlers reference the same theme values and element via
e.currentTarget so keyboard users see the focus indicator; keep existing
onMouseEnter/onMouseLeave logic intact and implement focus handlers analogously
for both elements.

- Fix 1: Reply guards — only call onMarkAsRead if handler exists and executes
- Fix 2: Textarea disabled state when session unavailable
- Fix 3: Align groupId collapse key across buildRows, filter, and T-key toggle
- Fix 4: ARIA roles — role="button" on interactive sidebar headers (was "presentation")
- Fix 5: Focus ring styling on Focus/Expand buttons (consistent with Close)
- Fix 6: Guard stale pendingInboxQuickReply when target session is deleted
- Nice-to-have: sessionExists guard on textarea onKeyDown
- Nice-to-have: previousActiveSessionId guard in microtask
- Nice-to-have: Extract getGroupCollapseKey() helper for DRY

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@felipeggv felipeggv force-pushed the feat/encore-inbox-tabdesc-fuzzy branch from 826ca20 to a28ac12 Compare February 28, 2026 08:12
Felipe Gobbi and others added 5 commits February 28, 2026 05:41
Thread allSlashCommands (built-in + custom + speckit + openspec + agent)
from App.tsx through AgentInbox to FocusModeView. Includes fuzzy matching,
keyboard navigation (arrows/Enter/Escape), and dropdown rendering.
Perf-gated: filtering only runs when dropdown is open, ref-based onChange
skips no-op state updates to avoid keystroke lag.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix ARIA role hierarchy in FocusModeView sidebar: change group headers
  from role="button" to role="presentation" (children of role="list"
  must be listitem or presentation, not button)
- Add fuzzy score-based sorting to slash command autocomplete in
  useInputKeyDown (was using boolean fuzzyMatch without scoring,
  now matches InputArea/FocusModeView pattern with fuzzyMatchWithScore)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…te preservation, tab name trimming and textarea ref stability
… column names

The inbox quick reply effect (`App.tsx`) entered an infinite loop because
processInput() calls Zustand setSessions() (synchronous), which re-triggers
the effect before React commits the batched setPendingInboxQuickReply(null).
Added inboxQuickReplyFiredRef as a synchronous guard to prevent re-entry.

Also fixed stats-db column names from camelCase (startTime, createdAt) to
snake_case (start_time, created_at) to match the actual SQLite schema.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.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