Skip to content

feat: star for sessions and captures (#72)#102

Open
graydawnc wants to merge 4 commits intomainfrom
feat/session-bookmarks
Open

feat: star for sessions and captures (#72)#102
graydawnc wants to merge 4 commits intomainfrom
feat/session-bookmarks

Conversation

@graydawnc
Copy link
Copy Markdown
Collaborator

@graydawnc graydawnc commented Apr 21, 2026

Closes #72.

Summary

Adds a star primitive that spans both first-party sessions (Claude Code / Codex / Gemini transcripts) and connector-indexed captures (YouTube likes, Twitter bookmarks, GitHub stars, Reddit saves, XHS notes, …). Users mark high-value items in-place from the search result row, from the session detail header, or from the Starred view itself; and they resurface those items via a persistent top-right entry, a scoped filter tab on search results, and a full-text search scoped to starred content.

star-demo.mp4

Design

Why unified, not parallel tables. An earlier iteration had session_stars; the final schema is a single stars(item_type, item_uuid, starred_at) table. Sessions and captures are product-level peers in Spool (both appear in the same search results and source-chip row), so treating their stars as peers makes the Starred view a one-liner (ORDER BY starred_at DESC across both kinds) instead of a JS-level merge of two lists. Parallel tables made sense for sessions/captures because those shapes genuinely diverge; a star record is (referent, time) and doesn't need to.

Why no FK. deleteSessionByFilePath drops session rows when transcripts disappear, and captures can vanish when a connector is uninstalled. A cascade delete would destroy stars for items that may come back. The table keeps its rows; all read paths JOIN to filter orphans so the UI never surfaces dead references.

Why not also starrable: messages, projects, anything else. Out of scope for this PR. item_type is constrained via a CHECK so future additions are explicit schema changes.

Features

Starring an item

  • Session detail header — star button next to Copy Session ID.
  • Fragment row in search results — always-visible ★ (muted outline when unstarred, filled accent when starred). No hover-reveal: discoverable at first look.
  • Capture row in search results — same pattern; e.preventDefault() keeps the <a> wrapper from navigating.
  • Any of these three moments is the most common one to say "I want to keep this."

Finding starred items

  • Top-right entry button — persistent, visible on every view, shows count when non-zero; toggles into the Starred view.
  • Starred view — mixed chronological list of sessions + captures ordered by starred_at DESC. Empty state, filter state, and post-search state all have copy. Clicking a session opens it; clicking a capture opens the URL.
  • Starred tab in global search — appears in the source-filter row whenever any result in the current query is starred; filters down to them.

Searching within starred

  • On the Starred view, typing in the search bar runs a scoped FTS (onlyStarred=true) across message content of starred sessions and text of starred captures. Results render via the existing FragmentResults component with snippet highlighting.
  • The search bar shifts its placeholder to Filter starred… and switches to a subtle accent border so the scope shift is visually signalled.
  • Pressing Enter escapes back to global search as a power-user exit.

Implementation notes

Core (packages/core)

  • Migration v4 creates stars and drops any earlier session_stars left over from intermediate dev builds.
  • starItem / unstarItem / isStarred / getStarredUuidsByType / listStarredItems — the full API in five functions.
  • listStarredItems orphan-filters in SQL (WHERE ... AND EXISTS (...)) so LIMIT counts only live rows. Details for sessions and captures are batch-fetched via IN (...), then reassembled in starred_at order.
  • searchFragments and searchCaptures gained an onlyStarred option that injects an EXISTS (SELECT 1 FROM stars ...) predicate into every search path (FTS, FTS-fallback, LIKE-fallback).

Renderer (packages/app/src/renderer)

  • StarButton is a single shared component; used five places, no copy-paste.
  • Badges.tsx hosts the shared SourceBadge / PlatformBadge.
  • shared/formatDate.ts has the relative-date helper that was copy-pasted across the codebase.
  • App-level starred state splits into starredSessions: Set<string>, starredCaptures: Set<string> (complete, cheap, refreshed on mount), and starredItems: StarredItem[] (up to 200, refreshed only when entering the Starred view). Set updates are identity-preserving when content hasn't changed, so search-result rows don't re-render on unrelated star changes.

Main (packages/app/src/main)

  • Four new IPC handlers: star-item, unstar-item, list-starred-items, get-starred-uuids.
  • The search cache is keyed on onlyStarred, and star-item / unstar-item clear it to keep scoped results consistent.

Test plan

  • pnpm -F @spool-lab/core test — 166 passing (10 new in stars.test.ts including CHECK constraint, cross-type UUID independence, orphan filtering, persistence across DB reopen).
  • Core tsc --noEmit clean.
  • App tsc -p tsconfig.json --noEmit clean.
  • pnpm -F @spool/app run build:electron builds.
  • Manual QA: star a session from detail view → appears in Starred view.
  • Manual QA: star a capture from search result → appears in Starred view.
  • Manual QA: Starred view filter matches content inside starred sessions (full-text, not just titles).
  • Manual QA: Starred tab in global search appears only when at least one result is starred; filtering it hides unstarred rows.
  • Manual QA: unstar from Starred view removes the row without a round-trip flicker.
  • Manual QA: restart the app — starred state persists.

Notes

  • This PR does not add a spool star <uuid> CLI command; deferred until the UI flow has settled.
  • Orphan stars (referent gone but star row remains) are filtered silently. A future "show archived" toggle could surface them; not built yet.

Lets users mark high-value items across Spool — agent sessions and
external captures alike — and surface them via a dedicated Starred
view, a scoped filter on global search, and a persistent top-right
entry button.

Schema: single `stars(item_type, item_uuid, starred_at)` table with a
CHECK constraint on item_type. Keyed by natural UUID, no FK — orphan
stars are filtered at read time so transient referent absence (e.g.
transcript file removed then restored) doesn't destroy state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@graydawnc graydawnc changed the title feat: star/bookmark for sessions and captures (#72) feat: star for sessions and captures (#72) Apr 21, 2026
graydawnc and others added 3 commits April 21, 2026 15:42
- Fix home star entry pushing logo below center — use absolute
  positioning instead of adding a flex slot
- Split multi-statement-per-line useCallback bodies (project uses
  no-trailing-semicolon style; avoid semi-chaining even within braces)
- Replace \`const formatDate = formatRelativeDate\` aliases with direct
  calls
- Promote dense filter predicate to \`isSameStarredItem\` helper
- Match existing .then/.catch style in refreshStarred{Uuids,Items}
  instead of cramped try/catch single-liners
- Extract \`highlighted\` boolean in StarredEntryButton; drop the
  spread-ternary on the Star icon props
- Drop JSDoc on self-evident StarButton props; keep only the
  non-obvious \`insideAnchor\` WHY comment
- Type IPC star handlers with \`StarKind\` instead of repeating the
  literal union

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- StarButton md variant: 26px -> 24px (h-6 w-6) to match the adjacent
  Copy Session ID button height in the session detail header; sm stays
  20px
- Replace default focus ring with focus-visible:ring-accent/40 so a
  mouse-clicked star no longer leaves an orange outline box
- Starred view rows: move the star button inline with the first-line
  meta flex (same row as the date) instead of vertical-centering it
  against the whole 3-line block. Session rows switch the outer element
  to role=button with keyboard handler (buttons cannot nest in HTML);
  capture rows keep the <a> root and pass insideAnchor=true so the star
  click does not navigate.
- Starred header: leading-none on the label / count / filter-note so
  the star optical-aligns with text centers
- Session row star size md -> sm; reserves the md variant for the
  detail header

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Search result rows: star now sits at the right end of the meta line,
  after the date (badge > meta > date > star). Previous layout put the
  star between meta and date, which visually split the time from its
  source-context group.
- StarredEntryButton count: translate-y-[0.5px] to optical-center the
  digit with the adjacent filled Star icon

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@graydawnc graydawnc marked this pull request as ready for review April 21, 2026 10:09
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.

Feature Request: Add "Bookmark/Favorite" functionality for Sessions

1 participant