Skip to content

feat(web): port session bell filter from desktop Left Bar#884

Merged
pedramamini merged 2 commits intoRunMaestro:rcfrom
chr1syy:feat/webui-session-bell
Apr 23, 2026
Merged

feat(web): port session bell filter from desktop Left Bar#884
pedramamini merged 2 commits intoRunMaestro:rcfrom
chr1syy:feat/webui-session-bell

Conversation

@chr1syy
Copy link
Copy Markdown
Contributor

@chr1syy chr1syy commented Apr 22, 2026

Summary

  • Ports the desktop session bell to the web UI's LeftPanel: a bell button in the panel header that toggles the agent list to show only sessions that are active (busy), have unread tabs, or contain busy/unread worktree children.
  • Filter logic matches src/renderer/hooks/session/useSortedSessions.ts passesUnreadFilter exactly so web and desktop stay in sync.
  • Worktree children are always kept in the filtered set when their parent passes, so expanded parents still render their full child list.
  • Indicator dot on the bell when any session has unread activity while the filter is off; the filter auto-disables if the unread set empties; empty state reads "No active or unread agents" when nothing matches.

No server / hook / props-interface changes — AITabData.hasUnread was already flowing through the existing WebSocket contract (src/web/hooks/useWebSocket.ts).

Test plan

  • Open the web UI with a session whose agent is busy → bell shows an indicator dot.
  • Toggle the bell on → list narrows to only active/unread sessions.
  • Toggle it off again → full list returns.
  • With the bell on, verify a parent that passes still shows all of its worktree children.
  • With the bell on, mark all agents as idle/read → filter auto-disables.
  • With the bell on and nothing unread, empty state "No active or unread agents" appears.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Bell filter toggle in the mobile left panel to show only active/busy/unread sessions; auto-disables when none match and shows an empty-state message.
  • Enhancements
    • Filter state lifted to the mobile app level so the setting persists when the panel is toggled.
  • Tests
    • Updated component tests to accept the new filter props.

Mirrors the desktop `useSortedSessions.passesUnreadFilter` so the web
LeftPanel can restrict the agent list to sessions that are active, busy,
have unread tabs, or contain busy/unread worktree children. The bell
button sits in the panel header, shows an accent dot when any session
has unread activity, and auto-disables if the unread set empties.

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

coderabbitai Bot commented Apr 22, 2026

📝 Walkthrough

Walkthrough

Lifts the mobile "bell filter" state to the app, adds showUnreadOnly/setShowUnreadOnly props to LeftPanel, implements unread/busy detection and top-level session filtering (preserving children for matching parents), auto-disables the filter when no matches, and adds a header bell toggle plus empty-state messaging.

Changes

Cohort / File(s) Summary
Mobile Left Panel
src/web/mobile/LeftPanel.tsx
Adds showUnreadOnly and setShowUnreadOnly props; implements unread/busy detection, passesUnreadFilter logic (keeps active session, accounts for worktree parent/child), derives visibleSessions as filtered top-level sessions while preserving children, auto-disables filter if no matches, updates header UI with bell toggle/unread dot and empty-state message.
App state lift
src/web/mobile/App.tsx
Adds showUnreadAgentsOnly state and passes showUnreadOnly/setShowUnreadOnly into LeftPanel to persist filter across panel toggles.
Tests (mock update)
src/__tests__/web/mobile/App.test.tsx
Updates LeftPanel mock prop types to accept the two new props (showUnreadOnly, setShowUnreadOnly) used by App.test.tsx.

Sequence Diagram(s)

(No sequence diagrams generated — changes are UI/state lifting within a small set of components and do not introduce a multi-component sequential flow that requires visualization.)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

ready to merge

Poem

🐰 A bell upon the mobile hill, I found,
Filtering sessions with a gentle bound,
Unread and busy, parents kept in sight,
I hop and mark each glowing dot of light,
Hooray — the panel rings both day and night! 🔔

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: porting the desktop session bell filter to the mobile web UI LeftPanel, which aligns with the core objective of the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

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

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

❤️ Share

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

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 22, 2026

