Compare commits
4 Commits
b60501e80f
...
b030734915
| Author | SHA1 | Date | |
|---|---|---|---|
| b030734915 | |||
| c66ce4ae16 | |||
| 54f909c80c | |||
| 2a6186e9ce |
@@ -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
|
### Development Principles
|
||||||
|
|
||||||
|
|||||||
127
README.md
Normal file
127
README.md
Normal 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.
|
||||||
@@ -153,14 +153,16 @@ export function MessageBubble({
|
|||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const url = `${window.location.origin}${window.location.pathname}#msg-${message.uuid}`;
|
const text = message.category === "tool_call"
|
||||||
navigator.clipboard.writeText(url).then(() => {
|
? `${message.toolName || "Tool Call"}\n${message.toolInput || ""}`
|
||||||
|
: message.content;
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
setLinkCopied(true);
|
setLinkCopied(true);
|
||||||
setTimeout(() => setLinkCopied(false), 1500);
|
setTimeout(() => setLinkCopied(false), 1500);
|
||||||
}).catch(() => {});
|
}).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"
|
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 ? (
|
{linkCopied ? (
|
||||||
<svg className="w-4 h-4 text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
<svg className="w-4 h-4 text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||||
|
|||||||
@@ -69,10 +69,12 @@ export const CATEGORY_LABELS: Record<MessageCategory, string> = {
|
|||||||
system_message: "System Messages",
|
system_message: "System Messages",
|
||||||
hook_progress: "Hook/Progress",
|
hook_progress: "Hook/Progress",
|
||||||
file_snapshot: "File Snapshots",
|
file_snapshot: "File Snapshots",
|
||||||
summary: "Summaries",
|
summary: "Compactions",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_HIDDEN_CATEGORIES: MessageCategory[] = [
|
export const DEFAULT_HIDDEN_CATEGORIES: MessageCategory[] = [
|
||||||
"thinking",
|
"tool_result",
|
||||||
|
"system_message",
|
||||||
"hook_progress",
|
"hook_progress",
|
||||||
|
"file_snapshot",
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -48,15 +48,17 @@ describe("filters", () => {
|
|||||||
expect(filtered.find((m) => m.category === "thinking")).toBeUndefined();
|
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);
|
const defaultEnabled = new Set(ALL_CATEGORIES);
|
||||||
for (const cat of DEFAULT_HIDDEN_CATEGORIES) {
|
for (const cat of DEFAULT_HIDDEN_CATEGORIES) {
|
||||||
defaultEnabled.delete(cat);
|
defaultEnabled.delete(cat);
|
||||||
}
|
}
|
||||||
const filtered = filterMessages(messages, defaultEnabled);
|
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.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", () => {
|
it("all-off filter returns empty array", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user