UI: sidebar chat list, credit usage, theme#498
UI: sidebar chat list, credit usage, theme#498skylerwww wants to merge 3 commits intoOpenSecretCloud:masterfrom
Conversation
📝 WalkthroughWalkthroughThe PR refactors conversation project API calls from instance-based to module-level functions, adds mock sidebar chat generation for development, updates component styling and layout (widening sidebar from 280px to 296px), refactors CreditUsage to use Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsGit: Failed to clone repository. Please run the 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. Review rate limit: 0/1 reviews remaining, refill in 60 minutes.Comment |
- Refine chat history rows, project layout, and ellipsis/pin alignment - Simplify CreditUsage to rich card; hover for token details - Sidebar nav spacing; theme, logos, and wordmark tweaks Made-with: Cursor
4d4244e to
050dc36
Compare
|
|
||
| const totalLive = billingStatus?.total_tokens; | ||
| const usedLive = billingStatus?.used_tokens; | ||
| const hasRealUsage = totalLive != null && totalLive > 0 && usedLive != null && usedLive > 0; |
There was a problem hiding this comment.
🔴 CreditUsage returns null for users with 0 used tokens, hiding plan info that was previously shown by the removed Plan Badge
The PR removes the standalone Plan Badge from AccountMenu.tsx (old lines 297-305) that previously always showed the plan name when billingStatus existed. Plan name display is now solely handled by CreditUsage. However, CreditUsage.tsx:132 requires usedLive > 0 in the hasRealUsage check: when a user has a paid plan but used_tokens === 0 (e.g., new subscriber, or right after a billing cycle reset), hasRealUsage is false, useMock is false in production, and CreditUsage returns null at line 139. This means these users will see no plan name, no credit bar, and no token info in the sidebar — a regression from the old UI where the Badge always displayed the plan name.
Affected condition in CreditUsage.tsx
Line 132: const hasRealUsage = totalLive != null && totalLive > 0 && usedLive != null && usedLive > 0;
The usedLive > 0 clause causes hasRealUsage to be false when a user has legitimately 0 used tokens. This was fine when the Plan Badge existed independently, but now that CreditUsage is the only component showing plan info, it needs to handle the usedLive === 0 case.
Prompt for agents
The hasRealUsage check at CreditUsage.tsx:132 requires usedLive > 0, but since the Plan Badge was removed from AccountMenu.tsx and CreditUsage is now the only component that shows the plan name in the sidebar, this condition causes CreditUsage to return null for users with 0 used tokens (new subscribers, post-billing-reset). The fix should relax the condition to allow CreditUsage to render when billing data exists with total_tokens > 0, even if used_tokens is 0. For example, change the condition to: `const hasRealUsage = totalLive != null && totalLive > 0 && usedLive != null;` — removing the `usedLive > 0` requirement. Verify that the rest of the component handles usedLive === 0 gracefully (percentUsed would be 0, percentRemaining 100, tokensRemaining equals total — all valid states for the new bar UI).
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
frontend/src/components/UnifiedChat.tsx (1)
3233-3255:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUpdate the fixed banner offsets for the 296px sidebar.
The error and TTS alerts still use
md:left-[calc(50%+140px)], which was tuned for the old 280px layout. After the sidebar width change they’ll sit 8px too far left on desktop.Suggested fix
- <div className="fixed top-16 left-1/2 -translate-x-1/2 z-50 w-full max-w-2xl px-4 md:left-[calc(50%+140px)]"> + <div className="fixed top-16 left-1/2 -translate-x-1/2 z-50 w-full max-w-2xl px-4 md:left-[calc(50%+148px)]">🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/components/UnifiedChat.tsx` around lines 3233 - 3255, The fixed alert banners still use md:left-[calc(50%+140px)] which centers them for the old 280px sidebar; update those instances in UnifiedChat.tsx (the error alert block and the playbackError alert block) to md:left-[calc(50%+148px)] to match the new 296px sidebar width (and search for any other occurrences of md:left-[calc(50%+140px)] and replace similarly).frontend/src/components/ProjectDetailView.tsx (1)
642-656:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAdd an accessible label to the row actions button.
The icon-only
MoreHorizontaltrigger has no accessible name, so screen readers won’t announce what it does.Suggested fix
<Button type="button" variant="ghost" size="icon" className="absolute right-2 top-2 h-8 w-8 text-foreground/40 transition-colors hover:text-foreground group-hover:text-foreground focus-visible:text-foreground" + aria-label="Chat actions" onClick={(event) => { event.preventDefault(); event.stopPropagation(); }} >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/components/ProjectDetailView.tsx` around lines 642 - 656, The MoreHorizontal icon trigger is icon-only and lacks an accessible name; update the DropdownMenuTrigger/Button (inside the !isSelectionMode branch) to include a concise accessible label (e.g., add aria-label="Row actions" or aria-label={`More actions for ${rowId || 'row'}`}) and also add aria-haspopup="menu" (and aria-expanded bound to menu state if available) so screen readers announce it as a menu trigger; modify the Button props where MoreHorizontal is rendered to include these attributes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/components/ChatHistoryList.tsx`:
- Around line 1257-1265: Add accessible names to the icon-only overflow menu
buttons in ChatHistoryList.tsx by adding an explicit aria-label (and optionally
title) to the button elements that use SIDEBAR_ELLIPSIS_BTN and render the
<MoreHorizontal ... /> icon (e.g., the button with onClick that calls
event.preventDefault()/event.stopPropagation()); update each occurrence
(including the other instances using MoreHorizontal at the other noted spots) to
include a descriptive aria-label like "Open row menu" or "More options for this
chat" so screen readers can identify the control.
- Around line 1237-1249: The selection checkbox is rendered for mock rows even
when selection is blocked; update the conditional around the Checkbox in
ChatHistoryList (the isSelectionMode block that renders Checkbox and uses
toggleSelection and conversation.id) to also check that the row is not a mock
(e.g., conversation.isMock or conversation.mock !== true) so mock rows do not
render the checkbox or respond to clicks; keep existing stopPropagation handlers
and onCheckedChange/toggleSelection logic for real rows.
In `@frontend/src/components/CreditUsage.tsx`:
- Around line 50-53: The toPlanNameLabel helper returns " Plan" when rawPlanName
is blank/whitespace; change it to default to "Pro" after trimming by first
normalizing rawPlanName to an empty string, trimming, then using a fallback:
const cleaned = (rawPlanName ?? "").trim() || "Pro"; then detect hasPlanSuffix
and return cleaned or `${cleaned} Plan`; update within the toPlanNameLabel
function.
- Around line 127-153: The component CreditUsage treats a zero used token state
as "no real usage" because hasRealUsage checks usedLive > 0; change the
condition to treat any non-null usedLive (including 0) as real usage when
totalLive > 0 (e.g., hasRealUsage should be totalLive != null && totalLive > 0
&& usedLive != null), so the real-zero state shows the billing card and prevents
DEV mock from overriding it; update the dependent logic (useMock,
total/used/productName/usageResetDate/apiBalance) to rely on this corrected
hasRealUsage so mockPreset/scenario only apply when no real billing data exists.
---
Outside diff comments:
In `@frontend/src/components/ProjectDetailView.tsx`:
- Around line 642-656: The MoreHorizontal icon trigger is icon-only and lacks an
accessible name; update the DropdownMenuTrigger/Button (inside the
!isSelectionMode branch) to include a concise accessible label (e.g., add
aria-label="Row actions" or aria-label={`More actions for ${rowId || 'row'}`})
and also add aria-haspopup="menu" (and aria-expanded bound to menu state if
available) so screen readers announce it as a menu trigger; modify the Button
props where MoreHorizontal is rendered to include these attributes.
In `@frontend/src/components/UnifiedChat.tsx`:
- Around line 3233-3255: The fixed alert banners still use
md:left-[calc(50%+140px)] which centers them for the old 280px sidebar; update
those instances in UnifiedChat.tsx (the error alert block and the playbackError
alert block) to md:left-[calc(50%+148px)] to match the new 296px sidebar width
(and search for any other occurrences of md:left-[calc(50%+140px)] and replace
similarly).
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: b12eeb6d-05c3-4a17-b00d-2de2b84b64c3
⛔ Files ignored due to path filters (2)
frontend/public/maple-logo-dark.svgis excluded by!**/*.svgfrontend/public/maple-logo.svgis excluded by!**/*.svg
📒 Files selected for processing (18)
frontend/src/chat.cssfrontend/src/components/AccountMenu.tsxfrontend/src/components/ChatHistoryList.tsxfrontend/src/components/ConversationProjectDialog.tsxfrontend/src/components/ConversationProjectPicker.tsxfrontend/src/components/CreditUsage.tsxfrontend/src/components/MapleWordmark.tsxfrontend/src/components/ProjectDetailView.tsxfrontend/src/components/Sidebar.tsxfrontend/src/components/TopNav.tsxfrontend/src/components/UnifiedChat.tsxfrontend/src/components/markdown.tsxfrontend/src/contexts/ThemeContext.tsxfrontend/src/index.cssfrontend/src/routes/_auth.chat.$chatId.tsxfrontend/src/utils/mockSidebarChats.tsfrontend/src/utils/paginatedLists.tsfrontend/tailwind.config.js
| {isSelectionMode ? ( | ||
| <div | ||
| className={`absolute left-1.5 top-1/2 ${ROW_CHECKBOX_Z} -translate-y-1/2`} | ||
| onClick={(e) => e.stopPropagation()} | ||
| > | ||
| <Checkbox | ||
| checked={isSelected} | ||
| onCheckedChange={() => toggleSelection(conversation.id)} | ||
| onClick={(event) => event.stopPropagation()} | ||
| className="data-[state=checked]:bg-primary" | ||
| /> | ||
| </div> | ||
| ) : null} |
There was a problem hiding this comment.
Hide selection checkboxes for mock rows in selection mode.
Mock rows still render a checkbox even though selection is intentionally blocked, which creates a confusing no-op control.
💡 Suggested patch
- {isSelectionMode ? (
+ {isSelectionMode && !isMockRow ? (
<div
className={`absolute left-1.5 top-1/2 ${ROW_CHECKBOX_Z} -translate-y-1/2`}
onClick={(e) => e.stopPropagation()}
>
<Checkbox
checked={isSelected}
onCheckedChange={() => toggleSelection(conversation.id)}
onClick={(event) => event.stopPropagation()}
className="data-[state=checked]:bg-primary"
/>
</div>
) : null}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/components/ChatHistoryList.tsx` around lines 1237 - 1249, The
selection checkbox is rendered for mock rows even when selection is blocked;
update the conditional around the Checkbox in ChatHistoryList (the
isSelectionMode block that renders Checkbox and uses toggleSelection and
conversation.id) to also check that the row is not a mock (e.g.,
conversation.isMock or conversation.mock !== true) so mock rows do not render
the checkbox or respond to clicks; keep existing stopPropagation handlers and
onCheckedChange/toggleSelection logic for real rows.
| <button | ||
| className={SIDEBAR_ELLIPSIS_BTN} | ||
| onClick={(event) => { | ||
| event.preventDefault(); | ||
| event.stopPropagation(); | ||
| }} | ||
| > | ||
| <Folder className="mr-2 h-4 w-4" /> | ||
| No project | ||
| <MoreHorizontal className="h-4 w-4" strokeWidth={ICON_STROKE} /> | ||
| </button> |
There was a problem hiding this comment.
Add accessible names to icon-only overflow menu buttons.
These triggers are icon-only and currently lack aria-label, making row actions ambiguous for screen-reader users.
♿ Suggested patch
<button
+ type="button"
+ aria-label={`Open actions for ${title}`}
className={SIDEBAR_ELLIPSIS_BTN}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
}}
> <button
+ type="button"
+ aria-label={`Open actions for project ${project.name}`}
className={SIDEBAR_ELLIPSIS_BTN}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
}}
> <button
+ type="button"
+ aria-label={`Open actions for ${chat.title}`}
className={SIDEBAR_ELLIPSIS_BTN}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>Also applies to: 1407-1415, 1530-1538
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/components/ChatHistoryList.tsx` around lines 1257 - 1265, Add
accessible names to the icon-only overflow menu buttons in ChatHistoryList.tsx
by adding an explicit aria-label (and optionally title) to the button elements
that use SIDEBAR_ELLIPSIS_BTN and render the <MoreHorizontal ... /> icon (e.g.,
the button with onClick that calls
event.preventDefault()/event.stopPropagation()); update each occurrence
(including the other instances using MoreHorizontal at the other noted spots) to
include a descriptive aria-label like "Open row menu" or "More options for this
chat" so screen readers can identify the control.
| function toPlanNameLabel(rawPlanName: string | undefined): string { | ||
| const cleaned = (rawPlanName ?? "Pro").trim(); | ||
| const hasPlanSuffix = /\bplan\b/i.test(cleaned); | ||
| return hasPlanSuffix ? cleaned : `${cleaned} Plan`; |
There was a problem hiding this comment.
Fall back when the plan name is empty.
If rawPlanName is blank or whitespace, this helper returns " Plan", which will surface as a broken badge label. Defaulting to "Pro" after trimming would avoid the bad UI.
Suggested fix
function toPlanNameLabel(rawPlanName: string | undefined): string {
- const cleaned = (rawPlanName ?? "Pro").trim();
+ const cleaned = (rawPlanName ?? "Pro").trim() || "Pro";
const hasPlanSuffix = /\bplan\b/i.test(cleaned);
return hasPlanSuffix ? cleaned : `${cleaned} Plan`;
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/components/CreditUsage.tsx` around lines 50 - 53, The
toPlanNameLabel helper returns " Plan" when rawPlanName is blank/whitespace;
change it to default to "Pro" after trimming by first normalizing rawPlanName to
an empty string, trimming, then using a fallback: const cleaned = (rawPlanName
?? "").trim() || "Pro"; then detect hasPlanSuffix and return cleaned or
`${cleaned} Plan`; update within the toPlanNameLabel function.
| export function CreditUsage({ mockScenario }: { mockScenario?: MockScenario }) { | ||
| const { billingStatus } = useLocalState(); | ||
|
|
||
| const totalLive = billingStatus?.total_tokens; | ||
| const usedLive = billingStatus?.used_tokens; | ||
| const hasRealUsage = totalLive != null && totalLive > 0 && usedLive != null && usedLive > 0; | ||
|
|
||
| const mockFlag = readMockScenario(); | ||
| const useMock = !hasRealUsage && import.meta.env.DEV && mockFlag !== null && mockFlag !== "off"; | ||
| const forcedMock = import.meta.env.DEV ? mockScenario : undefined; | ||
| const scenario = forcedMock ?? (mockFlag !== null && mockFlag !== "off" ? mockFlag : null); | ||
| const useMock = !hasRealUsage && import.meta.env.DEV && scenario !== null; | ||
|
|
||
| if (!hasRealUsage && !useMock) { | ||
| return null; | ||
| } | ||
|
|
||
| const mock = useMock ? mockPreset(mockFlag as MockScenario) : null; | ||
| const mock = useMock ? mockPreset(scenario as MockScenario) : null; | ||
| const total = hasRealUsage ? totalLive! : mock!.total_tokens; | ||
| const used = hasRealUsage ? usedLive! : mock!.used_tokens; | ||
| const productName = hasRealUsage ? billingStatus?.product_name : "Pro"; | ||
| const usageResetDate = hasRealUsage ? billingStatus!.usage_reset_date : mockUsageResetIso(); | ||
| const apiBalance = hasRealUsage ? billingStatus!.api_credit_balance : mock?.api_credit_balance; | ||
|
|
||
| const percentUsed = Math.min(100, Math.max(0, (used / total) * 100)); | ||
| const roundedPercent = Math.round(percentUsed); | ||
| const tone = usageTone(percentUsed); | ||
| const barColor = toneColor(tone); | ||
| const percentRemaining = Math.max(0, 100 - percentUsed); | ||
| const roundedRemaining = Math.round(percentRemaining); | ||
| const tokensRemaining = Math.max(0, total - used); |
There was a problem hiding this comment.
Treat zero used tokens as a real billing state.
hasRealUsage still requires usedLive > 0, so a freshly reset account with 0 used tokens falls through to the mock/null path and the card disappears. That also lets the DEV mock preview mask a real zero-usage account.
Suggested fix
- const hasRealUsage = totalLive != null && totalLive > 0 && usedLive != null && usedLive > 0;
+ const hasRealUsage = totalLive != null && totalLive > 0 && usedLive != null;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/components/CreditUsage.tsx` around lines 127 - 153, The
component CreditUsage treats a zero used token state as "no real usage" because
hasRealUsage checks usedLive > 0; change the condition to treat any non-null
usedLive (including 0) as real usage when totalLive > 0 (e.g., hasRealUsage
should be totalLive != null && totalLive > 0 && usedLive != null), so the
real-zero state shows the billing card and prevents DEV mock from overriding it;
update the dependent logic (useMock,
total/used/productName/usageResetDate/apiBalance) to rely on this corrected
hasRealUsage so mockPreset/scenario only apply when no real billing data exists.

Summary
Polish sidebar chat history, CreditUsage card behavior, and related theme/assets.
Changes
CreditUsage: single rich layout; token detail on bar hover; mock previewSidebar/ nav spacing above projectsMade with Cursor
Summary by CodeRabbit
New Features
Style