Skip to content

Add sidebar thread folders (folders view, scoped archived views, shared components)#258

Open
brsbl wants to merge 58 commits into
mainfrom
bb/otto-s1-sidebar-nested-folders-thr_js7dkc3iwv
Open

Add sidebar thread folders (folders view, scoped archived views, shared components)#258
brsbl wants to merge 58 commits into
mainfrom
bb/otto-s1-sidebar-nested-folders-thr_js7dkc3iwv

Conversation

@brsbl

@brsbl brsbl commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds thread folders to the sidebar — group personal threads into folders via a dedicated Folders organization mode — and reworks the sidebar's view‑options and archived surfaces to use shared components with variants, so the project view and folders view can't drift. Built on the otto‑s1 spec, then refined across QA.

Folders

  • Group personal threads into folders; toggle via Organize → Project / Folders.
  • Folder rows mirror project rows: a New‑thread (+) button plus a menu with View archived threads / Rename / Remove.
  • Explicit folder creation (New folder); rename/remove update member threads.
  • Drag a thread onto a folder to file it. Spring‑loaded drop: after a short hover the folder auto‑expands and shows an optimistic preview row — deferred so dragging through/out of a folder no longer sticks.

Organize & Sort menus

  • Split into two triggers: Organize (Layers icon) and Sort (up/down‑arrows icon).
  • Sort shows a single state arrow per field — none / ↓ descending / ↑ ascending; re‑clicking the active field flips direction. Alphabetical defaults to A→Z.
  • Fixed an alphabetical‑sort bug where leaf threads and folders sorted in opposite directions.
  • Menus size to their content; section labels match the standard dropdown hierarchy.

Archived views

  • One ArchivedThreadsView for every scope (project / personal‑loose / folder); the scope shows in the AppLayout breadcrumb (Threads › <folder> › Archived), not a bespoke in‑body heading.
  • Per‑folder View archived threads (/archived?folder=…). The personal/loose list now excludes foldered threads via a new unfiled filter. Menu items renamed to "View archived threads".

Shared components (the no‑duplication goal)

  • ManualThreadTreeItems — the single place that maps tree items to rows for every view (project, chronological, folders), plus ThreadTreeLoadingSkeleton.
  • SidebarDisplayOptionsActions + SidebarThreadsSectionActions — shared header action clusters used by both modes.
  • ArchivedThreadsView + breadcrumb variants for the archived pages.

Other polish

  • Path‑missing project rows show only the red warning icon, flush right (no /+).
  • Sidebar tooltips match the agent message action bar (300ms open delay).
  • Restored the sidebar New‑thread label; dropped "folder" from the path‑missing tooltip + aria‑label.

Server / data

  • Optional folderPath and unfiled filters on the thread‑list query, threaded contract → route → db, with db regression tests.

Testing

  • typecheck (30/30), lint, and prettier --check clean.
  • Sidebar unit tests including new sortComparator (sort direction + leaf/folder consistency) and db folderPath/unfiled filter tests.
  • Archived filters verified end‑to‑end against the dev server.
  • ⚠️ Drag‑and‑drop and tooltip timing verified manually (no headless browser available).

Notes

  • Nested folder creation is descoped in the UI (folders are flat); the data model still stores full folder paths.

🤖 Generated with Claude Code

brsbl and others added 30 commits June 18, 2026 18:27
First session of the Sidebar Nested Folders feature: the client-only
foundation that later sessions build on.

- folderPath.ts: pure parseThreadFolderPath / normalizeThreadTitle /
  titleCreatesFolder, plus buildFolderKey. "/" in a thread title is read
  as a folder separator; titles are split → trimmed → emptied → re-joined.
- folderPath.test.ts: covers every normalization rule and the
  titleCreatesFolder / buildFolderKey boundaries (18 cases).
- sidebarCollapsedAtoms.ts: three persisted prefs mirroring the existing
  atom pattern — sidebarGroupByAtom ("none"|"folder", default "none"),
  sidebarCollapsedFoldersAtom (string[]), folderOnboardingSeenAtom (bool).

