Compare commits

..

4 Commits

Author SHA1 Message Date
b030734915 Add comprehensive README with architecture and usage docs
Introduce a README covering the full project: quick start,
feature inventory (session navigation, message display, search,
filtering, redaction, export), architecture overview with
directory layout, tech stack table, API endpoint reference,
npm scripts, environment variables, and security properties.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 10:42:35 -05:00
c66ce4ae16 Change message copy button to copy content instead of anchor link
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>
2026-01-30 10:42:30 -05:00
54f909c80c Revise default hidden categories to reduce noise in session view
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>
2026-01-30 10:42:21 -05:00
2a6186e9ce Rename AGENTS.md header from CLAUDE.md to Agent Instructions
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>
2026-01-30 10:42:13 -05:00
5 changed files with 143 additions and 10 deletions

View File

@@ -1,6 +1,6 @@
# CLAUDE.md
# Agent Instructions
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Guidance for Claude Code and other AI agents working in this repository.
### Development Principles

127
README.md Normal file
View File

@@ -0,0 +1,127 @@
# Session Viewer
Browse, filter, redact, and export Claude Code sessions as self-contained HTML files.
Session Viewer reads session data from `~/.claude/projects/` and presents it in a dark-themed web interface with full-text search, message filtering, sensitive data redaction, and one-click HTML export.
## Quick Start
```bash
# Install dependencies
npm install
# Run in development mode (starts both server and client with hot reload)
npm run dev
# Or run from the CLI entry point (opens browser automatically)
node bin/session-viewer.js
```
The API server runs on `http://localhost:3848` and the Vite dev server on `http://localhost:3847` (proxying API requests to the backend).
## Features
### Session Navigation
- **Project grouping** -- Sessions are organized by their originating project directory.
- **Session metadata** -- Each session displays its summary, first prompt, message count, duration, and last-modified date.
- **Sorted by recency** -- Most recently modified sessions appear first.
### Message Display
- **Nine message categories** -- `user_message`, `assistant_text`, `thinking`, `tool_call`, `tool_result`, `system_message`, `hook_progress`, `file_snapshot`, and `summary`, each with a distinct color indicator.
- **Collapsible sections** -- Thinking blocks, tool calls, and tool results collapse by default to reduce noise.
- **Diff rendering** -- Git-style diffs are auto-detected and rendered with line-level syntax coloring.
- **Code blocks** -- Fenced code blocks display a language label and a copy-to-clipboard button.
- **Time gap indicators** -- Gaps of more than five minutes between messages are shown as labeled dividers.
- **Hash anchor links** -- Each message has a copyable anchor link (`#msg-{uuid}`) for deep linking.
### Search
- **Full-text search** across message content and tool input.
- **Match cycling** -- Navigate between matches with `Enter`/`Shift+Enter` or `Ctrl+G`/`Ctrl+Shift+G`.
- **Match counter** -- Displays current position and total matches (e.g., "3/12").
- **Minimap** -- A scrollbar overlay shows match positions as yellow ticks with a viewport indicator. Click a tick to jump to that match.
- **Dimming** -- Non-matching messages are visually dimmed while search is active.
- **Keyboard shortcut** -- Press `/` to focus the search bar; `Escape` to clear and blur.
### Filtering
- **Category toggles** -- Show or hide any of the nine message categories. Thinking and hook progress are hidden by default.
- **Filters compose with search** -- Category filters and search operate independently.
### Redaction
- **Manual redaction** -- Click the eye icon on any message to select it, then confirm to redact. Redacted messages are replaced by a visual divider.
- **Auto-redaction** -- Toggle automatic detection of sensitive data (API keys, tokens, secrets, PII) using 37 regex patterns derived from gitleaks. Matching content is replaced with `[REDACTED]` inline.
- **Export-aware** -- Both manual and auto-redaction states are respected during export.
### Export
- **Self-contained HTML** -- Export the current session view as a standalone HTML file with embedded CSS and syntax highlighting.
- **Respects filters** -- The export includes only visible messages and applies the current redaction state.
- **Clean filenames** -- Output files are named `session-{id}.html` with sanitized IDs.
## Architecture
```
src/
client/ React + Vite frontend
components/ UI components (SessionList, SessionViewer, MessageBubble, etc.)
hooks/ useSession, useFilters
lib/ Markdown rendering, sensitive redactor, types, utilities
styles/ CSS custom properties, Tailwind, design tokens
server/ Express backend
routes/ /api/sessions, /api/export
services/ Session discovery, JSONL parser, HTML exporter
shared/ Types shared between client and server
bin/ CLI entry point
tests/
unit/ Vitest unit tests
e2e/ Playwright end-to-end tests
fixtures/ Test data
```
### Tech Stack
| Layer | Technology |
| -------- | --------------------------------------- |
| Frontend | React 18, Tailwind CSS, Vite |
| Backend | Express 4, Node.js |
| Markdown | marked + marked-highlight + highlight.js|
| Testing | Vitest (unit), Playwright (e2e) |
| Language | TypeScript (strict mode) |
### API Endpoints
| Method | Path | Description |
| ------ | --------------------- | -------------------------------------- |
| GET | `/api/health` | Health check |
| GET | `/api/sessions` | List all sessions (30s server cache) |
| GET | `/api/sessions/:id` | Get session detail with parsed messages|
| POST | `/api/export` | Generate self-contained HTML export |
## Scripts
| Script | Description |
| ---------------- | ---------------------------------------------- |
| `npm run dev` | Start server + client with hot reload |
| `npm run build` | TypeScript compile + Vite production build |
| `npm run test` | Run unit tests (Vitest) |
| `npm run test:e2e` | Run end-to-end tests (Playwright) |
| `npm run typecheck` | TypeScript type checking |
| `npm run lint` | ESLint |
## Configuration
| Variable | Default | Description |
| -------------------------- | -------- | --------------------------------- |
| `PORT` | `3848` | API server port |
| `SESSION_VIEWER_OPEN_BROWSER` | unset | Set to `1` to auto-open browser on start |
## Security
- **Path traversal protection** -- Session discovery validates all paths stay within `~/.claude/projects/`.
- **HTML escaping** -- All user content is escaped before interpolation in exports.
- **Filename sanitization** -- Export filenames strip non-alphanumeric characters.
- **Localhost binding** -- Both the API server and Vite dev server bind to `127.0.0.1` only.

