{"repo":"NousResearch/hermes-agent","public_id":"42987","display_label":"Desktop sidebar section header overlap fix","summary":"These issues and pull requests address a bug where section headers in the desktop sidebar can overlap each other. The work involves CSS and layout changes to prevent this overlap behavior.","subsystem":"apps/desktop","counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":43147,"merged_at":"2026-06-10T01:11:46+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-06-10T01:11:46+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":42987,"pr_number":43147,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-10T01:11:46+00:00"}],"members":[{"kind":"issue","number":42987,"title":"Desktop sidebar: Sessions and messaging headers can overlap","url":null,"state":"closed"},{"kind":"pr","number":42988,"title":"fix(desktop): prevent sidebar section header overlap","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"8bb65295532c7d353f081e3aaf63ed6f06ca1bd0","number":0},{"kind":"pr","value":"#43147","number":43147},{"kind":"pr","value":"#42987","number":42987}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":43147,"title":"fix(desktop): sidebar sections never overlap — two-mode CSS scroll + collapse/cap groups","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["apps/desktop/src/app/chat/sidebar/cron-jobs-section.tsx","apps/desktop/src/app/chat/sidebar/index.tsx","apps/desktop/src/app/chat/sidebar/load-more-row.tsx","apps/desktop/src/store/layout.ts","apps/desktop/src/styles.css"],"fix_hunks":[{"file":"apps/desktop/src/app/chat/sidebar/cron-jobs-section.tsx","hunks":["@@ -14,6 +14,8 @@ import type { CronJob } from '@/types/hermes'\n import { jobState, jobTitle, STATE_DOT } from '../../cron/job-state'\n import { SidebarPanelLabel } from '../../shell/sidebar-label'\n \n+import { SidebarLoadMoreRow } from './load-more-row'\n+\n const INACTIVE_STATES = new Set(['completed', 'disabled', 'error', 'paused'])\n \n // Recent runs shown in the inline quick-peek — enough to glance at history","@@ -24,6 +26,11 @@ const PEEK_RUN_LIMIT = 5\n // open peek so a freshly-fired run shows up within a few seconds.\n const PEEK_POLL_INTERVAL_MS = 8000\n \n+// Keep the section compact: show a few jobs up front, reveal more in larger\n+// steps on demand (mirrors the messaging sections in the sidebar).\n+const INITIAL_VISIBLE_JOBS = 3\n+const LOAD_MORE_STEP = 10\n+\n const relativeFmt = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto', style: 'short' })\n \n // Localized \"in 5 min\" / \"2 hr ago\" without hand-rolled strings — picks the","@@ -33,17 +40,25 @@ function relativeTime(targetMs: number, nowMs: number): string {\n   const abs = Math.abs(diff)\n   const sign = diff < 0 ? -1 : 1\n \n-  if (abs < 60_000) {return relativeFmt.format(sign * Math.round(abs / 1000), 'second')}\n+  if (abs < 60_000) {\n+    return relativeFmt.format(sign * Math.round(abs / 1000), 'second')\n+  }\n \n-  if (abs < 3_600_000) {return relativeFmt.format(sign * Math.round(abs / 60_000), 'minute')}\n+  if (abs < 3_600_000) {\n+    return relativeFmt.format(sign * Math.round(abs / 60_000), 'minute')\n+  }\n \n-  if (abs < 86_400_000) {return relativeFmt.format(sign * Math.round(abs / 3_600_000), 'hour')}\n+  if (abs < 86_400_000) {\n+    return relativeFmt.format(sign * Math.round(abs / 3_600_000), 'hour')\n+  }\n \n   return relativeFmt.format(sign * Math.round(abs / 86_400_000), 'day')\n }\n \n function nextRunMs(job: CronJob): null | number {\n-  if (!job.next_run_at) {return null}\n+  if (!job.next_run_at) {\n+    return null\n+  }\n \n   const ms = Date.parse(job.next_run_at)\n ","@@ -54,7 +69,9 @@ function nextRunMs(job: CronJob): null | number {\n // the timestamp is what tells them apart. Compact (no year, no seconds) for the\n // narrow sidebar.\n function formatRunTime(seconds?: null | number): string {\n-  if (!seconds) {return '—'}\n+  if (!seconds) {\n+    return '—'\n+  }\n \n   const date = new Date(seconds * 1000)\n ","@@ -90,11 +107,15 @@ export function SidebarCronJobsSection({\n   const [nowMs, setNowMs] = useState(() => Date.now())\n   // Single-open inline peek so the section stays scannable.\n   const [peekJobId, setPeekJobId] = useState<null | string>(null)\n+  // Rows revealed so far; starts compact, grows in steps via \"load more\".\n+  const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_JOBS)\n \n   // One clock for the whole section (rows are pure) so the countdowns tick\n   // without re-rendering the rest of the sidebar. Only runs while expanded.\n   useEffect(() => {\n-    if (!open) {return}\n+    if (!open) {\n+      return\n+    }\n \n     const id = window.setInterval(() => setNowMs(Date.now()), 1000)\n ","@@ -108,17 +129,25 @@ export function SidebarCronJobsSection({\n       const an = nextRunMs(a)\n       const bn = nextRunMs(b)\n \n-      if (an !== null && bn !== null && an !== bn) {return an - bn}\n+      if (an !== null && bn !== null && an !== bn) {\n+        return an - bn\n+      }\n \n-      if (an === null && bn !== null) {return 1}\n+      if (an === null && bn !== null) {\n+        return 1\n+      }\n \n-      if (an !== null && bn === null) {return -1}\n+      if (an !== null && bn === null) {\n+        return -1\n+      }\n \n       return jobTitle(a).localeCompare(jobTitle(b))\n     })\n   }, [jobs])\n \n-  const shown = sorted.slice(0, max)\n+  const cap = Math.min(visibleCount, max)\n+  const shown = sorted.slice(0, cap)\n+  const hiddenCount = Math.min(sorted.length, max) - shown.length\n   // When capped, signal \"50+\" rather than implying the list is complete.\n   const countLabel = jobs.length > max ? `${max}+` : String(jobs.length)\n ","@@ -139,7 +168,7 @@ export function SidebarCronJobsSection({\n         </button>\n       </div>\n       {open && (\n-        <SidebarGroupContent className=\"flex max-h-72 shrink-0 flex-col gap-px overflow-y-auto overscroll-contain pb-1.75\">\n+        <SidebarGroupContent className=\"flex max-h-72 flex-col gap-px overflow-y-auto overscroll-contain pb-1.75 compact:max-h-none compact:overflow-visible\">\n           {shown.map(job => (\n             <CronJobSidebarRow\n               expanded={peekJobId === job.id}","@@ -152,6 +181,12 @@ export function SidebarCronJobsSection({\n               onTrigger={() => onTriggerJob(job.id)}\n             />\n           ))}\n+          {hiddenCount > 0 && (\n+            <SidebarLoadMoreRow\n+              onClick={() => setVisibleCount(count => count + LOAD_MORE_STEP)}\n+              step={Math.min(LOAD_MORE_STEP, hiddenCount)}\n+            />\n+          )}\n         </SidebarGroupContent>\n       )}\n     </SidebarGroup>","@@ -181,11 +216,7 @@ function CronJobSidebarRow({\n   const next = nextRunMs(job)\n   const label = jobTitle(job)\n \n-  const meta = INACTIVE_STATES.has(state)\n-    ? (c.states[state] ?? state)\n-    : next !== null\n-      ? relativeTime(next, nowMs)\n-      : '—'\n+  const meta = INACTIVE_STATES.has(state) ? (c.states[state] ?? state) : next !== null ? relativeTime(next, nowMs) : '—'\n \n   return (\n     <div>","@@ -257,13 +288,7 @@ function CronJobSidebarRow({\n   )\n }\n \n-function CronJobSidebarRuns({\n-  jobId,\n-  onOpenRun\n-}: {\n-  jobId: string\n-  onOpenRun: (sessionId: string) => void\n-}) {\n+function CronJobSidebarRuns({ jobId, onOpenRun }: { jobId: string; onOpenRun: (sessionId: string) => void }) {\n   const { t } = useI18n()\n   const c = t.cron\n   const selectedSessionId = useStore($selectedStoredSessionId)","@@ -275,16 +300,22 @@ function CronJobSidebarRuns({\n     const load = () =>\n       getCronJobRuns(jobId, PEEK_RUN_LIMIT)\n         .then(result => {\n-          if (!cancelled) {setRuns(result)}\n+          if (!cancelled) {\n+            setRuns(result)\n+          }\n         })\n         .catch(() => {\n-          if (!cancelled) {setRuns(prev => prev ?? [])}\n+          if (!cancelled) {\n+            setRuns(prev => prev ?? [])\n+          }\n         })\n \n     void load()\n \n     const intervalId = window.setInterval(() => {\n-      if (document.visibilityState === 'visible') {void load()}\n+      if (document.visibilityState === 'visible') {\n+        void load()\n+      }\n     }, PEEK_POLL_INTERVAL_MS)\n \n     return () => {"]},{"file":"apps/desktop/src/app/chat/sidebar/index.tsx","hunks":["@@ -48,6 +48,7 @@ import {\n   $pinnedSessionIds,\n   $sidebarAgentsGrouped,\n   $sidebarCronOpen,\n+  $sidebarMessagingOpenIds,\n   $sidebarOpen,\n   $sidebarOverlayMounted,\n   $sidebarPinsOpen,","@@ -64,6 +65,7 @@ import {\n   setSidebarSessionOrderIds,\n   setSidebarWorkspaceOrderIds,\n   SIDEBAR_SESSIONS_PAGE_SIZE,\n+  toggleSidebarMessagingOpen,\n   unpinSession\n } from '@/store/layout'\n import {","@@ -93,12 +95,19 @@ import { SidebarPanelLabel } from '../../shell/sidebar-label'\n import type { SidebarNavItem } from '../../types'\n \n import { SidebarCronJobsSection } from './cron-jobs-section'\n+import { SidebarLoadMoreRow } from './load-more-row'\n import { ProfileRail } from './profile-switcher'\n import { SidebarSessionRow } from './session-row'\n import { VirtualSessionList } from './virtual-session-list'\n \n const VIRTUALIZE_THRESHOLD = 25\n \n+// Non-session groups (messaging platforms) stay compact: show a few rows up\n+// front, reveal more in larger steps on demand. Keeps a busy platform from\n+// dominating the sidebar before the user asks to see it.\n+const NON_SESSION_INITIAL_ROWS = 3\n+const NON_SESSION_LOAD_STEP = 10\n+\n // Render the modifier key the user actually presses on this platform. The\n // global accelerator is bound to both Cmd+N (macOS) and Ctrl+N (everywhere\n // else) in desktop-controller.tsx, but the hint should match muscle memory.","@@ -128,6 +137,16 @@ const WORKSPACE_PAGE = 5\n const PROFILE_INITIAL_PAGE = 5\n const GROUP_DND_ID_PREFIX = 'group:'\n \n+// Two modes via the `compact` height variant (styles.css):\n+//   tall    → each section is shrink-0, capped, its own scroller; Sessions is flex-1.\n+//   compact → COMPACT_FLAT drops the caps so the whole stack scrolls as one.\n+// Sections stay shrink-0 so none can be squeezed below its content and bleed onto\n+// the next — the flexbox `min-height: auto` overlap trap that caused the bug.\n+const COMPACT_FLAT = 'compact:max-h-none compact:overflow-visible'\n+\n+// A non-session group's scroll body: own scroller when tall, flattened when compact.\n+const GROUP_BODY = cn('overflow-y-auto overscroll-contain', COMPACT_FLAT)\n+\n const groupDndId = (id: string) => `${GROUP_DND_ID_PREFIX}${id}`\n \n const parseGroupDndId = (id: string) =>","@@ -334,7 +353,9 @@ export function ChatSidebar({\n   const [newSessionKbdFlash, setNewSessionKbdFlash] = useState(false)\n   const [profileLoadMorePending, setProfileLoadMorePending] = useState<Record<string, boolean>>({})\n   const [messagingLoadMorePending, setMessagingLoadMorePending] = useState<Record<string, boolean>>({})\n-  const [messagingOpen, setMessagingOpen] = useState<Record<string, boolean>>({})\n+  const messagingOpenIds = useStore($sidebarMessagingOpenIds)\n+  // Per-platform count of rows currently revealed (starts at NON_SESSION_INITIAL_ROWS).\n+  const [messagingVisible, setMessagingVisible] = useState<Record<string, number>>({})\n   const searchInputRef = useRef<HTMLInputElement>(null)\n   const trimmedQuery = searchQuery.trim()\n ","@@ -538,6 +559,18 @@ export function ChatSidebar({\n     [onLoadMoreMessaging]\n   )\n \n+  // Reveal another batch of a platform's rows; fetch from the backend too if we\n+  // run past what's loaded and more remain on disk.\n+  const revealMoreMessaging = (platform: string, loaded: number, hasMore: boolean) => {\n+    const next = (messagingVisible[platform] ?? NON_SESSION_INITIAL_ROWS) + NON_SESSION_LOAD_STEP\n+\n+    setMessagingVisible(prev => ({ ...prev, [platform]: next }))\n+\n+    if (next > loaded && hasMore) {\n+      loadMoreForMessaging(platform)\n+    }\n+  }\n+\n   // Each messaging platform is its own self-managed section: split the\n   // separately-fetched messaging slice by source, newest platform first, rows\n   // within a platform by recency. Per-platform totals (when a \"load more\" has","@@ -650,6 +683,12 @@ export function ChatSidebar({\n \n   const displayAgentGroups = showAllProfiles ? profileGroups : agentsGrouped ? agentGroups : undefined\n \n+  // The recents list owns its own (virtualized) scroll container only when it's a\n+  // long flat list. In that case it must keep its scroller even in short mode, so\n+  // we don't flatten it (flattening would defeat virtualization). Short flat lists\n+  // and grouped views flatten into the single outer scroll instead.\n+  const recentsVirtualizes = !displayAgentGroups?.length && displayAgentSessions.length >= VIRTUALIZE_THRESHOLD\n+\n   useEffect(() => {\n     if (!displayAgentGroups?.length || showAllProfiles) {\n       return","@@ -781,9 +820,7 @@ export function ChatSidebar({\n                       <item.icon className=\"size-4 shrink-0 text-[color-mix(in_srgb,currentColor_72%,transparent)]\" />\n                       {contentVisible && (\n                         <>\n-                          <span className=\"min-w-0 flex-1 truncate\">\n-                            {s.nav[item.id] ?? item.label}\n-                          </span>\n+                          <span className=\"min-w-0 flex-1 truncate\">{s.nav[item.id] ?? item.label}</span>\n                           {isNewSession && (\n                             <KbdGroup\n                               className={cn('ml-auto', newSessionKbdFlash && 'opacity-100!')}","@@ -812,175 +849,191 @@ export function ChatSidebar({\n           </div>\n         )}\n \n-        {contentVisible && showSessionSections && trimmedQuery && (\n-          <SidebarSessionsSection\n-            activeSessionId={activeSidebarSessionId}\n-            contentClassName=\"flex min-h-0 flex-1 flex-col gap-px overflow-y-auto overscroll-contain pb-1.75\"\n-            emptyState={\n-              <div className=\"grid min-h-24 place-items-center rounded-lg px-2 text-center text-xs text-(--ui-text-tertiary)\">\n-                {s.noMatch(trimmedQuery)}\n-              </div>\n-            }\n-            label={s.results}\n-            labelMeta={String(searchResults.length)}\n-            onArchiveSession={onArchiveSession}\n-            onDeleteSession={onDeleteSession}\n-            onResumeSession={onResumeSession}\n-            onToggle={() => undefined}\n-            onTogglePin={pinSession}\n-            open\n-            pinned={false}\n-            rootClassName=\"min-h-0 flex-1 p-0\"\n-            sessions={searchResults}\n-            workingSessionIdSet={workingSessionIdSet}\n-          />\n-        )}\n+        {contentVisible && showSessionSections && (\n+          <div className=\"flex min-h-0 flex-1 flex-col overflow-y-auto overscroll-contain pb-1.75\">\n+            {trimmedQuery && (\n+              <SidebarSessionsSection\n+                activeSessionId={activeSidebarSessionId}\n+                contentClassName=\"flex min-h-0 flex-1 flex-col gap-px overflow-y-auto overscroll-contain pb-1.75\"\n+                emptyState={\n+                  <div className=\"grid min-h-24 place-items-center rounded-lg px-2 text-center text-xs text-(--ui-text-tertiary)\">\n+                    {s.noMatch(trimmedQuery)}\n+                  </div>\n+                }\n+                label={s.results}\n+                labelMeta={String(searchResults.length)}\n+                onArchiveSession={onArchiveSession}\n+                onDeleteSession={onDeleteSession}\n+                onResumeSession={onResumeSession}\n+                onToggle={() => undefined}\n+                onTogglePin={pinSession}\n+                open\n+                pinned={false}\n+                rootClassName=\"min-h-32 flex-1 overflow-hidden p-0\"\n+                sessions={searchResults}\n+                workingSessionIdSet={workingSessionIdSet}\n+              />\n+            )}\n \n-        {contentVisible && showSessionSections && !trimmedQuery && (\n-          <SidebarSessionsSection\n-            activeSessionId={activeSidebarSessionId}\n-            contentClassName=\"flex min-h-10 shrink-0 flex-col gap-px rounded-lg pb-2 pt-1\"\n-            dndSensors={dndSensors}\n-            emptyState={<SidebarPinnedEmptyState />}\n-            label={s.pinned}\n-            onArchiveSession={onArchiveSession}\n-            onDeleteSession={onDeleteSession}\n-            onReorder={handlePinnedDragEnd}\n-            onResumeSession={onResumeSession}\n-            onToggle={() => setSidebarPinsOpen(!pinsOpen)}\n-            onTogglePin={unpinSession}\n-            open={pinsOpen}\n-            pinned\n-            rootClassName=\"shrink-0 p-0 pb-1\"\n-            sessions={pinnedSessions}\n-            sortable={pinnedSessions.length > 1}\n-            workingSessionIdSet={workingSessionIdSet}\n-          />\n-        )}\n+            {!trimmedQuery && (\n+              <SidebarSessionsSection\n+                activeSessionId={activeSidebarSessionId}\n+                contentClassName={cn('flex max-h-44 flex-col gap-px rounded-lg pb-2 pt-1', GROUP_BODY)}\n+                dndSensors={dndSensors}\n+                emptyState={<SidebarPinnedEmptyState />}\n+                label={s.pinned}\n+                onArchiveSession={onArchiveSession}\n+                onDeleteSession={onDeleteSession}\n+                onReorder={handlePinnedDragEnd}\n+                onResumeSession={onResumeSession}\n+                onToggle={() => setSidebarPinsOpen(!pinsOpen)}\n+                onTogglePin={unpinSession}\n+                open={pinsOpen}\n+                pinned\n+                rootClassName=\"shrink-0 p-0 pb-1\"\n+                sessions={pinnedSessions}\n+                sortable={pinnedSessions.length > 1}\n+                workingSessionIdSet={workingSessionIdSet}\n+              />\n+            )}\n \n-        {contentVisible && showSessionSections && !trimmedQuery && (\n-          <SidebarSessionsSection\n-            activeSessionId={activeSidebarSessionId}\n-            contentClassName={cn(\n-              'flex min-h-0 flex-1 flex-col overflow-y-auto overscroll-contain pb-1.75',\n-              // Separate profile sections clearly in the ALL view; rows inside\n-              // each group keep their own tight gap-px rhythm.\n-              showAllProfiles ? 'gap-3' : 'gap-px'\n+            {!trimmedQuery && (\n+              <SidebarSessionsSection\n+                activeSessionId={activeSidebarSessionId}\n+                contentClassName={cn(\n+                  'flex min-h-0 flex-1 flex-col overflow-y-auto overscroll-contain pb-1.75',\n+                  // Separate profile sections clearly in the ALL view; rows inside\n+                  // each group keep their own tight gap-px rhythm.\n+                  showAllProfiles ? 'gap-3' : 'gap-px',\n+                  // Flatten into the single scroll when compact — unless this is the\n+                  // virtualized long list, which must keep its own scroller.\n+                  !recentsVirtualizes && COMPACT_FLAT\n+                )}\n+                dndSensors={dndSensors}\n+                emptyState={showSessionSkeletons ? <SidebarSessionSkeletons /> : <SidebarAllPinnedState />}\n+                footer={\n+                  // Hide \"load more\" only when workspace-grouped (those groups page\n+                  // themselves). ALL-profiles now pages per-profile from each profile\n+                  // header; the global footer only applies to non-ALL views.\n+                  !showAllProfiles && !agentsGrouped && !showSessionSkeletons && hasMoreSessions ? (\n+                    <SidebarLoadMoreRow\n+                      loading={sessionsLoading}\n+                      onClick={onLoadMoreSessions}\n+                      step={Math.min(SIDEBAR_SESSIONS_PAGE_SIZE, remainingSessionCount)}\n+                    />\n+                  ) : null\n+                }\n+                forceEmptyState={showSessionSkeletons}\n+                groups={displayAgentGroups}\n+                headerAction={\n+                  // Always reserve the icon-xs (size-6) slot so the header keeps the\n+                  // same height whether or not the toggle renders — otherwise the\n+                  // \"Sessions\" label jumps when switching to the ALL-profiles view.\n+                  // Grouping operates on unpinned recents; if everything is pinned\n+                  // the toggle does nothing, and it's irrelevant in the ALL-profiles\n+                  // view (always grouped by profile), so hide the button (not the slot).\n+                  <div className=\"grid size-6 shrink-0 place-items-center\">\n+                    {!showAllProfiles && agentSessions.length > 0 ? (\n+                      <Tip label={agentsGrouped ? s.groupTitleGrouped : s.groupTitleUngrouped}>\n+                        <Button\n+                          aria-label={agentsGrouped ? s.groupAriaGrouped : s.groupAriaUngrouped}\n+                          className={cn(\n+                            'text-(--ui-text-tertiary) opacity-70 hover:bg-(--ui-control-hover-background) hover:text-foreground hover:opacity-100 focus-visible:opacity-100',\n+                            agentsGrouped && 'bg-(--ui-control-active-background) text-foreground opacity-100'\n+                          )}\n+                          onClick={event => {\n+                            event.stopPropagation()\n+                            setSidebarRecentsOpen(true)\n+                            setSidebarAgentsGrouped(!agentsGrouped)\n+                          }}\n+                          size=\"icon-xs\"\n+                          variant=\"ghost\"\n+                        >\n+                          <Codicon name={agentsGrouped ? 'list-unordered' : 'root-folder'} size=\"0.75rem\" />\n+                        </Button>\n+                      </Tip>\n+                    ) : null}\n+                  </div>\n+                }\n+                label={s.sessions}\n+                labelMeta={recentsMeta}\n+                onArchiveSession={onArchiveSession}\n+                onDeleteSession={onDeleteSession}\n+                onNewSessionInWorkspace={showAllProfiles ? undefined : onNewSessionInWorkspace}\n+                onReorder={showAllProfiles ? undefined : handleAgentDragEnd}\n+                onResumeSession={onResumeSession}\n+                onToggle={() => setSidebarRecentsOpen(!agentsOpen)}\n+                onTogglePin={pinSession}\n+                open={agentsOpen}\n+                pinned={false}\n+                rootClassName={cn(\n+                  'min-h-32 flex-1 overflow-hidden p-0',\n+                  !recentsVirtualizes && 'compact:min-h-0 compact:flex-none compact:overflow-visible'\n+                )}\n+                sessions={displayAgentSessions}\n+                sortable={!showAllProfiles && agentSessions.length > 1}\n+                workingSessionIdSet={workingSessionIdSet}\n+              />\n             )}\n-            dndSensors={dndSensors}\n-            emptyState={showSessionSkeletons ? <SidebarSessionSkeletons /> : <SidebarAllPinnedState />}\n-            footer={\n-              // Hide \"load more\" only when workspace-grouped (those groups page\n-              // themselves). ALL-profiles now pages per-profile from each profile\n-              // header; the global footer only applies to non-ALL views.\n-              !showAllProfiles && !agentsGrouped && !showSessionSkeletons && hasMoreSessions ? (\n-                <SidebarLoadMoreRow\n-                  loading={sessionsLoading}\n-                  onClick={onLoadMoreSessions}\n-                  step={Math.min(SIDEBAR_SESSIONS_PAGE_SIZE, remainingSessionCount)}\n-                />\n-              ) : null\n-            }\n-            forceEmptyState={showSessionSkeletons}\n-            groups={displayAgentGroups}\n-            headerAction={\n-              // Always reserve the icon-xs (size-6) slot so the header keeps the\n-              // same height whether or not the toggle renders — otherwise the\n-              // \"Sessions\" label jumps when switching to the ALL-profiles view.\n-              // Grouping operates on unpinned recents; if everything is pinned\n-              // the toggle does nothing, and it's irrelevant in the ALL-profiles\n-              // view (always grouped by profile), so hide the button (not the slot).\n-              <div className=\"grid size-6 shrink-0 place-items-center\">\n-                {!showAllProfiles && agentSessions.length > 0 ? (\n-                  <Tip label={agentsGrouped ? s.groupTitleGrouped : s.groupTitleUngrouped}>\n-                    <Button\n-                      aria-label={agentsGrouped ? s.groupAriaGrouped : s.groupAriaUngrouped}\n-                      className={cn(\n-                        'text-(--ui-text-tertiary) opacity-70 hover:bg-(--ui-control-hover-background) hover:text-foreground hover:opacity-100 focus-visible:opacity-100',\n-                        agentsGrouped && 'bg-(--ui-control-active-background) text-foreground opacity-100'\n-                      )}\n-                      onClick={event => {\n-                        event.stopPropagation()\n-                        setSidebarRecentsOpen(true)\n-                        setSidebarAgentsGrouped(!agentsGrouped)\n-                      }}\n-                      size=\"icon-xs\"\n-                      variant=\"ghost\"\n-                    >\n-                      <Codicon name={agentsGrouped ? 'list-unordered' : 'root-folder'} size=\"0.75rem\" />\n-                    </Button>\n-                  </Tip>\n-                ) : null}\n-              </div>\n-            }\n-            label={s.sessions}\n-            labelMeta={recentsMeta}\n-            onArchiveSession={onArchiveSession}\n-            onDeleteSession={onDeleteSession}\n-            onNewSessionInWorkspace={showAllProfiles ? undefined : onNewSessionInWorkspace}\n-            onReorder={showAllProfiles ? undefined : handleAgentDragEnd}\n-            onResumeSession={onResumeSession}\n-            onToggle={() => setSidebarRecentsOpen(!agentsOpen)}\n-            onTogglePin={pinSession}\n-            open={agentsOpen}\n-            pinned={false}\n-            rootClassName=\"min-h-0 flex-1 p-0\"\n-            sessions={displayAgentSessions}\n-            sortable={!showAllProfiles && agentSessions.length > 1}\n-            workingSessionIdSet={workingSessionIdSet}\n-          />\n-        )}\n \n-        {contentVisible && showSessionSections && !trimmedQuery &&\n-          messagingGroups.map(group => (\n-            <SidebarSessionsSection\n-              activeSessionId={activeSidebarSessionId}\n-              contentClassName=\"flex max-h-56 shrink-0 flex-col gap-px overflow-y-auto overscroll-contain pb-1.75\"\n-              emptyState={null}\n-              footer={\n-                group.hasMore ? (\n-                  <SidebarLoadMoreRow\n-                    loading={Boolean(messagingLoadMorePending[group.sourceId])}\n-                    onClick={() => loadMoreForMessaging(group.sourceId)}\n-                    step={Math.max(0, group.total - group.sessions.length)}\n+            {!trimmedQuery &&\n+              messagingGroups.map(group => {\n+                const visible = messagingVisible[group.sourceId] ?? NON_SESSION_INITIAL_ROWS\n+                const shownSessions = group.sessions.slice(0, visible)\n+                // More to show if rows are hidden behind the cap, or the backend\n+                // still has older threads on disk.\n+                const canRevealMore = visible < group.sessions.length || group.hasMore\n+\n+                return (\n+                  <SidebarSessionsSection\n+                    activeSessionId={activeSidebarSessionId}\n+                    contentClassName={cn('flex max-h-56 flex-col gap-px pb-1.75', GROUP_BODY)}\n+                    emptyState={null}\n+                    footer={\n+                      canRevealMore ? (\n+                        <SidebarLoadMoreRow\n+                          loading={Boolean(messagingLoadMorePending[group.sourceId])}\n+                          onClick={() => revealMoreMessaging(group.sourceId, group.sessions.length, group.hasMore)}\n+                          step={Math.min(NON_SESSION_LOAD_STEP, Math.max(0, group.total - shownSessions.length))}\n+                        />\n+                      ) : null\n+                    }\n+                    key={group.sourceId}\n+                    label={group.label}\n+                    labelIcon={\n+                      <PlatformAvatar\n+                        className=\"size-4 rounded-[4px] text-[0.5625rem] [&_svg]:size-3\"\n+                        platformId={group.sourceId}\n+                        platformName={group.label}\n+                      />\n+                    }\n+                    labelMeta={countLabel(group.sessions.length, group.total)}\n+                    onArchiveSession={onArchiveSession}\n+                    onDeleteSession={onDeleteSession}\n+                    onResumeSession={onResumeSession}\n+                    onToggle={() => toggleSidebarMessagingOpen(group.sourceId)}\n+                    onTogglePin={pinSession}\n+                    open={messagingOpenIds.includes(group.sourceId)}\n+                    pinned={false}\n+                    rootClassName=\"shrink-0 p-0\"\n+                    sessions={shownSessions}\n+                    workingSessionIdSet={workingSessionIdSet}\n                   />\n-                ) : null\n-              }\n-              key={group.sourceId}\n-              label={group.label}\n-              labelIcon={\n-                <PlatformAvatar\n-                  className=\"size-4 rounded-[4px] text-[0.5625rem] [&_svg]:size-3\"\n-                  platformId={group.sourceId}\n-                  platformName={group.label}\n-                />\n-              }\n-              labelMeta={countLabel(group.sessions.length, group.total)}\n-              onArchiveSession={onArchiveSession}\n-              onDeleteSession={onDeleteSession}\n-              onResumeSession={onResumeSession}\n-              onToggle={() =>\n-                setMessagingOpen(prev => ({ ...prev, [group.sourceId]: prev[group.sourceId] === false }))\n-              }\n-              onTogglePin={pinSession}\n-              open={messagingOpen[group.sourceId] !== false}\n-              pinned={false}\n-              rootClassName=\"shrink-0 p-0\"\n-              sessions={group.sessions}\n-              workingSessionIdSet={workingSessionIdSet}\n-            />\n-          ))}\n-\n-        {contentVisible && !trimmedQuery && cronJobs.length > 0 && (\n-          <SidebarCronJobsSection\n-            jobs={cronJobs}\n-            label={s.cronJobs}\n-            onManageJob={onManageCronJob}\n-            onOpenRun={onResumeSession}\n-            onToggle={() => setSidebarCronOpen(!cronOpen)}\n-            onTriggerJob={onTriggerCronJob}\n-            open={cronOpen}\n-          />\n+                )\n+              })}\n+\n+            {!trimmedQuery && cronJobs.length > 0 && (\n+              <SidebarCronJobsSection\n+                jobs={cronJobs}\n+                label={s.cronJobs}\n+                onManageJob={onManageCronJob}\n+                onOpenRun={onResumeSession}\n+                onToggle={() => setSidebarCronOpen(!cronOpen)}\n+                onTriggerJob={onTriggerCronJob}\n+                open={cronOpen}\n+              />\n+            )}\n+          </div>\n         )}\n \n         {contentVisible && !showSessionSections && <div className=\"min-h-0 flex-1\" />}","@@ -1222,6 +1275,7 @@ function SidebarSessionsSection({\n     inner = (\n       <VirtualSessionList\n         activeSessionId={activeSessionId}\n+        className={contentClassName}\n         onArchiveSession={onArchiveSession}\n         onDeleteSession={onDeleteSession}\n         onResumeSession={onResumeSession}","@@ -1446,30 +1500,3 @@ interface SortableSessionRowProps {\n function SortableSidebarSessionRow(props: SortableSessionRowProps) {\n   return <SidebarSessionRow {...props} {...useSortableBindings(props.session.id)} />\n }\n-\n-interface SidebarLoadMoreRowProps {\n-  loading: boolean\n-  onClick: () => void\n-  step: number\n-}\n-\n-function SidebarLoadMoreRow({ loading, onClick, step }: SidebarLoadMoreRowProps) {\n-  const { t } = useI18n()\n-  const label = loading ? t.sidebar.loading : step > 0 ? t.sidebar.loadCount(step) : t.sidebar.loadMore\n-\n-  return (\n-    <button\n-      className=\"flex min-h-5 items-center gap-1.5 self-start bg-transparent pl-2 text-left text-[0.6875rem] text-(--ui-text-tertiary) transition-colors duration-100 ease-out hover:text-foreground hover:transition-none disabled:cursor-default disabled:opacity-60 disabled:hover:text-(--ui-text-tertiary)\"\n-      disabled={loading}\n-      onClick={onClick}\n-      type=\"button\"\n-    >\n-      {/* Seat the icon in the same w-3.5 column session rows use for their dot\n-          so the chevron + label line up with the rows above. */}\n-      <span className=\"grid w-3.5 shrink-0 place-items-center\">\n-        <Codicon className=\"opacity-70\" name={loading ? 'loading' : 'chevron-down'} size=\"0.75rem\" spinning={loading} />\n-      </span>\n-      <span>{label}</span>\n-    </button>\n-  )\n-}"]},{"file":"apps/desktop/src/app/chat/sidebar/load-more-row.tsx","hunks":["@@ -0,0 +1,30 @@\n+import { Codicon } from '@/components/ui/codicon'\n+import { useI18n } from '@/i18n'\n+\n+interface SidebarLoadMoreRowProps {\n+  step: number\n+  onClick: () => void\n+  loading?: boolean\n+}\n+\n+// \"Load N more\" affordance shared by the recents, messaging, and cron sections.\n+// The chevron sits in the same w-3.5 column the rows use for their dot, so it\n+// lines up with the list above.\n+export function SidebarLoadMoreRow({ step, onClick, loading = false }: SidebarLoadMoreRowProps) {\n+  const { t } = useI18n()\n+  const label = loading ? t.sidebar.loading : step > 0 ? t.sidebar.loadCount(step) : t.sidebar.loadMore\n+\n+  return (\n+    <button\n+      className=\"flex min-h-5 items-center gap-1.5 self-start bg-transparent pl-2 text-left text-[0.6875rem] text-(--ui-text-tertiary) transition-colors duration-100 ease-out hover:text-foreground hover:transition-none disabled:cursor-default disabled:opacity-60 disabled:hover:text-(--ui-text-tertiary)\"\n+      disabled={loading}\n+      onClick={onClick}\n+      type=\"button\"\n+    >\n+      <span className=\"grid w-3.5 shrink-0 place-items-center\">\n+        <Codicon className=\"opacity-70\" name={loading ? 'loading' : 'chevron-down'} size=\"0.75rem\" spinning={loading} />\n+      </span>\n+      <span>{label}</span>\n+    </button>\n+  )\n+}"]},{"file":"apps/desktop/src/store/layout.ts","hunks":["@@ -23,6 +23,7 @@ export const SIDEBAR_SESSIONS_PAGE_SIZE = 50\n const SIDEBAR_PINNED_STORAGE_KEY = 'hermes.desktop.pinnedSessions'\n const SIDEBAR_AGENTS_GROUPED_STORAGE_KEY = 'hermes.desktop.agentsGroupedByWorkspace'\n const SIDEBAR_CRON_OPEN_STORAGE_KEY = 'hermes.desktop.sidebarCronOpen'\n+const SIDEBAR_MESSAGING_OPEN_STORAGE_KEY = 'hermes.desktop.sidebarMessagingOpen'\n const SIDEBAR_SESSION_ORDER_STORAGE_KEY = 'hermes.desktop.sessionOrder'\n const SIDEBAR_WORKSPACE_ORDER_STORAGE_KEY = 'hermes.desktop.workspaceOrder'\n const PANES_FLIPPED_STORAGE_KEY = 'hermes.desktop.panesFlipped'","@@ -68,6 +69,10 @@ export const $sidebarRecentsOpen = atom(true)\n // default (it only renders at all when cron sessions exist) so the\n // scheduler's `[IMPORTANT: …]` first-message previews don't spam recents.\n export const $sidebarCronOpen = atom(storedBoolean(SIDEBAR_CRON_OPEN_STORAGE_KEY, false))\n+// Messaging platform sections collapse by default (they can be numerous and\n+// tall). We persist the ids the user has *explicitly expanded*, so the default\n+// stays collapsed unless they've opened a platform before.\n+export const $sidebarMessagingOpenIds = atom<string[]>(storedStringArray(SIDEBAR_MESSAGING_OPEN_STORAGE_KEY))\n export const $sidebarAgentsGrouped = atom(storedBoolean(SIDEBAR_AGENTS_GROUPED_STORAGE_KEY, false))\n // When true, the sessions sidebar moves to the right and the file browser +\n // preview rail move to the left — a mirror of the default layout.","@@ -77,6 +82,7 @@ export const $sessionsLimit = atom(SIDEBAR_SESSIONS_PAGE_SIZE)\n \n $pinnedSessionIds.subscribe(ids => persistStringArray(SIDEBAR_PINNED_STORAGE_KEY, [...ids]))\n $sidebarCronOpen.subscribe(open => persistBoolean(SIDEBAR_CRON_OPEN_STORAGE_KEY, open))\n+$sidebarMessagingOpenIds.subscribe(ids => persistStringArray(SIDEBAR_MESSAGING_OPEN_STORAGE_KEY, [...ids]))\n $sidebarSessionOrderIds.subscribe(ids => persistStringArray(SIDEBAR_SESSION_ORDER_STORAGE_KEY, [...ids]))\n $sidebarWorkspaceOrderIds.subscribe(ids => persistStringArray(SIDEBAR_WORKSPACE_ORDER_STORAGE_KEY, [...ids]))\n $sidebarAgentsGrouped.subscribe(grouped => persistBoolean(SIDEBAR_AGENTS_GROUPED_STORAGE_KEY, grouped))","@@ -139,6 +145,14 @@ export function setSidebarCronOpen(open: boolean) {\n   $sidebarCronOpen.set(open)\n }\n \n+export function toggleSidebarMessagingOpen(sourceId: string) {\n+  const current = $sidebarMessagingOpenIds.get()\n+\n+  $sidebarMessagingOpenIds.set(\n+    current.includes(sourceId) ? current.filter(id => id !== sourceId) : [...current, sourceId]\n+  )\n+}\n+\n export function setSidebarAgentsGrouped(grouped: boolean) {\n   $sidebarAgentsGrouped.set(grouped)\n }"]},{"file":"apps/desktop/src/styles.css","hunks":["@@ -5,6 +5,10 @@\n @import '@vscode/codicons/dist/codicon.css';\n @custom-variant dark (&:is(.dark *));\n \n+/* Sidebar sections: tall viewports give each its own scroller; compact ones\n+   (this variant) flatten everything into one shared scroll. See ChatSidebar. */\n+@custom-variant compact (@media (max-height: 768px));\n+\n @font-face {\n   font-family: 'Collapse';\n   font-style: normal;","@@ -266,10 +270,12 @@\n     --dt-user-bubble: var(--ui-chat-bubble-background);\n     --dt-user-bubble-border: var(--ui-stroke-tertiary);\n \n-    --dt-font-sans: 'Segoe WPC', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif,\n-      'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji', emoji;\n-    --dt-font-mono: 'Cascadia Code', 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, Consolas, monospace,\n+    --dt-font-sans:\n+      'Segoe WPC', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif,\n       'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji', emoji;\n+    --dt-font-mono:\n+      'Cascadia Code', 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, Consolas, monospace, 'Apple Color Emoji',\n+      'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji', emoji;\n     --dt-base-size: 1rem;\n     --dt-line-height: 1.5;\n     --dt-letter-spacing: 0;","@@ -914,7 +920,11 @@ canvas {\n \n   display: block;\n   inline-size: var(--fit-available-space);\n-  font-size: clamp(var(--fit-min, 1em), 1em * var(--fit-ratio), var(--fit-max, infinity * 1px) - var(--fit-support-sentinel));\n+  font-size: clamp(\n+    var(--fit-min, 1em),\n+    1em * var(--fit-ratio),\n+    var(--fit-max, infinity * 1px) - var(--fit-support-sentinel)\n+  );\n }\n \n @container (inline-size > 0) {\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}