Skip to content

feat: Tab Description as Encore Feature#469

Open
felipeggv wants to merge 36 commits intoRunMaestro:mainfrom
felipeggv:feat/tab-description
Open

feat: Tab Description as Encore Feature#469
felipeggv wants to merge 36 commits intoRunMaestro:mainfrom
felipeggv:feat/tab-description

Conversation

@felipeggv
Copy link

@felipeggv felipeggv commented Feb 26, 2026

Summary

Adds inline-editable tab descriptions as an Encore Feature.

Note: This PR is stacked on top of #468 (Unified Inbox). The tab-description-specific changes are the last 5 commits. Merge #468 first, then this PR will show only the tab description diff.

  • Encore Feature gate: tabDescription flag in EncoreFeatureFlags — disabled by default, toggle in Settings > Encore tab
  • Description field: New description property on AITab interface
  • Inline editing: Hover over any AI tab to see the description overlay with click-to-edit
  • Overflow handling: 3-line max with ellipsis, maxWidth 320px, word-break for long text
  • Settings toggle: Tab Description card in Settings > Encore tab with info text

Changes (tab-description only)

5 commits, 8 files, ~210 lines added:

  • types/index.tstabDescription flag + description field on AITab
  • settingsStore.ts — default tabDescription: false
  • SettingsModal.tsx — Tab Description toggle card in Encore tab
  • useTabHandlers.tshandleUpdateTabDescription handler
  • TabBar.tsx — inline-editable description in hover overlay
  • App.tsx / MainPanel.tsx / useMainPanelProps.ts — prop threading

Depends on

Test plan

  • ESLint clean
  • TypeScript clean
  • Manual: Enable Tab Description in Settings > Encore
  • Manual: Hover over AI tab, verify description overlay appears
  • Manual: Click description area, verify inline editing works
  • Manual: Enter long text, verify 3-line ellipsis and maxWidth constraint

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Unified Inbox (Beta): modal with List and Focus views, openable via hamburger menu, Quick Actions, or Alt/Option+I; supports per-item navigation, quick-reply, open‑&‑reply, mark‑as‑read, thinking toggles, and focus-mode navigation.
    • Tab Descriptions: enable in Settings to add/edit per-tab descriptions from the tab overlay.
  • Bug Fixes & Accessibility

    • Improved keyboard navigation, ARIA roles, focus handling, scroll/proximity auto-scroll, group collapse behavior, and selection sync.

Felipe Gobbi and others added 17 commits February 26, 2026 15:48
Adds src/renderer/types/agent-inbox.ts with InboxItem, InboxFilterMode,
InboxSortMode, InboxViewMode, STATUS_LABELS, and STATUS_COLORS.
All imports resolve against current codebase (SessionState at types/index.ts:53).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Register Unified Inbox as an Encore Feature with type flag (boolean)
and default value (false) in both EncoreFeatureFlags interface and
DEFAULT_ENCORE_FEATURES constant.

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

Port 4 files from feature/inbox-focus-polish into current architecture:
- useAgentInbox.ts: data aggregation hook (filter, sort, smart summaries)
- AgentInbox/index.tsx: modal shell with list/focus view switching
- AgentInbox/InboxListView.tsx: virtualized list (react-window → @tanstack/react-virtual)
- AgentInbox/FocusModeView.tsx: focus mode detail view with reply input

Also adds:
- AGENT_INBOX priority (555) in modalPriorities.ts
- agentInbox ModalId, AgentInboxModalData, and modal actions in modalStore.ts

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

- Add agentInboxOpen reactive state to useModalActions() in modalStore.ts
- Add lazy import for AgentInbox component in App.tsx
- Destructure agentInboxOpen/setAgentInboxOpen from useModalActions()
- Create handleAgentInboxNavigateToSession callback (close modal + switch session)
- Add gated modal render: encoreFeatures.unifiedInbox && agentInboxOpen
- Wire conditional setters for SessionList and QuickActions/AppModals
- Add setAgentInboxOpen to keyboard handler ref for future keybinding support
- 2 expected TS errors for props not yet updated (phases 06-08)

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

Added agentInbox shortcut definition (Alt+I) to shortcuts.ts and wired
the keyboard handler in useMainKeyboardHandler.ts, gated behind
encoreFeatures.unifiedInbox. App.tsx context already had setAgentInboxOpen
wired from Phase 05.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wire setAgentInboxOpen through all prop interfaces (HamburgerMenuContentProps,
SessionListProps, UseSessionListPropsDeps) and add conditionally-rendered
Inbox button after Director's Notes in the hamburger menu. Button only
appears when the Encore feature gate provides the setter.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Added onOpenUnifiedInbox prop to QuickActionsModal and threaded it through
AppModals (both AppUtilityModalsProps and AppModalsProps interfaces).
The action entry appears conditionally after Director's Notes, matching
the existing Encore gate pattern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prefix onMarkAsRead with _ since it's destructured but not yet used
in the FocusModeView component body.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace unconditional scroll-to-bottom on every log update with smart
proximity detection: only auto-scroll when user is within 150px of bottom
or when navigating to a new item. Prevents scroll hijacking while reading
earlier messages during active agent output.

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

- Bug 1: Changed group header <div> to <button type="button"> with
  onKeyDown handler for Enter/Space keyboard accessibility
- Bug 2: Scoped collapse filtering to only apply when sortMode is
  'grouped' or 'byAgent', preventing hidden items in other sort modes
- Bug 3: Added guard useEffect to reset parent selectedIndex when it
  points to a collapsed-away item

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Overlay animation: duration-150 → duration-100
- InboxListView header: px-6 → px-4 py-3, text-base → text-lg, added Bot icon (w-5 h-5, accent)
- FocusModeView header: added py-3, removed fixed height, text-sm font-bold → text-lg font-semibold
- Footer padding aligned to px-4

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…'s Notes tokens

Consistent px-4 horizontal padding across header, info bar, content scroll area,
and reply input. Info bar uses py-2 padding instead of fixed height. Reply input
uses py-3 matching the header. Scroll area uses uniform p-4 padding.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Star indicators were oversized and redundant — the "Starred" filter
already communicates starred state. Removed star icon from inbox list
rows, focus sidebar items, focus info bar badge, and unicode star
from filter pill label. Cleaned up unused Star imports from lucide-react.

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

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…10 integration checks green

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
processInput already adds the user log entry, so the optimistic push
in handleAgentInboxQuickReply was creating duplicates.

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

coderabbitai bot commented Feb 26, 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 lazy-loaded Unified Agent Inbox (list + focus views), data hook and types, modal/store/shortcut wiring, settings feature flags, per-tab descriptions and handlers, TabBar inline editing UI, keyboard/menu integrations, accessibility and scrolling fixes, and prop/hook threading across app shell components.

Changes

