diff --git a/CLAUDE.md b/CLAUDE.md index 2f976c1..9bd9735 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,10 +6,10 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co **lore** is a keyboard-driven TUI (Terminal User Interface) for browsing Claude Code session history. It reads session transcripts from `~/.claude/projects//*.jsonl` and provides rich navigation, filtering, and search across sessions. -Current status: **v0.6.0 — All planned phases complete.** Implemented: +Current status: **v0.7.0 — All planned phases plus the v0.7 cleanup and feature pass complete.** Implemented: - Session list (3.1) with relative-time bucketing and query preview (first user message). -- Inline project (`p`), branch (`b`), and fuzzy (`f`) filters with fuzzy ranking. +- Inline project (`p`), branch (`b`), and fuzzy (`f`) filters with fuzzy ranking (DRY'd in v0.7 around a single `fuzzyFilterSessions` helper). - Session detail (3.2) with collapsible tool turns, copy-prompt, re-run, diff rendering for `Edit` / `Write` tool calls, and turn position indicator (`turn N/M`). - Half-page scrolling (`d`/`u`) in all navigable modes. - Full-text search (3.3): SQLite FTS5 index (Phase 5a) with linear-scan fallback. @@ -20,6 +20,9 @@ Current status: **v0.6.0 — All planned phases complete.** Implemented: - Sidechain handling (Phase 7): sub-agent transcripts are filtered from the session list and viewable inline in the detail view by expanding Agent tool turns. - Help overlay (`?`) with mode-specific keybindings. - Per-mode viewport scrolling, mode-specific footers, and one-shot flash messages for no-op keys. +- **Session bookmarks (v0.7, `m` and `M` keys)**: toggle a `★` on any session, persist to `/lore/bookmarks.json`, filter to bookmarks-only with `M`. Composable with the `f`/`p`/`b` filters. +- **Timeline activity heatmap (v0.7, `T` key)**: 8-week × 7-day grid showing session counts by day; navigate with `h`/`l` (or `←`/`→`), `enter` to filter the list to the highlighted date. +- **Render chrome unification (v0.7)**: every mode goes through dedicated `render*Header` and `render*Footer` functions; back-nav hint is consistent across all sub-views (`q/esc/h/← back`); list shows `q quit`. Skipped sessions surface in the list header as "(N skipped)". See `DESIGN.md` for the full product vision and phasing roadmap. @@ -43,7 +46,7 @@ LORE_PROJECTS_DIR=/path/to/projects ./lore The binary reads from `~/.claude/projects/` (created by Claude Code) by default, scans for `.jsonl` session transcripts, and displays them in a sortable list grouped by recency (today, yesterday, this week, etc.). The projects directory can be overridden via the `--dir` flag (highest precedence) or the `LORE_PROJECTS_DIR` environment variable; resolution lives in `lore.go::resolveProjectsDir`. -The FTS5 search index is cached at `/lore/index.db` (e.g. `~/.cache/lore/index.db` on Linux, `~/Library/Caches/lore/index.db` on macOS) and is populated lazily on first search. +The FTS5 search index is cached at `/lore/index.db` (e.g. `~/.cache/lore/index.db` on Linux, `~/Library/Caches/lore/index.db` on macOS) and is populated lazily on first search. Bookmarks are persisted alongside it at `/lore/bookmarks.json`. ## Tests @@ -75,16 +78,18 @@ The project uses [Bubble Tea](https://github.com/charmbracelet/bubbletea) for th **Main files:** - `lore.go`: Entry point. `Run()` parses flags (`-v`/`--version`, `--dir`), resolves the projects dir (`--dir` > `LORE_PROJECTS_DIR` > `~/.claude/projects`), and starts the Bubble Tea program. -- `model.go`: The Bubble Tea `model` struct and per-mode key dispatchers (`handleListKey`, `handleDetailKey`, `handleSearchKey`, `handleProjectKey`, `handleRerunKey`, `handleStatsKey`, `handleFilterEntryKey`). Also lazy-opens the FTS5 index on first search. -- `render.go`: `View()`, mode-specific renderers (`renderListView`, `renderDetailView`, `renderSearchView`, `renderStatsView`, etc.), the help overlay, and all Lipgloss styles. -- `session.go`: `Session` struct and `scanSessions()` / `parseSessionMetadata()` — reads only the first `user` event of each `.jsonl`. Also extracts `Query` (first user message text) via `extractQuery()`. -- `bucket.go`: `timeBucket()` returns labels like "today", "yesterday", "this week" for relative-time grouping. -- `detail.go`: Mode constants (`modeList`, `modeDetail`, `modeSearch`, `modeProject`, `modeRerun`, `modeStats`), `turn` struct, `parseTurnsFromJSONL()`, assistant/tool block extraction, and sidechain linking (`loadSessionTurns`, `sidechainsDir`, `loadSidechainTurns`). +- `model.go`: The Bubble Tea `model` struct and per-mode key dispatchers (`handleListKey`, `handleDetailKey`, `handleSearchKey`, `handleProjectKey`, `handleRerunKey`, `handleStatsKey`, `handleTimelineKey`). Also lazy-opens the FTS5 index on first search and best-effort-loads bookmarks on startup. +- `render.go`: `View()`, mode-specific renderers and dedicated `render*Header` / `render*Footer` functions (`renderListView`, `renderDetailView`, `renderSearchView`, `renderStatsView`, `renderTimelineView`, etc.), the help overlay, and all Lipgloss styles. Layout constants (`projectColWidth`, `branchColWidth`, `fixedCols`, `rerunMaxLines`, `snippetMaxLen`) live at the top. +- `session.go`: `Session` struct and `scanSessions()` / `parseSessionMetadata()` — reads only the first `user` event of each `.jsonl`. Also extracts `Query` (first user message text) via `extractQuery()`. `scanSessions` returns `(sessions, warnings, err)`; warnings carry a short message per skipped file. +- `bucket.go`: `timeBucket()` returns labels like "today", "yesterday", "this week" for relative-time grouping. Also provides `startOfDay()` used by both bucketing and the timeline heatmap. +- `detail.go`: Mode constants (`modeList`, `modeDetail`, `modeSearch`, `modeProject`, `modeRerun`, `modeStats`, `modeTimeline`), `turn` struct, `parseTurnsFromJSONL()`, assistant/tool block extraction, and sidechain linking (`loadSessionTurns`, `sidechainsDir`, `loadSidechainTurns`). - `search.go`: `searchSessions()` — linear-scan full-text search returning `SearchHit`s ranked by hit count. Used as the fallback path when the FTS5 index is unavailable. - `index.go`: SQLite FTS5 search index. `OpenIndex(cacheDir)`, `Index.Sync(projectsDir)` for incremental mtime-based reindexing, `Index.Search(query)` for ranked FTS5 lookups, and `extractSessionText()` for indexable content. - `stats.go`: Usage-stats data layer. `parseSessionStats()` sums `assistant.message.usage` token counts; `estimateCost()` applies a per-million-token pricing table (Opus / Sonnet / Haiku); `formatTokenCount()` adds k/M suffixes. - `project.go`: `groupByBranch()` and project-view rendering helpers. -- `filter.go`: `fuzzyFilterCandidates()` — wraps `github.com/sahilm/fuzzy` for the `p` / `b` inline filters. +- `filter.go`: `fuzzyFilterCandidates()` (raw fuzzy ranking) plus `fuzzyFilterSessions(text, candidate, sessions)` — the generic helper that backs the three branches of `applyFilter` in v0.7. +- `bookmark.go` (v0.7): `loadBookmarks` / `saveBookmarks` / `toggleBookmark` / `bookmarksFile()`. Bookmarks are stored as a small JSON `map[string]bool` keyed by session ID; only `true` entries are persisted. +- `timeline.go` (v0.7): `Heatmap` data structure and `buildHeatmap(sessions, now)` — builds a 7×8 (Mon..Sun × 8 weeks) activity grid. `heatmapBucket(count)` maps a count to one of four intensity levels. - `clipboard.go`: `copyToClipboard()` — tries pbcopy, wl-copy, then xclip. - `rerun.go`: `rerunClaude()` returns a `tea.Cmd` that wraps `tea.ExecProcess` so the child `claude` invocation takes over the terminal cleanly. - `viewport.go`: `clampOffset()` and `sliceLines()` — the two primitives every renderer uses for edge-snap scrolling. @@ -103,15 +108,18 @@ The project uses [Bubble Tea](https://github.com/charmbracelet/bubbletea) for th - `Query`: First user message text, extracted from `message.content` of the first user event. Used as the primary label in list and project views; falls back to `Slug` when empty. - `Timestamp`: Extracted from the first user event. -**Model**: Bubble Tea state machine. The `mode` field switches between `modeList`, `modeDetail`, `modeSearch`, `modeProject`, `modeRerun`, and `modeStats`. Each mode has its own cursor and viewport offset (`listOffset`, `detailOffset`, `searchOffset`, `projectOffset`, `statsOffset`) so navigating away and back preserves position. +**Model**: Bubble Tea state machine. The `mode` field switches between `modeList`, `modeDetail`, `modeSearch`, `modeProject`, `modeRerun`, `modeStats`, and `modeTimeline`. Each mode has its own cursor and viewport offset (`listOffset`, `detailOffset`, `searchOffset`, `projectOffset`, `statsOffset`) so navigating away and back preserves position. Notable per-mode state: -- **List**: `sessions`, `visibleSessions`, `cursor`, `filterMode` / `filterText` / `appliedFilterMode` for the inline `p` / `b` / `f` filters. +- **List**: `sessions`, `visibleSessions`, `cursor`, `filterMode` / `filterText` / `appliedFilterMode` for the inline `p` / `b` / `f` filters. `bookmarkOnly` toggles the `M` filter; `dateFilter` (set by `enter` from the timeline) restricts the visible list to one calendar day. `applyFilter` composes all three filters. `warnings` carries skipped-file messages from `scanSessions`, surfaced in the list header. - **Detail**: `detailSession`, `turns`, `cursorDetail`, `expandedTurns` (which tool turns are unfolded), `justCopied`, `sidechainTurns` (lazy-loaded sidechain content keyed by turn index). - **Search**: `searchMode` (entry vs. results), `searchQuery`, `searchResults`, `searchCursor`. The FTS5 `index` is lazy-opened on the first `enter` press and falls back to linear scan on miss/error. - **Project**: `projectCWD`, `projectSessions`, `projectCursor`. - **Rerun**: `rerunPrompt`, `rerunCWD`, plus an injectable `rerunFn` so tests can substitute `tea.ExecProcess`. - **Stats**: `statsData` (slice of `statsRow`), `statsCursor`, `statsOffset`. Computed by `computeStatsRows` from the in-memory session list when `S` is pressed. +- **Timeline**: `timelineCursor` (the highlighted day). The heatmap itself is rebuilt on each render from `m.sessions`; the cursor is bounded to the 8-week window. + +Cross-mode bookmark state lives on the model: `bookmarks map[string]bool`, `bookmarksPath string`. Toggled via `m` in list / detail; persisted on every change. The model also injects `clipboardFn` (default `copyToClipboard`) and `rerunFn` (default `rerunClaude`) so tests can swap real-system effects for fakes. The `projectsDir` field carries the resolved projects path so the FTS5 sync knows where to walk. @@ -154,10 +162,13 @@ The full key map is also surfaced in-app via the `?` overlay. Authoritative refe - `p`: Inline project filter (type query, `enter` to apply, `esc` to cancel). - `b`: Inline branch filter. - `f`: Fuzzy filter across slug, project, and branch simultaneously. +- `m`: Bookmark / unbookmark the selected session (persists to disk). +- `M`: Toggle bookmark-only filter (binary; composes with the fuzzy filters). - `P`: Open the project view scoped to the selected session's CWD. - `S`: Open the usage stats panel. +- `T`: Open the timeline activity heatmap. - `/`: Enter full-text search. -- `esc`: Clear an applied filter. +- `esc`: Clear any applied filter (fuzzy / bookmark-only / date). - `?`: Show help overlay. - `q`: Quit. @@ -166,6 +177,7 @@ The full key map is also surfaced in-app via the `?` overlay. Authoritative refe - `space`: Expand or collapse a tool turn (the cursor must be on one). Agent turns with sidechains load the sub-agent conversation inline. - `y`: Copy the user prompt at-or-before the cursor to the clipboard. - `r`: Re-run with the current user prompt (enters re-run mode). +- `m`: Bookmark / unbookmark this session. - `/`: Enter full-text search. - `esc` / `q` / `h` / `←`: Back to list. @@ -177,7 +189,9 @@ The full key map is also surfaced in-app via the `?` overlay. Authoritative refe **Re-run mode** (`modeRerun`): `enter` to spawn `claude` with the chosen prompt and CWD (lore returns to the session list when `claude` exits); `esc` / `q` / `h` / `←` to cancel and return to detail. -**Stats mode** (`modeStats`): `j` / `k`, `g` / `G` to navigate the per-session table; `esc` / `q` / `h` / `←` to return to the list. Columns: project · branch · model · input tokens · output tokens · estimated cost. Token counts use `k` / `M` suffixes; cost is computed from a built-in pricing table for Opus / Sonnet / Haiku families and shown as `--` for unknown models. +**Stats mode** (`modeStats`): `j` / `k`, `g` / `G`, `d` / `u` to navigate the per-session table; `esc` / `q` / `h` / `←` to return to the list. Columns: project · branch · model · input tokens · output tokens · estimated cost. Token counts use `k` / `M` suffixes; cost is computed from a built-in pricing table for Opus / Sonnet / Haiku families and shown as `--` for unknown models. + +**Timeline mode** (`modeTimeline`): `h` / `←` and `l` / `→` move the cursor across days in an 8-week × 7-day activity heatmap. `enter` filters the list to the highlighted date and returns to list mode. `esc` / `q` / `h` / `←` returns without filtering. The grid renders Mon..Sun rows × 8 columns (oldest..newest), shaded by session count: 0 (dim), 1-2 (light), 3-5 (medium), 6+ (bright). Footer shows the highlighted date and its session count. ### Testing Strategy @@ -191,9 +205,11 @@ Tests import `bubbletea` directly to send messages (e.g., `keyMsg()`) to the mod `View()` dispatches by `m.mode` to the per-mode renderer. Each renderer follows the same shape: header, divider, body lines (sliced through `clampOffset` + `sliceLines` from `viewport.go`), divider, footer. The `?` help overlay short-circuits the entire view. -Styling is done via Lipgloss `NewStyle()` instances defined at the top of `render.go`. +Every mode has dedicated `render*Header` and `render*Footer` functions (no inline header/footer construction inside the View renderer). Footer hints follow a uniform `key action` format separated by three spaces; sub-views all show `q/esc/h/← back`, while the list shows `q quit`. Flash messages are rendered through one path in every footer (precedence over hints). + +Styling is done via Lipgloss `NewStyle()` instances defined at the top of `render.go`. Layout constants (`projectColWidth`, `branchColWidth`, `fixedCols`, `rerunMaxLines`, `snippetMaxLen`) are package-level so list and search rows render identical column widths. -Body math goes through one of `listBodyLines`, `detailBodyLines`, `searchBodyLines`, or `projectBodyLines`. Each returns `(lines []string, cursorLine int)` so the viewport can edge-snap the offset to keep the cursor visible. +Body math goes through one of `listBodyLines`, `detailBodyLines`, `searchBodyLines`, `projectBodyLines`, or `statsBodyLines`. Each returns `(lines []string, cursorLine int)` so the viewport can edge-snap the offset to keep the cursor visible. Timeline mode renders its grid directly in `renderTimelineView` (fixed 7×8 layout, no scrolling). ### Phasing Notes @@ -208,6 +224,9 @@ Body math goes through one of `listBodyLines`, `detailBodyLines`, `searchBodyLin | 5c — Cost/usage stats panel | ✅ Complete (`stats.go`, `S` from list mode) | | 6 — Repo split into `github.com/zpenka/lore` | ✅ Done (this is that repo) | | 7 — Quality-of-life | ✅ Complete (back-nav, re-run return-to-list, turn indicator, configurable projects dir, sidechain handling) | +| v0.7 — Cleanup pass | ✅ Complete (unified footers/headers, DRY filter, layout constants, dead-code removal, missing unit tests, scan warnings) | +| v0.7 — Bookmarks (2A) | ✅ Complete (`bookmark.go`, `m`/`M` keys, ★ markers in list/search/project) | +| v0.7 — Timeline heatmap (2B) | ✅ Complete (`timeline.go`, `T` key, enter filters list to a day) | ## Repo Layout @@ -219,20 +238,22 @@ lore/ ├── go.mod, go.sum ├── lore.go # Entry point, Run(), resolveProjectsDir() ├── model.go # Bubble Tea model + per-mode key dispatchers -├── render.go # View(), per-mode renderers, help overlay, Lipgloss styles -├── session.go # Session struct, scanSessions(), parseSessionMetadata() -├── detail.go # Mode constants, turn struct, JSONL → turns parser, sidechain linking +├── render.go # View(), per-mode header/footer/body renderers, help overlay, Lipgloss styles, layout constants +├── session.go # Session struct, scanSessions() (sessions, warnings, err), parseSessionMetadata() +├── detail.go # Mode constants (incl. modeTimeline), turn struct, JSONL → turns parser, sidechain linking ├── search.go # Linear-scan full-text search (fallback path) ├── index.go # SQLite FTS5 search index (Phase 5a) ├── stats.go # Token-usage parsing + cost estimation (Phase 5c) ├── project.go # Branch grouping + project view rendering -├── filter.go # Fuzzy-ranked p/b/f inline filter +├── filter.go # fuzzyFilterCandidates + fuzzyFilterSessions helper +├── bookmark.go # Bookmarks JSON storage (load/save/toggle) — v0.7 +├── timeline.go # Heatmap grid + buildHeatmap + heatmapBucket — v0.7 ├── clipboard.go # pbcopy / wl-copy / xclip wrapper ├── rerun.go # tea.ExecProcess wrapper for the claude child process ├── viewport.go # clampOffset() + sliceLines() scrolling primitives ├── wrap.go # wrapText() for multi-line turn bodies -├── bucket.go # timeBucket(), relative-time grouping -├── *_test.go # Unit and smoke tests +├── bucket.go # timeBucket(), startOfDay(), relative-time grouping +├── *_test.go # Unit and smoke tests (incl. bookmark_test.go, timeline_test.go) ├── .github/ │ ├── pull_request_template.md │ └── workflows/ci.yml @@ -244,10 +265,10 @@ lore/ ### Add a new key binding -1. Identify which mode owns the key. Edit the matching `handle*Key` method in `model.go` (`handleListKey`, `handleDetailKey`, `handleSearchKey`, `handleProjectKey`, `handleRerunKey`). +1. Identify which mode owns the key. Edit the matching `handle*Key` method in `model.go` (`handleListKey`, `handleDetailKey`, `handleSearchKey`, `handleProjectKey`, `handleRerunKey`, `handleStatsKey`, `handleTimelineKey`). 2. Update the relevant `renderHelpOverlay` block in `render.go` so the `?` overlay matches. 3. If the key affects the footer, update the corresponding `render*Footer` in `render.go` / `project.go`. -4. Test via the matching `TestModel_*` test in `model_test.go` (or the per-feature test file: `search_test.go`, `project_test.go`, `rerun_test.go`, etc.). +4. Test via the matching `TestModel_*` test in `model_test.go` (or the per-feature test file: `search_test.go`, `project_test.go`, `rerun_test.go`, `bookmark_test.go`, `timeline_test.go`, etc.). ### Change a view's display diff --git a/DESIGN.md b/DESIGN.md index 5dd621d..38caebc 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -2,7 +2,7 @@ A keyboard-driven TUI for browsing your Claude Code session history. -> **Status:** v0.6.0 — All planned phases complete. +> **Status:** v0.7.0 — All planned phases plus the v0.7 cleanup and feature pass complete. > The repo split (Phase 6) has happened — this is the standalone > `github.com/zpenka/lore` module. See [Phasing](#phasing) for status. > @@ -284,6 +284,9 @@ Nothing else. Stay lean. | 5c | **Cost/usage stats panel** — token usage and estimated cost per session | ✅ Done | | 6 | Standalone `github.com/zpenka/lore` repo | ✅ Done | | 7 | **Quality-of-life** — sidechain handling, re-run UX, back-nav, turn indicator, configurable dir | ✅ Done | +| v0.7 | **Cleanup pass** — unified footers/headers, DRY filter logic, layout constants, dead-code removal, missing unit tests, scan warnings surfaced in list header | ✅ Done | +| v0.7 | **Session bookmarks** — `m` toggles a `★`, `M` filters to bookmarks-only; persisted to `/lore/bookmarks.json` | ✅ Done | +| v0.7 | **Timeline activity heatmap** — `T` opens an 8-week × 7-day grid; `enter` filters list to a day | ✅ Done | Beyond the phased work, several quality-of-life items also landed: inline fuzzy ranking for the `p` / `b` filters, a `?` help overlay with @@ -360,6 +363,45 @@ Smaller improvements identified during the 0.4.0 code review: `~/.claude/projects/` location. Resolved by `resolveProjectsDir` in `lore.go`. +### v0.7 — Cleanup and features ✅ + +Two parts. First, a cleanup pass on the rendering and filter code: + +- **Unified footers** (`renderListFooter`, `renderDetailFooter`, + `renderSearchFooter`, `renderProjectFooter`, `renderRerunFooter`, + `renderStatsFooter`). All sub-views show `q/esc/h/← back`; list shows + `q quit`. Flash messages render through one path everywhere. +- **Consistent header chrome** — every mode goes through a dedicated + `render*Header` function, rendered with `headerStyle` on a single line. +- **DRY filter logic** — three near-identical branches in `applyFilter` + collapse into calls to a single `fuzzyFilterSessions(text, candidate, sessions)` + helper. +- **Layout constants** — `projectColWidth`, `branchColWidth`, `fixedCols`, + `rerunMaxLines`, `snippetMaxLen` at the top of `render.go`. Search and + list rows now share a single source of truth for column widths. +- **Dead-code removal** — `renderRows()` (marked "retained for tests" + with no callers) deleted. +- **Missing unit tests** — direct unit tests added for `extractQuery`, + `stripSystemTags`, `collapseWhitespace`. The other "missing" functions + named in the plan turned out to already have direct tests. +- **Scan warnings surfaced** — `scanSessions` now returns `(sessions, + warnings, err)`. The list header shows `(N skipped)` when warnings is + non-empty. No new logging; pure TUI surface. + +Then two features: + +- **Session bookmarks (2A)**: `bookmark.go`. `m` in list and detail + toggles a bookmark on the current session and persists to + `/lore/bookmarks.json`. `M` toggles a bookmark-only filter + (binary; composes with the `f`/`p`/`b` filters). Bookmarked sessions + show `★` in the leftmost column of list, search, and project rows. +- **Timeline heatmap (2B)**: `timeline.go`. `T` from list opens + `modeTimeline`, an 8-week × 7-day grid (Mon..Sun rows, oldest..newest + weeks). Cells are shaded by session count via `heatmapBucket(count)` + → 0 / 1-2 / 3-5 / 6+. `h`/`l` (or `←`/`→`) move the cursor across + days; `enter` applies a date filter and returns to list. The list's + esc handler clears the date filter alongside the others. + --- ## Open questions diff --git a/PLAN.md b/PLAN.md deleted file mode 100644 index 4a7977c..0000000 --- a/PLAN.md +++ /dev/null @@ -1,228 +0,0 @@ -# lore v0.7 — cleanup & new features plan - -> **Created:** 2025-05-05 -> **Status:** Draft — not yet started -> **Goal:** Clean up the codebase for consistency, then add two new features. -> Designed for parallel subagent execution where possible. - ---- - -## Part 1: Codebase Cleanup - -Six independent cleanup tasks. Each can be tackled by a separate subagent in -its own worktree branch, then merged sequentially. - -### 1A — Unify footer rendering - -**Problem:** Five modes render footers in four different ways. Search and -rerun build footers inline in their view functions; list, detail, project, -and stats each have their own footer function. The hint text is inconsistent -("back" vs "quit", "q/esc/h/←" vs "q/esc", missing `d/u` in stats). - -**Work:** -- Extract a single `renderModeFooter(mode, flash, hints)` helper or - at minimum ensure every mode uses a dedicated `render*Footer()` function. -- Standardize hint format: `key action` pairs separated by three spaces. -- All sub-views (detail, search results, project, rerun, stats) should - show `q/esc/h/← back`. List shows `q quit`. -- Add `d/u page` to stats footer (stats supports `d`/`u` scrolling but - the footer doesn't mention it). -- Flash message display should go through one path, not four. - -**Files:** `render.go`, `project.go` -**Tests:** `render_test.go` — add/update footer content assertions. - ---- - -### 1B — DRY the filter logic in `applyFilter` - -**Problem:** `model.go::applyFilter()` has three near-identical branches -for project / branch / fuzzy filtering — ~60 lines of duplicated -build-list → fuzzy-rank → map-back logic. - -**Work:** -- Extract a generic helper: `fuzzyFilterSessions(text string, candidates func(Session) string, sessions []Session) []Session`. -- Reduce `applyFilter` to three calls to that helper. - -**Files:** `model.go`, `filter.go` -**Tests:** `filter_test.go` — existing tests should still pass; add a -unit test for the extracted helper. - ---- - -### 1C — Remove dead code and extract magic numbers - -**Problem:** -- `renderRows()` in `render.go` is marked as "retained for tests" but no - test calls it. -- Hard-coded column widths (48, 12, 20, 26) and line limits (5, 80) are - scattered across render functions with no named constants. - -**Work:** -- Delete `renderRows()` if grep confirms zero callers. -- Extract constants: `fixedCols`, `projectColWidth`, `branchColWidth`, - `snippetMaxLen`, `rerunMaxLines`, etc. -- Align search result row widths to match list row widths (branch column - is 26 in search vs 20 in list — pick one). - -**Files:** `render.go` -**Tests:** Existing render tests; update any that assert exact row strings. - ---- - -### 1D — Add missing unit tests for untested utilities - -**Problem:** Several core functions have zero direct test coverage: -- `extractQuery()`, `stripSystemTags()`, `collapseWhitespace()` — session - metadata extraction used everywhere. -- `groupByBranch()` — project view's data layer. -- `formatTokenCount()`, `estimateCost()` — stats display. -- `clampOffset()`, `sliceLines()` — viewport primitives. - -**Work:** -- Add `viewport_test.go` for `clampOffset` / `sliceLines`. -- Add `stats_test.go` tests for `formatTokenCount` / `estimateCost`. -- Add tests for `extractQuery`, `stripSystemTags`, `collapseWhitespace` - (in `session_test.go` or a new file). -- Add `project_test.go` for `groupByBranch`. - -**Files:** New and existing `*_test.go` files. - ---- - -### 1E — Consistent header chrome - -**Problem:** Headers vary across modes — some show session counts, some -show turn position, some show project name. The inconsistency is fine -where it's intentional (each mode has different context), but the -*structure* should be uniform: left-aligned title, right-aligned context, -same style. - -**Work:** -- Audit each header: ensure all use `headerStyle`, ensure the divider - immediately follows, and ensure no mode builds the header inline in its - view function (rerun currently does). -- Extract `renderRerunHeader()` to match the pattern of the other modes. -- Ensure all headers render at exactly one line height (no wrapping). - -**Files:** `render.go` -**Tests:** `render_test.go` — assert header structure for rerun mode. - ---- - -### 1F — Normalize error handling in scan/parse paths - -**Problem:** `scanSessions` silently skips unreadable files, -`searchSession` returns nil on open failure, `computeStatsRows` produces -zero-stats rows on error. No visibility into what was lost. - -**Work:** -- Add a `warnings []string` field to the model. -- When `scanSessions` skips a file, append to warnings. -- Show warning count in the list footer (e.g. "142 sessions (3 skipped)"). -- Don't add logging — keep it TUI-native. - -**Files:** `model.go`, `session.go`, `render.go` -**Tests:** `session_test.go` — test with an unreadable fixture file. - ---- - -## Part 2: New Features - -Two new features that add genuine value for the target user (5–20 Claude -sessions/day across multiple repos). - -### 2A — Session bookmarks - -**What:** Let users mark sessions as important so they can find them later -without searching. A bookmarked session gets a `★` indicator in the list -and a dedicated filter to show only bookmarks. - -**Why:** Power users accumulate hundreds of sessions. The current tools -(search, project filter, branch filter) are great for "I know roughly -what I'm looking for" but bad for "I want to keep track of the 5 sessions -that represent key decisions." Bookmarks solve the latter. - -**Design:** -- Storage: a JSON file at `/lore/bookmarks.json` — a simple - `map[string]bool` keyed by session ID. Read-only access to Claude Code's - files is preserved; bookmarks are lore's own data. -- Keys: `m` (mark) in list and detail modes to toggle a bookmark on the - current session. Flash message confirms "bookmarked" / "unmarked". -- Filter: `M` in list mode shows only bookmarked sessions (similar UX to - the `p`/`b` filters but binary). -- Display: bookmarked sessions show `★` before the time column in list, - search, and project views. - -**Files (new):** `bookmark.go`, `bookmark_test.go` -**Files (modified):** `model.go`, `render.go`, `project.go` - -**Subagent plan:** -1. Red: tests for `loadBookmarks`, `saveBookmark`, `toggleBookmark`, - and model-level `m` / `M` key handling. -2. Green: implement `bookmark.go` + model integration. -3. Refactor: extract any shared filter logic. - ---- - -### 2B — Session timeline / activity heatmap - -**What:** A new mode (`T` from list) that shows a calendar-style heatmap -of session activity — how many sessions per day over the last 8 weeks, -rendered as a grid of intensity-shaded blocks. - -**Why:** Understanding your Claude usage patterns over time is useful for -productivity awareness ("am I over-relying on Claude for certain tasks?") -and for navigating to a specific day's work. Today there's no way to -answer "what did I work on last Tuesday?" without scrolling through the -time-bucketed list. - -**Design:** -- Layout: 7 rows (Mon–Sun) × 8 columns (weeks), newest week on the - right. Each cell is a two-character block (e.g. `██`) colored by - session count: 0 = dim, 1–2 = light, 3–5 = medium, 6+ = bright. - Below the grid: a legend row showing the intensity scale. -- Navigation: `h`/`l` or `←`/`→` to move the highlight across days. - The footer shows the date and session count for the highlighted day. - `enter` on a day filters the list to that date and returns to list - mode. -- Data: reuse the already-parsed `sessions` slice — just bucket by - `time.Time.Truncate(24h)` into a `map[string]int` of date → count. - -**Files (new):** `timeline.go`, `timeline_test.go` -**Files (modified):** `model.go` (new mode + `T` key), `detail.go` -(add `modeTimeline` constant), `render.go` (add `renderTimelineView`), -help overlay. - -**Subagent plan:** -1. Red: tests for `buildHeatmapData`, `heatmapColor`, timeline - navigation keys, and enter-to-filter. -2. Green: implement `timeline.go` + model integration + rendering. -3. Refactor: extract date-bucketing if it overlaps with `timeBucket`. - ---- - -## Execution Strategy - -These tasks are designed for parallel subagent execution: - -``` -Parallel batch 1 (cleanup — all independent): - ├── Agent 1A: footer unification (worktree: cleanup/footer) - ├── Agent 1B: DRY filter logic (worktree: cleanup/filter) - ├── Agent 1C: dead code + constants (worktree: cleanup/constants) - ├── Agent 1D: missing unit tests (worktree: cleanup/tests) - ├── Agent 1E: header chrome (worktree: cleanup/header) - └── Agent 1F: error handling (worktree: cleanup/errors) - -Sequential merge: 1A → 1B → 1C → 1D → 1E → 1F into main - -Parallel batch 2 (features — independent of each other, depend on cleanup): - ├── Agent 2A: bookmarks (worktree: feat/bookmarks) - └── Agent 2B: timeline heatmap (worktree: feat/timeline) - -Sequential merge: 2A → 2B into main -``` - -Each agent follows the repo's red → green → refactor contract and opens -a PR. Coverage gate (≥80%) must pass before merge. diff --git a/README.md b/README.md index 7324518..d381a42 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ go build ./cmd/lore The tool reads session transcripts from `~/.claude/projects/` and displays them in a sortable, navigable list. -**Current status (v0.6.0)**: All planned phases complete — session list with query preview (first user message) and project/branch/fuzzy filters, session detail with tool expansion / diff rendering / turn position indicator / sidechain expansion, FTS5-indexed full-text search (with linear-scan fallback), project view, re-run (returns to list on exit), half-page scrolling (`d`/`u`) in all modes, and a usage-stats panel showing token counts and estimated cost per session. See `DESIGN.md` for the full vision and roadmap. +**Current status (v0.7.0)**: All planned phases plus the v0.7 cleanup and feature pass complete — session list with query preview (first user message) and project/branch/fuzzy filters, session detail with tool expansion / diff rendering / turn position indicator / sidechain expansion, FTS5-indexed full-text search (with linear-scan fallback), project view, re-run (returns to list on exit), half-page scrolling (`d`/`u`) in all modes, a usage-stats panel showing token counts and estimated cost per session, **session bookmarks** with `★` markers and a bookmark-only filter, and a **timeline activity heatmap** showing 8 weeks of session activity. The render code was unified in v0.7: every mode goes through dedicated `render*Header` / `render*Footer` functions, footer hints and back-nav are consistent across all sub-views, and skipped sessions are surfaced in the list header as "(N skipped)". See `DESIGN.md` for the full vision and roadmap. ## Configuration @@ -24,18 +24,19 @@ The tool reads session transcripts from `~/.claude/projects/` and displays them - `--dir ` flag (highest precedence) - `LORE_PROJECTS_DIR` environment variable -The FTS5 search index is cached under the platform-appropriate user cache dir (e.g. `~/.cache/lore/index.db` on Linux). +The FTS5 search index and the bookmarks file (`bookmarks.json`) are cached under the platform-appropriate user cache dir (e.g. `~/.cache/lore/` on Linux, `~/Library/Caches/lore/` on macOS). ## Navigation Press `?` in any mode for the full keymap. Highlights: -- **List**: `j`/`k` move, `d`/`u` half-page, `g`/`G` jump, `enter` open, `p`/`b` filter project/branch, `f` fuzzy filter, `P` project view, `/` search, `S` usage stats, `q` quit. -- **Detail**: `d`/`u` half-page, `space` expand a tool turn (Agent turns with sidechains load the sub-conversation inline), `y` copy the nearest user prompt, `r` re-run that prompt, `/` search, `esc`/`q`/`h`/`←` back. +- **List**: `j`/`k` move, `d`/`u` half-page, `g`/`G` jump, `enter` open, `p`/`b` filter project/branch, `f` fuzzy filter, `m` bookmark, `M` bookmark-only filter, `P` project view, `/` search, `S` usage stats, `T` timeline heatmap, `q` quit. +- **Detail**: `d`/`u` half-page, `space` expand a tool turn (Agent turns with sidechains load the sub-conversation inline), `y` copy the nearest user prompt, `r` re-run that prompt, `m` bookmark this session, `/` search, `esc`/`q`/`h`/`←` back. - **Search**: type → `enter` to run, `j`/`k` or `d`/`u` through hits, `enter` to open, `esc`/`q`/`h`/`←` back. - **Project**: `j`/`k`, `d`/`u`, `enter` to open, `esc`/`q`/`h`/`←` back. Sessions are grouped by branch. - **Re-run**: `enter` to spawn `claude` with the chosen prompt and CWD; `esc`/`q`/`h`/`←` to cancel. - **Stats**: `j`/`k`, `g`/`G` to navigate; `esc`/`q`/`h`/`←` back. Columns: project · branch · model · input/output tokens · estimated cost. +- **Timeline**: `h`/`←` and `l`/`→` move the cursor across days in an 8-week activity heatmap; `enter` filters the list to the highlighted day; `esc`/`q` back. ## For contributors