Greptile Summary

This PR ports the desktop session bell filter to the web LeftPanel, adding a toggle button that narrows the agent list to sessions that are busy, have unread tabs, or have busy/unread worktree children. The filter logic closely mirrors useSortedSessions.passesUnreadFilter, the indicator dot auto-disables when the unread set empties, and an empty-state message is shown when nothing matches.

Confidence Score: 5/5

Safe to merge — all findings are P2 style/UX suggestions with no correctness or data-integrity concerns.

The filter logic is correct and faithfully mirrors the desktop implementation. Two P2 findings (connecting state omission and ephemeral filter state) are minor UX considerations that don't break any user path.

No files require special attention.

Important Files Changed

Filename Overview
src/web/mobile/LeftPanel.tsx Adds bell filter UI (toggle button + indicator dot + empty state) with filter logic that mirrors the desktop passesUnreadFilter; two P2 style findings: connecting state excluded from filter predicate, and filter state is not lifted so it resets on panel remount unlike collapsedGroups.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[sessions prop] --> B[buildWorktreeChildrenMap\nworktreeChildrenByParent]
    A --> C{showUnreadOnly?}
    C -- No --> D[visibleSessions = sessions]
    C -- Yes --> E["Filter: keep all children\n+ parents that passesUnreadFilter"]
    B --> E
    E --> D
    D --> F[groupSessions\nvisibleSessions]
    F --> G[grouped / worktreeChildrenMap]
    G --> H{grouped.length === 0\n&& showUnreadOnly\n&& sessions.length > 0?}
    H -- Yes --> I[Empty state:\n'No active or unread agents']
    H -- No --> J[Render session groups]
    A --> K[hasUnreadAgents]
    K --> L{Bell button indicator dot\nshown when hasUnreadAgents\n&& !showUnreadOnly}
    K --> M[useEffect auto-disable\nif showUnreadOnly && !hasUnreadAgents]
Loading

Reviews (1): Last reviewed commit: "feat(web): port session bell filter from..." | Re-trigger Greptile