View File

@@ -153,14 +153,16 @@ export function MessageBubble({
<button
onClick={(e) => {
e.stopPropagation();
const url = `${window.location.origin}${window.location.pathname}#msg-${message.uuid}`;
navigator.clipboard.writeText(url).then(() => {
const text = message.category === "tool_call"
? `${message.toolName || "Tool Call"}\n${message.toolInput || ""}`
: message.content;
navigator.clipboard.writeText(text).then(() => {
setLinkCopied(true);
setTimeout(() => setLinkCopied(false), 1500);
}).catch(() => {});
}}
className="flex items-center justify-center w-7 h-7 rounded-md text-foreground-muted hover:text-foreground hover:bg-surface-overlay/60 transition-colors"
title="Copy link to message"
title="Copy message content"
>
{linkCopied ? (
<svg className="w-4 h-4 text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>

View File

@@ -69,10 +69,12 @@ export const CATEGORY_LABELS: Record<MessageCategory, string> = {
system_message: "System Messages",
hook_progress: "Hook/Progress",
file_snapshot: "File Snapshots",
summary: "Summaries",
summary: "Compactions",
};
export const DEFAULT_HIDDEN_CATEGORIES: MessageCategory[] = [
"thinking",
"tool_result",
"system_message",
"hook_progress",
"file_snapshot",
];

View File

@@ -48,15 +48,17 @@ describe("filters", () => {
expect(filtered.find((m) => m.category === "thinking")).toBeUndefined();
});
it("default filter state has thinking and hooks disabled", () => {
it("default filter state hides tool_result, system, hooks, and snapshots", () => {
const defaultEnabled = new Set(ALL_CATEGORIES);
for (const cat of DEFAULT_HIDDEN_CATEGORIES) {
defaultEnabled.delete(cat);
}
const filtered = filterMessages(messages, defaultEnabled);
expect(filtered.find((m) => m.category === "thinking")).toBeUndefined();
expect(filtered.find((m) => m.category === "tool_result")).toBeUndefined();
expect(filtered.find((m) => m.category === "system_message")).toBeUndefined();
expect(filtered.find((m) => m.category === "hook_progress")).toBeUndefined();
expect(filtered).toHaveLength(7);
expect(filtered.find((m) => m.category === "file_snapshot")).toBeUndefined();
expect(filtered).toHaveLength(5);
});
it("all-off filter returns empty array", () => {