Cohort / File(s) Summary
Agent Inbox Core
src/renderer/components/AgentInbox/index.tsx, src/renderer/components/AgentInbox/InboxListView.tsx, src/renderer/components/AgentInbox/FocusModeView.tsx, src/renderer/hooks/useAgentInbox.ts, src/renderer/types/agent-inbox.ts
Adds AgentInbox component (list & focus modes), InboxListView (virtualized), FocusModeView, useAgentInbox hook, agent-inbox types, keyboard/navigation, grouping, filters, and public helper export.
Modal, Store & Priorities
src/renderer/stores/modalStore.ts, src/renderer/constants/modalPriorities.ts, src/renderer/constants/shortcuts.ts
Registers agentInbox modal and ModalData, exposes agentInboxOpen/setAgentInboxOpen/updateAgentInboxData, adds AGENT_INBOX priority (555) and agentInbox shortcut.
App Shell & Modal Wiring
src/renderer/App.tsx, src/renderer/components/AppModals.tsx, src/renderer/components/QuickActionsModal.tsx, src/renderer/components/SessionList.tsx
Lazy-loads AgentInbox in App, threads agentInboxOpen/setAgentInboxOpen and onOpenUnifiedInbox through modals, adds Unified Inbox quick action and menu entries, and wires navigation/close callbacks.
Tab Description Feature
src/renderer/components/TabBar.tsx, src/renderer/components/MainPanel.tsx, src/renderer/hooks/tabs/useTabHandlers.ts, src/renderer/hooks/props/useMainPanelProps.ts, src/renderer/types/index.ts, src/renderer/stores/settingsStore.ts
Adds optional description on AITab, tabDescription feature flag and Settings toggle, TabBar inline description editing (UI + keyboard), new handleUpdateTabDescription handler wired through useTabHandlers → useMainPanelProps → MainPanel → TabBar.
Settings, Shortcuts & Keyboard
src/renderer/components/SettingsModal.tsx, src/renderer/constants/shortcuts.ts, src/renderer/hooks/keyboard/useMainKeyboardHandler.ts
Adds Settings toggles for unifiedInbox and tabDescription, registers shortcut display, and handles keyboard shortcut (Alt/Option+I) to open inbox when feature and setter are available.
SessionList / Menu Integration
src/renderer/components/SessionList.tsx, src/renderer/hooks/props/useSessionListProps.ts
Adds Unified Inbox menu/button within hamburger menu and threads optional setAgentInboxOpen prop through session list prop hooks.
Accessibility, Scrolling & Misc UX
src/renderer/components/AgentInbox/InboxListView.tsx, src/renderer/components/AgentInbox/FocusModeView.tsx, src/renderer/components/LogViewer.tsx, playbooks/...`
Accessibility fixes (keyboardable headers, ARIA), proximity-based auto-scroll in FocusMode, LogViewer persistence effect, style/header alignment, and multiple playbook/docs describing porting and verification.

Sequence Diagram(s)

sequenceDiagram
  participant User as rgba(54,117,136,0.5) User
  participant App as rgba(19,90,60,0.5) AppShell
  participant ModalStore as rgba(120,20,140,0.5) ModalStore
  participant AgentInbox as rgba(200,80,30,0.5) AgentInbox
  participant Sessions as rgba(10,90,180,0.5) SessionManager
  participant AIEngine as rgba(180,40,100,0.5) AIEngine

  User->>App: trigger Unified Inbox (shortcut / menu)
  App->>ModalStore: setAgentInboxOpen(true)
  ModalStore-->>App: agentInboxOpen = true
  App->>AgentInbox: lazy-load & render (passes callbacks)
  AgentInbox->>Sessions: useAgentInbox(sessions, groups) / request items
  Sessions-->>AgentInbox: inbox items
  User->>AgentInbox: select item / quick reply / open-and-reply
  AgentInbox->>Sessions: onNavigateToSession(sessionId)
  AgentInbox->>AIEngine: processInput(text) (quick reply)
  AIEngine-->>Sessions: response / log entry
  AgentInbox->>ModalStore: setAgentInboxOpen(false) (optional close)
  ModalStore-->>App: agentInboxOpen = false
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 65.63% 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 'feat: Tab Description as Encore Feature' clearly and concisely describes the main change: adding tab descriptions as a new Encore feature flag.

✏️ 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 26, 2026

Greptile Summary

This PR adds inline-editable tab descriptions as a new Encore Feature, allowing users to add context notes to individual AI tabs. The implementation is clean and well-structured:

Key Implementation Details:

  • Feature gated behind encoreFeatures.tabDescription flag (disabled by default)
  • New description? field added to AITab interface
  • Inline editing appears in tab hover overlay with proper keyboard accessibility (Enter to save, Escape to cancel, Space/Enter to activate)
  • Description constrained to 3-line max with ellipsis overflow and 320px max width
  • Empty descriptions stored as undefined rather than empty strings for clean data
  • Settings toggle in Encore tab with helpful info text

Code Quality:

  • Follows existing codebase patterns (Encore Feature implementation, tab overlay menu pattern)
  • Proper cleanup when overlay closes via useEffect
  • Smart blur handling with skipNextBlurSaveRef to prevent double-saving
  • Handler properly wired through App → MainPanel → TabBar prop chain
  • Well-typed with TypeScript interfaces

The feature is production-ready with no bugs or logical issues found.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk - well-implemented feature addition with no bugs found
  • Score reflects clean implementation following established patterns, proper keyboard accessibility, smart edge case handling (blur/save logic), and appropriate feature gating. No logical errors, race conditions, or missing cleanup found. All prop threading is complete and correctly typed.
  • No files require special attention - all changes are straightforward and well-tested patterns

Important Files Changed

Filename Overview
src/renderer/types/index.ts Added tabDescription flag to EncoreFeatureFlags and description? field to AITab interface - clean type additions
src/renderer/hooks/tabs/useTabHandlers.ts Implemented handleUpdateTabDescription handler with proper trimming and undefined for empty values
src/renderer/components/SettingsModal.tsx Added Tab Description toggle card in Encore settings tab with proper styling and info text
src/renderer/components/TabBar.tsx Implemented inline-editable description UI with keyboard accessibility, 3-line clamp, and proper blur/save handling
src/renderer/App.tsx Wired handleUpdateTabDescription through prop chain with proper feature flag gating

Sequence Diagram

sequenceDiagram
    participant User
    participant TabBar
    participant Tab
    participant Handler
    participant Store

    User->>TabBar: Hover over AI tab
    TabBar->>Tab: Show overlay with description
    
    alt Description exists
        Tab->>User: Display description (3-line clamp)
    else No description
        Tab->>User: Show "Add description..." placeholder
    end
    
    User->>Tab: Click description button
    Tab->>Tab: Enter edit mode, focus textarea
    
    alt User presses Enter (no Shift)
        User->>Tab: Press Enter
        Tab->>Tab: Set skipNextBlurSaveRef = true
        Tab->>Handler: handleSaveDescription()
        Handler->>Store: Update tab.description (trimmed)
        Tab->>Tab: Exit edit mode
    else User presses Escape
        User->>Tab: Press Escape
        Tab->>Tab: Set skipNextBlurSaveRef = true
        Tab->>Tab: Restore original description
        Tab->>Tab: Exit edit mode
    else User clicks away (blur)
        User->>Tab: Blur textarea
        Tab->>Handler: handleSaveDescription()
        Handler->>Store: Update tab.description (trimmed)
        Tab->>Tab: Exit edit mode
    end
Loading

Last reviewed commit: e3e7a07

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

8 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 2436 to 2437
// Keep inbox processInput ref in sync with the latest processInput
inboxProcessInputRef.current = processInput;
Copy link

Choose a reason for hiding this comment

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

inboxProcessInputRef is not defined anywhere in the codebase - this will throw a ReferenceError at runtime

Suggested change
// Keep inbox processInput ref in sync with the latest processInput
inboxProcessInputRef.current = processInput;
// Remove this line - inboxProcessInputRef does not exist

className="w-3.5 h-3.5 mt-0.5 shrink-0"
style={{ color: theme.colors.textDim }}
/>
<span className={tab.description ? 'line-clamp-3 break-all' : 'italic'}>
Copy link

Choose a reason for hiding this comment

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

break-all can break mid-word awkwardly - use break-words instead for better readability

Suggested change
<span className={tab.description ? 'line-clamp-3 break-all' : 'italic'}>
<span className={tab.description ? 'line-clamp-3 break-words' : 'italic'}>

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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: 2

🤖 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 2436-2438: The code assigns to inboxProcessInputRef.current but
inboxProcessInputRef is not declared; add a declaration for inboxProcessInputRef
inside the same component (near other refs) using React.useRef / useRef with an
appropriate type that matches processInput (e.g., a MutableRefObject holding the
function or null) and initialize it (e.g., useRef<typeof processInput |
null>(null)) so the assignment inboxProcessInputRef.current = processInput is
valid at runtime.

In `@src/renderer/components/TabBar.tsx`:
- Around line 383-394: The Escape key handler in handleDescriptionKeyDown
currently sets isEditingDescription to false but does not prevent the input's
onBlur from calling handleSaveDescription, so cancel can still trigger a
blur-save; fix by adding a transient suppression flag (e.g.,
ignoreNextDescriptionBlurRef) that handleDescriptionKeyDown sets to true when
Escape (and optionally Enter when you explicitly saved) is pressed, and then
have the description input's onBlur check that flag before calling
handleSaveDescription (clear the flag after blur or via setTimeout to re-enable
normal behavior). Update handleDescriptionKeyDown, handleSaveDescription usage,
and the onBlur handler to consult ignoreNextDescriptionBlurRef to avoid
duplicate or unwanted saves.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 45a90c7 and ba08bcb.

📒 Files selected for processing (8)
  • src/renderer/App.tsx
  • src/renderer/components/MainPanel.tsx
  • src/renderer/components/SettingsModal.tsx
  • src/renderer/components/TabBar.tsx
  • src/renderer/hooks/props/useMainPanelProps.ts
  • src/renderer/hooks/tabs/useTabHandlers.ts
  • src/renderer/stores/settingsStore.ts
  • src/renderer/types/index.ts

@felipeggv felipeggv force-pushed the feat/tab-description branch from ba08bcb to d61340a Compare February 26, 2026 20:13
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: 12

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

377-395: ⚠️ Potential issue | 🟠 Major

Prevent blur-save from overriding Escape cancel (and double-saving on Enter).

At Line 391 you exit edit mode, but Line 743 still runs handleSaveDescription on blur. Escape can still persist edits, and Enter can cause a duplicate save via blur.

💡 Suggested fix
 	const [isEditingDescription, setIsEditingDescription] = useState(false);
 	const [descriptionDraft, setDescriptionDraft] = useState('');
 	const descriptionInputRef = useRef<HTMLTextAreaElement>(null);
+	const skipNextBlurSaveRef = useRef(false);
@@
 	const handleSaveDescription = useCallback(() => {
 		const trimmed = descriptionDraft.trim();
+		skipNextBlurSaveRef.current = true;
 		onUpdateDescription?.(tabId, trimmed);
 		setIsEditingDescription(false);
 	}, [onUpdateDescription, tabId, descriptionDraft]);
+
+	const handleDescriptionBlur = useCallback(() => {
+		if (skipNextBlurSaveRef.current) {
+			skipNextBlurSaveRef.current = false;
+			return;
+		}
+		handleSaveDescription();
+	}, [handleSaveDescription]);
@@
 			} else if (e.key === 'Escape') {
 				e.preventDefault();
+				skipNextBlurSaveRef.current = true;
+				setDescriptionDraft(tab.description || '');
 				setIsEditingDescription(false);
 			}
 		},
-		[handleSaveDescription]
+		[handleSaveDescription, tab.description]
 	);
@@
-											onBlur={handleSaveDescription}
+											onBlur={handleDescriptionBlur}

Also applies to: 743-744

🤖 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 377 - 395, The blur handler
is currently always calling handleSaveDescription which lets Escape or
Enter-triggered saves be overwritten or doubled; add a mutable ref (e.g.,
suppressBlurRef) and set suppressBlurRef.current = true inside
handleDescriptionKeyDown when handling Enter (after calling
handleSaveDescription) and when handling Escape (to prevent save), then in the
input/textarea onBlur handler check suppressBlurRef.current and skip calling
handleSaveDescription if true; finally clear suppressBlurRef (e.g., via
setTimeout(() => suppressBlurRef.current = false, 0) or after state updates) so
normal blurs continue to save. Ensure references to handleSaveDescription,
handleDescriptionKeyDown and setIsEditingDescription are used consistently.
🧹 Nitpick comments (5)
src/renderer/hooks/useAgentInbox.ts (1)

5-5: Align truncation length with the InboxItem.lastMessage contract.

This file truncates to 250 chars, while src/renderer/types/agent-inbox.ts documents lastMessage as truncated to 90. Consider making this consistent to avoid UI/data expectation drift.

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

In `@src/renderer/hooks/useAgentInbox.ts` at line 5, The truncation constant
MAX_MESSAGE_LENGTH in useAgentInbox.ts is set to 250 while the
InboxItem.lastMessage contract expects a 90-character truncation; update the
truncation to match the contract by changing MAX_MESSAGE_LENGTH to 90 (or derive
its value from the inbox type/contract if available) so the truncation logic
that produces InboxItem.lastMessage is consistent with the documented contract.
src/renderer/types/agent-inbox.ts (1)

35-42: Strengthen STATUS_COLORS typing to theme keys.

Typing this map as string loses compile-time validation. Prefer keyof ThemeColors so invalid color keys are caught immediately.

🔧 Suggested typing improvement
-import type { SessionState } from './index';
+import type { SessionState, ThemeColors } from './index';
@@
-export const STATUS_COLORS: Record<SessionState, string> = {
+export const STATUS_COLORS: Record<SessionState, keyof ThemeColors> = {
 	idle: 'success',
 	waiting_input: 'warning',
 	busy: 'info',
 	connecting: 'textMuted',
 	error: 'error',
 };
🤖 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 35 - 42, STATUS_COLORS is
currently typed as Record<SessionState, string> which loses compile-time
validation; change its type to Record<SessionState, keyof ThemeColors> (or keyof
Theme['colors'] if you prefer) so only valid theme color keys are allowed,
update the export to use this type for STATUS_COLORS, ensure ThemeColors (or
Theme) is imported/available in the module, and verify the existing values
(idle, waiting_input, busy, connecting, error ->
'success','warning','info','textMuted','error') exactly match keys on
ThemeColors, adjusting any mismatches.
src/renderer/stores/modalStore.ts (1)

141-146: Consider documenting the intentional loose typing.

The filterMode and sortMode are typed as string rather than their specific union types (InboxFilterMode/InboxSortMode). This appears intentional to avoid circular dependencies with types/agent-inbox.ts. A brief comment noting this design choice would help future maintainers understand why the types are looser here.

💡 Suggested documentation
 /** Agent Inbox modal data (persisted filter/sort/expand state) */
+// Note: filterMode/sortMode are string here to avoid circular import with types/agent-inbox.ts
+// Consumers should cast to InboxFilterMode/InboxSortMode at usage point
 export interface AgentInboxModalData {
 	filterMode?: string;
 	sortMode?: string;
 	isExpanded?: boolean;
 }
🤖 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 141 - 146, Add a short inline
comment above the AgentInboxModalData interface explaining that filterMode and
sortMode are intentionally typed as string (rather than
InboxFilterMode/InboxSortMode) to avoid a circular dependency with
types/agent-inbox.ts; reference the specific fields (filterMode, sortMode) and
the intended union types (InboxFilterMode/InboxSortMode) so future maintainers
understand the deliberate loose typing and rationale.
src/renderer/components/AgentInbox/index.tsx (1)

94-104: Consider memoizing the frozen item lookup for better performance.

The resolveFrozenItems function performs a nested lookup (liveItems.find()) for each frozen item, resulting in O(n*m) complexity. For typical inbox sizes this is fine, but if the inbox grows large, consider building a lookup Map for O(1) access.

💡 Performance optimization suggestion
 const resolveFrozenItems = useCallback(
 	(frozen: { sessionId: string; tabId: string }[]): InboxItem[] => {
+		// Build lookup map for O(1) access
+		const liveMap = new Map<string, InboxItem>();
+		for (const item of liveItems) {
+			liveMap.set(`${item.sessionId}:${item.tabId}`, item);
+		}
 		const resolved: InboxItem[] = [];
 		for (const key of frozen) {
-			const live = liveItems.find((i) => i.sessionId === key.sessionId && i.tabId === key.tabId);
+			const live = liveMap.get(`${key.sessionId}:${key.tabId}`);
 			if (live) resolved.push(live);
 		}
 		return resolved.length > 0 ? resolved : liveItems;
 	},
 	[liveItems]
 );
🤖 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 94 - 104,
resolveFrozenItems currently does an O(n*m) nested find over liveItems for each
frozen entry; create a memoized lookup Map (e.g., useMemo(() => new
Map(liveItems.map(i => [`${i.sessionId}:${i.tabId}`, i])), [liveItems])) keyed
by sessionId+tabId, then change resolveFrozenItems to O(m) by using that Map for
O(1) lookups of frozen entries; keep the existing fallback to return liveItems
when no resolved entries are found and ensure you reference resolveFrozenItems,
liveItems, and the frozen keys when making the change.
src/renderer/components/AgentInbox/FocusModeView.tsx (1)

680-685: Consider using requestAnimationFrame instead of setTimeout for focus.

The 200ms delay for focusing the reply input is a timing-based workaround. Using requestAnimationFrame would be more reliable as it fires after the browser has completed layout.

💡 Suggested improvement
 // Auto-focus reply input when entering focus mode or switching items
 useEffect(() => {
-	const timer = setTimeout(() => {
-		replyInputRef.current?.focus();
-	}, 200);
-	return () => clearTimeout(timer);
+	// Use double RAF to ensure layout is complete before focusing
+	const raf1 = requestAnimationFrame(() => {
+		const raf2 = requestAnimationFrame(() => {
+			replyInputRef.current?.focus();
+		});
+		// Store raf2 for cleanup
+		(raf1 as any).__inner = raf2;
+	});
+	return () => {
+		cancelAnimationFrame(raf1);
+		if ((raf1 as any).__inner) cancelAnimationFrame((raf1 as any).__inner);
+	};
 }, [item.sessionId, item.tabId]);
🤖 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 680 - 685,
Replace the setTimeout-based focus workaround in the useEffect with a
requestAnimationFrame approach: in the useEffect that depends on item.sessionId
and item.tabId, schedule replyInputRef.current?.focus() inside
requestAnimationFrame instead of setTimeout(200), store the returned raf id in a
variable, and ensure the cleanup function calls cancelAnimationFrame(rafId) to
avoid leaked callbacks; keep the same dependency array and behavior in the
FocusModeView component so focus still occurs after layout but without an
arbitrary timeout.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@playbooks/agent-inbox/2026-02-26-Encore-Inbox-Refactor/ENCORE-INBOX-07.md`:
- Around line 56-60: The gate text contradicts the earlier note about a
remaining lint error; update the Gate section to accurately reflect the expected
lint outcome by either removing "npm run lint passes" or clarifying that lint
passes except for the known pre-existing error (onOpenUnifiedInbox in
AppModalsProps). Reference the symbols AppModalsProps, onOpenUnifiedInbox and
setAgentInboxOpen (and the conditional render in SessionList) to ensure the gate
explicitly documents that lint should succeed except for the pre-existing
onOpenUnifiedInbox error or, alternatively, require fixing that error before
marking the gate as passed.

