Spliced from contributor PRs #1497 (Thanatos-Z) and #1513 (AlexeyDsov), which
both added the ability to filter the sidebar to sessions with no project_id
assigned. Lands here as a focused PR with the best of both:
## Synthesis decisions
- **Sentinel constant approach** (from #1497, Thanatos-Z): single state
variable (`_activeProject` set to `NO_PROJECT_FILTER` sentinel) instead
of a parallel `_showNoneProject` boolean. No two-state-machine ambiguity,
no risk of "All" + "Unassigned" both reading active. Clicking "All"
automatically clears the unassigned filter because there is only one
variable to reset.
- **Conditional rendering** (from #1497): the chip only appears when
there are actually unassigned sessions to filter to (`hasUnprojected`).
Common case where every session is organized → chip stays hidden,
uncluttered chip bar. The project-bar itself also renders when there
are unassigned sessions (was previously gated on `_allProjects.length`).
- **Dashed-border visual treatment** (from #1497): `.project-chip.no-project
{border-style:dashed;}` distinguishes the chip from real project chips
so it reads as a meta-filter ("things without a project") rather than
another project. Subtle but present.
- **"Unassigned" label** (new): clearer than #1497s "No project" (which
reads like a status filter) or #1513s "None" (which is ambiguous —
none of what?). Matches the conventional file-manager / task-tracker
mental model: "things not yet assigned to a category." Tooltip elaborates:
"Show conversations not yet assigned to a project."
- **Branched empty-state copy**: when the Unassigned filter is active
and the result is empty, show "No unassigned sessions." instead of
the generic "No sessions in this project yet."
## Tests
7 new tests in tests/test_sidebar_unassigned_filter.py pin every contract:
sentinel constant declared; filter logic uses !s.project_id when sentinel
is active; chip only renders when hasUnprojected; chip label and click
handler; visual treatment (dashed border + .no-project class); empty-state
copy branches on the active filter; All chip handler clears _activeProject
to null (would catch a regression if a parallel _showNoneProject boolean
is ever reintroduced).
Local full suite: 3929 → 3936 passing (+7).
Live verified at port 8789 with seeded data (5 projects + 73 unassigned
sessions in active profile): chip appears between "All" and project chips
when unassigned sessions exist; click cycles correctly; clicking a real
project hides the Unassigned chip from active state; clicking "All"
deactivates everything; dashed border present per getComputedStyle.
Co-authored-by: Thanatos-Z <thanatos-z@users.noreply.github.com>
Co-authored-by: Alexey Denisov <AlexeyDsov@users.noreply.github.com>