No rendering, tree-building, or rename changes yet (later sessions).
Tests, typecheck, and lint pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the opt-in "Group by: Folder" sidebar mode: top-level threads fold
into nested, collapsible folders derived from "/" in their titles. Pure
derived rendering — no DB/API/daemon changes.

Assembly (S2):
- SidebarFolderGroup item variant + bucketIntoFolders helper, folding the
  top-level item list into a nested folder tree. Folders render as a block
  above loose threads; folders, contents, and nested subfolders are each
  ordered by the active comparator (folders by their representative
  descendant). One parentKey-aware orderSiblingItems seam so S5 manual sort
  can swap ordering per list without re-cutting the tree walk.
- buildProjectThreadGroups / buildChronologicalThreadList take folderOptions
  and early-return today's output untouched under Group by: None.
- buildPinnedSidebarState folds pinned roots into folders ordered by
  comparePinnedRoots (pinned keeps its pinSortKey ordering), exposing
  rootItems while keeping rootNodes.

Rendering (S3):
- SidebarFolderRow collapsible header (icon, leaf name, descendant count,
  rolled-up activity) mirroring the parent/worktree row chrome.
- ProjectRow renders folder items + recurses; ThreadRow gains
  displayTitle/accessibleTitle so a folder member shows its leaf while
  keeping the full "Work › Q3 › Planning" path for a11y + tooltip.
- Collapse state in sidebarCollapsedFoldersAtom (read where rendered);
  selected thread's folder ancestors auto-expand.
- SidebarViewOptionsMenu gains a Group by (None / Folder) section; the
  existing organization "Group by" is relabeled "Organize by" to avoid the
  label collision.
