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>
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>
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>
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>
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>
The Express server was binding to both 127.0.0.1 and a specific Tailscale IP
(100.84.4.113), creating two separate http.Server instances. Simplify to a
single localhost binding. Also update Vite dev server to use 127.0.0.1 instead
of the Tailscale IP and disable the HMR error overlay which obscures the UI
during development.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The same HTML entity escaping logic was duplicated in three places:
MessageBubble.tsx, html-exporter.ts, and markdown.ts. Consolidate into
a single shared/escape-html.ts with a single-pass regex+lookup implementation
instead of five chained .replace() calls.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Full React 18 client application for interactive session browsing:
app.tsx:
- Root component orchestrating session list, viewer, filters, search,
redaction controls, and export — wires together useSession and
useFilters hooks
- Keyboard navigation: j/k or arrow keys for message focus, Escape to
clear search and redaction selection, "/" to focus search input
- Derives filtered messages, match count, visible UUIDs, and category
counts via useMemo to avoid render-time side effects
hooks/useSession.ts:
- Manages session list and detail fetching state (loading, error,
data) with useCallback-wrapped fetch functions
- Auto-loads session list on mount
hooks/useFilters.ts:
- Category filter state with toggle, set-all, and preset support
- Text search with debounced query propagation
- Manual redaction workflow: select messages, confirm to move to
redacted set, undo individual or all redactions, select-all-visible
- Auto-redact toggle for the sensitive-redactor module
- Returns memoized object to prevent unnecessary re-renders
components/SessionList.tsx:
- Two-phase navigation: project list → session list within a project
- Groups sessions by project, shows session count and latest modified
date per project, auto-drills into the correct project when a session
is selected externally
- Formats project directory names back to paths (leading dash → /)
components/SessionViewer.tsx:
- Renders filtered messages with redacted dividers inserted where
manually redacted messages were removed from the visible sequence
- Loading spinner, empty state for no session / no filter matches
- Scrolls focused message into view via ref
components/MessageBubble.tsx:
- Renders individual messages with category-specific Tailwind border
and background colors
- Markdown rendering via marked + highlight.js, with search term
highlighting that splits HTML tags to avoid corrupting attributes
- Click-to-select for manual redaction, visual selection indicator
- Auto-redact mode applies sensitive-redactor to content before render
- dangerouslySetInnerHTML is safe here: content is from local
user-owned JSONL files, not untrusted external input
components/FilterPanel.tsx:
- Checkbox list for all 9 message categories with auto-redact toggle
components/SearchBar.tsx:
- Debounced text input (200ms) with match count display
- "/" keyboard shortcut to focus, × button to clear
components/ExportButton.tsx:
- POSTs current session + visible/redacted UUIDs + auto-redact flag
to /api/export, downloads the returned HTML blob as a file
components/RedactedDivider.tsx:
- Dashed-line visual separator indicating redacted content gap
lib/types.ts:
- Re-exports shared types via @shared path alias for client imports
lib/constants.ts:
- Tailwind CSS class mappings per message category (border + bg colors)
lib/markdown.ts:
- Configured marked + highlight.js instance with search highlighting
that operates on text segments only (preserves HTML tags intact)
styles/main.css:
- Tailwind base/components/utilities, custom scrollbar, highlight.js
overrides, search highlight mark, redaction selection outline,
message dimming for non-matching search results
index.html + main.tsx:
- Vite entry point mounting React app into #root with StrictMode
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Server-side implementation for reading, parsing, and exporting Claude
Code session logs:
session-discovery.ts:
- Walks ~/.claude/projects/ directories, reads sessions-index.json from
each project folder, supports both the current {version, entries}
format and the legacy raw array format
- Aggregates sessions across all projects, sorted by most recently
modified first
- Gracefully handles missing directories, corrupt index files, and
missing entries
session-parser.ts:
- Parses Claude Code JSONL session files line-by-line into normalized
ParsedMessage objects
- Handles the full real-world format: type="user" (string or
ContentBlock array content), type="assistant" (text, thinking,
tool_use blocks), type="progress" (hook events with structured data
fields), type="summary" (summary text field), type="file-history-
snapshot", and silently skips type="system" (turn_duration metadata)
and type="queue-operation" (internal)
- Detects <system-reminder> tags in user messages and reclassifies them
as system_message category
- Resilient to malformed JSONL lines (skips with continue, no crash)
html-exporter.ts:
- Generates self-contained HTML exports with no external dependencies —
all CSS (layout, category-specific colors, syntax highlighting) is
inlined in a <style> block
- Dark theme (GitHub dark palette) with category-specific left border
colors and backgrounds matching the Claude Code aesthetic
- Renders markdown content via marked + highlight.js with syntax
highlighting, inserts "content redacted" dividers where redacted
messages were removed
- Outputs a complete <!DOCTYPE html> document with session metadata
header (project name, date, message count)
routes/sessions.ts:
- GET /api/sessions — returns all discovered sessions with 30-second
in-memory cache to avoid re-scanning the filesystem on every request
- GET /api/sessions/:id — looks up session by ID from cache, parses
the JSONL file, returns parsed messages
routes/export.ts:
- POST /api/export — accepts ExportRequest body, validates required
fields, generates HTML via the exporter, returns as a downloadable
attachment with sanitized filename
index.ts:
- Express app factory (createApp) with 50MB JSON body limit, health
check endpoint, session and export routers, and static file serving
for the built client
- Dual-bind to localhost and Tailscale IP (100.84.4.113) for local +
tailnet access, with optional browser-open on startup via
SESSION_VIEWER_OPEN_BROWSER env var
- Auto-start guard: only calls startServer() when run directly, not
when imported by tests
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Shared module consumed by both the Express server and the React client:
types.ts:
- ParsedMessage: the normalized message unit (uuid, category, content,
toolName, toolInput, timestamp, rawIndex) that the parser emits and
every downstream consumer (viewer, filter, export) operates on
- MessageCategory: 9-value union covering user_message, assistant_text,
thinking, tool_call, tool_result, system_message, hook_progress,
file_snapshot, and summary
- SessionEntry / SessionListResponse / SessionDetailResponse / ExportRequest:
API contract types for the sessions list, session detail, and HTML
export endpoints
- ALL_CATEGORIES, CATEGORY_LABELS, DEFAULT_HIDDEN_CATEGORIES: constants
for the filter panel UI and presets (thinking + hook_progress hidden
by default)
sensitive-redactor.ts:
- 34 regex patterns derived from gitleaks production config, organized
into Tier 1 (known secret formats: AWS, GitHub, GitLab, OpenAI,
Anthropic, HuggingFace, Perplexity, Stripe, Slack, SendGrid, Twilio,
GCP, Azure AD, Heroku, npm, PyPI, Sentry, JWT, PEM private keys,
generic API key assignments) and Tier 2 (PII/system info: home
directory paths, connection strings, URLs with credentials, email
addresses, IPv4 addresses, Bearer tokens, env var secret assignments)
- Keyword pre-filtering: each pattern declares keywords that must appear
in the text before the expensive regex is evaluated, following the
gitleaks performance optimization approach
- False-positive allowlists: example/test email domains, localhost/
documentation IPs (RFC 5737), noreply@anthropic.com
- Pure functions: redactSensitiveContent returns {sanitized, count,
categories}, redactString returns just the string, redactMessage
returns a new ParsedMessage with content and toolInput redacted
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>