Skip to content

calculate message sidebar counts and show in the sidebar UI, issue #361 - #377

Open
vanboom wants to merge 1 commit into
abhinavxd:mainfrom
vanboom:367-show-sidebar-counts
Open

calculate message sidebar counts and show in the sidebar UI, issue #361#377
vanboom wants to merge 1 commit into
abhinavxd:mainfrom
vanboom:367-show-sidebar-counts

Conversation

@vanboom

@vanboom vanboom commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Resolves #367 - please check closely, not sure if everything is in the correct place for efficiency.

Summary by CodeRabbit

New Features

  • Sidebar now displays numeric count badges for Assigned, Mentioned, and Unassigned conversations in the Inbox, providing a quick overview of conversation volumes.
  • Conversation counts automatically refresh when conversations are reassigned or their status is updated.

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR implements conversation count badges in the sidebar by adding state management to track Open-conversation totals across four categories (unassigned, assigned, all, mentioned), fetching those counts on component mount and after status/assignee mutations, and rendering count badges next to the corresponding Inbox navigation items.

Changes

Sidebar conversation count badges

Layer / File(s) Summary
Sidebar count API and reactive state
frontend/apps/main/src/stores/conversation.js
Introduces sidebarCounts reactive state with unassigned, assigned, all, and mentioned totals. Implements fetchSidebarCounts() that concurrently fetches Open-status conversation counts from four list endpoints, extracts totals from API responses, and exposes both via store export.
Refresh counts after status and assignee changes
frontend/apps/main/src/stores/conversation.js
updateStatus() and updateAssignee() now await fetchSidebarCounts() after their mutations succeed, keeping sidebar counts synchronized with conversation changes.
Sidebar badge display and initialization
frontend/apps/main/src/components/sidebar/Sidebar.vue
Adds onMounted import, calls fetchSidebarCounts() on component mount, and updates Inbox navigation items to display count badges for assigned, mentioned, and unassigned by reading from conversationStore.sidebarCounts.

Sequence Diagram

sequenceDiagram
  participant Sidebar as Sidebar Component
  participant Store as Conversation Store
  participant API as Conversation API
  Sidebar->>Store: fetchSidebarCounts()
  Store->>API: GET /conversations?status=Open&filters=unassigned
  API-->>Store: { data: { total: N } }
  Store->>API: GET /conversations?status=Open&filters=assigned
  API-->>Store: { data: { total: M } }
  Store->>API: GET /conversations?status=Open (all)
  API-->>Store: { data: { total: K } }
  Store->>API: GET /conversations?status=Open&filters=mentioned
  API-->>Store: { data: { total: L } }
  Store->>Store: sidebarCounts = { unassigned, assigned, all, mentioned }
  Store-->>Sidebar: counts updated
  Sidebar->>Sidebar: render badges with counts
Loading

Estimated Code Review Effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 Badges bloom in inbox light,
Tallies count from morning's flight,
Four concurrent calls combine,
Unassigned and assigned align,
Sidebar speaks the truth divine!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.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 clearly summarizes the main change: calculating sidebar counts and displaying them in the UI, with specific reference to issue #361.
Linked Issues check ✅ Passed The PR implements the core feature request from #367: displaying count badges for open conversations in the Inbox sidebar for Assigned, Mentioned, and Unassigned items.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing sidebar conversation counts: store logic to fetch counts and sidebar component updates to display them.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch 367-show-sidebar-counts

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
frontend/apps/main/src/components/sidebar/Sidebar.vue (1)

482-488: ⚠️ Potential issue | 🟡 Minor

Add/justify sidebar count for the “All” inbox