- PinnedThreadTree renders folders statically (drag-reorder and derived
  folders don't compose), keeping the sortable flat list when no folders.

Tests: bucketIntoFolders nesting, folders-first + representative-descendant
ordering under both comparators, child-stays-under-parent, env-group-in-
folder, pinned folder ordering, and the Group by: None regression (deep-
equal to the pre-change builder + folder branch never entered).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Sort options use a single state-driven arrow (none / down=desc / up=asc)
  instead of a check plus dual direction buttons; re-selecting the active
  field flips its direction.
- Organize trigger uses the Layers icon; Sort trigger uses up/down arrows.
- Align the menu section labels to the standard DropdownMenuLabel hierarchy
  used by the rest of the app's dropdowns.
- Make the View options menu story interactive and add TooltipProvider to the
  Ladle harness so tooltip-using stories render instead of crashing blank.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
brsbl and others added 15 commits June 19, 2026 14:10
Resolve thread-create-helpers conflict: keep main's request-carried
titleFallback and the branch's folderPath. Add folderPath: null to the
ThreadActionsMenu test fixture for the now-required field.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reads "Open project settings to fix".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Folder rows now expose a dedicated New thread (+) button alongside a
  "..." menu (Rename, Remove), mirroring the project row's action cluster
  instead of stuffing New thread into the menu.
- Size the sidebar/project/thread action and view-options dropdowns to
  their content (drop fixed w-44/w-52/w-56; the shared min-w-[8rem] floor
  remains) so menus wrap their contents.
- Clean up the project path-missing affordance: tooltip reads "Open project
  settings" and the icon's accessible name is "Project path not found".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The icon-only New thread action read inconsistently next to the labeled
Automations button below it. Bring back the "New thread" label (Search
stays an icon button alongside).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Folder rows now have a "View archived threads" action, opening a
folder-scoped archived list — the analog of a project's archived view.

- Add an optional folderPath filter to the thread list query end to end:
  server-contract schema -> list route -> db listThreads filter.
- useArchivedThreads accepts folderPath (keyed separately in the cache);
  the archived view reads it from a `?folder=` param and labels the folder.
- getFolderArchivedRoutePath builds the projectless archived route scoped
  by folder (folders live in the personal section).
- Thread the onViewArchivedThreadsInFolder callback through the folder row
  chain; the folder "..." menu shows View archived threads / Rename / Remove.
- Cover the db folderPath filter with a regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The sidebar's TooltipProvider opened tooltips instantly (delayDuration 0),
which felt flickery on hover. Use 300ms like MessageActionBar so sidebar
icon tooltips open/close with the same cadence. Mirror it in the Ladle
harness for story fidelity.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The personal "Threads" archived list (no folder selected) was showing
threads that live in a folder. Add an `unfiled` thread-list filter
(folderPath IS NULL) end to end and use it for the personal archived view,
so foldered archived threads appear only in their folder's archived list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The folder archived page diverged from the project/personal pages with a
bespoke in-body heading. Make all archived scopes render the one shared
view and carry their scope in the AppLayout breadcrumb instead:

- Rename ProjectArchivedThreadsView -> ArchivedThreadsView (it already
  serves project, personal/loose, and folder scopes) and drop the bespoke
  folder heading + folder-specific empty state so the body is identical.
- Add a folder segment to the projectless archived breadcrumb
  ("Threads > <folder> > Archived") and document title, mirroring how the
  project name scopes the project archived page.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Smoke testing surfaced that the title sort inverted the wrong way: its base
comparator ascends (A→Z), but getSidebarThreadComparator applied the same
"asc inverts" rule used for the descending time sorts. So leaf threads sorted
opposite to the folder/item comparator (folders Z→A while threads A→Z) and the
↑/↓ arrows read backwards for alphabetical.

- Special-case the ascending title base: asc keeps it (A→Z), desc inverts it
  (Z→A), for both the leaf and mixed folder/thread comparators.
- Default a newly selected Alphabetical sort to asc (A→Z); time sorts still
  default to desc (newest first).
- Export getSidebarThreadComparator and add direction/consistency tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dragging a thread onto a folder now reads as a real drop:

- onDragOver resolves the target folder (shared with onDragEnd so they agree),
  expands it if collapsed, and records a drag preview.
- The hovered folder renders an optimistic, non-interactive row for the dragged
  thread inside it (forcing the children area open) so there's a visible target
  to drop onto before the move commits.
- Preview/auto-expand apply only to real folders (not the loose root) and clear
  on drop/cancel; state updates are guarded so drag-over doesn't thrash renders.

useSidebarReorderDnd now forwards onDragStart/onDragOver/onDragCancel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When a project's local path is missing, the row now renders just the red
warning icon (flush right) instead of also showing the actions menu and new
thread button — the warning is the single relevant action (open settings to
fix). Normal rows keep the "..." + "+" actions. Right-click context menu is
unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Threads header in the folders view was missing the Organize/Sort actions,
and each view defined its own near-duplicate action clusters. Extract shared
components so the headers can't drift:

- SidebarDisplayOptionsActions: the Organize + Sort menu pair, used by the
  Projects, Folders, and Threads headers.
- SidebarThreadsSectionActions: the full Threads-header cluster (archived menu
  + display options + new thread), used by the Threads header in BOTH project
  mode and the folders view — so it now has Organize/Sort there too.

Rebalanced the two display-menu open states so each mode's two sections own one
("primary": Projects/Folders, plus Threads) and stay independent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ProjectThreadTree, ChronologicalThreadTree, and the Folders view each had their
own near-identical copy of "group + sortable list + map item rows" plus the
loading skeleton, so every row-prop change had to be repeated per view.

- Add ManualThreadTreeItems: the single place that maps thread-tree items to
  rows. Variants via props — fixed vs per-item projectId, and an optional
  sortableParentKey (wrap in a SortableContext, or let an outer one provide it
  for the split Folders/Threads view).
- Add ThreadTreeLoadingSkeleton for the repeated loading state.
- Route all three renderers through them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Expanding the hovered folder and inserting the drop-preview row mid-drag shifted
layout under the in-flow dragged item, so dragging a thread up out of its folder
got shoved back down ("stuck"). Defer both behind a short hover dwell so passing
through a folder doesn't mutate layout; the expand + preview only fire once the
pointer settles over a target. Drop still works immediately regardless.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@brsbl brsbl changed the title Add nested sidebar folders Add sidebar thread folders (folders view, scoped archived views, shared components) Jun 20, 2026
brsbl and others added 13 commits June 19, 2026 17:22
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The drop preview duplicated the dragged thread's title (once in the injected
row, once on the natural dragged row). Keep the placeholder as the drop target
but render it empty — the dragged row carries the title, like dragging a queued
message. Folders still highlight and spring-open on hover; dropping onto the
loose Threads section (a droppable parent) clears the thread's folder.