In `@playbooks/agent-inbox/2026-02-26-Encore-Inbox-Refactor/ENCORE-INBOX-14.md`:
- Around line 54-56: Update the contradictory test gate wording: replace the
bullet "- `npm run test` passes (all tests green)" with wording that reflects
the documented known failures (e.g., "- `npm run test` passes (except 3 known
failing tests)") so the gate aligns with the note that "3 known failing tests"
is recorded elsewhere in the doc; ensure any other test-gate bullets referencing
pass/green status (such as "`npm run lint`" / "`npm run lint:eslint`") remain
accurate or are similarly qualified if they have known exceptions.

In `@src/renderer/App.tsx`:
- Around line 6165-6172: The AgentInbox modal is missing props for several
action handlers so its UI cannot trigger inbox behaviors; pass the implemented
callbacks into the <AgentInbox> component: provide handleAgentInboxQuickReply,
handleAgentInboxOpenAndReply, handleAgentInboxMarkAsRead, and
handleAgentInboxToggleThinking as props (alongside existing theme, sessions,
groups, enterToSendAI, onClose, onNavigateToSession) so the modal uses those
handler functions when users quick-reply, open-and-reply, mark-as-read, or
toggle thinking.

In `@src/renderer/components/AgentInbox/FocusModeView.tsx`:
- Around line 467-533: The sidebar item div (key
`${itm.sessionId}-${itm.tabId}`) needs keyboard accessibility: add tabIndex={0}
and role="button", implement an onKeyDown handler that calls onNavigateItem(idx)
when Enter or Space is pressed, and add onFocus/onBlur handlers that mirror the
current onMouseEnter/onMouseLeave behavior (apply the same
backgroundColor/border styles when focused and remove them on blur); keep using
currentRowRef and isCurrent logic as-is so keyboard focus works the same as
click/hover.

In `@src/renderer/components/AgentInbox/InboxListView.tsx`:
- Around line 412-414: The label text for the high-context metric doesn't match
the implemented condition: the code computes highContext using items.filter((i)
=> i.contextUsage !== undefined && i.contextUsage >= 80).length but the UI label
reads "Context >80%"; update either the condition or the label so they
match—preferably change the label to "Context ≥80%" (or "Context >=80%")
wherever the label is rendered next to highContext, or if you prefer strict
greater-than change the filter predicate to i.contextUsage > 80 to match the
existing ">" wording.
- Around line 1150-1158: The inline style block in InboxListView.tsx is using
broad shorthand properties (border: 'none' and background: 'transparent') which
override the specific styles for borderBottom, borderLeft and backgroundColor
used to indicate selection (isRowSelected); remove or relocate these shorthand
properties so they do not override those specific rules—specifically, delete or
conditionally apply the 'border' and 'background' shorthands (or replace them
with non-conflicting values) in the style object that uses borderBottom,
borderLeft and backgroundColor for the row rendering logic tied to isRowSelected
and theme.colors so the selected/collapsed visuals can show correctly.
- Around line 1058-1063: The div with role="listbox" is not keyboard-focusable
and lacks key handlers; add tabIndex={0} to make it focusable and wire an
onKeyDown={handleListboxKeyDown} plus onFocus/onBlur handlers as needed, then
implement handleListboxKeyDown in the same component to handle
ArrowUp/ArrowDown/Home/End/Enter (moving the active item and updating the
aria-activedescendant via selectedItemId) and call existing selection/updating
logic so keyboard navigation works the same as mouse selection.

In `@src/renderer/components/SettingsModal.tsx`:
- Around line 3657-3665: The toggle buttons in SettingsModal are stateful but
don't expose their on/off state to assistive tech; update the button that calls
setEncoreFeatures and reads encoreFeatures.unifiedInbox to include an ARIA state
(e.g., add aria-pressed={encoreFeatures.unifiedInbox} or convert to
role="switch" aria-checked={encoreFeatures.unifiedInbox}) so screen readers can
announce the current state, and apply the same change to the other similar
toggle button in this component that flips another encoreFeatures property
(ensure you reference the same encoreFeatures property used in its onClick).