frontend/apps/main/src/components/sidebar/Sidebar.vue shows count badges for “assigned/mentioned/unassigned”, but the “All” inbox item renders no badge. frontend/apps/main/src/stores/conversation.js still fetches and stores sidebarCounts.all from getAllConversations, but that value isn’t used in the UI. Either display the “All” badge for consistency or remove the sidebarCounts.all tracking/fetch if it’s intentionally omitted.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/apps/main/src/components/sidebar/Sidebar.vue` around lines 482 -
488, The "All" inbox item in Sidebar.vue is missing the badge even though the
store (conversation.js) populates sidebarCounts.all from getAllConversations;
update the UI to display the same count badge used by other inbox items by
wiring sidebarCounts.all into the SidebarMenuButton for the "All" item (the
template around SidebarMenuButton / List / navigateToInbox('all')), ensuring it
uses the same badge component/prop pattern as the
"assigned/mentioned/unassigned" items; alternatively, if "All" should not show a
count, remove sidebarCounts.all and the getAllConversations fetch from the
conversation store (functions: getAllConversations, sidebarCounts) to keep state
consistent.
🧹 Nitpick comments (3)
frontend/apps/main/src/stores/conversation.js (2)

1114-1114: ⚡ Quick win

Remove debug logging from production code.

This console.debug statement logs the entire API response object, which can be verbose and is unnecessary in production. Debug logs should be removed before merging.

♻️ Proposed fix to remove debug logging
-    console.debug(unassignedRes)
     // Extract the raw database total row counts from the Go API response
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/apps/main/src/stores/conversation.js` at line 1114, Remove the
development debug log that prints the full API response: delete the
console.debug(unassignedRes) call in conversation.js (the unassignedRes debug
statement). If you need non-verbose tracing in production, replace it with a
conditional debug guard or use the app's logger (e.g., only log via store/Logger
when NODE_ENV !== 'production') and ensure any remaining logs do not serialize
the whole response object; otherwise simply remove the line.

1120-1120: ⚡ Quick win

Remove debug logging from production code.

This console.debug statement is unnecessary in production and should be removed before merging.

♻️ Proposed fix to remove debug logging
     sidebarCounts.mentioned = mentionedRes?.data?.data?.total || 0