Add a "Drag into" story for the drop-target highlight + empty placeholder.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The thread-folder migrations (0042 folder_path, 0043 thread_folders, 0044
project scoping) land after thread-search, so migrate-test scenarios that rewind
the ledger past them must drop the folder schema too — otherwise migrate()
re-runs ADD folder_path / CREATE thread_folders against a DB that still has them
(duplicate column / table already exists). Add dropThreadFolderSchema to the
reset/rollback paths (thread-search replay, post-0023, event-large-value, and
the fork/side-chat rewind).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Folders are organized by path and a single folder is meant to hold threads
from any project, but the prior implementation added a project_id scope to
thread_folders plus a projectId field on the create/rename/delete contract.
In practice the UI only ever sent projectId: null, and the global (null) path
matched zero threads (threadProjectScopeFilter returned 1 = 0), so renaming or
deleting a folder in the app orphaned every thread inside it. Filing a thread
into a folder also created a project-scoped folder row, diverging from the
global rows the create dialog made.

Remove project scoping entirely:
- Drop thread_folders.project_id (fold into migration 0043; delete the
  project-scoping migration 0044 + its snapshot/journal entry, no new
  migration needed since folders never shipped).
- Match a folder's threads by path across all projects on rename/delete.
- Strip projectId from the contract request/response schemas, the server
  route, the data layer, and the sidebar create/rename/delete calls.
- Replace the project-scope unit tests with cross-project coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sted-folders-thr_js7dkc3iwv

# Conflicts:
#	packages/db/drizzle/meta/0042_snapshot.json
#	packages/db/drizzle/meta/_journal.json
#	packages/db/test/migrate.test.ts
- Sidebar menus: rename Organize→Group by, drop Group from the loose Threads
  header, narrow the Group/Sort menus and style their titles as section labels.
- Add icons + grouping dividers to all ... menus (thread/project/folder/
  threads-section); envelope icon for the read toggle; "Mark read/unread" and
  "View archive" labels.
- Tooltips: keep only sort, group, and the new project/folder/thread buttons;
  remove the rest (incl. native title tooltips) and dismiss on pointer leave.
- Drag affordances: unify the folder/loose drop-preview into one inserted
  placeholder; grab cursor on draggable threads.
- Cursors: pointer for clickable rows by default; project/folder header rows are
  static (caret is the click target); pointer on carets/top buttons.
- Carets get the standard sidebar hover style.
- Search: title-first results with a labeled 2-line message snippet, "Recent"
  empty state showing project + relative time; drop the per-row glyph and the
  Archived pill.
- Subtle empty-state text/icons; lighter selection token; lighter user-message
  bubble border; full-width Queued header toggle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Deep-link sidebar search results to the matched message: carry the
  matched event sequence through the search contract/DB, and scroll to +
  briefly highlight that message on navigate (useScrollToSearchedMessage).
- Search: one click on the X fully dismisses (clear + close); hide the
  count badge on the Archived section.
- Prompt box: restore hover/open backgrounds on the model + permission
  pickers; drop the lingering native tooltip on the context-window
  indicator and restyle its popover to the menu surface with a usage bar.
- Make the content pane's rounded-left corner visible (paint the sidebar
  wrapper bg-sidebar) and bump the prompt box + stack-card corner radius.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Banner cards (queued, goal, todo, workflow, prompt-mode, background
  commands): make the whole header row the toggle target and pin the caret
  flush right; queued's caret moves out from beside the count.
- Add a subtle drop shadow to the prompt-box banner chrome (PromptStackCard).
- Context-window menu: right-align the token line's "% left"; add a
  ThreadContextWindowIndicator story (ring tones + open usage-bar menus) via a
  story-only defaultOpen hatch.
- Keep the composer-bootstrap cache warm for 5min (was 30s gcTime) so the
  expanded queued drawer renders from cache on rapid thread switches instead of
  reloading once its gate's cache was evicted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

1 participant