# Transcript Viewer

> Convert and browse session transcripts as HTML or Markdown. Supports Claude Code JSONL logs (auto-saved to ~/.claude/projects/) and GitHub Copilot CLI JSONL logs (auto-saved to ~/.copilot/session-state/*/events.jsonl). Auto-detects log source based on available directories and file format. Supports viewing the current session, a specific session by ID, agent background task output files, or all project sessions with optional date-range filtering.

- Skill: `rysweet/transcript-viewer` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds add rysweet/transcript-viewer`
- Raw SKILL.md: https://api.skillmd.com/api/skills/rysweet/transcript-viewer/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: rysweet (https://skillmd.com/u/rysweet)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/rysweet/transcript-viewer

---


# Transcript Viewer Skill

## Purpose

This skill converts and browses session transcripts from two supported tools:

- **Claude Code** — JSONL logs auto-saved to `~/.claude/projects/`
- **GitHub Copilot CLI** — JSONL logs auto-saved to `~/.copilot/session-state/*/events.jsonl`

It provides four browsing modes:

1. **Current session** — View the active session's transcript
2. **Specific session** — View a session by its ID
3. **Agent output** — View background task output files produced by subagents
4. **All sessions** — Browse all project sessions, with optional date-range filtering

## Tool Context Auto-Detection

Before browsing, detect which tool is active to set default log paths.
Prefer directory-based detection (more reliable than env vars); fall back to env vars
via the `AMPLIHACK_AGENT_BINARY` env var when directories don't exist:

```bash
# Primary: directory-based detection (most reliable)
if [[ -d "$HOME/.copilot/session-state" ]]; then
  # Check if there are any sessions present
  COPILOT_SESSIONS=$(ls -d "$HOME/.copilot/session-state/"*/ 2>/dev/null | wc -l)
  CLAUDE_SESSIONS=$(ls "$HOME/.claude/projects/"*/*.jsonl 2>/dev/null | wc -l)
  if [[ "$COPILOT_SESSIONS" -gt 0 && "$CLAUDE_SESSIONS" -eq 0 ]]; then
    TOOL_CONTEXT="copilot"
    DEFAULT_LOG_DIR="$HOME/.copilot/session-state"
  elif [[ "$CLAUDE_SESSIONS" -gt 0 ]]; then
    TOOL_CONTEXT="claude-code"
    DEFAULT_LOG_DIR="$HOME/.claude/projects"
  else
    # Both dirs exist but empty — use env vars to decide
    TOOL_CONTEXT="claude-code"
    DEFAULT_LOG_DIR="$HOME/.claude/projects"
  fi
elif [[ -d "$HOME/.claude/projects" ]]; then
  TOOL_CONTEXT="claude-code"
  DEFAULT_LOG_DIR="$HOME/.claude/projects"
# Fallback: env var detection (same vars as launcher_detector.py)
elif [[ -n "${CLAUDE_CODE_SESSION:-}${CLAUDE_SESSION_ID:-}${ANTHROPIC_API_KEY:-}" ]]; then
  TOOL_CONTEXT="claude-code"
  DEFAULT_LOG_DIR="$HOME/.claude/projects"
elif [[ -n "${GITHUB_COPILOT_TOKEN:-}${COPILOT_SESSION:-}" ]]; then
  # Note: GITHUB_TOKEN is intentionally excluded — it's too generic and
  # appears in non-Copilot CI contexts, causing false positives.
  TOOL_CONTEXT="copilot"
  DEFAULT_LOG_DIR="$HOME/.copilot/session-state"
else
  # Default to claude-code — safe fallback (most users)
  TOOL_CONTEXT="claude-code"
  DEFAULT_LOG_DIR="$HOME/.claude/projects"
fi
```

When both `~/.copilot/session-state/` and `~/.claude/projects/` exist with sessions,
offer the user a choice: "Found sessions for both Claude Code and GitHub Copilot CLI.
Which would you like to browse? [1] Claude Code [2] GitHub Copilot CLI"

The user can always override with an explicit path.

## Log Format Auto-Detection

When given a file path, detect its format before processing:

```bash
detect_log_format() {
  local file="$1"
  if [[ "$file" == *.jsonl ]]; then
    echo "jsonl"
  elif [[ "$file" == *.md ]]; then
    # Check for Copilot /share export signature — specific header only
    # Note: do NOT match on "/share" alone (too generic — appears in docs, READMEs)
    if grep -q "Copilot Session Export\|copilot-session" "$file" 2>/dev/null; then
      echo "copilot-markdown"
    else
      echo "markdown"
    fi
  elif [[ "$file" == *.log ]]; then
    # .log files may be plain text or agent JSONL; check content
    local first_char
    first_char=$(head -c 1 "$file" 2>/dev/null)
    if [[ "$first_char" == "{" ]]; then
      echo "jsonl"
    else
      echo "plain-log"
    fi
  else
    # Inspect first byte for other extensions
    local first_char
    first_char=$(head -c 1 "$file" 2>/dev/null)
    if [[ "$first_char" == "{" ]]; then
      echo "jsonl"
    else
      echo "unknown"
    fi
  fi
}
```

| Format             | Handler                                    |
| ------------------ | ------------------------------------------ |
| `jsonl`            | Pass to `claude-code-log`                  |
| `copilot-markdown` | Display inline (already readable Markdown) |
| `markdown`         | Display inline                             |
| `unknown`          | Warn and display raw                       |

## Tool Detection

Before running any command, check whether `claude-code-log` is available:

```bash
# Step 1: direct install check
which claude-code-log 2>/dev/null

# Step 2: npx fallback
npx --yes claude-code-log --version 2>/dev/null
```

Set `CCL` to the resolved command:

```bash
if which claude-code-log &>/dev/null; then
  CCL="claude-code-log"
elif npx --yes claude-code-log --version &>/dev/null 2>&1; then
  CCL="npx claude-code-log"
else
  CCL=""
fi
```

### Missing Tool — Graceful Error

If `CCL` is empty, display this message and stop:

```
claude-code-log is not installed.

To install it globally:
  npm install -g claude-code-log

Or run without installing (requires npx):
  npx claude-code-log --help

After installing, retry your request.
```

Do not attempt to install it automatically.

## Modes

### Mode 1: Current Session

**Trigger phrases**: "view current transcript", "show my current session", "current log"

**What to do**:

For **Claude Code** (`TOOL_CONTEXT="claude-code"`):

1. Find the most recently modified JSONL file under `~/.claude/projects/`:
   ```bash
   ls -t ~/.claude/projects/*/*.jsonl 2>/dev/null | head -1
   ```
2. Run:
   ```bash
   $CCL <path-to-jsonl> --format markdown
   ```
3. Display the Markdown output inline.

For **GitHub Copilot CLI** (`TOOL_CONTEXT="copilot"`):

1. Find the most recently modified session directory under `~/.copilot/session-state/`:
   ```bash
   ls -dt ~/.copilot/session-state/*/ 2>/dev/null | head -1
   ```
2. Read its `events.jsonl`:
   ```bash
   LATEST_SESSION=$(ls -dt ~/.copilot/session-state/*/ 2>/dev/null | head -1)
   $CCL "${LATEST_SESSION}events.jsonl" --format markdown
   ```
3. Display the Markdown output inline.

**Example output**:

```markdown
# Session: 2025-11-23 19:32

**Model**: claude-sonnet-4-6
**Messages**: 42

---

**User**: Fix the authentication bug in login.py
**Assistant**: I'll examine the file...
...
```

### Mode 2: Specific Session by ID

**Trigger phrases**: "view session <ID>", "show transcript <ID>", "open log <ID>"

**What to do**:

For **Claude Code** (`TOOL_CONTEXT="claude-code"`):

1. Search for the JSONL file matching the session ID:
   ```bash
   find ~/.claude/projects -name "*.jsonl" | xargs grep -l "<SESSION_ID>" 2>/dev/null | head -1
   ```
   Or, if the ID looks like a filename fragment, use:
   ```bash
   ls ~/.claude/projects/*/ | grep "<SESSION_ID>"
   ```
2. Run:
   ```bash
   $CCL <path-to-jsonl> --format markdown
   ```
3. Display the output. If no file matches, report:
   ```
   No session found with ID: <SESSION_ID>
   Available sessions: run "view all sessions" to list them.
   ```

For **GitHub Copilot CLI** (`TOOL_CONTEXT="copilot"`):

1. The session ID is the directory name under `~/.copilot/session-state/`. Check directly:
   ```bash
   SESSION_DIR="$HOME/.copilot/session-state/<SESSION_ID>"
   if [[ -d "$SESSION_DIR" ]]; then
     EVENTS_FILE="$SESSION_DIR/events.jsonl"
   fi
   ```
   If the full ID is not known, search for a partial match:
   ```bash
   ls -d ~/.copilot/session-state/*/ 2>/dev/null | grep "<SESSION_ID>"
   ```
2. Run:
   ```bash
   $CCL "$EVENTS_FILE" --format markdown
   ```
3. Display the output. If no directory matches, report:
   ```
   No Copilot session found with ID: <SESSION_ID>
   Available sessions: run "browse all sessions" to list them.
   ```

### Mode 3: Agent Background Task Output

**Trigger phrases**: "view agent output", "show background task output", "agent log"

**What to do**:

1. Find `.log` or `.jsonl` files created by background agent tasks. These are
   typically written to the current working directory or a temp path with a name
   matching `.agent-step-*.log` or similar:
   ```bash
   ls -t .agent-step-*.log 2>/dev/null
   ls -t /tmp/*.agent*.log 2>/dev/null
   ```
2. For each file found, run `detect_log_format <file>` to classify it:
   - `jsonl` → run `$CCL <file> --format markdown`
   - `plain-log` → display directly with `cat`
3. This ensures JSONL-formatted `.log` files (rare but possible) are rendered properly.
4. If no agent output files are found:
   ```
   No agent background task output files found in the current directory.
   Background agents write their output to files named .agent-step-<ID>.log.
   ```

### Mode 4: All Sessions (Browse)

**Trigger phrases**: "browse all sessions", "list transcripts", "view all sessions",
"show session history"

**What to do**:

For **Claude Code** (`TOOL_CONTEXT="claude-code"`):

1. List all JSONL files under `~/.claude/projects/`:
   ```bash
   find ~/.claude/projects -name "*.jsonl" -printf "%T@ %p\n" 2>/dev/null \
     | sort -rn | awk '{print $2}'
   ```
2. For each file, extract the session date and first user message:
   ```bash
   $CCL <file> --format markdown --summary
   # If --summary flag is not supported, just show filename and date
   head -1 <file> | python3 -c "import sys,json; d=json.loads(sys.stdin.read()); print(d.get('timestamp',''))"
   ```
3. Print a summary table:
   ```
   Available Sessions (Claude Code)
   =================================
   #   Date                  Session ID / File
   1   2025-11-23 19:32:36   ~/.claude/projects/foo/abc123.jsonl
   2   2025-11-22 14:10:05   ~/.claude/projects/foo/def456.jsonl
   ...
   ```
4. Offer to open a specific session: "Enter a number to view that session."

For **GitHub Copilot CLI** (`TOOL_CONTEXT="copilot"`):

1. List all session directories under `~/.copilot/session-state/` sorted by modification time:
   ```bash
   ls -dt ~/.copilot/session-state/*/ 2>/dev/null
   ```
2. For each session directory, read its `events.jsonl` to extract the timestamp:
   ```bash
   for session_dir in $(ls -dt ~/.copilot/session-state/*/); do
     session_id=$(basename "$session_dir")
     events_file="$session_dir/events.jsonl"
     if [[ -f "$events_file" ]]; then
       timestamp=$(head -1 "$events_file" | python3 -c \
         "import sys,json; d=json.loads(sys.stdin.read()); print(d.get('timestamp',''))" 2>/dev/null)
       echo "$timestamp  $session_id"
     fi
   done
   ```
3. Print a summary table:
   ```
   Available Sessions (GitHub Copilot CLI)
   ========================================
   #   Date                  Session ID
   1   2025-11-23 19:32:36   abc1234567890abcdef1234567890abcd
   2   2025-11-22 14:10:05   def4567890abcdef1234567890abcdef
   ...
   ```
4. Offer to open a specific session: "Enter a number to view that session."

#### Date-Range Filtering

When the user specifies a date range (e.g., "last 7 days", "between 2025-11-01 and 2025-11-30"):

```bash
# Filter by modification time (last N days)
find ~/.claude/projects -name "*.jsonl" -mtime -7

# Filter by date range using find -newer
find ~/.claude/projects -name "*.jsonl" \
  -newer /tmp/start_date_ref \
  ! -newer /tmp/end_date_ref
```

Create the reference files with `touch -d`:

```bash
touch -d "2025-11-01" /tmp/start_date_ref
touch -d "2025-11-30" /tmp/end_date_ref
```

## Output Formats

### Markdown (default)

Pass `--format markdown` to `claude-code-log`. The output is printed inline in the
conversation. Best for quick reading in the terminal or Claude Code.

### HTML

Pass `--format html` to `claude-code-log`. Write the output to a file and open it:

```bash
$CCL <file> --format html > /tmp/transcript-view.html
open /tmp/transcript-view.html 2>/dev/null \
  || xdg-open /tmp/transcript-view.html 2>/dev/null \
  || echo "HTML saved to /tmp/transcript-view.html — open it in your browser."
```

The user can request HTML explicitly: "view transcript as HTML" or "export to HTML".

## GitHub Copilot CLI Support

### Automatic Log Persistence

GitHub Copilot CLI **automatically saves** session data to disk at:

```
~/.copilot/session-state/{session-id}/
```

Each session directory contains:

- `events.jsonl` — JSONL session history (similar format to Claude Code logs)
- `workspace.yaml` — session metadata (working directory, session ID, etc.)
- `plan.md` — implementation plan for the session
- `checkpoints/` — compaction history (older event snapshots)

Legacy sessions may also exist at `~/.copilot/history-session-state/`.

### JSONL Format

Copilot `events.jsonl` contains one JSON object per line, similar to Claude Code format:

```json
{"type":"user","message":{"role":"user","content":[{"type":"text","text":"Fix the bug"}]},"timestamp":"2025-11-23T19:32:36Z","sessionId":"abc1234567890"}
{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"I'll examine the file..."}]},"timestamp":"2025-11-23T19:32:40Z","sessionId":"abc1234567890"}
```

`claude-code-log` handles parsing these files since the format is compatible.

### Viewing a Copilot Session

When the user wants to view a Copilot session (by directory detection or explicit path):

1. Locate the `events.jsonl` for the session:

   ```bash
   # Current session (most recent)
   LATEST_SESSION=$(ls -dt ~/.copilot/session-state/*/ 2>/dev/null | head -1)
   EVENTS_FILE="${LATEST_SESSION}events.jsonl"

   # Specific session by ID
   EVENTS_FILE="$HOME/.copilot/session-state/<SESSION_ID>/events.jsonl"
   ```

2. Run:
   ```bash
   $CCL "$EVENTS_FILE" --format markdown
   ```
3. Display the output inline.

### Detecting Available Sessions

To check if Copilot sessions exist:

```bash
if [[ -d "$HOME/.copilot/session-state" ]]; then
  SESSION_COUNT=$(ls -d "$HOME/.copilot/session-state/"*/ 2>/dev/null | wc -l)
  if [[ "$SESSION_COUNT" -gt 0 ]]; then
    echo "Found $SESSION_COUNT Copilot session(s)"
  fi
fi
```

## Full Workflow

When the user invokes this skill, follow this decision tree:

```
0. Detect tool context (directory-based first, then env var fallback):
   - ~/.copilot/session-state/ exists with sessions → copilot
   - ~/.claude/projects/ exists with sessions       → claude-code
   - Both exist with sessions → offer user a choice
   - Directory detection fails, use env vars:
     - CLAUDE_CODE_SESSION / CLAUDE_SESSION_ID / ANTHROPIC_API_KEY set → claude-code
     - GITHUB_COPILOT_TOKEN / COPILOT_SESSION set → copilot
   - Neither → default to claude-code (safe fallback)

1. Detect CCL (which claude-code-log / npx fallback)
   → If missing: show install instructions and STOP
   → Required for ALL modes (both Claude Code and Copilot use JSONL format)

2. If user provides an explicit file path:
   a. Detect its format (detect_log_format function above)
   b. jsonl or events.jsonl → pass to $CCL
   c. unknown → warn and display raw

3. Determine mode from user message:
   - mentions "current" or no session specified → Mode 1 (Current Session)
   - mentions a session ID or hash             → Mode 2 (Specific Session)
   - mentions "agent" or "background"          → Mode 3 (Agent Output)
   - mentions "all", "browse", "list"          → Mode 4 (All Sessions)

4. Determine output format:
   - "as HTML" or "export HTML" → html
   - default                    → markdown

5. Execute the appropriate mode (using TOOL_CONTEXT to choose correct paths) and display results.
```

## Error Handling

| Situation                                      | Response                                                                             |
| ---------------------------------------------- | ------------------------------------------------------------------------------------ |
| `claude-code-log` not installed                | Show install instructions, stop                                                      |
| JSONL file not found                           | "No session file found at <path>"                                                    |
| Session ID not found                           | "No session with ID <ID>. Run 'browse all sessions' to list available ones."         |
| No agent output files                          | "No agent background task output found in current directory."                        |
| Empty JSONL file                               | "Session file is empty — no messages to display."                                    |
| Date range produces no results                 | "No sessions found between <start> and <end>."                                       |
| `claude-code-log` returns non-zero exit        | Display stderr and suggest `--help`                                                  |
| `~/.copilot/session-state/` exists but empty   | "No Copilot sessions found. Start a session with GitHub Copilot CLI to create logs." |
| Copilot session dir exists but no events.jsonl | "Session directory found but events.jsonl is missing at <path>"                      |
| Unknown file format                            | Warn user and display raw content                                                    |

## Implementation Notes

### Detecting Session IDs

Claude Code session IDs are UUID-like strings. If the user writes something like
"view session abc123" or "show log def456", treat the last word as the session ID
and search for matching JSONL filenames.

### JSONL Structure (Claude Code)

Claude Code session JSONL files contain one JSON object per line:

```json
{"type":"user","message":{"role":"user","content":[{"type":"text","text":"..."}]},"timestamp":"...","sessionId":"..."}
{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"..."}]},"timestamp":"..."}
```

`claude-code-log` handles parsing; this skill does not re-implement it.

### Copilot JSONL Structure (GitHub Copilot CLI)

Copilot CLI `events.jsonl` files contain one JSON object per line, stored at
`~/.copilot/session-state/{session-id}/events.jsonl`:

```json
{"type":"user","message":{"role":"user","content":[{"type":"text","text":"..."}]},"timestamp":"...","sessionId":"..."}
{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"..."}]},"timestamp":"...","sessionId":"..."}
```

The format is compatible with `claude-code-log`. Additional per-session files:

- `workspace.yaml` — session metadata
- `plan.md` — implementation plan
- `checkpoints/` — compaction history (older snapshots)

Legacy sessions may be stored at `~/.copilot/history-session-state/` with the same structure.

### Philosophy Alignment

- **Thin wrapper**: Delegates to `claude-code-log` for all JSONL (both Claude Code and Copilot)
- **Graceful degradation**: Clear error messages when tool is missing or no sessions found
- **Single responsibility**: Only views/converts transcripts, never modifies them
- **No hidden state**: All file paths are shown to the user

## Limitations

- Requires `claude-code-log` (npm) or `npx` to convert JSONL (both Claude Code and Copilot) to HTML/Markdown
- Cannot view transcripts from remote machines
- Date filtering relies on filesystem modification times, not session timestamps
- `--summary` flag availability depends on `claude-code-log` version
- Legacy Copilot sessions in `~/.copilot/history-session-state/` are not auto-discovered (must use explicit path)
- When both Claude Code and Copilot sessions exist, the skill prompts the user to choose rather than merging them

## Quick Reference

| User says                      | Tool        | Mode             | Command                                                                 |
| ------------------------------ | ----------- | ---------------- | ----------------------------------------------------------------------- |
| "view current transcript"      | Claude Code | Current session  | `$CCL <latest.jsonl> --format markdown`                                 |
| "show session abc123"          | Claude Code | Specific session | `$CCL ~/.claude/projects/**/*abc123*.jsonl --format markdown`           |
| "view agent output"            | Claude Code | Agent output     | `cat .agent-step-*.log` or `$CCL *.jsonl`                               |
| "browse all sessions"          | Claude Code | All sessions     | list + summarize all `~/.claude/projects/**/*.jsonl`                    |
| "view transcript as HTML"      | Claude Code | Any + HTML       | `$CCL <file> --format html > /tmp/view.html`                            |
| "last 7 days" (with browse)    | Claude Code | Date filter      | `find ... -mtime -7`                                                    |
| "view current copilot session" | Copilot     | Current session  | `$CCL ~/.copilot/session-state/<latest>/events.jsonl --format markdown` |
| "show copilot session abc123"  | Copilot     | Specific session | `$CCL ~/.copilot/session-state/abc123/events.jsonl --format markdown`   |
| "browse copilot sessions"      | Copilot     | All sessions     | list dirs in `~/.copilot/session-state/`, show session IDs              |
| "view copilot session as HTML" | Copilot     | Any + HTML       | `$CCL <events.jsonl> --format html > /tmp/view.html`                    |