-    console.debug(sidebarCounts)
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/apps/main/src/stores/conversation.js` at line 1120, Remove the stray
production debug log by deleting the console.debug(sidebarCounts) call in the
conversation store; locate the usage of the sidebarCounts variable in the file
(e.g., where sidebarCounts is computed or used) and remove the console.debug
invocation so no console.debug(sidebarCounts) remains in the module (do not
replace with console logs — simply remove).
frontend/apps/main/src/components/sidebar/Sidebar.vue (1)

450-452: ⚡ Quick win

Add ARIA labels to count badges for screen reader accessibility.

The count badges display numbers without context for screen reader users. Add aria-label attributes to make these accessible, e.g., aria-label="${count} open assigned conversations".

♿ Proposed fix to add ARIA labels

For the assigned badge (lines 450-452):

-                    <span class="ml-auto bg-gray-200 text-gray-700 px-2 py-0.5 rounded-full text-xs font-semibold">
+                    <span 
+                      class="ml-auto bg-gray-200 text-gray-700 px-2 py-0.5 rounded-full text-xs font-semibold"
+                      :aria-label="`${conversationStore.sidebarCounts.assigned} open assigned conversations`"
+                    >
                       {{ conversationStore.sidebarCounts.assigned }}
                     </span>

Apply similar changes to the mentioned (lines 462-464) and unassigned (lines 475-477) badges.

Also applies to: 462-464, 475-477

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/apps/main/src/components/sidebar/Sidebar.vue` around lines 450 -
452, The count badges (span elements rendering
conversationStore.sidebarCounts.assigned,
conversationStore.sidebarCounts.mentioned, and
conversationStore.sidebarCounts.unassigned) lack ARIA context; update each badge
span to include an aria-label that embeds the numeric value and context (e.g.,
"{count} assigned conversations" or similar) so screen readers announce
meaningful text for assigned, mentioned, and unassigned counts.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/apps/main/src/stores/conversation.js`:
- Around line 1099-1121: fetchSidebarCounts currently awaits Promise.all([...])
of api.getUnassignedConversations, api.getAssignedConversations,
api.getAllConversations and api.getMentionedConversations with no error
handling; wrap the concurrent calls in error handling (prefer using
Promise.allSettled or a try/catch around Promise.all) so one failing request
doesn't reject the whole function, log any errors, and set
sidebarCounts.unassigned/assigned/all/mentioned to 0 (or retain previous values)
when their corresponding promise fails; update fetchSidebarCounts and any
callers (e.g., the onMounted in Sidebar.vue) to avoid unhandled rejections and
ensure the UI continues to render even if some API calls fail.
- Line 685: The call to fetchSidebarCounts() should be wrapped in its own
try-catch so failures to refresh the sidebar do not run the surrounding catch
that reports the status-update as failed; after the status update succeeds (the
await on the status update at line ~684), replace the direct await
fetchSidebarCounts() with: try { await fetchSidebarCounts() } catch (err) {
console.error('Failed to refresh sidebar counts', err) } (or use the existing
logger) so that sidebar refresh errors are logged/handled locally and do not
trigger the status-update failure path.
- Line 747: The call to fetchSidebarCounts() should be wrapped in its own
try-catch so failures don't get attributed to the assignee update in
updateStatus; modify the code after the successful assignee update to run await
fetchSidebarCounts() inside a new try block and catch any error separately,
logging a distinct message like "Failed to refresh sidebar counts" (or using the
same error reporting utility used elsewhere) and not rethrowing so the assignee
update remains considered successful.

---

Outside diff comments:
In `@frontend/apps/main/src/components/sidebar/Sidebar.vue`:
- Around line 482-488: The "All" inbox item in Sidebar.vue is missing the badge
even though the store (conversation.js) populates sidebarCounts.all from
getAllConversations; update the UI to display the same count badge used by other
inbox items by wiring sidebarCounts.all into the SidebarMenuButton for the "All"
item (the template around SidebarMenuButton / List / navigateToInbox('all')),
ensuring it uses the same badge component/prop pattern as the
"assigned/mentioned/unassigned" items; alternatively, if "All" should not show a
count, remove sidebarCounts.all and the getAllConversations fetch from the
conversation store (functions: getAllConversations, sidebarCounts) to keep state
consistent.

---

Nitpick comments:
In `@frontend/apps/main/src/components/sidebar/Sidebar.vue`:
- Around line 450-452: The count badges (span elements rendering
conversationStore.sidebarCounts.assigned,
conversationStore.sidebarCounts.mentioned, and
conversationStore.sidebarCounts.unassigned) lack ARIA context; update each badge
span to include an aria-label that embeds the numeric value and context (e.g.,
"{count} assigned conversations" or similar) so screen readers announce
meaningful text for assigned, mentioned, and unassigned counts.

In `@frontend/apps/main/src/stores/conversation.js`:
- Line 1114: Remove the development debug log that prints the full API response:
delete the console.debug(unassignedRes) call in conversation.js (the
unassignedRes debug statement). If you need non-verbose tracing in production,
replace it with a conditional debug guard or use the app's logger (e.g., only
log via store/Logger when NODE_ENV !== 'production') and ensure any remaining
logs do not serialize the whole response object; otherwise simply remove the
line.
- Line 1120: Remove the stray production debug log by deleting the
console.debug(sidebarCounts) call in the conversation store; locate the usage of
the sidebarCounts variable in the file (e.g., where sidebarCounts is computed or
used) and remove the console.debug invocation so no console.debug(sidebarCounts)
remains in the module (do not replace with console logs — simply remove).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7b9fd9e3-c55f-4b2a-8d23-bb8867e52edc

📥 Commits

Reviewing files that changed from the base of the PR and between edbc026 and 7ea4526.

📒 Files selected for processing (2)
  • frontend/apps/main/src/components/sidebar/Sidebar.vue
  • frontend/apps/main/src/stores/conversation.js

conversation.data.status = v
try {
await api.updateConversationStatus(conversation.data.uuid, { status: v })
await fetchSidebarCounts()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚖️ Poor tradeoff

Wrap sidebar count refresh in separate try-catch to avoid misleading error messages.

If fetchSidebarCounts() fails, the existing catch block (lines 686-692) will execute and tell the user that the status update failed. However, the status update actually succeeded (line 684), and only the sidebar count refresh failed. This creates a confusing UX where the conversation status changes but the user sees a failure message.

🛡️ Proposed fix to isolate sidebar count errors
     try {
       await api.updateConversationStatus(conversation.data.uuid, { status: v })
-      await fetchSidebarCounts()
     } catch (error) {
       if (conversation.data) conversation.data.status = previous
       emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
         variant: 'destructive',
         description: handleHTTPError(error).message
       })
+      return
     }
+    // Refresh counts silently; don't block or show errors if this fails
+    fetchSidebarCounts().catch((err) => {
+      console.warn('Failed to refresh sidebar counts after status update:', err)
+    })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/apps/main/src/stores/conversation.js` at line 685, The call to
fetchSidebarCounts() should be wrapped in its own try-catch so failures to
refresh the sidebar do not run the surrounding catch that reports the
status-update as failed; after the status update succeeds (the await on the
status update at line ~684), replace the direct await fetchSidebarCounts() with:
try { await fetchSidebarCounts() } catch (err) { console.error('Failed to
refresh sidebar counts', err) } (or use the existing logger) so that sidebar
refresh errors are logged/handled locally and do not trigger the status-update
failure path.

async function updateAssignee (type, v) {
try {
await api.updateAssignee(conversation.data.uuid, type, v)
await fetchSidebarCounts()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚖️ Poor tradeoff

Wrap sidebar count refresh in separate try-catch to avoid misleading error messages.

Same issue as in updateStatus (line 685): if fetchSidebarCounts() fails, the existing catch block (lines 748-753) will show an error saying the assignee update failed, but the assignee update at line 746 actually succeeded.

🛡️ Proposed fix to isolate sidebar count errors
     try {
       await api.updateAssignee(conversation.data.uuid, type, v)
-      await fetchSidebarCounts()
     } catch (error) {
       emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
         variant: 'destructive',
         description: handleHTTPError(error).message
       })
+      return
     }
+    // Refresh counts silently; don't block or show errors if this fails
+    fetchSidebarCounts().catch((err) => {
+      console.warn('Failed to refresh sidebar counts after assignee update:', err)
+    })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/apps/main/src/stores/conversation.js` at line 747, The call to
fetchSidebarCounts() should be wrapped in its own try-catch so failures don't
get attributed to the assignee update in updateStatus; modify the code after the
successful assignee update to run await fetchSidebarCounts() inside a new try
block and catch any error separately, logging a distinct message like "Failed to
refresh sidebar counts" (or using the same error reporting utility used
elsewhere) and not rethrowing so the assignee update remains considered
successful.

Comment on lines +1099 to +1121
async function fetchSidebarCounts() {
const openFilter = [{
model: 'conversation_statuses',
field: 'name',
operator: 'equals',
value: 'Open'
}]
const filters = JSON.stringify(openFilter)
// Fire all requests simultaneously. page_size: 1 keeps payload sizes tiny.
const [unassignedRes, assignedRes, allRes, mentionedRes] = await Promise.all([
api.getUnassignedConversations({ page: 1, page_size: 1, filters: filters }),
api.getAssignedConversations({ page: 1, page_size: 1, filters: filters }),
api.getAllConversations({ page: 1, page_size: 1, filters: filters }),
api.getMentionedConversations({ page: 1, page_size: 1, filters: filters })
])
console.debug(unassignedRes)
// Extract the raw database total row counts from the Go API response
sidebarCounts.unassigned = unassignedRes?.data?.data?.total || 0
sidebarCounts.assigned = assignedRes?.data?.data?.total || 0
sidebarCounts.all = allRes?.data?.data?.total || 0
sidebarCounts.mentioned = mentionedRes?.data?.data?.total || 0
console.debug(sidebarCounts)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add error handling to prevent unhandled promise rejections.

fetchSidebarCounts() uses Promise.all() without error handling. If any of the four API calls fail, the entire function rejects. This is called from onMounted in Sidebar.vue (line 238) without a try-catch, which could cause an unhandled promise rejection during component initialization and break the sidebar UI.

🛡️ Proposed fix to add error handling
   async function fetchSidebarCounts() {
     const openFilter = [{
       model: 'conversation_statuses',
       field: 'name',
       operator: 'equals',
       value: 'Open'
     }]
     const filters = JSON.stringify(openFilter)
-    // Fire all requests simultaneously. page_size: 1 keeps payload sizes tiny.
-    const [unassignedRes, assignedRes, allRes, mentionedRes] = await Promise.all([
-      api.getUnassignedConversations({ page: 1, page_size: 1, filters: filters }),
-      api.getAssignedConversations({ page: 1, page_size: 1, filters: filters }),
-      api.getAllConversations({ page: 1, page_size: 1, filters: filters }),
-      api.getMentionedConversations({ page: 1, page_size: 1, filters: filters })
-    ])
-    console.debug(unassignedRes)
-    // Extract the raw database total row counts from the Go API response
-    sidebarCounts.unassigned = unassignedRes?.data?.data?.total || 0
-    sidebarCounts.assigned = assignedRes?.data?.data?.total || 0
-    sidebarCounts.all = allRes?.data?.data?.total || 0
-    sidebarCounts.mentioned = mentionedRes?.data?.data?.total || 0
-    console.debug(sidebarCounts)
+    try {
+      // Fire all requests simultaneously. page_size: 1 keeps payload sizes tiny.
+      const [unassignedRes, assignedRes, allRes, mentionedRes] = await Promise.all([
+        api.getUnassignedConversations({ page: 1, page_size: 1, filters: filters }),
+        api.getAssignedConversations({ page: 1, page_size: 1, filters: filters }),
+        api.getAllConversations({ page: 1, page_size: 1, filters: filters }),
+        api.getMentionedConversations({ page: 1, page_size: 1, filters: filters })
+      ])
+      // Extract the raw database total row counts from the Go API response
+      sidebarCounts.unassigned = unassignedRes?.data?.data?.total || 0
+      sidebarCounts.assigned = assignedRes?.data?.data?.total || 0
+      sidebarCounts.all = allRes?.data?.data?.total || 0
+      sidebarCounts.mentioned = mentionedRes?.data?.data?.total || 0
+    } catch (error) {
+      console.warn('Failed to fetch sidebar counts:', error)
+      // Keep existing counts on error rather than resetting to 0
+    }
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/apps/main/src/stores/conversation.js` around lines 1099 - 1121,
fetchSidebarCounts currently awaits Promise.all([...]) of
api.getUnassignedConversations, api.getAssignedConversations,
api.getAllConversations and api.getMentionedConversations with no error
handling; wrap the concurrent calls in error handling (prefer using
Promise.allSettled or a try/catch around Promise.all) so one failing request
doesn't reject the whole function, log any errors, and set
sidebarCounts.unassigned/assigned/all/mentioned to 0 (or retain previous values)
when their corresponding promise fails; update fetchSidebarCounts and any
callers (e.g., the onMounted in Sidebar.vue) to avoid unhandled rejections and
ensure the UI continues to render even if some API calls fail.

@abhinavxd

Copy link
Copy Markdown
Owner

Two things on the approach:

The useful sidebar number is how many open conversations are in each list. That's the actionable count for an agent.
The agent doesn't care if there are 50 closed conversations in the "my inbox".

And the 4 API calls to get it are wasteful and they get the total count. We can replace them with a single backend query behind a new handler:

Something like this

SELECT
  COUNT(*) FILTER (WHERE assigned_user_id = $1)                          AS assigned,
  COUNT(*) FILTER (WHERE assigned_user_id IS NULL AND assigned_team_id IS NULL) AS unassigned,
  COUNT(*) FILTER (WHERE EXISTS (SELECT 1 FROM conversation_mentions cm
      WHERE cm.conversation_id = conversations.id
        AND (cm.mentioned_user_id = $1
              OR EXISTS (SELECT 1 FROM team_members tm
                        WHERE tm.team_id = cm.mentioned_team_id AND tm.user_id = $1)))) AS mentioned,
  COUNT(*)                                                              AS "all"
FROM conversations
WHERE status_id IN (SELECT id FROM conversation_statuses WHERE category = 'open');

{{ t('globals.terms.unassigned') }}
</span>

<span class="ml-auto bg-gray-200 text-gray-700 px-2 py-0.5 rounded-full text-xs font-semibold">

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

the badge uses hardcoded bg-gray-200 text-gray-700. Please use theme tokens instead (defined in frontend/tailwind.config.cjs, e.g. bg-sidebar-accent text-sidebar-accent-foreground or
bg-muted text-muted-foreground).

Or could use Badge component directly ( not sure )

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.

[Feature Request] Show count of open conversations in the Inbox list

2 participants