In `@src/renderer/components/TabBar.tsx`:
- Around line 734-773: The tab-description UI is currently reachable only via
hover; make it keyboard-accessible by adding tabIndex and focus/keyboard
handlers: add tabIndex={0} to the description button (the element that currently
renders when isEditingDescription is false) and to the surrounding description
container if needed, wire onFocus to call handleStartEditDescription (or open
the editor/overlay) and add onKeyDown to the same element to open the editor on
Enter/Space; ensure the textarea (descriptionInputRef) retains focus when
entering edit mode and keep existing handlers handleSaveDescription and
handleDescriptionKeyDown for keyboard save/escape behavior; also add appropriate
aria-label/role to the button for screen readers and use theme/tab.description
variables unchanged.

In `@src/renderer/hooks/tabs/useTabHandlers.ts`:
- Around line 1077-1086: The handler handleUpdateTabDescription stores
whitespace-only descriptions; before updating, trim the incoming description and
if the trimmed string is empty set description to undefined. Inside the
setSessions callback used from useSessionStore.getState(), normalize the value
passed into the aiTabs mapping (use a local const like normalized =
description?.trim(); then use normalized ? normalized : undefined) so aiTabs
entries get undefined for empty/whitespace-only descriptions instead of a
whitespace string.

In `@src/renderer/hooks/useAgentInbox.ts`:
- Around line 149-172: Grouping currently uses item.sessionName as the Map key
(agentGroups and the local variable key), which merges distinct sessions that
share a name; change the grouping key to be identity-safe by using
item.sessionId (or a composite like `${item.sessionId}:${item.sessionName}`)
when creating agentGroups and when pushing to groupMeta, while keeping
sessionName available for display and alphabetical fallbacks in the final sort;
update references to key in groupMeta.sort to compare a.sessionName /
b.sessionName (or parse the composite) so unread counts and per-session item
lists remain correct and isolated per session.
- Line 24: The current read-filter in useAgentInbox.ts (the return expression
that uses hasUnread and sessionState) incorrectly excludes items that are read
but in other session states; change the logic in the read branch so it only
depends on hasUnread (i.e., treat "read" as !hasUnread) and remove the
sessionState condition from the expression (update the return in the filter
function that currently references hasUnread and sessionState).

---

Duplicate comments:
In `@src/renderer/components/TabBar.tsx`:
- Around line 377-395: The blur handler is currently always calling
handleSaveDescription which lets Escape or Enter-triggered saves be overwritten
or doubled; add a mutable ref (e.g., suppressBlurRef) and set
suppressBlurRef.current = true inside handleDescriptionKeyDown when handling
Enter (after calling handleSaveDescription) and when handling Escape (to prevent
save), then in the input/textarea onBlur handler check suppressBlurRef.current
and skip calling handleSaveDescription if true; finally clear suppressBlurRef
(e.g., via setTimeout(() => suppressBlurRef.current = false, 0) or after state
updates) so normal blurs continue to save. Ensure references to
handleSaveDescription, handleDescriptionKeyDown and setIsEditingDescription are
used consistently.

---

Nitpick comments:
In `@src/renderer/components/AgentInbox/FocusModeView.tsx`:
- Around line 680-685: Replace the setTimeout-based focus workaround in the
useEffect with a requestAnimationFrame approach: in the useEffect that depends
on item.sessionId and item.tabId, schedule replyInputRef.current?.focus() inside
requestAnimationFrame instead of setTimeout(200), store the returned raf id in a
variable, and ensure the cleanup function calls cancelAnimationFrame(rafId) to
avoid leaked callbacks; keep the same dependency array and behavior in the
FocusModeView component so focus still occurs after layout but without an
arbitrary timeout.

In `@src/renderer/components/AgentInbox/index.tsx`:
- Around line 94-104: resolveFrozenItems currently does an O(n*m) nested find
over liveItems for each frozen entry; create a memoized lookup Map (e.g.,
useMemo(() => new Map(liveItems.map(i => [`${i.sessionId}:${i.tabId}`, i])),
[liveItems])) keyed by sessionId+tabId, then change resolveFrozenItems to O(m)
by using that Map for O(1) lookups of frozen entries; keep the existing fallback
to return liveItems when no resolved entries are found and ensure you reference
resolveFrozenItems, liveItems, and the frozen keys when making the change.

In `@src/renderer/hooks/useAgentInbox.ts`:
- Line 5: The truncation constant MAX_MESSAGE_LENGTH in useAgentInbox.ts is set
to 250 while the InboxItem.lastMessage contract expects a 90-character
truncation; update the truncation to match the contract by changing
MAX_MESSAGE_LENGTH to 90 (or derive its value from the inbox type/contract if
available) so the truncation logic that produces InboxItem.lastMessage is
consistent with the documented contract.

In `@src/renderer/stores/modalStore.ts`:
- Around line 141-146: Add a short inline comment above the AgentInboxModalData
interface explaining that filterMode and sortMode are intentionally typed as
string (rather than InboxFilterMode/InboxSortMode) to avoid a circular
dependency with types/agent-inbox.ts; reference the specific fields (filterMode,
sortMode) and the intended union types (InboxFilterMode/InboxSortMode) so future
maintainers understand the deliberate loose typing and rationale.

In `@src/renderer/types/agent-inbox.ts`:
- Around line 35-42: STATUS_COLORS is currently typed as Record<SessionState,
string> which loses compile-time validation; change its type to
Record<SessionState, keyof ThemeColors> (or keyof Theme['colors'] if you prefer)
so only valid theme color keys are allowed, update the export to use this type
for STATUS_COLORS, ensure ThemeColors (or Theme) is imported/available in the
module, and verify the existing values (idle, waiting_input, busy, connecting,
error -> 'success','warning','info','textMuted','error') exactly match keys on
ThemeColors, adjusting any mismatches.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ba08bcb and d61340a.

📒 Files selected for processing (30)
  • playbooks/agent-inbox/2026-02-26-Encore-Inbox-Refactor/ENCORE-INBOX-02.md
  • playbooks/agent-inbox/2026-02-26-Encore-Inbox-Refactor/ENCORE-INBOX-04.md
  • playbooks/agent-inbox/2026-02-26-Encore-Inbox-Refactor/ENCORE-INBOX-05.md
  • playbooks/agent-inbox/2026-02-26-Encore-Inbox-Refactor/ENCORE-INBOX-07.md
  • playbooks/agent-inbox/2026-02-26-Encore-Inbox-Refactor/ENCORE-INBOX-08.md
  • playbooks/agent-inbox/2026-02-26-Encore-Inbox-Refactor/ENCORE-INBOX-10.md
  • playbooks/agent-inbox/2026-02-26-Encore-Inbox-Refactor/ENCORE-INBOX-11.md
  • playbooks/agent-inbox/2026-02-26-Encore-Inbox-Refactor/ENCORE-INBOX-12.md
  • playbooks/agent-inbox/2026-02-26-Encore-Inbox-Refactor/ENCORE-INBOX-14.md
  • 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/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/keyboard/useMainKeyboardHandler.ts
  • src/renderer/hooks/props/useMainPanelProps.ts
  • src/renderer/hooks/props/useSessionListProps.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
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/renderer/types/index.ts
  • src/renderer/components/MainPanel.tsx

Comment on lines 467 to 533
return (
<div
key={`${itm.sessionId}-${itm.tabId}`}
ref={isCurrent ? currentRowRef : undefined}
onClick={() => onNavigateItem(idx)}
className="flex items-center gap-2 px-3 cursor-pointer transition-colors"
style={{
height: 48,
backgroundColor: isCurrent ? `${theme.colors.accent}15` : 'transparent',
borderLeft: isCurrent
? `2px solid ${theme.colors.accent}`
: '2px solid transparent',
}}
onMouseEnter={(e) => {
if (!isCurrent)
e.currentTarget.style.backgroundColor = `${theme.colors.accent}08`;
}}
onMouseLeave={(e) => {
if (!isCurrent) e.currentTarget.style.backgroundColor = 'transparent';
}}
>
{/* Status dot */}
<span
className="flex-shrink-0"
style={{
width: 6,
height: 6,
borderRadius: '50%',
backgroundColor: statusColor,
}}
/>
{/* Name + preview vertical stack */}
<div className="flex-1 flex flex-col gap-0.5 min-w-0">
<span
className="text-xs truncate"
style={{
color: isCurrent ? theme.colors.textMain : theme.colors.textDim,
fontWeight: isCurrent ? 600 : 400,
}}
>
{itm.tabName || 'Tab'}
</span>
{previewText && (
<span
className="text-[10px] truncate"
style={{ color: theme.colors.textDim, opacity: 0.5 }}
>
{previewText}
</span>
)}
</div>
{/* Indicators: unread */}
{itm.hasUnread && (
<span
className="flex-shrink-0"
style={{
width: 6,
height: 6,
borderRadius: '50%',
backgroundColor: theme.colors.accent,
alignSelf: 'flex-start',
marginTop: 2,
}}
/>
)}
</div>
);
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 keyboard accessibility to sidebar items.

The sidebar item rows use onClick for selection but lack tabIndex and keyboard event handlers. Users relying on keyboard navigation cannot access these items. As per coding guidelines, components needing keyboard focus should include tabIndex and focus event handlers.

