fix(desktop): honest sidebar load-more — source-scoped lists, matching totals, pin-aware counts - #151
Merged
Merged
Conversation
… match listable rows The sidebar's load-more math was structurally dishonest: the recents fetch had no source filter, so cron and messaging-platform sessions (129 cron + 92 telegram in the primary profile today) rode the unified list and inflated every total, while the cron/messaging sidebar slices documented in the session store had NO writers at all — the messaging sections never rendered and a pinned cron run could only resolve while it happened to sit in the recents page. Restore the source-scoping machinery the fork merge dropped (upstream carries it): /api/sessions and /api/profiles/sessions accept source= / exclude_sources= and thread them into BOTH the page query and the count (session_count/surfaced_session_count gain exclude_sources), so a section's total can only count rows that section can ever list. The desktop fetches recents with exclude_sources=cron+messaging, the cron slice with source=cron, the messaging slice with the inverse exclusion split per platform, and per-platform pagers resolve exact totals. The recents fetch is also scoped to the active profile, with a scope-change effect (identity-stable refreshSessions variant of upstream's reactive dep) so switching to a small profile refetches instead of showing the previous scope's page. Tests: wiring-capture test for both params on page+count (the exact 'param silently dropped in a merge' failure mode this repairs), plus a real-DB test pinning rows==total for all three sidebar slices. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nned rows The Sessions label counted unpinned rows against a total that includes pinned ones, so pinning a single row left the count stuck at '17/18' forever — advertising a page that can never arrive (the operator's exact report). Pinned rows are always loaded (the refresh keep-set preserves them), so drop them from BOTH sides of the label; the footer already used pin-consistent math and now agrees with the label in the unpinned universe. Messaging sections get the same honesty: pinned rows are hidden from the section (they live in Pinned) but still count as LOADED for the count label and per-platform load-more, so hiding them can't make the pager think more rows remain on disk. Section recency now comes from every loaded row so a platform doesn't reshuffle when its newest thread gets pinned, and a platform only drops its section when it has nothing left to show AND nothing more on disk. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
🔎 Lint report:
|
| Rule | Count |
|---|---|
invalid-argument-type |
6 |
invalid-parameter-default |
1 |
First entries
hermes_cli/web_server.py:1814: [invalid-argument-type] invalid-argument-type: Argument to bound method `SessionDB.list_sessions_rich` is incorrect: Expected `list[str]`, found `(list[str] & ~AlwaysFalsy) | None`
hermes_cli/web_server.py:1750: [invalid-parameter-default] invalid-parameter-default: Default value of type `None` is not assignable to annotated parameter type `str`
hermes_cli/web_server.py:1717: [invalid-argument-type] invalid-argument-type: Argument to bound method `SessionDB.surfaced_session_count` is incorrect: Expected `list[str]`, found `(list[str] & ~AlwaysFalsy) | None`
hermes_cli/web_server.py:1813: [invalid-argument-type] invalid-argument-type: Argument to bound method `SessionDB.list_sessions_rich` is incorrect: Expected `str`, found `(str & ~AlwaysFalsy) | None`
hermes_cli/web_server.py:1824: [invalid-argument-type] invalid-argument-type: Argument to bound method `SessionDB.session_count` is incorrect: Expected `list[str]`, found `(list[str] & ~AlwaysFalsy) | None`
hermes_cli/web_server.py:1823: [invalid-argument-type] invalid-argument-type: Argument to bound method `SessionDB.session_count` is incorrect: Expected `str`, found `(str & ~AlwaysFalsy) | None`
hermes_cli/web_server.py:1716: [invalid-argument-type] invalid-argument-type: Argument to bound method `SessionDB.surfaced_session_count` is incorrect: Expected `str`, found `(str & ~AlwaysFalsy) | None`
✅ Fixed issues: none
Unchanged: 5544 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The operator reported "Load more" appearing when nothing visibly overflows, and count labels that never settle (the morning recording showed
Sessions 17/18with everything loaded). Investigation found the affordance was structurally dishonest, for three independent reasons:store/session.tshad no writers at all (setCronSessions/setMessagingSessionsare referenced only in the store): the per-platform messaging sections never rendered, and a pinned cron run resolved only while it happened to sit in the recents page. The source-scoping machinery upstream carries (source=/exclude_sources=on both session endpoints + the controller slice fetches) was dropped in a fork merge.exclude_sourcesonsession_count/surfaced_session_count, any filtered page was paired with an unfiltered total — "Load more" then advertises pages that can never arrive.17/18forever after pinning one row), and the messaging sections (post drag-to-pin) treated pinned rows as not-loaded, over-reporting their pagers.What changed
Commit 1 —
fix(desktop): restore source-scoped session lists…(port of upstream's design, fork-only):hermes_state.py—session_count+surfaced_session_countgainexclude_sources(mirroringlist_sessions_rich, which already had it), so counts can apply the exact filter their paired page uses.hermes_cli/web_server.py—/api/sessionsand/api/profiles/sessionsacceptsource=/exclude_sources=(comma-separated) and thread them into BOTH the page query and the count, per profile on the unified endpoint.apps/desktop/src/hermes.ts—SessionSourceFilter+ thefilterargument onlistAllProfileSessions.apps/desktop/src/app/desktop-controller.tsx— recents fetch excludescron+ messaging sources;refreshCronSessions(source=cron, pin resolution),refreshMessagingSessions(inverse exclusion, split per platform, truncation flag), andloadMoreMessagingForPlatform(per-source pager resolving exact totals) now actually populate the store slices; recents are scoped to the active profile with a scope-change refetch effect (identity-stable variant of upstream's reactive dep —refreshSessionsflows intouseGatewayBoot/usePromptActions, so its identity is kept stable and the effect owns the refetch);onLoadMoreMessagingis finally passed to the sidebar.tests/hermes_cli/test_web_server.py— wiring-capture test asserting both params reach page AND count on/api/sessions(the exact "param silently dropped in a merge" failure mode), plus a seeded-DB test pinningtotal == listable rowsfor all three slices (recents-excluded, cron, telegram) on/api/profiles/sessions.Commit 2 —
fix(desktop): make sidebar count labels and load-more honest about pinned rows(upstream-relevant):apps/desktop/src/app/chat/sidebar/index.tsx— pinned rows are dropped from BOTH sides of the Sessions label (they're always loaded via the refresh keep-set, so the unpinned remainder is identical in both universes and the label now agrees with the footer); messaging sections count pinned rows as loaded (loadedCount) for their label + pager while still housing the rows in Pinned, take section recency from every loaded row (no reshuffle when the newest thread gets pinned), and only drop a section when it has nothing to show AND nothing more on disk.How to review
hermes_state.py— the two count functions; the added clause is byte-identical tolist_sessions_rich's.web_server.py— both handlers threadsource/exclude_listto page + count; nothing else moved.desktop-controller.tsx—SIDEBAR_EXCLUDED_SOURCES/MESSAGING_EXCLUDED_SOURCESconstants and the three new callbacks; note the comment explaining the deliberate deviation from upstream (scope read at call time + separate effect, instead of a reactive dep).sidebar/index.tsx—pinnedFromRecentsCount/agentKnownTotalfor the label;loadedCount/latestActivityinmessagingGroupsand their two render-site uses.Evidence
tests/hermes_cli/test_web_server.py: 248 passed;tests/test_hermes_state.py -k "count or list_sessions": 21 passed.state.db, auth header included): old vs new recents paths return consistentrows == totalwith zero rows lost;source=cron/source=telegramslices return exactly their own rows with matching totals. (The live DB currently lists few unarchived roots — the operator ran the freshly-restored Archive All earlier today — so the synthetic-DB test carries the cardinality assertions.)Sessions 4with no Load more (previously4/5+ phantom footer); bumping the total to 9 → header4/8+ footer "Load 4 more" (label and footer agree in the unpinned universe); Discord section with both conversations loaded and one pinned → headerDiscord 2, one row rendered, no phantom per-platform Load more.npm run type-check,npx eslint(all touched files), productionvite build: pass. Full renderer vitest suite: failure set byte-identical to the feat(desktop): drag-to-pin follow-ups — positional drop, messaging pins render, drag hint #146 baseline run (diff-verified).Verification
…/.venv/bin/python -m pytest tests/hermes_cli/test_web_server.py -q→ 248 passed.npx vitest run --environment jsdom→ no new failures vs the recorded baseline set (diffclean).npm run type-check/npx eslint …/npm run build→ pass.HERMES_HOME=<copy> python -c "...TestClient..."withX-Hermes-Session-Tokenheader —/api/profiles/sessions?exclude_sources=cron,telegram,…returnstotal == rowsfor the operator's data.Risks / gaps
$sessions(the merge keep-set preserves pinned ones indefinitely); they don't render in recents (pin-filtered) and only feed pin resolution — accepted, self-corrects for unpinned rows on the next page fetch.exclude_sourcesuses SQLNOT IN, which drops NULL-source rows; the operator's DBs have zero NULL sources (verified: cli/tui/cron/telegram/api_server/unknown) and the schema declaressource TEXT NOT NULL— non-actionable here, and matching upstream's exact semantics keeps the port faithful.message_count=0(the operator's 92 telegram rows today) stay invisible under themin_messages=1policy shared with recents — pre-existing policy, out of scope; noted on MeshBoard task hermes-desktop-sidebar-load-more-honesty.Collaborators