Initialize beads (br) for dependency-aware issue tracking in this project.
Beads provides a lightweight task database with graph-aware triage, used
for coordinating work on the JSONL-first discovery feature.
Files added:
- .beads/config.yaml: Project configuration (issue prefix, defaults)
- .beads/issues.jsonl: Issue database with JSONL-first discovery tasks
- .beads/metadata.json: Beads metadata (commit correlation, etc.)
- .beads/.gitignore: Ignore SQLite databases, lock files, temp files
Also ignore .bv/ (beads viewer local state) in project .gitignore.
Beads is non-invasive: it never executes git/jj commands. The .beads/
directory is manually committed alongside code changes.
Usage:
- br ready --json: Find unblocked work
- br update <id> --status in_progress: Claim a task
- br close <id> --reason "done": Complete a task
- bv --robot-triage: Get graph-aware recommendations
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace Inter with Geist font from jsDelivr CDN for a more modern look
that aligns with Vercel's design system.
Changes:
- index.html: Add Geist font stylesheet from jsDelivr
- main.css: Update font-family to use "Geist" as primary
- tailwind.config.js: Update fontFamily.sans to start with "Geist"
Geist is Vercel's open-source font family designed for readability and
clarity in developer tools. It pairs well with JetBrains Mono for code
blocks (already in use).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add density toggle, mobile sidebar behavior, and various polish improvements
to the session viewer interface.
Density toggle (comfortable/compact):
- Persisted to localStorage as "session-viewer-density"
- Compact mode reduces message padding and spacing
- Toggle button in nav bar with LayoutRows icon
- Visual indicator when compact mode is active
Mobile sidebar:
- Sidebar slides in/out with transform transition
- Hamburger menu button visible on mobile (md:hidden)
- Backdrop overlay when sidebar is open
- Auto-close sidebar after selecting a session
Session info improvements:
- Show project/session title in nav bar when viewing session
- Add session ID display with copy button in SessionViewer
- Copy button shows check icon for 1.5s after copying
SessionList enhancements:
- Relative time format: "5m ago", "2h ago", "3d ago"
- Total message count per project in project list
- Truncated project names showing last 2 path segments
- Full path available in title attribute
MessageBubble:
- Add compact prop for reduced padding
- Add accentBorder property to category colors
- Migrate inline SVGs to shared Icons
SessionViewer:
- Sticky session info header with glass effect
- Add compact prop that propagates to MessageBubble
- Keyboard shortcut hints in empty state
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Rewrite session discovery to be filesystem-first, addressing the widespread
bug where Claude Code's sessions-index.json files are unreliable (87 MB of
unindexed sessions, 17% loss rate across all projects).
Architecture: Three-tier metadata lookup
Tier 1 - Index validation (instant):
- Parse sessions-index.json into Map<sessionId, IndexEntry>
- Validate entry.modified against actual file stat.mtimeMs
- Use 1s tolerance to account for ISO string → filesystem mtime rounding
- Trust content fields only (messageCount, summary, firstPrompt)
- Timestamps always come from fs.stat, never from index
Tier 2 - Persistent cache hit (instant):
- Check MetadataCache by (filePath, mtimeMs, size)
- If match, use cached metadata
- Survives server restarts
Tier 3 - Full JSONL parse (~5-50ms/file):
- Call extractSessionMetadata() with shared parser helpers
- Cache result for future lookups
Key correctness guarantees:
- All .jsonl files appear regardless of index state
- SessionEntry timestamps always from fs.stat (list ordering never stale)
- Message counts exact (shared helpers ensure parser parity)
- Duration computed from JSONL timestamps, not index
Performance:
- Bounded concurrency: 32 concurrent operations per project
- mapWithLimit() prevents file handle exhaustion
- Warm start <1s (stat all files, in-memory lookups)
- Cold start ~3-5s for 3,103 files (stat + parse phases)
TOCTOU handling:
- Files that disappear between readdir and stat: silently skipped
- Files that disappear between stat and read: silently skipped
- File actively being written: partial parse handled gracefully
Include PRD document that drove this implementation with detailed
requirements, edge cases, and verification plan.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Introduce MetadataCache class in metadata-cache.ts that persists extracted
session metadata to ~/.cache/session-viewer/metadata.json for fast warm
starts across server restarts.
Key features:
- Invalidation keyed on (mtimeMs, size): If either changes, entry is
re-extracted via Tier 3 parsing. This catches both content changes
and file truncation/corruption.
- Dirty-flag write-behind: Only writes to disk when entries have changed,
coalescing multiple discovery passes into a single write operation.
- Atomic writes: Uses temp file + rename pattern to prevent corruption
from crashes during write. Safe for concurrent server restarts.
- Stale entry pruning: Removes entries for files that no longer exist
on disk during the save operation.
- Graceful degradation: Missing or corrupt cache file triggers fallback
to Tier 3 extraction for all files (cache rebuilt on next save).
Cache file format:
{
"version": 1,
"entries": {
"/path/to/session.jsonl": {
"mtimeMs": 1234567890,
"size": 12345,
"messageCount": 42,
"firstPrompt": "...",
"summary": "...",
"firstTimestamp": "...",
"lastTimestamp": "..."
}
}
}
Test coverage includes:
- Cache hit/miss/invalidation behavior
- Dirty flag triggers write only when entries changed
- Concurrent save coalescing
- Stale entry pruning on save
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Introduce extractSessionMetadata() in a new session-metadata.ts module
that extracts only what the list view needs from JSONL files:
- messageCount: Uses shared countMessagesForLine() for exact parity
- firstPrompt: First non-system-reminder user message, truncated to 200 chars
- summary: Last type="summary" line's summary field
- firstTimestamp/lastTimestamp: For duration computation
Design goals:
- Parser parity: Uses forEachJsonlLine() and countMessagesForLine() from
session-parser.ts, ensuring list counts always match detail-view counts
- No string building: Avoids JSON.stringify and markdown processing
- 2-3x faster than full parse: Only captures metadata, skips content
- Graceful degradation: Handles malformed lines identically to full parser
This is the Tier 3 data source for JSONL-first session discovery. When
neither the sessions-index.json nor the persistent cache has valid data,
this function extracts fresh metadata from the file.
Test coverage includes:
- Output matches parseSessionContent().length on sample fixtures
- Duration extraction from JSONL timestamps
- firstPrompt extraction skips system-reminder content
- Empty files return zero counts and empty strings
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Introduce three shared helpers in session-parser.ts that both the full
parser and the lightweight metadata extractor can use:
- forEachJsonlLine(content, onLine): Iterates JSONL lines with consistent
malformed-line handling. Skips invalid JSON lines identically to how
parseSessionContent handles them. Returns parse error count for diagnostics.
- countMessagesForLine(parsed): Returns the number of messages a single
JSONL line expands into, using the same classification rules as the
full parser. User arrays expand tool_result and text blocks; assistant
arrays expand thinking, text, and tool_use.
- classifyLine(parsed): Classifies a parsed line into one of 8 types
(user, assistant, system, progress, summary, file_snapshot, queue, other).
The internal extractMessages() function now uses these shared helpers,
ensuring no behavior change while enabling the upcoming metadata extraction
service to reuse the same logic. This guarantees list counts can never drift
from detail-view counts, regardless of future parser changes.
Test coverage includes:
- Malformed line handling parity with full parser
- Parse error counting for truncated/corrupted files
- countMessagesForLine output matches extractMessages().length
- Edge cases: empty files, progress events, array content expansion
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Self-contained 701-line HTML document providing a visual map of the
session-viewer codebase architecture. Includes interactive node
diagrams for client, server, shared, and build layers with
dependency relationships.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Test fixture updates:
- Add toolUseId fields (toolu_read1, toolu_edit1) to tool_use blocks
- Add parentToolUseID-linked progress events for read and edit tools
- Add orphaned SessionStart progress event (no parent)
- Update tool_result references to match new toolUseId values
- Add bash_progress and mcp_progress subtypes for subtype derivation
session-parser tests (7 new):
- toolUseId extraction from tool_use blocks with and without id field
- parentToolUseId and progressSubtype extraction from hook_progress
- Subtype derivation for bash_progress, mcp_progress, agent_progress
- Fallback to "hook" for unknown data types
- Undefined parentToolUseId when field is absent
progress-grouper tests (7 new):
- Partition parented progress into toolProgress map
- Remove parented progress from filtered messages array
- Keep orphaned progress (no parentToolUseId) in main stream
- Keep progress with invalid parentToolUseId (no matching tool_call)
- Empty input handling
- Sort each group by rawIndex
- Multiple tool_call parents tracked independently
agent-progress-parser tests (full suite):
- Parse user text events with prompt/agentId metadata extraction
- Parse tool_use blocks into AgentToolCall events
- Parse tool_result blocks with content extraction
- Parse text content as text_response with line counting
- Handle multiple content blocks in single turn
- Post-pass tool_result→tool_call linking (sourceTool, language)
- Empty input and malformed JSON → raw_content fallback
- stripLineNumbers for cat-n prefixed output
- summarizeToolCall for Read, Grep, Glob, Bash, Task, WarpGrep, etc.
ProgressBadge component tests:
- Collapsed state shows pill counts, hides content
- Expanded state shows all event content via markdown
- Subtype counting accuracy
- Agent-only events route to AgentProgressView
AgentProgressView component tests:
- Prompt banner rendering with truncation
- Agent ID and turn count display
- Summary rows with timestamps and tool names
- Click-to-expand drill-down content
html-exporter tests (8 new):
- Collapsible rendering for thinking, tool_call, tool_result
- Toggle button and JavaScript inclusion
- Non-collapsible messages lack collapse attributes
- Diff content detection and highlighting
- Progress badge rendering with toolProgress data
filters tests (2 new):
- hook_progress included/excluded by category toggle
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
SearchBar:
- Switch from fixed w-80/sm:w-96 to fluid min-w-80 max-w-md w-full
- Adjust left padding for better visual alignment with search icon
SessionList:
- Memoize project grouping computation with useMemo
- Move horizontal padding from per-item margin (mx-2 + width calc hack)
to container padding (px-2) for cleaner layout and full-width buttons
- Remove inline width override that was compensating for the old margins
Tooltip:
- Increase offset from 8px to 12px for better visual separation
CSS:
- Add prose-message-progress variant with compact 11px mono typography
for progress event content (code blocks, tables, links, blockquotes)
- Reduce search minimap marker height from 4px to 3px
- Normalize prose-message line-height: paragraphs 1.625→1.6, list
items 1.5→1.6 for consistent rhythm
- Switch custom checkbox checkmark sizing from fixed px to percentages
for better scaling across different zoom levels
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Major overhaul of the static HTML export to match the interactive viewer:
Collapsible messages:
- thinking, tool_call, and tool_result categories render collapsed by
default with data-collapsed attribute
- Chevron toggle button rotates on expand/collapse
- Collapsed preview shows: line count (thinking), tool name (tool_call),
or first 120 chars (tool_result)
- Print media query forces all sections expanded and hides toggle UI
Diff highlighting:
- tool_result content is checked for diff patterns (hunk headers, +/-
lines) via isDiffContent() heuristic
- Diff content renders with color-coded spans: green additions, red
deletions, purple hunk headers, gray meta lines
Progress badges:
- tool_call messages with associated progress events render a clickable
pill row showing event counts by subtype (hook/bash/mcp/agent)
- Clicking toggles a drawer with timestamped event log
- Subtype colors match the client-side ProgressBadge component
Interactive JavaScript:
- Export now includes a <script> block for toggle interactivity
- Single delegated click handler on document for both collapsible
messages and progress drawers
CSS additions:
- Left color bar via ::before pseudo-element on .message
- Collapsible toggle, collapsed preview, diff highlighting, and
progress badge/drawer styles
- Print overrides to show all content and hide interactive controls
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
MessageBubble:
- Accept progressEvents and progressEnabled props
- Render ProgressBadge below tool_call content when progress data exists
- Normalize left padding from mixed pl-5/px-4 to consistent px-5
SessionViewer:
- Thread toolProgress map and progressEnabled flag through to messages
- Look up progress events by toolUseId for each tool_call message
- Fix hash-anchor scroll firing on every filter toggle by tracking
whether scroll has occurred per session load (hashScrolledRef)
- Increase vertical spacing on time gap dividers and header area
App:
- Derive progressEnabled from hook_progress category filter state
- Pass toolProgress and progressEnabled to SessionViewer
- Optimize sensitiveCount to compute across all session messages
(not filtered subset) to avoid re-running 37 regex patterns on
every filter toggle
- Tighten redaction selection badge gap from 2 to 1.5
useSession:
- Add AbortController to loadSession to cancel stale in-flight requests
when user rapidly switches sessions
- Only clear loading state if the completing request is still current
- Ignore AbortError exceptions from cancelled fetches
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Three new components for rendering tool progress events inline:
ProgressBadge — Attaches below tool_call messages. Shows color-coded
pills counting events by subtype (hook/bash/mcp/agent). Expands on
click to show either a timestamped event log (mixed subtypes) or the
full AgentProgressView (all-agent subtypes). Lazy-renders markdown
only when expanded via useMemo.
ProgressGroup — Standalone progress divider for orphaned progress
events in the message stream. Centered pill-style summary with event
count, subtype breakdown, and time range. Expands to show the same
timestamped log format as ProgressBadge.
AgentProgressView — Rich drill-down view for agent sub-conversations.
Parses agent_progress JSON via parseAgentEvents into a structured
activity feed with:
- Header showing quoted prompt, agent ID, turn count, and time range
- Per-event rows with timestamp, SVG icon (10 tool-specific icons),
short tool name, and summarized description
- Click-to-expand drill-down rendering tool inputs as JSON code blocks,
tool results with language-detected syntax highlighting (after
stripping cat-n line numbers), and text responses as markdown
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
New agent-progress-parser.ts provides rich parsing of agent_progress
JSON payloads from hook_progress events into a structured event stream:
Event types: AgentToolCall, AgentToolResult, AgentTextResponse,
AgentUserText, AgentRawContent — unified under AgentEvent discriminated
union with ParsedAgentProgress metadata (prompt, agentId, timestamps,
turnCount).
Key capabilities:
- Parse nested message.message.content blocks from agent progress JSON
- Post-pass linking of tool_results to their preceding tool_calls via
toolUseId map, enriching results with sourceTool and language hints
- Language detection from file paths (30+ extensions mapped)
- stripLineNumbers() to remove cat-n style prefixes for syntax detection
- summarizeToolCall() for human-readable one-line summaries of common
Claude tools (Read, Write, Edit, Grep, Glob, Bash, Task, WarpGrep)
Also re-exports ProgressSubtype from client types barrel.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
New progress-grouper service partitions ParsedMessage arrays into two
outputs: a filtered messages list (orphaned progress stays inline) and
a toolProgress map keyed by parentToolUseId. Only hook_progress events
whose parentToolUseId matches an existing tool_call are extracted; all
others remain in the main message stream. Each group is sorted by
rawIndex for chronological display.
Session route integration:
- Pipe parseSession output through groupProgress before responding
- Return toolProgress map alongside messages in session detail endpoint
Cache improvements:
- Deduplicate concurrent getCachedSessions() calls with a shared
in-flight promise (cachePromise) to prevent thundering herd on
multiple simultaneous requests
- Track cache generation to avoid stale writes when a newer discovery
supersedes an in-flight one
- Clear cachePromise on refresh=1 to force a fresh discovery cycle
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Introduce ProgressSubtype union ("hook" | "bash" | "mcp" | "agent") and
three new fields on ParsedMessage: toolUseId, parentToolUseId, and
progressSubtype. These enable linking hook_progress events to the
tool_call that spawned them and classifying progress by source.
Session parser changes:
- Extract `id` from tool_use content blocks into toolUseId
- Extract `tool_use_id` from tool_result blocks into toolUseId (was
previously misassigned to toolName)
- Read `parentToolUseID` from raw progress lines
- Derive progressSubtype from the `data.type` field using a new
deriveProgressSubtype() helper
- Add `toolProgress` map to SessionDetailResponse for grouped progress
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Wrap SessionViewer in an ErrorBoundary so render errors show a
recovery UI instead of a white screen.
Sync the selected session ID with the URL search param (?session=).
On initial load, load the session from the URL if it exists in the
session list. On session change, update the URL via
history.replaceState without triggering navigation.
Add j/k keyboard navigation to step through filtered messages.
Search focus takes precedence over keyboard focus; both reset when
filters or session changes.
Add a refresh button in the sidebar header that calls the new
refreshSessions hook, with a spinning icon while loading.
Pass sensitiveCount to FilterPanel for the new badge display.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Messages at index 20+ no longer receive the animate-fade-in class or
animationDelay inline style. This avoids scheduling hundreds of CSS
animations on large sessions where the stagger would be invisible
anyway (the earlier cap of 300ms max delay was already clamping them).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Arrow Up/Down keys now cycle through search matches while the input
is focused, matching the behavior of browser find-in-page. Navigation
buttons gain active:scale-95 press feedback and explicit sizing.
The right-side control region is now conditionally rendered: keyboard
hint (/) shows when empty, match count + nav + clear show when active.
A visual divider separates navigation arrows from the clear button.
The match count badge highlights the current position number with a
distinct weight.
Tests cover empty state visibility, active search state rendering,
arrow key and button navigation, and clear button behavior.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Tool call inputs and structured data categories (hook_progress,
file_snapshot) now attempt JSON.parse + JSON.stringify(_, null, 2)
before escaping to HTML. Non-JSON content passes through unchanged.
The detection fast-paths by checking the first non-whitespace
character for { or [ before attempting parse.
Also renames the copy state variable from linkCopied to contentCopied
to match the current behavior of copying message content rather than
anchor links.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
useSession now exposes a refreshSessions() callback that fetches
/api/sessions?refresh=1. The sessions route checks for the refresh
query parameter and resets the cache timestamp to zero, forcing a
fresh scan of ~/.claude/projects/ on the next request.
This enables users to pick up new sessions without restarting the
server or waiting for the 30-second cache to expire.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
React class component that catches render errors in child trees and
displays a styled error panel with the exception message and a
'Try again' button that resets state. Follows the existing design
system with red accent colors and gradient icon background.
Tests verify normal rendering, error capture with message display,
and the presence of the recovery button.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Introduce a reusable Tooltip component with delayed hover reveal,
viewport-aware horizontal nudging, and smooth CSS entrance animation.
Supports top/bottom positioning via a data-side attribute.
FilterPanel now wraps the auto-redact checkbox in a Tooltip that
explains what auto-redaction detects. When sensitive messages exist
in the current view, a red pill badge displays the count next to
the label, giving users immediate visibility into how many messages
contain detectable secrets before toggling auto-redact on.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Export a new countSensitiveMessages() function that returns how many
messages in an array contain at least one sensitive pattern match.
Checks both content and toolInput fields, counting each message at
most once regardless of how many matches it contains.
Tests verify zero counts for clean messages, correct counting with
mixed sensitive/clean messages, and the single-count-per-message
invariant when multiple secrets appear in one message.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Category toggles and the auto-redact checkbox now survive page
reloads. On mount, useFilters reads from localStorage keys
session-viewer:enabledCategories and session-viewer:autoRedact,
falling back to defaults when storage is empty, corrupted, or
contains invalid category names. Each state change writes back
to localStorage in a useEffect.
Tests cover round-trip persistence, invalid data recovery, corrupted
JSON fallback, and the boolean coercion for auto-redact.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace the minimal beads workflow section with detailed documentation
for all agent tooling: br (beads_rust) CLI with workflow patterns and
key invariants, bv robot mode with triage/planning/graph analysis/
history/forecasting commands, hybrid semantic search, static site
export, ast-grep vs ripgrep guidance, Morph WarpGrep usage, UBS
static analysis, and cass cross-agent session search.
Each section includes concrete command examples, jq quick references,
and anti-patterns to avoid. The structure follows a discovery-first
approach — agents start with triage, then drill into specifics.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The per-message copy button previously copied a URL with a hash
anchor (#msg-{uuid}) for deep linking. Replace this with copying
the actual message content: for tool_call messages it copies the
tool name and input; for all other categories it copies the text
body. This is more immediately useful — users copying message
content is far more common than sharing anchor links.
Button title updated from "Copy link to message" to "Copy message
content" to match the new behavior.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Change the default-hidden message categories from [thinking,
hook_progress] to [tool_result, system_message, hook_progress,
file_snapshot]. This hides the verbose machine-oriented categories
by default while keeping thinking blocks visible — they contain
useful reasoning context that users typically want to see.
Also rename the "summary" category label from "Summaries" to
"Compactions" to better reflect what Claude's summary messages
actually represent (context-window compaction artifacts).
Tests updated to match the new defaults: the filter test now
asserts that tool_result, system_message, hook_progress, and
file_snapshot are all excluded, producing 5 visible messages
instead of the previous 7.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The file was originally scaffolded with the CLAUDE.md boilerplate
header. Update the title and subtitle to reflect that this document
is guidance for any AI agent, not just Claude Code specifically.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Session fetch (useSession.ts):
- Wrap the session ID in encodeURIComponent before interpolating
into the fetch URL. Session IDs can contain characters like '+'
or '/' that would corrupt the path without encoding.
Export route (export.ts):
- Add validation that redactedMessageUuids, when present, is an
array. Previously only visibleMessageUuids was checked, so a
malformed redactedMessageUuids value (e.g. a string or object)
would silently pass validation and potentially cause downstream
type errors in the HTML exporter.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The search index (app.tsx) and the per-message match check
(SessionViewer.tsx) now also search msg.toolInput when present.
This means searching for a file path, command, or argument that
appears in a tool call's input will correctly highlight and
navigate to that message, rather than dimming it as a non-match.
Both locations use the same compound condition so the match index
and the visual dimming/focus logic stay in sync.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace try/catch with isNaN guard in the HTML exporter's
formatTimestamp, matching the same cleanup applied client-side.
Downgrade the export button from btn-primary to btn-secondary so it
doesn't compete visually with the main content area. The primary blue
gradient was overly prominent for a utility action.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Time gaps:
- Insert a horizontal divider with duration label ("12m later",
"1h 30m later") between consecutive visible messages separated
by more than 5 minutes
- Computed during the display list build pass alongside redacted
dividers, so no additional traversal is needed
Hash anchor navigation:
- Each message div now has id="msg-{uuid}" for deep linking
- On load, if the URL contains a #msg-* hash, scroll that message
into view with smooth centering and a 3-second highlight ring
- Works with the copy-link feature added to MessageBubble headers
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Custom marked renderer wraps fenced code blocks in a .code-block-wrapper
div containing a language label badge (top-right) and a copy button.
The language label shows the fenced language identifier in uppercase.
A delegated click handler on the document root intercepts clicks on
[data-copy-code] buttons, reads the sibling <code> element's text
content, writes it to the clipboard, and shows a "Copied!" / "Failed"
confirmation that auto-reverts after 1.5 seconds. Delegated handling
is necessary because code blocks are rendered via dangerouslySetInnerHTML
and don't participate in React's synthetic event system.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Collapsible blocks:
- Thinking, tool_call, and tool_result messages start collapsed by default
- Chevron toggle in the header expands/collapses content
- Collapsed preview shows line count (thinking), tool name (tool_call),
or first line truncated to 120 chars (tool_result)
- Collapsed blocks skip expensive markdown/highlight rendering entirely
Diff rendering:
- Detect unified diff content via hunk header + add/delete line heuristics
(requires both @@ headers AND +/- lines to avoid false positives on
YAML or markdown lists with leading dashes)
- Render diffs with color-coded line classes: green additions, red
deletions, blue hunk headers, and muted meta/header lines
- Add full diff-view CSS with background tints and block-level spans
Header actions (appear on hover):
- Copy link button: copies a #msg-{uuid} anchor URL to clipboard with
a checkmark confirmation animation
- Redaction toggle button: replaces the previous whole-card onClick
handler with an explicit eye-slash icon button, colored red when
selected — more discoverable and less accident-prone
Style adjustments:
- Raise dimmed message opacity from 0.2/0.45 to 0.35/0.65 for better
readability during search filtering
- Fix btn-secondary hover border using explicit rgba value instead of
Tailwind opacity modifier (which was generating invalid CSS)
- Position copy button below language label when both are present
- Simplify formatTimestamp with isNaN guard instead of try/catch
- Use fixed h-10 header height for consistent vertical alignment
- Brighten user and assistant message backgrounds (bg-surface-overlay)
to visually distinguish them from other message types
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Show human-readable session duration (e.g. "23m", "1h 15m") in the
session list metadata row when duration > 0. Add formatSessionDuration
helper that handles sub-minute, minute-only, and hour+minute ranges.
Also replace try/catch in formatDate with an isNaN guard on the parsed
Date, which is more idiomatic and avoids swallowing unrelated errors.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Extend SessionEntry with an optional duration field (milliseconds)
computed from the delta between created and modified timestamps.
The computeDuration helper handles missing or invalid dates gracefully,
returning 0 for any edge case. This enables downstream UI to show
how long each session lasted without additional API calls.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Ignore node_modules, dist output, TypeScript build info,
environment files, editor configs, and OS metadata files.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace hardcoded absolute paths in test assertions with dynamically
constructed paths matching the temp directory. This makes tests portable
across environments where path.resolve() produces different results.
Add test verifying that absolute paths pointing outside the projects
directory (e.g. /etc/shadow.jsonl) are rejected by the discovery filter.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Widen the test include pattern from 'src/client/components/**/*.test.tsx'
to 'src/client/**/*.test.{ts,tsx}' so that tests in src/client/lib/ (e.g.
markdown.test.ts) are discovered automatically.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Visual overhaul of exported HTML to match the new client dark design:
- Replace category-specific CSS classes with inline border/dot/text styles
from a CATEGORY_STYLES map matching client-side colors
- Add message header layout with category dot, label, and timestamp
- Add Inter font family, refined prose typography, and proper code styling
- Add print-friendly media query
- Redesign redacted divider with SVG eye-slash icon and red accent
- Add SVG icons to session header metadata (project, date, message count)
- Fix singular/plural for '1 message' vs 'N messages'
Performance: Skip markdown parsing for hook_progress, tool_result, and
file_snapshot categories (structured data). Render as preformatted text
instead, avoiding expensive marked.parse() on large JSON blobs (~300ms each).
Replace local escapeHtml with shared/escape-html module. Add formatTimestamp
helper. Add cast safety comment for marked.parse() sync usage.
Update test to verify singular message count ('1 message' not '1 messages').
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Performance: Replace hljs.highlightAuto() fallback with plain escapeHtml()
for unlabeled code blocks. highlightAuto tries every grammar (~6.7ms/block)
while escapeHtml costs ~0.04ms. With thousands of unlabeled blocks in
typical sessions this dominated render time.
Import shared escapeHtml instead of the local duplicate. Import github-dark
highlight.js theme CSS directly.
Fix highlightSearchText to avoid corrupting HTML entities: split text on
entity patterns (& < etc.) before applying search regex, so searching
for 'amp' does not break & into &<mark>amp</mark>;.
Add unit tests for highlightSearchText covering: plain text matches, empty
queries, avoiding matches inside HTML tags, and preserving HTML entities.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implement full-featured search navigation across the session viewer:
App.tsx: Compute matchIndices (filtered message indices matching the query),
track currentMatchPosition state, and provide goToNext/goToPrev callbacks.
Add scroll tracking via ResizeObserver + scroll events for minimap viewport.
Restructure toolbar layout with centered search bar and right-aligned export.
Pass focusedIndex to SessionViewer for scroll-into-view behavior.
SearchBar: Redesign as a unified search container with integrated match count
badge, prev/next navigation arrows, clear button, and keyboard hint (/).
Add keyboard shortcuts: Enter/Shift+Enter for next/prev, Ctrl+G/Ctrl+Shift+G
for navigation, Escape to clear and blur. Show 'X/N' position indicator and
'No results' state.
SessionViewer: Add data-msg-index attributes for scroll targeting via
querySelector instead of individual refs. Memoize displayItems list. Add
MessageSkeleton component for loading state. Add empty state illustrations
with icons and descriptive text. Apply staggered fade-in animations and
search-match-focused outline to the active match.
SearchMinimap: New component rendering match positions as amber ticks on a
narrow strip overlaying the scroll area's right edge. Includes viewport
position indicator and click-to-jump behavior.
Add unit tests for SearchMinimap: empty/zero states, tick rendering,
active tick styling, viewport indicator, and click handler.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
MessageBubble: Replace border-left colored bars with rounded cards featuring
accent strips, category dot indicators, and timestamp display. Use shared
escapeHtml. Render tool_result, hook_progress, and file_snapshot as
preformatted text instead of markdown (avoids expensive marked.parse on
large JSON/log blobs).
ExportButton: Add state machine (idle/exporting/success/error) with animated
icons, gradient backgrounds, and auto-reset timers. Replace alert() with
inline error state.
FilterPanel: Add collapsible panel with category dot colors, enable count
badge, custom checkbox styling, and smooth animations.
SessionList: Replace text loading state with skeleton placeholders. Add
empty state illustration with descriptive text. Style session items as
rounded cards with hover/selected states, glow effects, and staggered
entry animations. Add project name decode explanation comment.
RedactedDivider: Add eye-slash SVG icon, red accent color, and styled
dashed lines replacing plain text divider.
useFilters: Remove unused exports (setAllCategories, setPreset, undoRedaction,
clearAllRedactions, selectAllVisible, getMatchCount) to reduce hook surface
area. Match counting moved to App component for search navigation.
SessionList.test: Update assertions for skeleton loading state and expanded
empty state text.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Establish a cohesive dark UI foundation:
- Define CSS custom properties for surfaces (4-level depth hierarchy),
borders, foreground text (3-tier), accent colors, and canvas background
- Add Inter (text) and JetBrains Mono (code) font loading via Google Fonts
- Extend Tailwind with semantic color tokens, typography scale (caption/body/
subheading/heading), box shadows (card, glow), and animations (fade-in,
slide-in, skeleton shimmer)
- Add component-layer utilities: .btn system (primary/secondary/ghost/danger),
.glass frosted overlays, .custom-checkbox, .skeleton loaders
- Add .prose-message typographic styles for rendered markdown content
- Add search minimap CSS (tick marks, viewport indicator)
- Restyle scrollbars for thin dark appearance (WebKit + Firefox)
- Replace hardcoded Tailwind color classes in CATEGORY_COLORS with semantic
tokens (dot/border/text) mapped to the new design system
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The keyword pre-filter used case-sensitive string matching for all patterns,
but several regex patterns use the /i flag (e.g. generic_api_key). This meant
inputs like 'ApiKey = "secret"' would skip the keyword check for 'api_key'
and miss the redaction entirely.
Changes:
- Add caseInsensitive parameter to hasKeyword() that lowercases both content
and keywords before comparison
- Detect /i flag on pattern regex and pass it through automatically
- Narrow IP address keywords from ["."] to ["0.", "1.", ..., "9."] to reduce
false-positive regex invocations on content containing periods
- Fix email regex character class [A-Z|a-z] → [A-Za-z] (the pipe was literal)
- Add clarifying comment on url_with_creds pattern
- Add test cases for mixed-case and UPPER_CASE key assignments
- Relax SECRET_KEY test assertion to accept either redaction label
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Security: Reject session paths containing '..' traversal segments or
non-.jsonl extensions before resolving them. This prevents a malicious
sessions-index.json from tricking the viewer into reading arbitrary files.
Performance: Process all project directories concurrently with Promise.all
instead of sequentially awaiting each one. Each directory's stat + readFile
is independent I/O that benefits from parallelism.
Add test case verifying that traversal paths and non-JSONL paths are rejected
while valid paths pass through.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>