♿ Suggested accessibility fix
 return (
 	<div
 		key={`${itm.sessionId}-${itm.tabId}`}
 		ref={isCurrent ? currentRowRef : undefined}
 		onClick={() => onNavigateItem(idx)}
+		onKeyDown={(e) => {
+			if (e.key === 'Enter' || e.key === ' ') {
+				e.preventDefault();
+				onNavigateItem(idx);
+			}
+		}}
+		tabIndex={0}
+		role="option"
+		aria-selected={isCurrent}
 		className="flex items-center gap-2 px-3 cursor-pointer transition-colors"
 		style={{
📝 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
return (
<div
key={`${itm.sessionId}-${itm.tabId}`}
ref={isCurrent ? currentRowRef : undefined}
onClick={() => onNavigateItem(idx)}
className="flex items-center gap-2 px-3 cursor-pointer transition-colors"
style={{
height: 48,
backgroundColor: isCurrent ? `${theme.colors.accent}15` : 'transparent',
borderLeft: isCurrent
? `2px solid ${theme.colors.accent}`
: '2px solid transparent',
}}
onMouseEnter={(e) => {
if (!isCurrent)
e.currentTarget.style.backgroundColor = `${theme.colors.accent}08`;
}}
onMouseLeave={(e) => {
if (!isCurrent) e.currentTarget.style.backgroundColor = 'transparent';
}}
>
{/* Status dot */}
<span
className="flex-shrink-0"
style={{
width: 6,
height: 6,
borderRadius: '50%',
backgroundColor: statusColor,
}}
/>
{/* Name + preview vertical stack */}
<div className="flex-1 flex flex-col gap-0.5 min-w-0">
<span
className="text-xs truncate"
style={{
color: isCurrent ? theme.colors.textMain : theme.colors.textDim,
fontWeight: isCurrent ? 600 : 400,
}}
>
{itm.tabName || 'Tab'}
</span>
{previewText && (
<span
className="text-[10px] truncate"
style={{ color: theme.colors.textDim, opacity: 0.5 }}
>
{previewText}
</span>
)}
</div>
{/* Indicators: unread */}
{itm.hasUnread && (
<span
className="flex-shrink-0"
style={{
width: 6,
height: 6,
borderRadius: '50%',
backgroundColor: theme.colors.accent,
alignSelf: 'flex-start',
marginTop: 2,
}}
/>
)}
</div>
);
return (
<div
key={`${itm.sessionId}-${itm.tabId}`}
ref={isCurrent ? currentRowRef : undefined}
onClick={() => onNavigateItem(idx)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onNavigateItem(idx);
}
}}
tabIndex={0}
role="option"
aria-selected={isCurrent}
className="flex items-center gap-2 px-3 cursor-pointer transition-colors"
style={{
height: 48,
backgroundColor: isCurrent ? `${theme.colors.accent}15` : 'transparent',
borderLeft: isCurrent
? `2px solid ${theme.colors.accent}`
: '2px solid transparent',
}}
onMouseEnter={(e) => {
if (!isCurrent)
e.currentTarget.style.backgroundColor = `${theme.colors.accent}08`;
}}
onMouseLeave={(e) => {
if (!isCurrent) e.currentTarget.style.backgroundColor = 'transparent';
}}
>
{/* Status dot */}
<span
className="flex-shrink-0"
style={{
width: 6,
height: 6,
borderRadius: '50%',
backgroundColor: statusColor,
}}
/>
{/* Name + preview vertical stack */}
<div className="flex-1 flex flex-col gap-0.5 min-w-0">
<span
className="text-xs truncate"
style={{
color: isCurrent ? theme.colors.textMain : theme.colors.textDim,
fontWeight: isCurrent ? 600 : 400,
}}
>
{itm.tabName || 'Tab'}
</span>
{previewText && (
<span
className="text-[10px] truncate"
style={{ color: theme.colors.textDim, opacity: 0.5 }}
>
{previewText}
</span>
)}
</div>
{/* Indicators: unread */}
{itm.hasUnread && (
<span
className="flex-shrink-0"
style={{
width: 6,
height: 6,
borderRadius: '50%',
backgroundColor: theme.colors.accent,
alignSelf: 'flex-start',
marginTop: 2,
}}
/>
)}
</div>
);
🤖 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 467 - 533,
The sidebar item div (key `${itm.sessionId}-${itm.tabId}`) needs keyboard
accessibility: add tabIndex={0} and role="button", implement an onKeyDown
handler that calls onNavigateItem(idx) when Enter or Space is pressed, and add
onFocus/onBlur handlers that mirror the current onMouseEnter/onMouseLeave
behavior (apply the same backgroundColor/border styles when focused and remove
them on blur); keep using currentRowRef and isCurrent logic as-is so keyboard
focus works the same as click/hover.

Comment on lines 3657 to 3665
<button
className="w-full flex items-center justify-between p-4 text-left"
onClick={() =>
setEncoreFeatures({
...encoreFeatures,
unifiedInbox: !encoreFeatures.unifiedInbox,
})
}
>
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

Expose toggle state to assistive tech.

These are stateful toggle controls, but they don’t expose on/off state via ARIA. Add aria-pressed (or role="switch" + aria-checked) so screen readers can announce current state.

♿ Suggested update
-								<button
+								<button
 									className="w-full flex items-center justify-between p-4 text-left"
+									aria-pressed={encoreFeatures.unifiedInbox}
 									onClick={() =>
 										setEncoreFeatures({
 											...encoreFeatures,
 											unifiedInbox: !encoreFeatures.unifiedInbox,
 										})
 									}
 								>
@@
-								<button
+								<button
 									className="w-full flex items-center justify-between p-4 text-left"
+									aria-pressed={encoreFeatures.tabDescription}
 									onClick={() =>
 										setEncoreFeatures({
 											...encoreFeatures,
 											tabDescription: !encoreFeatures.tabDescription,
 										})
 									}
 								>

Also applies to: 3740-3747

🤖 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 3657 - 3665, The
toggle buttons in SettingsModal are stateful but don't expose their on/off state
to assistive tech; update the button that calls setEncoreFeatures and reads
encoreFeatures.unifiedInbox to include an ARIA state (e.g., add
aria-pressed={encoreFeatures.unifiedInbox} or convert to role="switch"
aria-checked={encoreFeatures.unifiedInbox}) so screen readers can announce the
current state, and apply the same change to the other similar toggle button in
this component that flips another encoreFeatures property (ensure you reference
the same encoreFeatures property used in its onClick).

Felipe Gobbi and others added 3 commits February 26, 2026 18:22
…gation for Unified Inbox

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…11y in Unified Inbox

- FocusModeView: Add resizeCleanupRef + useEffect unmount guard so
  mousemove/mouseup listeners are removed if component unmounts mid-drag
- useAgentInbox: Group byAgent items by sessionId (unique) instead of
  sessionName (display-only) to prevent merging different agents with
  the same name
- InboxListView: Add tabIndex={0} and onKeyDown to the listbox div so
  it is keyboard-focusable and navigable

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

- Replace "session" with "agent" in all user-facing labels (empty states, ARIA labels, menu text)
- Align MAX_MESSAGE_LENGTH (250→90) with documented InboxItem.lastMessage truncation
- Fix read filter to show all non-unread items regardless of session state
- Add role="switch" and aria-checked to Encore feature toggle buttons
- Guard agentInbox keyboard shortcut on setAgentInboxOpen callback availability
- Add keyboard a11y (tabIndex, role="option", aria-selected, onKeyDown) to FocusModeView sidebar
- Add onFocus/onBlur handlers to dialog container per focus guidelines
- Fix context threshold label (>80% → ≥80%) to match >= 80 condition
- Remove background/border shorthand overrides from group header styles
- Fix playbook doc contradictions (ENCORE-INBOX-07, -11, -14)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@pedramamini pedramamini self-assigned this Feb 26, 2026
Felipe Gobbi and others added 2 commits February 26, 2026 18:39
…coreFeatureFlags

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

Adds the Encore Feature toggle card in Settings > Encore Features for Tab
Descriptions, following the Unified Inbox pattern. Also adds the handler
in useTabHandlers.ts for updating tab descriptions (empty string clears).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Felipe Gobbi and others added 4 commits February 26, 2026 18:39
…line-editable overlay UI

Thread handleUpdateTabDescription from App.tsx through useMainPanelProps, MainPanel,
and TabBar with Encore feature gating. Add inline-editable description section in
the tab hover overlay between header and actions, with textarea editing, Enter to save,
Shift+Enter for newlines, Escape to cancel, and auto-reset on overlay close.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add maxWidth 320px and break-words/break-all rules to tab hover overlay
to prevent layout overflow with long tab names, session IDs, and descriptions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Added tabIndex={0}, aria-label, and onKeyDown handler for Enter/Space
on the description edit button in the tab hover overlay. Widened
handleStartEditDescription to accept KeyboardEvent in addition to
MouseEvent.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@felipeggv felipeggv force-pushed the feat/tab-description branch from d61340a to e3e7a07 Compare February 26, 2026 21:51
Move onSelectedLevelsChange call from inside setSelectedLevelsState
updater to a dedicated useEffect, preventing Zustand store set()
from firing during React state update.

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: 2

♻️ Duplicate comments (4)
src/renderer/components/SettingsModal.tsx (1)

3744-3751: ⚠️ Potential issue | 🟡 Minor

Expose Tab Description toggle state to assistive tech.

This toggle is stateful but does not expose on/off state, so screen readers won’t announce it properly.

Suggested fix
 								<button
+									role="switch"
+									aria-checked={encoreFeatures.tabDescription}
 									className="w-full flex items-center justify-between p-4 text-left"
 									onClick={() =>
 										setEncoreFeatures({
 											...encoreFeatures,
 											tabDescription: !encoreFeatures.tabDescription,
 										})
 									}
 								>
🤖 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 3744 - 3751, The
toggle button for tabDescription in SettingsModal lacks accessible state; update
the element that currently calls setEncoreFeatures (the button wrapping the
tabDescription toggle) to expose its on/off state to assistive tech by adding an
appropriate ARIA attribute—e.g., aria-pressed={encoreFeatures.tabDescription} if
keeping role=button, or role="switch" with
aria-checked={encoreFeatures.tabDescription}—so screen readers will announce the
current state when users interact with setEncoreFeatures and
encoreFeatures.tabDescription.
src/renderer/components/TabBar.tsx (1)

585-603: ⚠️ Potential issue | 🟠 Major

Description editing is still not keyboard-reachable from the tab itself.

Line 597 and Line 598 only open the overlay on hover, so keyboard users still can’t reach the Line 768 description edit control unless the overlay is already mouse-opened.

♿ Suggested fix (focus-open path for the overlay entry point)
 		<div
 			ref={setTabRef}
 			data-tab-id={tab.id}
@@
 			onMouseEnter={handleMouseEnter}
 			onMouseLeave={handleMouseLeave}
+			tabIndex={0}
+			onFocus={handleMouseEnter}
+			onBlur={handleMouseLeave}
 			draggable
 			onDragStart={handleTabDragStart}

As per coding guidelines: src/renderer/components/**/*.{ts,tsx}: Add tabIndex attribute and focus event handlers when implementing components that need keyboard focus.

Also applies to: 677-703, 746-793

🤖 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 585 - 603, The tab element
currently only exposes the overlay on hover, making the description edit
unreachable via keyboard; add keyboard focus handling by giving the tab
container a tabIndex (e.g., tabIndex={0}) and wire focus/blur handlers (e.g.,
onFocus={handleMouseEnter} and onBlur={handleMouseLeave} or dedicated
handleFocus/handleBlur) and a key handler (onKeyDown) to open the overlay with
Enter/Space; update the element that uses setTabRef and handlers (the div with
ref={setTabRef}, onMouseEnter/onMouseLeave, onClick, etc.) so keyboard users can
open the same overlay that handleMouseEnter currently opens and reach the
description edit control.
src/renderer/components/AgentInbox/InboxListView.tsx (1)

1058-1065: ⚠️ Potential issue | 🟠 Major

Focusable listbox has no visible focus state.

Line 1064 removes the outline, but no replacement focus style is applied on the listbox itself, making keyboard focus location unclear.

♿ Suggested fix (restore explicit focus styling)
 			<div
 				role="listbox"
 				tabIndex={0}
 				onKeyDown={handleKeyDown}
+				onFocus={(e) => {
+					e.currentTarget.style.outline = `2px solid ${theme.colors.accent}`;
+					e.currentTarget.style.outlineOffset = '-2px';
+				}}
+				onBlur={(e) => {
+					e.currentTarget.style.outline = 'none';
+				}}
 				aria-activedescendant={selectedItemId}
 				aria-label="Inbox items"
 				className="outline-none"

As per coding guidelines: src/renderer/components/**/*.{ts,tsx}: Add tabIndex attribute and focus event handlers when implementing components that need keyboard 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 1058 -
1065, The listbox div currently removes the default outline via
className="outline-none" leaving no visible focus indicator; restore a visible
focus style and add focus handlers: remove or replace "outline-none" on the
element in InboxListView (the div with role="listbox", tabIndex={0},
onKeyDown={handleKeyDown}, aria-activedescendant={selectedItemId}) with a
focus-visible style (e.g., a ring/outline class or inline focus style such as
focus:ring-2/focus:ring-offset and a contrasting color) and wire up onFocus and
onBlur handlers (e.g., handleListboxFocus / handleListboxBlur or small inline
handlers) to manage focus state if needed per accessibility guidelines so
keyboard users can see where focus is.
src/renderer/components/AgentInbox/FocusModeView.tsx (1)

482-509: ⚠️ Potential issue | 🟡 Minor

Focusable sidebar items still miss onFocus/onBlur parity with hover styles.

These rows are keyboard-focusable and keyboard-activatable, but keyboard focus still lacks equivalent visual treatment.

♿ Suggested fix
 								onMouseEnter={(e) => {
 									if (!isCurrent)
 										e.currentTarget.style.backgroundColor = `${theme.colors.accent}08`;
 								}}
 								onMouseLeave={(e) => {
 									if (!isCurrent) e.currentTarget.style.backgroundColor = 'transparent';
 								}}
+								onFocus={(e) => {
+									if (!isCurrent)
+										e.currentTarget.style.backgroundColor = `${theme.colors.accent}08`;
+								}}
+								onBlur={(e) => {
+									if (!isCurrent) e.currentTarget.style.backgroundColor = 'transparent';
+								}}
 							>

As per coding guidelines src/renderer/components/**/*.{ts,tsx}: Add tabIndex attribute and focus event handlers when implementing components that need keyboard focus.

🤖 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 482 - 509,
The focusable row div handling click/keyboard lacks onFocus/onBlur to mirror the
hover visuals; add onFocus and onBlur handlers on the same element (the element
keyed by `${itm.sessionId}-${itm.tabId}` and using currentRowRef/onNavigateItem)
so that when focused (and not isCurrent) you set the same backgroundColor as
onMouseEnter (`${theme.colors.accent}08`) and on blur revert to 'transparent'
(and ensure the isCurrent styling still wins), or preferably reuse the same
logic used in onMouseEnter/onMouseLeave to keep hover and keyboard focus visuals
consistent while respecting the selected (isCurrent) state.
🧹 Nitpick comments (1)
src/renderer/hooks/useAgentInbox.ts (1)

69-72: Prefer AITab['logs'] typing for summary/timestamp helpers.

These helpers operate on tab logs, so typing them as Session['aiLogs'] is looser than needed and can drift over time.

Suggested refactor
-import type { Session, Group } from '../types';
+import type { Session, Group, AITab } from '../types';
@@
 export function generateSmartSummary(
-	logs: Session['aiLogs'] | undefined,
+	logs: AITab['logs'] | undefined,
 	sessionState: Session['state']
 ): string {
@@
-function deriveTimestamp(logs: Session['aiLogs'] | undefined, tabCreatedAt: number): number {
+function deriveTimestamp(logs: AITab['logs'] | undefined, tabCreatedAt: number): number {

Also applies to: 113-113

🤖 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 69 - 72, The helper
functions (e.g., generateSmartSummary) are typed too loosely as
Session['aiLogs']; change their parameter types to AITab['logs'] (and any other
helper on the same file noted in the review around the second occurrence) so
they operate on tab log shapes directly; update the function signatures
(generateSmartSummary and the helper at the other noted location) to accept
AITab['logs'] and adjust any internal references if necessary to the more
specific shape.
🤖 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 1543-1576: The current handleAgentInboxQuickReply uses hardcoded
setTimeouts (150ms/100ms) to switch sessions which races under variable renders;
remove those time-based switches and instead route the message deterministically
by either (A) making inboxProcessInputRef accept an explicit target
(sessionId/tabId) and calling inboxProcessInputRef.current(text, { sessionId,
tabId }) after updating sessions via setSessions, or (B) implement a
pendingSendRef + useEffect that watches activeSessionId and, once the state
update from setActiveSessionId completes, invokes
inboxProcessInputRef.current(text) and then restores previousActiveSessionId—do
not rely on setTimeouts; update handleAgentInboxQuickReply, inboxProcessInputRef
usage, and any related state restoration logic accordingly.

In `@src/renderer/components/AgentInbox/index.tsx`:
- Around line 53-68: The current useEffect cleanup cancels the
requestAnimationFrame stored in rafIdRef which prevents the focus-restoration
scheduled in handleClose from running; change the cleanup to stop calling
cancelAnimationFrame (or only null the ref) so the RAF scheduled by handleClose
can execute and focus triggerRef, i.e., update the useEffect cleanup that
references rafIdRef to not call cancelAnimationFrame(rafIdRef.current) (or add a
guard so it only cancels unrelated RAFs) while leaving handleClose’s
requestAnimationFrame logic intact so onClose -> requestAnimationFrame -> focus
restoration runs as intended.

---

Duplicate comments:
In `@src/renderer/components/AgentInbox/FocusModeView.tsx`:
- Around line 482-509: The focusable row div handling click/keyboard lacks
onFocus/onBlur to mirror the hover visuals; add onFocus and onBlur handlers on
the same element (the element keyed by `${itm.sessionId}-${itm.tabId}` and using
currentRowRef/onNavigateItem) so that when focused (and not isCurrent) you set
the same backgroundColor as onMouseEnter (`${theme.colors.accent}08`) and on
blur revert to 'transparent' (and ensure the isCurrent styling still wins), or
preferably reuse the same logic used in onMouseEnter/onMouseLeave to keep hover
and keyboard focus visuals consistent while respecting the selected (isCurrent)
state.

In `@src/renderer/components/AgentInbox/InboxListView.tsx`:
- Around line 1058-1065: The listbox div currently removes the default outline
via className="outline-none" leaving no visible focus indicator; restore a
visible focus style and add focus handlers: remove or replace "outline-none" on
the element in InboxListView (the div with role="listbox", tabIndex={0},
onKeyDown={handleKeyDown}, aria-activedescendant={selectedItemId}) with a
focus-visible style (e.g., a ring/outline class or inline focus style such as
focus:ring-2/focus:ring-offset and a contrasting color) and wire up onFocus and
onBlur handlers (e.g., handleListboxFocus / handleListboxBlur or small inline
handlers) to manage focus state if needed per accessibility guidelines so
keyboard users can see where focus is.

In `@src/renderer/components/SettingsModal.tsx`:
- Around line 3744-3751: The toggle button for tabDescription in SettingsModal
lacks accessible state; update the element that currently calls
setEncoreFeatures (the button wrapping the tabDescription toggle) to expose its
on/off state to assistive tech by adding an appropriate ARIA attribute—e.g.,
aria-pressed={encoreFeatures.tabDescription} if keeping role=button, or
role="switch" with aria-checked={encoreFeatures.tabDescription}—so screen
readers will announce the current state when users interact with
setEncoreFeatures and encoreFeatures.tabDescription.

In `@src/renderer/components/TabBar.tsx`:
- Around line 585-603: The tab element currently only exposes the overlay on
hover, making the description edit unreachable via keyboard; add keyboard focus
handling by giving the tab container a tabIndex (e.g., tabIndex={0}) and wire
focus/blur handlers (e.g., onFocus={handleMouseEnter} and
onBlur={handleMouseLeave} or dedicated handleFocus/handleBlur) and a key handler
(onKeyDown) to open the overlay with Enter/Space; update the element that uses
setTabRef and handlers (the div with ref={setTabRef}, onMouseEnter/onMouseLeave,
onClick, etc.) so keyboard users can open the same overlay that handleMouseEnter
currently opens and reach the description edit control.

---

Nitpick comments:
In `@src/renderer/hooks/useAgentInbox.ts`:
- Around line 69-72: The helper functions (e.g., generateSmartSummary) are typed
too loosely as Session['aiLogs']; change their parameter types to AITab['logs']
(and any other helper on the same file noted in the review around the second
occurrence) so they operate on tab log shapes directly; update the function
signatures (generateSmartSummary and the helper at the other noted location) to
accept AITab['logs'] and adjust any internal references if necessary to the more
specific shape.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d61340a and e3e7a07.

📒 Files selected for processing (17)
  • playbooks/agent-inbox/2026-02-26-Encore-Inbox-Refactor/ENCORE-INBOX-07.md
  • playbooks/agent-inbox/2026-02-26-Encore-Inbox-Refactor/ENCORE-INBOX-11.md
  • playbooks/agent-inbox/2026-02-26-Encore-Inbox-Refactor/ENCORE-INBOX-14.md
  • 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/MainPanel.tsx
  • src/renderer/components/SessionList.tsx
  • src/renderer/components/SettingsModal.tsx
  • src/renderer/components/TabBar.tsx
  • 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/settingsStore.ts
  • src/renderer/types/index.ts
✅ Files skipped from review due to trivial changes (1)
  • playbooks/agent-inbox/2026-02-26-Encore-Inbox-Refactor/ENCORE-INBOX-14.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/renderer/components/MainPanel.tsx
  • src/renderer/hooks/tabs/useTabHandlers.ts
  • src/renderer/stores/settingsStore.ts

Comment on lines +53 to +68
useEffect(() => {
return () => {
if (rafIdRef.current !== null) {
cancelAnimationFrame(rafIdRef.current);
}
};
}, []);

const handleClose = useCallback(() => {
onClose();
rafIdRef.current = requestAnimationFrame(() => {
rafIdRef.current = null;
if (triggerRef.current && triggerRef.current instanceof HTMLElement) {
triggerRef.current.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

Unmount cleanup currently cancels the close-time focus restoration.

At Line 53, the unmount cleanup cancels the same requestAnimationFrame scheduled in handleClose, so focus restoration often never runs after modal close.

💡 Suggested fix
-	const triggerRef = useRef<Element | null>(document.activeElement);
-	const rafIdRef = useRef<number | null>(null);
-	useEffect(() => {
-		return () => {
-			if (rafIdRef.current !== null) {
-				cancelAnimationFrame(rafIdRef.current);
-			}
-		};
-	}, []);
+	const triggerRef = useRef<HTMLElement | null>(
+		document.activeElement instanceof HTMLElement ? document.activeElement : null
+	);

 	const handleClose = useCallback(() => {
-		onClose();
-		rafIdRef.current = requestAnimationFrame(() => {
-			rafIdRef.current = null;
-			if (triggerRef.current && triggerRef.current instanceof HTMLElement) {
-				triggerRef.current.focus();
-			}
-		});
+		const trigger = triggerRef.current;
+		onClose();
+		requestAnimationFrame(() => {
+			trigger?.focus();
+		});
 	}, [onClose]);
🤖 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 53 - 68, The
current useEffect cleanup cancels the requestAnimationFrame stored in rafIdRef
which prevents the focus-restoration scheduled in handleClose from running;
change the cleanup to stop calling cancelAnimationFrame (or only null the ref)
so the RAF scheduled by handleClose can execute and focus triggerRef, i.e.,
update the useEffect cleanup that references rafIdRef to not call
cancelAnimationFrame(rafIdRef.current) (or add a guard so it only cancels
unrelated RAFs) while leaving handleClose’s requestAnimationFrame logic intact
so onClose -> requestAnimationFrame -> focus restoration runs as intended.

@felipeggv
Copy link
Author

All review feedback addressed: Escape/blur save race condition (skipNextBlurSaveRef), break-all to break-words, whitespace trim for descriptions, keyboard accessibility for edit action. Inherits all Inbox fixes from #468 via rebase. Requesting re-review. @coderabbitai review @greptile review

@coderabbitai
Copy link

coderabbitai bot commented Feb 26, 2026

@felipeggv, thank you for addressing all the review feedback! I'll re-review the PR now, focusing on:

  • The Escape/blur race condition fix with skipNextBlurSaveRef
  • The break-words adjustment for text overflow
  • Whitespace trimming on description updates
  • Keyboard accessibility for the edit action
  • Integration with the inherited Inbox fixes from #468

Starting the review now.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

…indicators

- Replace non-deterministic setTimeout with requestAnimationFrame in
  handleAgentInboxQuickReply; use activeSessionIdRef to avoid stale closures
- Add role="switch" + aria-checked to tabDescription Encore toggle for
  consistency with unifiedInbox toggle
- Add onFocus/onBlur handlers to FocusModeView sidebar items mirroring
  onMouseEnter/onMouseLeave hover background
- Add visible focus outline to InboxListView listbox via onFocus/onBlur
  inline handlers (keeps outline-none class to prevent double-outline)

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 (3)
src/renderer/components/AgentInbox/FocusModeView.tsx (1)

971-989: Consider adding keyboard accessibility to the resize handle.

The sidebar resize handle only supports mouse drag. For full keyboard accessibility, consider allowing users to resize with keyboard (e.g., focus the handle and use arrow keys). This is a low-priority enhancement since resize handles are commonly mouse-only.

🤖 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 971 - 989,
The resize handle only supports mouse dragging; add keyboard accessibility by
making the div focusable (tabIndex=0), giving it an appropriate role/aria-label
(e.g., role="separator" aria-orientation="vertical"
aria-valuenow/aria-valuemin/aria-valuemax tied to the sidebar width), and adding
an onKeyDown handler that adjusts the sidebar width state (the same state
mutated by handleResizeStart/drag logic — e.g., sidebarWidth/setSidebarWidth) in
response to ArrowLeft/ArrowRight (and Home/End/PageUp/PageDown if desired); also
add visible focus styles and ensure isResizingRef logic continues to prevent
hover-style resets while keyboard resizing.
src/renderer/components/SettingsModal.tsx (2)

3646-3729: Unified Inbox Encore card: solid a11y + clear gating UI.
role="switch" + aria-checked on the header button is consistent and should read well in assistive tech (Line 3659-3662).

Two optional nits:

  1. Add type="button" for robustness.
  2. Consider clarifying the shortcut hint to include Linux if applicable (currently “Alt+I (Windows)”).
Proposed diff (optional)
 								<button
 									role="switch"
 									aria-checked={encoreFeatures.unifiedInbox}
+									type="button"
 									className="w-full flex items-center justify-between p-4 text-left"
 									onClick={() =>
 										setEncoreFeatures({
 											...encoreFeatures,
 											unifiedInbox: !encoreFeatures.unifiedInbox,
 										})
 									}
 								>
@@
 								{encoreFeatures.unifiedInbox && (
 									<div className="px-4 pb-4">
 										<p className="text-xs" style={{ color: theme.colors.textDim }}>
-											Access via Option+I (Mac) or Alt+I (Windows). Aggregates status updates,
+											Access via Option+I (Mac) or Alt+I (Windows/Linux). Aggregates status updates,
 											errors, and action items from all running agents into a single
 											keyboard-navigable view.
 										</p>
 									</div>
 								)}
🤖 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 3646 - 3729, The
header toggle for the Unified Inbox (the button that reads and sets
encoreFeatures.unifiedInbox) should include an explicit type attribute and a
Linux shortcut mention: add type="button" to the button element that currently
has role="switch" and aria-checked={encoreFeatures.unifiedInbox}, and update the
shortcut text shown when encoreFeatures.unifiedInbox is true (the paragraph text
under the Unified Inbox Info) to mention Linux (e.g., "Option+I (Mac), Alt+I
(Windows) or Alt+I (Linux)").

3731-3801: Tab Descriptions Encore card: consistent switch semantics + clear user value.
Same optional hardening here: add type="button" on the header button.

Proposed diff (optional)
 								<button
 									role="switch"
 									aria-checked={encoreFeatures.tabDescription}
+									type="button"
 									className="w-full flex items-center justify-between p-4 text-left"
 									onClick={() =>
 										setEncoreFeatures({
 											...encoreFeatures,
 											tabDescription: !encoreFeatures.tabDescription,
 										})
 									}
 								>
🤖 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 3731 - 3801, The
header toggle button for the Tab Descriptions feature lacks an explicit type,
which can cause unintended form submits; update the button in SettingsModal (the
button using aria-checked={encoreFeatures.tabDescription} and onClick that calls
setEncoreFeatures({... , tabDescription: !encoreFeatures.tabDescription})) to
include type="button" so it behaves as a non-submitting toggle while preserving
the existing aria and style behavior.
🤖 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 1551-1560: The handlers handleAgentInboxQuickReply and
handleAgentInboxOpenAndReply activate a session/tab via setSessions but do not
ensure the tab is in AI input mode or clear file focus, so replies can be routed
through terminal/file execution; update the setSessions update for the matching
session/tab (same block that sets activeTabId and clears hasUnread) to also set
inputMode: 'ai' and activeFileTabId: null on the returned session object (use
the sessionId, tabId, aiTabs, inputMode and activeFileTabId symbols to locate
the logic), ensuring both handlers perform the same mutation so quick-reply
flows always force AI mode and clear file tab focus.

---

Nitpick comments:
In `@src/renderer/components/AgentInbox/FocusModeView.tsx`:
- Around line 971-989: The resize handle only supports mouse dragging; add
keyboard accessibility by making the div focusable (tabIndex=0), giving it an
appropriate role/aria-label (e.g., role="separator" aria-orientation="vertical"
aria-valuenow/aria-valuemin/aria-valuemax tied to the sidebar width), and adding
an onKeyDown handler that adjusts the sidebar width state (the same state
mutated by handleResizeStart/drag logic — e.g., sidebarWidth/setSidebarWidth) in
response to ArrowLeft/ArrowRight (and Home/End/PageUp/PageDown if desired); also
add visible focus styles and ensure isResizingRef logic continues to prevent
hover-style resets while keyboard resizing.

In `@src/renderer/components/SettingsModal.tsx`:
- Around line 3646-3729: The header toggle for the Unified Inbox (the button
that reads and sets encoreFeatures.unifiedInbox) should include an explicit type
attribute and a Linux shortcut mention: add type="button" to the button element
that currently has role="switch" and aria-checked={encoreFeatures.unifiedInbox},
and update the shortcut text shown when encoreFeatures.unifiedInbox is true (the
paragraph text under the Unified Inbox Info) to mention Linux (e.g., "Option+I
(Mac), Alt+I (Windows) or Alt+I (Linux)").
- Around line 3731-3801: The header toggle button for the Tab Descriptions
feature lacks an explicit type, which can cause unintended form submits; update
the button in SettingsModal (the button using
aria-checked={encoreFeatures.tabDescription} and onClick that calls
setEncoreFeatures({... , tabDescription: !encoreFeatures.tabDescription})) to
include type="button" so it behaves as a non-submitting toggle while preserving
the existing aria and style behavior.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e3e7a07 and 57e1618.

📒 Files selected for processing (5)
  • src/renderer/App.tsx
  • src/renderer/components/AgentInbox/FocusModeView.tsx
  • src/renderer/components/AgentInbox/InboxListView.tsx
  • src/renderer/components/LogViewer.tsx
  • src/renderer/components/SettingsModal.tsx

Comment on lines +1551 to +1560
// Activate the target tab and mark as read (processInput adds the user log entry)
setSessions((prev) =>
prev.map((s) => {
if (s.id !== sessionId) return s;
return {
...s,
activeTabId: tabId,
aiTabs: s.aiTabs.map((t) => (t.id === tabId ? { ...t, hasUnread: false } : t)),
};
})
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Force AI mode in inbox reply flows before writing/sending text.

handleAgentInboxQuickReply and handleAgentInboxOpenAndReply switch session/tab, but they do not force inputMode: 'ai' and activeFileTabId: null. If the target session is in terminal mode, quick-reply can route text through the wrong execution path.

🐛 Proposed fix
@@
 			setSessions((prev) =>
 				prev.map((s) => {
 					if (s.id !== sessionId) return s;
 					return {
 						...s,
 						activeTabId: tabId,
+						activeFileTabId: null,
+						inputMode: 'ai' as const,
 						aiTabs: s.aiTabs.map((t) => (t.id === tabId ? { ...t, hasUnread: false } : t)),
 					};
 				})
 			);
@@
 			setSessions((prev) =>
 				prev.map((s) => {
 					if (s.id !== sessionId) return s;
 					return {
 						...s,
 						activeTabId: tabId,
+						activeFileTabId: null,
+						inputMode: 'ai' as const,
 						aiTabs: s.aiTabs.map((t) =>
 							t.id === tabId ? { ...t, inputValue: text, hasUnread: false } : t
 						),
 					};
 				})
 			);

Also applies to: 1586-1590

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

In `@src/renderer/App.tsx` around lines 1551 - 1560, The handlers
handleAgentInboxQuickReply and handleAgentInboxOpenAndReply activate a
session/tab via setSessions but do not ensure the tab is in AI input mode or
clear file focus, so replies can be routed through terminal/file execution;
update the setSessions update for the matching session/tab (same block that sets
activeTabId and clears hasUnread) to also set inputMode: 'ai' and
activeFileTabId: null on the returned session object (use the sessionId, tabId,
aiTabs, inputMode and activeFileTabId symbols to locate the logic), ensuring
both handlers perform the same mutation so quick-reply flows always force AI
mode and clear file tab focus.

- Replace O(n*m) .find() loop in resolveFrozenItems with O(n+m) Map lookup
- Replace setTimeout(200) with double requestAnimationFrame for reply focus
- Add circular dependency comment to AgentInboxModalData loose string types
- Add comment explaining STATUS_COLORS cannot use keyof ThemeColors

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 (3)
src/renderer/components/AgentInbox/index.tsx (1)

291-292: Remove empty event handlers.

The empty onFocus and onBlur handlers serve no purpose and add minor noise to the code.

🧹 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 291 - 292, Remove
the no-op event handlers by deleting the empty onFocus and onBlur props from the
JSX in the AgentInbox component (remove the onFocus={() => {}} and onBlur={() =>
{}} occurrences) so the element no longer includes unused handlers and behavior
is unchanged.
src/renderer/stores/modalStore.ts (1)

666-669: Consider tightening the type for updateAgentInboxData.

The Record<string, unknown> type is looser than Partial<AgentInboxModalData>. While this avoids the circular dependency, it also allows invalid keys to be passed without type errors.

💡 Alternative: inline the known keys
-updateAgentInboxData: (data: Record<string, unknown>) => updateModalData('agentInbox', data),
+updateAgentInboxData: (data: { filterMode?: string; sortMode?: string; isExpanded?: boolean }) =>
+	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 666 - 669, The
updateAgentInboxData function currently accepts a loose Record<string, unknown>;
tighten it to Partial<AgentInboxModalData> to catch invalid keys by changing the
parameter type on updateAgentInboxData to Partial<AgentInboxModalData>. If
importing AgentInboxModalData would create a circular dependency, instead
declare a small inline type (e.g. AgentInboxModalPayload with the known keys
used by the modal) and use Partial<AgentInboxModalPayload> for
updateAgentInboxData to preserve type safety while avoiding the cycle; keep the
function name updateAgentInboxData and the modal id 'agentInbox' unchanged.
src/renderer/components/AgentInbox/FocusModeView.tsx (1)

978-996: Consider adding keyboard accessibility to the resize handle.

The resize handle responds to mouse events but lacks keyboard support. Users relying on keyboard navigation cannot resize the sidebar. While not blocking, adding keyboard controls would improve accessibility.

♿ Optional keyboard resize support
 <div
 	onMouseDown={handleResizeStart}
+	tabIndex={0}
+	role="separator"
+	aria-orientation="vertical"
+	aria-label="Resize sidebar"
+	onKeyDown={(e) => {
+		if (e.key === 'ArrowLeft') {
+			e.preventDefault();
+			setSidebarWidth((w) => Math.max(160, w - 10));
+		} else if (e.key === 'ArrowRight') {
+			e.preventDefault();
+			setSidebarWidth((w) => Math.min(400, w + 10));
+		}
+	}}
 	style={{
 		width: 4,
 		cursor: 'col-resize',
🤖 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 978 - 996,
The resize handle div currently only supports mouse events; make it keyboard
accessible by adding tabIndex={0} and an onKeyDown handler on the same element
to adjust the sidebar width when ArrowLeft/ArrowRight (and optionally Home/End)
are pressed, invoking the same resize logic used by handleResizeStart (or
factoring the width-update logic into a shared function) and respecting
isResizingRef state; also add focus/blur handlers to mirror the
mouseEnter/mouseLeave visual feedback using theme colors so keyboard users see
the active state.
🤖 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/FocusModeView.tsx`:
- Around line 978-996: The resize handle div currently only supports mouse
events; make it keyboard accessible by adding tabIndex={0} and an onKeyDown
handler on the same element to adjust the sidebar width when
ArrowLeft/ArrowRight (and optionally Home/End) are pressed, invoking the same
resize logic used by handleResizeStart (or factoring the width-update logic into
a shared function) and respecting isResizingRef state; also add focus/blur
handlers to mirror the mouseEnter/mouseLeave visual feedback using theme colors
so keyboard users see the active state.

In `@src/renderer/components/AgentInbox/index.tsx`:
- Around line 291-292: Remove the no-op event handlers by deleting the empty
onFocus and onBlur props from the JSX in the AgentInbox component (remove the
onFocus={() => {}} and onBlur={() => {}} occurrences) so the element no longer
includes unused handlers and behavior is unchanged.

In `@src/renderer/stores/modalStore.ts`:
- Around line 666-669: The updateAgentInboxData function currently accepts a
loose Record<string, unknown>; tighten it to Partial<AgentInboxModalData> to
catch invalid keys by changing the parameter type on updateAgentInboxData to
Partial<AgentInboxModalData>. If importing AgentInboxModalData would create a
circular dependency, instead declare a small inline type (e.g.
AgentInboxModalPayload with the known keys used by the modal) and use
Partial<AgentInboxModalPayload> for updateAgentInboxData to preserve type safety
while avoiding the cycle; keep the function name updateAgentInboxData and the
modal id 'agentInbox' unchanged.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 57e1618 and 7b15472.

📒 Files selected for processing (4)
  • src/renderer/components/AgentInbox/FocusModeView.tsx
  • src/renderer/components/AgentInbox/index.tsx
  • src/renderer/stores/modalStore.ts
  • src/renderer/types/agent-inbox.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/renderer/types/agent-inbox.ts

Felipe Gobbi and others added 7 commits February 26, 2026 20:22
The agent filter was using exact substring matching (.includes()),
making it impossible to find agents with non-contiguous character queries.
Now uses the existing fuzzyMatch() utility for consistent behavior
with File Search Modal and Tab Switcher.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Slash command search was using startsWith() prefix matching, requiring
exact contiguous character input. Now uses fuzzyMatchWithScore() with
3-tier ranking (prefix > fuzzy name > fuzzy description) in InputArea
and fuzzyMatch() in useInputKeyDown for consistent keyboard navigation.

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

Extract shared filterAndSortSlashCommands() utility to ensure both InputArea
(visual dropdown) and useInputKeyDown (Enter/Tab handler) produce identically
ordered command lists. Previously, InputArea sorted by relevance score while
useInputKeyDown kept original array order, causing the wrong command to be
inserted when pressing Enter/Tab.

Also fixes SessionList group auto-expansion to use fuzzyMatch() consistently
instead of substring .includes(), matching the main sessionCategories filter.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prefix match compared lowercased cmdName against potentially mixed-case
searchTerm, making it case-sensitive while fuzzyMatchWithScore was
case-insensitive. Now lowercases searchTerm once at function entry.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Clamp selectedSlashCommandIndex in useInputKeyDown before accessing
  filteredCommands, preventing silent no-op when index exceeds list length
  after the user continues typing
- Align SessionList group-expansion useEffect with sessionCategories memo:
  add worktree children branch/name matching and parentSessionId filtering,
  fix missing sessions and worktreeChildrenByParentId dependencies
- Add 11 tests for filterAndSortSlashCommands covering mode filtering,
  prefix vs fuzzy ranking, case insensitivity, description matching,
  empty results, and generic type preservation
- Remove duplicate SlashCommand interface from InputArea.tsx, import
  SlashCommandEntry from search.ts instead

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When filteredCommands.length === 0, ArrowDown/ArrowUp/Enter/Tab handlers
would compute invalid indices (e.g. Math.min(prev+1, -1)). Added early
return guard that only allows Escape when no commands match.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The 'filters out aiOnly commands in terminal mode' test used '/r' as
input, which with fuzzy matching now matches '/clear' (the 'r' in
'clear'). Changed to '/rn' which only fuzzy-matches '/run', correctly
testing that aiOnly commands are excluded in terminal mode.

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.

2 participants