Comment on lines +115 to +117
function sessionHasUnreadActivity(session: Session): boolean {
return (session.aiTabs?.some((tab) => tab.hasUnread) ?? false) || session.state === 'busy';
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 connecting state excluded from bell filter

sessionHasUnreadActivity only tests state === 'busy', so a session in the connecting state is invisible to the bell filter even though getGroupStatus (line 46) and the status-color helper (line 54) both treat connecting as an active/busy state. A user will see the orange pulsing dot but the session won't appear when the bell filter is on, making the filter feel inconsistent with the visual indicators.

Note: this intentionally diverges from the desktop passesUnreadFilter (which also omits connecting), so accepting the divergence is also a valid choice — worth an explicit decision either way.

Suggested change
function sessionHasUnreadActivity(session: Session): boolean {
return (session.aiTabs?.some((tab) => tab.hasUnread) ?? false) || session.state === 'busy';
}
function sessionHasUnreadActivity(session: Session): boolean {
return (session.aiTabs?.some((tab) => tab.hasUnread) ?? false) || session.state === 'busy' || session.state === 'connecting';
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Intentional parity with desktop useSortedSessions.passesUnreadFilter — desktop also omits connecting. connecting is a transient state (seconds), and including it would cause sessions to flash in and out of the filtered list during connection. Keeping the divergence from getGroupStatus as-is: the bell answers "what needs my attention", which connecting sessions don't (yet). Happy to revisit as a cross-platform change if the desktop side changes.

Comment on lines 663 to 665
[setCollapsedGroups]
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Bell filter state resets on panel close

showUnreadOnly is local component state, so it resets to false every time the panel is unmounted and remounted (common on mobile). By contrast, collapsedGroups is deliberately lifted to props with the comment "persists across panel open/close." If the bell filter should behave similarly, it should be lifted to the parent and passed in the same way — otherwise users on mobile will lose their filter preference every time they close the panel.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — fixed in 6372d3c. Lifted showUnreadOnly to App.tsx using the same pattern as collapsedGroups so the filter survives LeftPanel unmount/remount.

`LeftPanel` is conditionally rendered under `{showLeftPanel && ...}`,
so it remounts on every open/close. The bell filter was local state
and reset every time, which is noticeable on mobile. Lifts
`showUnreadOnly` to `App.tsx` using the same pattern as
`collapsedGroups`.

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

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/web/mobile/LeftPanel.tsx (1)

680-684: Nit: include setShowUnreadOnly in the effect deps.

setShowUnreadOnly is now a prop rather than a local useState setter, so it's not guaranteed to be referentially stable for future callers (though in practice MobileApp passes a stable useState setter today). Adding it to the dependency array will also satisfy react-hooks/exhaustive-deps without changing behavior.

♻️ Proposed tweak
 	useEffect(() => {
 		if (showUnreadOnly && !hasUnreadAgents) {
 			setShowUnreadOnly(false);
 		}
-	}, [showUnreadOnly, hasUnreadAgents]);
+	}, [showUnreadOnly, hasUnreadAgents, setShowUnreadOnly]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/web/mobile/LeftPanel.tsx` around lines 680 - 684, The useEffect that
checks showUnreadOnly and hasUnreadAgents omits the prop setter
setShowUnreadOnly from its dependency array; update the useEffect deps to
include setShowUnreadOnly so the effect depends on the prop setter as well
(locate the useEffect block referencing showUnreadOnly, hasUnreadAgents and call
setShowUnreadOnly) — this keeps behavior identical but ensures correct hooks
dependencies and satisfies react-hooks/exhaustive-deps.
🤖 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/web/mobile/LeftPanel.tsx`:
- Around line 680-684: The useEffect that checks showUnreadOnly and
hasUnreadAgents omits the prop setter setShowUnreadOnly from its dependency
array; update the useEffect deps to include setShowUnreadOnly so the effect
depends on the prop setter as well (locate the useEffect block referencing
showUnreadOnly, hasUnreadAgents and call setShowUnreadOnly) — this keeps
behavior identical but ensures correct hooks dependencies and satisfies
react-hooks/exhaustive-deps.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: abc74406-3b18-4bbb-bfb7-618fb1117709

📥 Commits

Reviewing files that changed from the base of the PR and between 95a5d94 and 6372d3c.

📒 Files selected for processing (3)
  • src/__tests__/web/mobile/App.test.tsx
  • src/web/mobile/App.tsx
  • src/web/mobile/LeftPanel.tsx
✅ Files skipped from review due to trivial changes (1)
  • src/tests/web/mobile/App.test.tsx

@pedramamini
Copy link
Copy Markdown
Collaborator

Thanks for the contribution, @chr1syy! 🙏

Nice clean port — the web passesUnreadFilter faithfully mirrors the desktop useSortedSessions.passesUnreadFilter, lifting the filter state into MobileApp so it survives panel remounts is the right call, and the worktree-children-preserved-when-parent-passes detail is exactly the behavior we want. Empty-state + auto-disable when the unread set empties is a thoughtful touch.

No merge conflicts, approving. One optional follow-up from CodeRabbit worth considering: adding setShowUnreadOnly to the useEffect dep array in LeftPanel.tsx (~line 684) to satisfy react-hooks/exhaustive-deps — purely cosmetic, behavior is identical.

Approving ✅

@chr1syy chr1syy added the ready to merge This PR is ready to merge label Apr 22, 2026
@pedramamini pedramamini merged commit 3156de6 into RunMaestro:rc Apr 23, 2026
3 checks passed
pedramamini added a commit that referenced this pull request Apr 23, 2026
- LeftPanel (mobile): include setShowUnreadOnly in the auto-disable
  effect's deps so React's exhaustive-deps lint stays satisfied now
  that the setter arrives via props.
- web-server-factory: collapse the inline `import('../../shared/...').
  Shortcut` to the existing top-level type import.
- agents.test (SSH detect): tighten the call-count check to
  AGENT_DEFINITIONS.length so a regression that silently skips agents
  is caught instead of just dropping below the prior `> 0` threshold.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved ready to merge This PR is ready to merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants