claude-code-pretty 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. claude_code_pretty-0.1.0/.bumpversion.cfg +8 -0
  2. claude_code_pretty-0.1.0/.github/images/demo.png +0 -0
  3. claude_code_pretty-0.1.0/.github/test-prompt.md +15 -0
  4. claude_code_pretty-0.1.0/.github/workflows/callable-ci.yml +77 -0
  5. claude_code_pretty-0.1.0/.github/workflows/prs.yml +9 -0
  6. claude_code_pretty-0.1.0/.github/workflows/push-to-main.yml +10 -0
  7. claude_code_pretty-0.1.0/.github/workflows/release.yml +59 -0
  8. claude_code_pretty-0.1.0/.gitignore +12 -0
  9. claude_code_pretty-0.1.0/.pre-commit-config.yaml +7 -0
  10. claude_code_pretty-0.1.0/Makefile +27 -0
  11. claude_code_pretty-0.1.0/PKG-INFO +90 -0
  12. claude_code_pretty-0.1.0/README.md +61 -0
  13. claude_code_pretty-0.1.0/pyproject.toml +55 -0
  14. claude_code_pretty-0.1.0/src/claudecodepretty/__init__.py +1 -0
  15. claude_code_pretty-0.1.0/src/claudecodepretty/cli.py +54 -0
  16. claude_code_pretty-0.1.0/src/claudecodepretty/colors.py +56 -0
  17. claude_code_pretty-0.1.0/src/claudecodepretty/constants.py +9 -0
  18. claude_code_pretty-0.1.0/src/claudecodepretty/handlers/__init__.py +16 -0
  19. claude_code_pretty-0.1.0/src/claudecodepretty/handlers/assistant.py +24 -0
  20. claude_code_pretty-0.1.0/src/claudecodepretty/handlers/base.py +81 -0
  21. claude_code_pretty-0.1.0/src/claudecodepretty/handlers/result.py +27 -0
  22. claude_code_pretty-0.1.0/src/claudecodepretty/handlers/stream.py +39 -0
  23. claude_code_pretty-0.1.0/src/claudecodepretty/handlers/system.py +16 -0
  24. claude_code_pretty-0.1.0/src/claudecodepretty/handlers/tools/__init__.py +31 -0
  25. claude_code_pretty-0.1.0/src/claudecodepretty/handlers/tools/bash.py +6 -0
  26. claude_code_pretty-0.1.0/src/claudecodepretty/handlers/tools/edit.py +6 -0
  27. claude_code_pretty-0.1.0/src/claudecodepretty/handlers/tools/glob.py +6 -0
  28. claude_code_pretty-0.1.0/src/claudecodepretty/handlers/tools/grep.py +12 -0
  29. claude_code_pretty-0.1.0/src/claudecodepretty/handlers/tools/notebook.py +6 -0
  30. claude_code_pretty-0.1.0/src/claudecodepretty/handlers/tools/read.py +6 -0
  31. claude_code_pretty-0.1.0/src/claudecodepretty/handlers/tools/task.py +11 -0
  32. claude_code_pretty-0.1.0/src/claudecodepretty/handlers/tools/todo.py +18 -0
  33. claude_code_pretty-0.1.0/src/claudecodepretty/handlers/tools/write.py +6 -0
  34. claude_code_pretty-0.1.0/src/claudecodepretty/handlers/user.py +51 -0
  35. claude_code_pretty-0.1.0/src/claudecodepretty/modes/__init__.py +4 -0
  36. claude_code_pretty-0.1.0/src/claudecodepretty/modes/replay.py +34 -0
  37. claude_code_pretty-0.1.0/src/claudecodepretty/modes/stream.py +44 -0
  38. claude_code_pretty-0.1.0/src/claudecodepretty/parser.py +40 -0
  39. claude_code_pretty-0.1.0/tests/__init__.py +0 -0
  40. claude_code_pretty-0.1.0/tests/fixtures/sample_session.jsonl +7 -0
  41. claude_code_pretty-0.1.0/tests/test_parser.py +180 -0
@@ -0,0 +1,8 @@
1
+ [bumpversion]
2
+ current_version = 0.1.0
3
+ commit = False
4
+ tag = False
5
+
6
+ [bumpversion:file:pyproject.toml]
7
+ search = version = "{current_version}"
8
+ replace = version = "{new_version}"
@@ -0,0 +1,15 @@
1
+ You are being tested to exercise all Claude Code internal tools. Do each step below in order:
2
+
3
+ 1. Use Glob to list all files matching `*.sh` in the current directory
4
+ 2. Use Read to read the file `show-colors.sh`
5
+ 3. Use Write to create a file called `tmp-test-output.txt` with the content "hello from claude test"
6
+ 4. Use Edit to change "hello from claude test" to "hello from claude test - updated" in `tmp-test-output.txt`
7
+ 5. Use Grep to search for the word "color" in all `.sh` files
8
+ 6. Use Bash to run `echo "bash tool works" && date`
9
+ 7. Use the Task tool to launch a subagent with subagent_type "general-purpose" and the following prompt: "Do ALL of these steps in order: (a) Use Glob to find every file in the repo recursively with **/*. (b) Use Read to read ALL files you found - read them in parallel. (c) Use WebSearch to search for 'ANSI color codes terminal 2025 best practices'. (d) Use WebFetch to fetch https://en.wikipedia.org/wiki/ANSI_escape_code and extract the list of standard color codes. (e) Based on what you read from the repo files and the web research, use Write to create a file called 'subagent-report.md' with a detailed report that includes: a summary of what this repo does, a list of all ANSI colors currently used in claude-pretty.sh, a comparison with standard ANSI colors from your web research, and recommendations for additional colors that could improve readability. Make the report thorough."
10
+ 8. Use the Task tool to launch a subagent with subagent_type "Bash" to run: "echo '--- bash subagent test ---' && uname -a && node --version && which claude && echo '--- done ---'"
11
+ 9. Use MultiEdit on `tmp-test-output.txt` to make two changes at once: replace "hello" with "hey" and replace "updated" with "modified"
12
+ 10. Use Bash to run `echo '{"cells":[{"cell_type":"code","source":["print(1+1)"],"metadata":{},"outputs":[],"execution_count":null}],"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python"}},"nbformat":4,"nbformat_minor":5}' > tmp-test.ipynb` to create a test notebook
13
+ 11. Use NotebookEdit on `tmp-test.ipynb` to insert a new cell with edit_mode "insert" and cell_type "code" with the source `print("notebook edit works")`
14
+ 12. Use TodoWrite to create a todo list with 3 items: "step one" (completed), "step two" (in_progress), "step three" (pending)
15
+ 13. Print a final summary saying "All tools tested successfully"
@@ -0,0 +1,77 @@
1
+ name: CI
2
+
3
+ on:
4
+ workflow_call:
5
+
6
+ jobs:
7
+ check:
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - uses: actions/checkout@v4
11
+ - uses: actions/setup-python@v5
12
+ with:
13
+ python-version: "3.12"
14
+ - run: pip install -e ".[dev]"
15
+ - run: ruff check .
16
+ - run: ruff format --check .
17
+
18
+ test:
19
+ runs-on: ubuntu-latest
20
+ strategy:
21
+ matrix:
22
+ python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
23
+ steps:
24
+ - uses: actions/checkout@v4
25
+ - uses: actions/setup-python@v5
26
+ with:
27
+ python-version: ${{ matrix.python-version }}
28
+ - run: pip install -e ".[dev]"
29
+ - run: pytest -v
30
+
31
+ e2e:
32
+ strategy:
33
+ matrix:
34
+ os: [ubuntu-latest, windows-latest, macos-latest]
35
+ runs-on: ${{ matrix.os }}
36
+ steps:
37
+ - uses: actions/checkout@v4
38
+ - uses: actions/setup-python@v5
39
+ with:
40
+ python-version: "3.12"
41
+ - run: pip install -e ".[dev]"
42
+ - run: pytest -v
43
+
44
+ live:
45
+ runs-on: ubuntu-latest
46
+ if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
47
+ steps:
48
+ - uses: actions/checkout@v4
49
+
50
+ - uses: actions/setup-python@v5
51
+ with:
52
+ python-version: "3.12"
53
+
54
+ - uses: actions/setup-node@v4
55
+ with:
56
+ node-version: "20"
57
+
58
+ - run: pip install -e .
59
+
60
+ - name: Install Claude Code
61
+ run: npm install -g @anthropic-ai/claude-code
62
+
63
+ - name: Run claudep with test prompt
64
+ continue-on-error: true
65
+ timeout-minutes: 10
66
+ env:
67
+ CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
68
+ run: |
69
+ claudep -p "$(cat .github/test-prompt.md)"
70
+
71
+ - name: Upload session logs
72
+ if: always()
73
+ uses: actions/upload-artifact@v4
74
+ with:
75
+ name: claude-session-logs
76
+ path: ~/.claude/projects/
77
+ retention-days: 7
@@ -0,0 +1,9 @@
1
+ name: PRs
2
+
3
+ on:
4
+ pull_request:
5
+
6
+ jobs:
7
+ ci:
8
+ uses: ./.github/workflows/callable-ci.yml
9
+ secrets: inherit
@@ -0,0 +1,10 @@
1
+ name: Push to main
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+
7
+ jobs:
8
+ ci:
9
+ uses: ./.github/workflows/callable-ci.yml
10
+ secrets: inherit
@@ -0,0 +1,59 @@
1
+ name: Release
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ bump:
7
+ description: "Version bump type"
8
+ required: true
9
+ type: choice
10
+ options:
11
+ - patch
12
+ - minor
13
+ - major
14
+ - initial
15
+
16
+ permissions:
17
+ contents: write
18
+ id-token: write
19
+
20
+ jobs:
21
+ release:
22
+ runs-on: ubuntu-latest
23
+ environment: pypi
24
+ steps:
25
+ - uses: actions/checkout@v4
26
+ with:
27
+ fetch-depth: 0
28
+
29
+ - uses: actions/setup-python@v5
30
+ with:
31
+ python-version: "3.12"
32
+
33
+ - run: pip install --upgrade pip bump2version build
34
+
35
+ - name: Configure git
36
+ run: |
37
+ git config user.name "github-actions[bot]"
38
+ git config user.email "github-actions[bot]@users.noreply.github.com"
39
+
40
+ - name: Bump version
41
+ if: inputs.bump != 'initial'
42
+ run: bump2version ${{ inputs.bump }}
43
+
44
+ - name: Read new version
45
+ id: version
46
+ run: echo "version=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")" >> "$GITHUB_OUTPUT"
47
+
48
+ - name: Build
49
+ run: python -m build
50
+
51
+ - name: Publish to PyPI
52
+ uses: pypa/gh-action-pypi-publish@release/v1
53
+
54
+ - name: Commit and tag
55
+ run: |
56
+ git add -A
57
+ git commit -m "chore: release v${{ steps.version.outputs.version }}" || true
58
+ git tag "v${{ steps.version.outputs.version }}"
59
+ git push origin main --tags
@@ -0,0 +1,12 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ *.egg
10
+ .eggs/
11
+ .ignore/
12
+ tmp-*
@@ -0,0 +1,7 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ rev: v0.9.1
4
+ hooks:
5
+ - id: ruff
6
+ args: [--fix]
7
+ - id: ruff-format
@@ -0,0 +1,27 @@
1
+ install:
2
+ python3 -m venv .venv
3
+ .venv/bin/pip install -e ".[dev]"
4
+ .venv/bin/pre-commit install
5
+
6
+ check:
7
+ .venv/bin/ruff check .
8
+ .venv/bin/ruff format --check .
9
+
10
+ format:
11
+ .venv/bin/ruff check --fix .
12
+ .venv/bin/ruff format .
13
+
14
+ test:
15
+ .venv/bin/pytest -v
16
+
17
+ test-replay:
18
+ .venv/bin/claudep -f tests/fixtures/sample_session.jsonl
19
+
20
+ build:
21
+ .venv/bin/pip install hatch
22
+ .venv/bin/hatch build
23
+
24
+ clean:
25
+ rm -rf .venv dist build *.egg-info src/*.egg-info
26
+
27
+ .PHONY: install check format test test-replay build clean
@@ -0,0 +1,90 @@
1
+ Metadata-Version: 2.4
2
+ Name: claude-code-pretty
3
+ Version: 0.1.0
4
+ Summary: Pretty formatter for Claude Code stream-json output
5
+ Project-URL: Homepage, https://github.com/lucasvtiradentes/claude-code-pretty
6
+ Project-URL: Repository, https://github.com/lucasvtiradentes/claude-code-pretty
7
+ Project-URL: Issues, https://github.com/lucasvtiradentes/claude-code-pretty/issues
8
+ Author: lucasvtiradentes
9
+ License-Expression: MIT
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Software Development
23
+ Requires-Python: >=3.9
24
+ Provides-Extra: dev
25
+ Requires-Dist: pre-commit>=4; extra == 'dev'
26
+ Requires-Dist: pytest>=7; extra == 'dev'
27
+ Requires-Dist: ruff>=0.9; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # claude-code-pretty
31
+
32
+ Pretty formatter for Claude Code - stream sessions in real-time or replay saved .jsonl files.
33
+
34
+ <div align="center">
35
+ <img src=".github/images/demo.png" width="70%">
36
+ </div>
37
+
38
+ ## Features
39
+
40
+ - stream mode - run claude with pretty output in real-time
41
+ - replay mode - replay saved .jsonl session files
42
+ - tool display - formatted output for Glob, Grep, Bash, Read, Edit, etc.
43
+ - markdown - renders **bold** and `code` with ANSI styles
44
+ - subagent depth - visual indentation for nested Task calls
45
+ - cost tracking - shows duration, cost, tokens per session
46
+
47
+ ## Installation
48
+
49
+ ```bash
50
+ pip install claude-code-pretty
51
+ ```
52
+
53
+ Aliases: `claudep`, `ccp`, `claude-code-pretty`
54
+
55
+ ## Usage
56
+
57
+ ```bash
58
+ # stream mode - run claude with pretty output
59
+ claudep -p "explain this code"
60
+
61
+ # replay mode - replay a saved session
62
+ claudep -f ~/.claude/projects/.../session.jsonl
63
+ ```
64
+
65
+ Stream mode runs claude with these flags automatically:
66
+ ```
67
+ --print --verbose --dangerously-skip-permissions --output-format stream-json --include-partial-messages
68
+ ```
69
+
70
+ ## Environment
71
+
72
+ | Variable | Default | Description |
73
+ |--------------------------|---------|-------------------------------------|
74
+ | CP_TOOL_RESULT_MAX_CHARS | 300 | Max chars for tool results (0=hide) |
75
+ | CP_READ_PREVIEW_LINES | 5 | Lines to preview from Read (0=hide) |
76
+
77
+ ## Development
78
+
79
+ ```bash
80
+ make install # setup venv + install
81
+ make test # run pytest
82
+ make check # ruff lint
83
+
84
+ # dev alias
85
+ ln -sf $(pwd)/.venv/bin/claudep ~/.local/bin/claudepd
86
+ ```
87
+
88
+ ## License
89
+
90
+ MIT
@@ -0,0 +1,61 @@
1
+ # claude-code-pretty
2
+
3
+ Pretty formatter for Claude Code - stream sessions in real-time or replay saved .jsonl files.
4
+
5
+ <div align="center">
6
+ <img src=".github/images/demo.png" width="70%">
7
+ </div>
8
+
9
+ ## Features
10
+
11
+ - stream mode - run claude with pretty output in real-time
12
+ - replay mode - replay saved .jsonl session files
13
+ - tool display - formatted output for Glob, Grep, Bash, Read, Edit, etc.
14
+ - markdown - renders **bold** and `code` with ANSI styles
15
+ - subagent depth - visual indentation for nested Task calls
16
+ - cost tracking - shows duration, cost, tokens per session
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pip install claude-code-pretty
22
+ ```
23
+
24
+ Aliases: `claudep`, `ccp`, `claude-code-pretty`
25
+
26
+ ## Usage
27
+
28
+ ```bash
29
+ # stream mode - run claude with pretty output
30
+ claudep -p "explain this code"
31
+
32
+ # replay mode - replay a saved session
33
+ claudep -f ~/.claude/projects/.../session.jsonl
34
+ ```
35
+
36
+ Stream mode runs claude with these flags automatically:
37
+ ```
38
+ --print --verbose --dangerously-skip-permissions --output-format stream-json --include-partial-messages
39
+ ```
40
+
41
+ ## Environment
42
+
43
+ | Variable | Default | Description |
44
+ |--------------------------|---------|-------------------------------------|
45
+ | CP_TOOL_RESULT_MAX_CHARS | 300 | Max chars for tool results (0=hide) |
46
+ | CP_READ_PREVIEW_LINES | 5 | Lines to preview from Read (0=hide) |
47
+
48
+ ## Development
49
+
50
+ ```bash
51
+ make install # setup venv + install
52
+ make test # run pytest
53
+ make check # ruff lint
54
+
55
+ # dev alias
56
+ ln -sf $(pwd)/.venv/bin/claudep ~/.local/bin/claudepd
57
+ ```
58
+
59
+ ## License
60
+
61
+ MIT
@@ -0,0 +1,55 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "claude-code-pretty"
7
+ version = "0.1.0"
8
+ description = "Pretty formatter for Claude Code stream-json output"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ authors = [{ name = "lucasvtiradentes" }]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Environment :: Console",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.9",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Programming Language :: Python :: 3.14",
26
+ "Topic :: Software Development",
27
+ ]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/lucasvtiradentes/claude-code-pretty"
31
+ Repository = "https://github.com/lucasvtiradentes/claude-code-pretty"
32
+ Issues = "https://github.com/lucasvtiradentes/claude-code-pretty/issues"
33
+
34
+ [project.optional-dependencies]
35
+ dev = ["pytest>=7", "ruff>=0.9", "pre-commit>=4"]
36
+
37
+ [project.scripts]
38
+ claudep = "claudecodepretty.cli:main"
39
+ ccp = "claudecodepretty.cli:main"
40
+ claude-code-pretty = "claudecodepretty.cli:main"
41
+
42
+ [tool.hatch.build.targets.wheel]
43
+ packages = ["src/claudecodepretty"]
44
+
45
+ [tool.hatch.build.targets.wheel.sources]
46
+ "src" = ""
47
+
48
+ [tool.pytest.ini_options]
49
+ testpaths = ["tests"]
50
+
51
+ [tool.ruff]
52
+ line-length = 120
53
+
54
+ [tool.ruff.lint]
55
+ select = ["E", "F", "I"]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,54 @@
1
+ import sys
2
+
3
+ from claudecodepretty import __version__
4
+ from claudecodepretty.constants import CLI_NAME
5
+ from claudecodepretty.modes import run_replay, run_stream
6
+
7
+
8
+ def print_help():
9
+ print(f"""{CLI_NAME} - Pretty formatter for Claude Code stream-json output
10
+
11
+ Usage:
12
+ {CLI_NAME} [OPTIONS] [CLAUDE_ARGS...]
13
+ {CLI_NAME} -f <session.jsonl>
14
+
15
+ Options:
16
+ -f, --file FILE Replay a saved session .jsonl file
17
+ -h, --help Show this help
18
+ -v, --version Show version
19
+
20
+ Environment:
21
+ CP_TOOL_RESULT_MAX_CHARS Max chars for tool results (default: 300, 0=hide)
22
+ CP_READ_PREVIEW_LINES Lines to preview from Read (default: 5, 0=hide)
23
+
24
+ Examples:
25
+ {CLI_NAME} -p "explain this code"
26
+ {CLI_NAME} -f ~/.claude/projects/.../session.jsonl
27
+ cat session.jsonl | {CLI_NAME} -f -""")
28
+
29
+
30
+ def main():
31
+ args = sys.argv[1:]
32
+
33
+ if not args or "-h" in args or "--help" in args:
34
+ print_help()
35
+ sys.exit(0)
36
+
37
+ if "-v" in args or "--version" in args:
38
+ print(__version__)
39
+ sys.exit(0)
40
+
41
+ if "-f" in args or "--file" in args:
42
+ try:
43
+ idx = args.index("-f") if "-f" in args else args.index("--file")
44
+ file_path = args[idx + 1]
45
+ sys.exit(run_replay(file_path))
46
+ except IndexError:
47
+ print("Error: -f requires a file path")
48
+ sys.exit(1)
49
+
50
+ sys.exit(run_stream(args))
51
+
52
+
53
+ if __name__ == "__main__":
54
+ main()
@@ -0,0 +1,56 @@
1
+ BOLD = "\033[1m"
2
+ INVERSE = "\033[7m"
3
+ GREEN = "\033[32m"
4
+ ORANGE = "\033[33m"
5
+ PURPLE = "\033[35m"
6
+ CYAN = "\033[36m"
7
+ BLUE = "\033[34m"
8
+ YELLOW = "\033[93m"
9
+ DIM = "\033[90m"
10
+ RED = "\033[31m"
11
+ RESET = "\033[0m"
12
+
13
+
14
+ def green(text: str) -> str:
15
+ return f"{GREEN}{text}{RESET}"
16
+
17
+
18
+ def orange(text: str) -> str:
19
+ return f"{ORANGE}{text}{RESET}"
20
+
21
+
22
+ def purple(text: str) -> str:
23
+ return f"{PURPLE}{text}{RESET}"
24
+
25
+
26
+ def cyan(text: str) -> str:
27
+ return f"{CYAN}{text}{RESET}"
28
+
29
+
30
+ def blue(text: str) -> str:
31
+ return f"{BLUE}{text}{RESET}"
32
+
33
+
34
+ def yellow(text: str) -> str:
35
+ return f"{YELLOW}{text}{RESET}"
36
+
37
+
38
+ def dim(text: str) -> str:
39
+ return f"{DIM}{text}{RESET}"
40
+
41
+
42
+ def red(text: str) -> str:
43
+ return f"{RED}{text}{RESET}"
44
+
45
+
46
+ def bold(text: str) -> str:
47
+ return f"{BOLD}{text}{RESET}"
48
+
49
+
50
+ def render_markdown(text: str) -> str:
51
+ import re
52
+
53
+ text = re.sub(r"\*\*(.+?)\*\*", rf"{BOLD}\1{RESET}", text)
54
+ text = re.sub(r"__(.+?)__", rf"{BOLD}\1{RESET}", text)
55
+ text = re.sub(r"`([^`]+)`", rf"{INVERSE}\1{RESET}", text)
56
+ return text
@@ -0,0 +1,9 @@
1
+ import os
2
+
3
+ CLI_NAME = "claudep"
4
+ DIST_NAME = "claude-code-pretty"
5
+ INDENT = " "
6
+ HIDE_TOOLS = {"Write", "TodoWrite", "Read", "Glob", "Grep", "Bash", "Task", "Edit", "MultiEdit", "NotebookEdit"}
7
+
8
+ TOOL_RESULT_MAX_CHARS = int(os.environ.get("CP_TOOL_RESULT_MAX_CHARS", "300"))
9
+ READ_PREVIEW_LINES = int(os.environ.get("CP_READ_PREVIEW_LINES", "5"))
@@ -0,0 +1,16 @@
1
+ from claudecodepretty.handlers.assistant import handle_assistant_message
2
+ from claudecodepretty.handlers.base import ParseResult, ParserState
3
+ from claudecodepretty.handlers.result import handle_result
4
+ from claudecodepretty.handlers.stream import handle_stream_event
5
+ from claudecodepretty.handlers.system import handle_system
6
+ from claudecodepretty.handlers.user import handle_user_message
7
+
8
+ __all__ = [
9
+ "ParseResult",
10
+ "ParserState",
11
+ "handle_system",
12
+ "handle_stream_event",
13
+ "handle_user_message",
14
+ "handle_assistant_message",
15
+ "handle_result",
16
+ ]
@@ -0,0 +1,24 @@
1
+ from claudecodepretty.colors import render_markdown
2
+ from claudecodepretty.handlers.base import ParseResult, ParserState
3
+ from claudecodepretty.handlers.tools import dispatch_tool
4
+
5
+
6
+ def handle_assistant_message(data: dict, state: ParserState, result: ParseResult):
7
+ message = data.get("message", {})
8
+ content = message.get("content", [])
9
+
10
+ if not isinstance(content, list):
11
+ return
12
+
13
+ if state.mode == "replay":
14
+ for block in content:
15
+ if block.get("type") == "text":
16
+ result.add(render_markdown(block.get("text", "")))
17
+
18
+ for block in content:
19
+ if block.get("type") != "tool_use":
20
+ continue
21
+
22
+ name = block.get("name", "")
23
+ inp = block.get("input", {})
24
+ dispatch_tool(name, inp, state, result)
@@ -0,0 +1,81 @@
1
+ from dataclasses import dataclass, field
2
+
3
+ from claudecodepretty.colors import BOLD, DIM, INVERSE, RESET
4
+
5
+
6
+ @dataclass
7
+ class ParserState:
8
+ mode: str = "stream"
9
+ current_tool: str = ""
10
+ subagent_depth: int = 0
11
+ sp: str = ""
12
+ in_bold: bool = False
13
+ in_code: bool = False
14
+ pending_char: str = ""
15
+
16
+ def update_sp(self):
17
+ self.sp = "".join(f"{DIM}│{RESET} " for _ in range(self.subagent_depth))
18
+
19
+ def increment_depth(self):
20
+ self.subagent_depth += 1
21
+ self.update_sp()
22
+
23
+ def decrement_depth(self):
24
+ if self.subagent_depth > 0:
25
+ self.subagent_depth -= 1
26
+ self.update_sp()
27
+
28
+ def _get_style(self) -> str:
29
+ if self.in_bold and self.in_code:
30
+ return BOLD + INVERSE
31
+ if self.in_bold:
32
+ return BOLD
33
+ if self.in_code:
34
+ return INVERSE
35
+ return ""
36
+
37
+ def render_text(self, text: str) -> str:
38
+ out = []
39
+ i = 0
40
+ buf = self.pending_char + text
41
+ self.pending_char = ""
42
+
43
+ while i < len(buf):
44
+ ch = buf[i]
45
+ if ch == "*" and not self.in_code:
46
+ if i + 1 < len(buf):
47
+ if buf[i + 1] == "*":
48
+ self.in_bold = not self.in_bold
49
+ out.append(RESET + self._get_style())
50
+ i += 2
51
+ continue
52
+ else:
53
+ out.append(ch)
54
+ i += 1
55
+ else:
56
+ self.pending_char = "*"
57
+ break
58
+ elif ch == "`":
59
+ self.in_code = not self.in_code
60
+ out.append(RESET + self._get_style())
61
+ i += 1
62
+ else:
63
+ out.append(ch)
64
+ i += 1
65
+
66
+ return "".join(out)
67
+
68
+
69
+ @dataclass
70
+ class ParseResult:
71
+ output: str = ""
72
+ messages: list[str] = field(default_factory=list)
73
+
74
+ def add(self, text: str):
75
+ self.messages.append(text)
76
+
77
+ def add_inline(self, text: str):
78
+ self.messages.append(text)
79
+
80
+ def get_output(self) -> str:
81
+ return "".join(self.messages)
@@ -0,0 +1,27 @@
1
+ from claudecodepretty.colors import DIM, RED, RESET
2
+ from claudecodepretty.handlers.base import ParseResult, ParserState
3
+
4
+
5
+ def handle_result(data: dict, state: ParserState, result: ParseResult):
6
+ while state.subagent_depth > 0:
7
+ result.add(f"{state.sp}{DIM}└───────────────────────────────{RESET}\n")
8
+ state.decrement_depth()
9
+
10
+ if data.get("is_error"):
11
+ result.add(f"\n{RED}[error] {data.get('result', 'unknown error')}{RESET}\n")
12
+ else:
13
+ duration_ms = data.get("duration_ms", 0)
14
+ duration = f"{duration_ms / 1000:.1f}"
15
+ cost = f"{data.get('total_cost_usd', 0):.4f}"
16
+ turns = data.get("num_turns", 0)
17
+ usage = data.get("usage", {})
18
+ input_tokens = sum(
19
+ [
20
+ usage.get("input_tokens", 0),
21
+ usage.get("cache_read_input_tokens", 0),
22
+ usage.get("cache_creation_input_tokens", 0),
23
+ ]
24
+ )
25
+ output_tokens = usage.get("output_tokens", 0)
26
+ stats = f"{duration}s, ${cost}, {turns} turns, {input_tokens} in / {output_tokens} out"
27
+ result.add(f"\n{DIM}[done] {stats}{RESET}\n")
@@ -0,0 +1,39 @@
1
+ from claudecodepretty.colors import DIM, PURPLE, RED, RESET
2
+ from claudecodepretty.constants import HIDE_TOOLS
3
+ from claudecodepretty.handlers.base import ParseResult, ParserState
4
+
5
+
6
+ def handle_stream_event(data: dict, state: ParserState, result: ParseResult):
7
+ event = data.get("event", {})
8
+ event_type = event.get("type", "")
9
+
10
+ if event_type == "content_block_start":
11
+ if state.subagent_depth > 0:
12
+ result.add(f"{state.sp}{DIM}└───────────────────────────────{RESET}\n")
13
+ state.decrement_depth()
14
+
15
+ block = event.get("content_block", {})
16
+ if block.get("type") == "tool_use":
17
+ name = block.get("name", "")
18
+ state.current_tool = name
19
+ if name not in HIDE_TOOLS:
20
+ result.add(f"\n{state.sp}{PURPLE}[{name}] ")
21
+
22
+ elif event_type == "content_block_delta":
23
+ delta = event.get("delta", {})
24
+ delta_type = delta.get("type", "")
25
+
26
+ if delta_type == "text_delta":
27
+ result.add_inline(state.render_text(delta.get("text", "")))
28
+ elif delta_type == "input_json_delta":
29
+ if state.current_tool not in HIDE_TOOLS:
30
+ result.add_inline(delta.get("partial_json", ""))
31
+
32
+ elif event_type == "content_block_stop":
33
+ if state.current_tool not in HIDE_TOOLS:
34
+ result.add(f"{RESET}\n")
35
+ state.current_tool = ""
36
+
37
+ elif event_type == "error":
38
+ error = event.get("error", str(event))
39
+ result.add(f"\n{state.sp}{RED}[error] {error}{RESET}\n")
@@ -0,0 +1,16 @@
1
+ from claudecodepretty.colors import DIM, RESET
2
+ from claudecodepretty.constants import INDENT
3
+ from claudecodepretty.handlers.base import ParseResult, ParserState
4
+
5
+
6
+ def handle_system(data: dict, state: ParserState, result: ParseResult):
7
+ if data.get("subtype") == "init":
8
+ session_id = data.get("session_id", "")
9
+ cwd = data.get("cwd", "").replace("/", "-").replace("_", "-")
10
+ model = data.get("model", "")
11
+ model_name = model.split("-")[1] if "-" in model else model
12
+
13
+ result.add(f"{DIM}[session]\n")
14
+ result.add(f"{INDENT}id: {session_id}\n")
15
+ result.add(f"{INDENT}path: ~/.claude/projects/{cwd}/{session_id}.jsonl\n")
16
+ result.add(f"{INDENT}model: {model_name}{RESET}\n\n")
@@ -0,0 +1,31 @@
1
+ from claudecodepretty.handlers.base import ParseResult, ParserState
2
+ from claudecodepretty.handlers.tools.bash import handle_bash
3
+ from claudecodepretty.handlers.tools.edit import handle_edit
4
+ from claudecodepretty.handlers.tools.glob import handle_glob
5
+ from claudecodepretty.handlers.tools.grep import handle_grep
6
+ from claudecodepretty.handlers.tools.notebook import handle_notebook
7
+ from claudecodepretty.handlers.tools.read import handle_read
8
+ from claudecodepretty.handlers.tools.task import handle_task
9
+ from claudecodepretty.handlers.tools.todo import handle_todo
10
+ from claudecodepretty.handlers.tools.write import handle_write
11
+
12
+
13
+ def dispatch_tool(name: str, inp: dict, state: ParserState, result: ParseResult):
14
+ if name == "TodoWrite":
15
+ handle_todo(inp, state, result)
16
+ elif name == "Write":
17
+ handle_write(inp, state, result)
18
+ elif name == "Read":
19
+ handle_read(inp, state, result)
20
+ elif name == "Glob":
21
+ handle_glob(inp, state, result)
22
+ elif name == "Grep":
23
+ handle_grep(inp, state, result)
24
+ elif name in ("Edit", "MultiEdit"):
25
+ handle_edit(name, inp, state, result)
26
+ elif name == "NotebookEdit":
27
+ handle_notebook(inp, state, result)
28
+ elif name == "Bash":
29
+ handle_bash(inp, state, result)
30
+ elif name == "Task":
31
+ handle_task(inp, state, result)
@@ -0,0 +1,6 @@
1
+ from claudecodepretty.colors import PURPLE, RESET
2
+ from claudecodepretty.handlers.base import ParseResult, ParserState
3
+
4
+
5
+ def handle_bash(inp: dict, state: ParserState, result: ParseResult):
6
+ result.add(f"\n{state.sp}{PURPLE}[Bash] {inp.get('command', '')}{RESET}\n")
@@ -0,0 +1,6 @@
1
+ from claudecodepretty.colors import ORANGE, RESET
2
+ from claudecodepretty.handlers.base import ParseResult, ParserState
3
+
4
+
5
+ def handle_edit(name: str, inp: dict, state: ParserState, result: ParseResult):
6
+ result.add(f"\n{state.sp}{ORANGE}[{name}] {inp.get('file_path', '')}{RESET}\n")
@@ -0,0 +1,6 @@
1
+ from claudecodepretty.colors import PURPLE, RESET
2
+ from claudecodepretty.handlers.base import ParseResult, ParserState
3
+
4
+
5
+ def handle_glob(inp: dict, state: ParserState, result: ParseResult):
6
+ result.add(f"\n{state.sp}{PURPLE}[Glob] {inp.get('pattern', '')}{RESET}\n")
@@ -0,0 +1,12 @@
1
+ from claudecodepretty.colors import PURPLE, RESET
2
+ from claudecodepretty.handlers.base import ParseResult, ParserState
3
+
4
+
5
+ def handle_grep(inp: dict, state: ParserState, result: ParseResult):
6
+ pattern = inp.get("pattern", "")
7
+ path = inp.get("path", "")
8
+ if path:
9
+ path = path.split("/")[-1]
10
+ result.add(f'\n{state.sp}{PURPLE}[Grep] "{pattern}" in {path}{RESET}\n')
11
+ else:
12
+ result.add(f'\n{state.sp}{PURPLE}[Grep] "{pattern}"{RESET}\n')
@@ -0,0 +1,6 @@
1
+ from claudecodepretty.colors import ORANGE, RESET
2
+ from claudecodepretty.handlers.base import ParseResult, ParserState
3
+
4
+
5
+ def handle_notebook(inp: dict, state: ParserState, result: ParseResult):
6
+ result.add(f"\n{state.sp}{ORANGE}[NotebookEdit] {inp.get('notebook_path', '')}{RESET}\n")
@@ -0,0 +1,6 @@
1
+ from claudecodepretty.colors import GREEN, RESET
2
+ from claudecodepretty.handlers.base import ParseResult, ParserState
3
+
4
+
5
+ def handle_read(inp: dict, state: ParserState, result: ParseResult):
6
+ result.add(f"\n{state.sp}{GREEN}[Read] {inp.get('file_path', '')}{RESET}\n")
@@ -0,0 +1,11 @@
1
+ from claudecodepretty.colors import BLUE, DIM, RESET
2
+ from claudecodepretty.handlers.base import ParseResult, ParserState
3
+
4
+
5
+ def handle_task(inp: dict, state: ParserState, result: ParseResult):
6
+ prompt = inp.get("prompt", inp.get("description", ""))[:50]
7
+ model = inp.get("model", "sonnet")
8
+ result.add(f'\n{state.sp}{BLUE}[Task] "{prompt}" ({model}){RESET}\n')
9
+ if state.mode == "stream":
10
+ state.increment_depth()
11
+ result.add(f"{state.sp}{DIM}┌───────────────────────────────{RESET}\n")
@@ -0,0 +1,18 @@
1
+ from claudecodepretty.colors import DIM, GREEN, ORANGE, RESET, YELLOW
2
+ from claudecodepretty.constants import INDENT
3
+ from claudecodepretty.handlers.base import ParseResult, ParserState
4
+
5
+
6
+ def handle_todo(inp: dict, state: ParserState, result: ParseResult):
7
+ result.add(f"\n{state.sp}{YELLOW}[Todo]{RESET}\n")
8
+ for todo in inp.get("todos", []):
9
+ status = todo.get("status", "pending")
10
+ text = todo.get("content", "")
11
+ if status == "completed":
12
+ mark = f"{GREEN}[x]{RESET}"
13
+ elif status == "in_progress":
14
+ mark = f"{ORANGE}[~]{RESET}"
15
+ else:
16
+ mark = f"{DIM}[ ]{RESET}"
17
+ result.add(f"{state.sp}{INDENT}{mark} {text}\n")
18
+ result.add("\n")
@@ -0,0 +1,6 @@
1
+ from claudecodepretty.colors import ORANGE, RESET
2
+ from claudecodepretty.handlers.base import ParseResult, ParserState
3
+
4
+
5
+ def handle_write(inp: dict, state: ParserState, result: ParseResult):
6
+ result.add(f"\n{state.sp}{ORANGE}[Write] {inp.get('file_path', '')}{RESET}\n")
@@ -0,0 +1,51 @@
1
+ import re
2
+
3
+ from claudecodepretty.colors import DIM, GREEN, RED, RESET
4
+ from claudecodepretty.constants import INDENT, READ_PREVIEW_LINES, TOOL_RESULT_MAX_CHARS
5
+ from claudecodepretty.handlers.base import ParseResult, ParserState
6
+
7
+
8
+ def handle_user_message(data: dict, state: ParserState, result: ParseResult):
9
+ message = data.get("message", {})
10
+ content = message.get("content", "")
11
+
12
+ if isinstance(content, str):
13
+ if state.mode == "replay":
14
+ text = content[:200]
15
+ result.add(f"\n{GREEN}[user]{RESET} {text}")
16
+ if len(content) > 200:
17
+ result.add(f"{DIM}...{RESET}")
18
+ result.add("\n")
19
+ return
20
+
21
+ if not isinstance(content, list) or not content:
22
+ return
23
+
24
+ first = content[0]
25
+ content_type = first.get("type", "")
26
+
27
+ if content_type == "tool_result":
28
+ tool_content = first.get("content", "")
29
+
30
+ if isinstance(tool_content, str):
31
+ if tool_content.startswith(("Todos have been", "The file")) and "has been" in tool_content:
32
+ return
33
+ if "<tool_use_error>" in tool_content:
34
+ error_msg = re.sub(r"<[^>]*>", "", tool_content)
35
+ result.add(f"{state.sp}{RED}{INDENT}✗ {error_msg}{RESET}\n\n")
36
+ return
37
+
38
+ if "\n" in tool_content:
39
+ if READ_PREVIEW_LINES == 0:
40
+ return
41
+ lines = tool_content.split("\n")[:READ_PREVIEW_LINES]
42
+ for line in lines:
43
+ result.add(f"{state.sp}{DIM}{INDENT}→ {line}{RESET}\n")
44
+ if len(tool_content.split("\n")) > READ_PREVIEW_LINES:
45
+ result.add(f"{state.sp}{INDENT}...\n")
46
+ result.add("\n")
47
+ else:
48
+ if TOOL_RESULT_MAX_CHARS == 0:
49
+ return
50
+ text = tool_content if TOOL_RESULT_MAX_CHARS < 0 else tool_content[:TOOL_RESULT_MAX_CHARS]
51
+ result.add(f"{state.sp}{DIM}{INDENT}→ {text}{RESET}\n\n")
@@ -0,0 +1,4 @@
1
+ from claudecodepretty.modes.replay import run_replay
2
+ from claudecodepretty.modes.stream import run_stream
3
+
4
+ __all__ = ["run_stream", "run_replay"]
@@ -0,0 +1,34 @@
1
+ import os
2
+ import sys
3
+
4
+ from claudecodepretty.colors import DIM, RESET
5
+ from claudecodepretty.handlers import ParserState
6
+ from claudecodepretty.parser import parse_json_line
7
+
8
+
9
+ def run_replay(file_path: str) -> int:
10
+ state = ParserState(mode="replay")
11
+
12
+ if file_path == "-":
13
+ source = sys.stdin
14
+ print(f"{DIM}[replay] stdin{RESET}\n")
15
+ else:
16
+ if not os.path.exists(file_path):
17
+ print(f"Error: File not found: {file_path}")
18
+ return 1
19
+ source = open(file_path, "r")
20
+ print(f"{DIM}[replay] {file_path}{RESET}\n")
21
+
22
+ try:
23
+ for line in source:
24
+ line = line.strip()
25
+ if line:
26
+ result = parse_json_line(line, state)
27
+ output = result.get_output()
28
+ if output:
29
+ print(output, end="", flush=True)
30
+ finally:
31
+ if file_path != "-":
32
+ source.close()
33
+
34
+ return 0
@@ -0,0 +1,44 @@
1
+ import subprocess
2
+ import sys
3
+
4
+ from claudecodepretty.handlers import ParserState
5
+ from claudecodepretty.parser import parse_json_line
6
+
7
+
8
+ def run_stream(args: list[str]) -> int:
9
+ cmd = [
10
+ "claude",
11
+ "--print",
12
+ "--verbose",
13
+ "--dangerously-skip-permissions",
14
+ "--output-format",
15
+ "stream-json",
16
+ "--include-partial-messages",
17
+ *args,
18
+ ]
19
+
20
+ state = ParserState(mode="stream")
21
+
22
+ process = subprocess.Popen(
23
+ cmd,
24
+ stdout=subprocess.PIPE,
25
+ stderr=subprocess.STDOUT,
26
+ text=True,
27
+ bufsize=1,
28
+ )
29
+
30
+ try:
31
+ for line in process.stdout:
32
+ line = line.strip()
33
+ if line:
34
+ result = parse_json_line(line, state)
35
+ output = result.get_output()
36
+ if output:
37
+ print(output, end="", flush=True)
38
+ except KeyboardInterrupt:
39
+ process.terminate()
40
+ sys.exit(130)
41
+ finally:
42
+ process.wait()
43
+
44
+ return process.returncode
@@ -0,0 +1,40 @@
1
+ import json
2
+
3
+ from claudecodepretty.colors import RED, RESET
4
+ from claudecodepretty.handlers import (
5
+ ParseResult,
6
+ ParserState,
7
+ handle_assistant_message,
8
+ handle_result,
9
+ handle_stream_event,
10
+ handle_system,
11
+ handle_user_message,
12
+ )
13
+
14
+ __all__ = ["parse_json_line", "ParserState", "ParseResult"]
15
+
16
+
17
+ def parse_json_line(line: str, state: ParserState) -> ParseResult:
18
+ result = ParseResult()
19
+
20
+ try:
21
+ data = json.loads(line)
22
+ except json.JSONDecodeError:
23
+ return result
24
+
25
+ msg_type = data.get("type", "")
26
+
27
+ if msg_type == "system":
28
+ handle_system(data, state, result)
29
+ elif msg_type == "stream_event":
30
+ handle_stream_event(data, state, result)
31
+ elif msg_type == "user":
32
+ handle_user_message(data, state, result)
33
+ elif msg_type == "assistant":
34
+ handle_assistant_message(data, state, result)
35
+ elif msg_type == "result":
36
+ handle_result(data, state, result)
37
+ elif msg_type == "error":
38
+ result.add(f"\n{state.sp}{RED}[error] {data.get('error', 'unknown error')}{RESET}")
39
+
40
+ return result
File without changes
@@ -0,0 +1,7 @@
1
+ {"type":"system","subtype":"init","session_id":"test-session-123","cwd":"/Users/test/project","model":"claude-sonnet-4-20250514"}
2
+ {"type":"user","message":{"content":"List all Python files"}}
3
+ {"type":"assistant","message":{"content":[{"type":"text","text":"I'll list all Python files for you."}]}}
4
+ {"type":"assistant","message":{"content":[{"type":"tool_use","name":"Glob","input":{"pattern":"*.py"}}]}}
5
+ {"type":"user","message":{"content":[{"type":"tool_result","content":"main.py\nutils.py\ntest.py"}]}}
6
+ {"type":"assistant","message":{"content":[{"type":"text","text":"Found 3 Python files: main.py, utils.py, and test.py."}]}}
7
+ {"type":"result","is_error":false,"duration_ms":2500,"total_cost_usd":0.025,"num_turns":2,"usage":{"input_tokens":500,"output_tokens":100}}
@@ -0,0 +1,180 @@
1
+ import json
2
+
3
+ from claudecodepretty.parser import ParserState, parse_json_line
4
+
5
+
6
+ def test_parse_system_init():
7
+ state = ParserState(mode="stream")
8
+ line = json.dumps(
9
+ {
10
+ "type": "system",
11
+ "subtype": "init",
12
+ "session_id": "abc-123",
13
+ "cwd": "/Users/test/project",
14
+ "model": "claude-sonnet-4-20250514",
15
+ }
16
+ )
17
+ result = parse_json_line(line, state)
18
+ output = result.get_output()
19
+
20
+ assert "[session]" in output
21
+ assert "abc-123" in output
22
+ assert "sonnet" in output
23
+
24
+
25
+ def test_parse_tool_use_glob():
26
+ state = ParserState(mode="stream")
27
+ line = json.dumps(
28
+ {
29
+ "type": "assistant",
30
+ "message": {
31
+ "content": [
32
+ {
33
+ "type": "tool_use",
34
+ "name": "Glob",
35
+ "input": {"pattern": "*.py"},
36
+ }
37
+ ]
38
+ },
39
+ }
40
+ )
41
+ result = parse_json_line(line, state)
42
+ output = result.get_output()
43
+
44
+ assert "[Glob]" in output
45
+ assert "*.py" in output
46
+
47
+
48
+ def test_parse_tool_use_bash():
49
+ state = ParserState(mode="stream")
50
+ line = json.dumps(
51
+ {
52
+ "type": "assistant",
53
+ "message": {
54
+ "content": [
55
+ {
56
+ "type": "tool_use",
57
+ "name": "Bash",
58
+ "input": {"command": "echo hello"},
59
+ }
60
+ ]
61
+ },
62
+ }
63
+ )
64
+ result = parse_json_line(line, state)
65
+ output = result.get_output()
66
+
67
+ assert "[Bash]" in output
68
+ assert "echo hello" in output
69
+
70
+
71
+ def test_parse_todo_write():
72
+ state = ParserState(mode="stream")
73
+ line = json.dumps(
74
+ {
75
+ "type": "assistant",
76
+ "message": {
77
+ "content": [
78
+ {
79
+ "type": "tool_use",
80
+ "name": "TodoWrite",
81
+ "input": {
82
+ "todos": [
83
+ {"status": "completed", "content": "item one"},
84
+ {"status": "in_progress", "content": "item two"},
85
+ {"status": "pending", "content": "item three"},
86
+ ]
87
+ },
88
+ }
89
+ ]
90
+ },
91
+ }
92
+ )
93
+ result = parse_json_line(line, state)
94
+ output = result.get_output()
95
+
96
+ assert "[Todo]" in output
97
+ assert "item one" in output
98
+ assert "item two" in output
99
+ assert "item three" in output
100
+
101
+
102
+ def test_parse_result():
103
+ state = ParserState(mode="stream")
104
+ line = json.dumps(
105
+ {
106
+ "type": "result",
107
+ "is_error": False,
108
+ "duration_ms": 5000,
109
+ "total_cost_usd": 0.05,
110
+ "num_turns": 3,
111
+ "usage": {
112
+ "input_tokens": 1000,
113
+ "output_tokens": 500,
114
+ },
115
+ }
116
+ )
117
+ result = parse_json_line(line, state)
118
+ output = result.get_output()
119
+
120
+ assert "[done]" in output
121
+ assert "5.0s" in output
122
+ assert "$0.0500" in output
123
+ assert "3 turns" in output
124
+
125
+
126
+ def test_parse_task_increments_depth():
127
+ state = ParserState(mode="stream")
128
+ line = json.dumps(
129
+ {
130
+ "type": "assistant",
131
+ "message": {
132
+ "content": [
133
+ {
134
+ "type": "tool_use",
135
+ "name": "Task",
136
+ "input": {"prompt": "do something", "model": "sonnet"},
137
+ }
138
+ ]
139
+ },
140
+ }
141
+ )
142
+ parse_json_line(line, state)
143
+
144
+ assert state.subagent_depth == 1
145
+ assert "│" in state.sp
146
+
147
+
148
+ def test_replay_mode_shows_user_prompt():
149
+ state = ParserState(mode="replay")
150
+ line = json.dumps(
151
+ {
152
+ "type": "user",
153
+ "message": {
154
+ "content": "Hello, can you help me?",
155
+ },
156
+ }
157
+ )
158
+ result = parse_json_line(line, state)
159
+ output = result.get_output()
160
+
161
+ assert "[user]" in output
162
+ assert "Hello" in output
163
+
164
+
165
+ def test_replay_mode_shows_assistant_text():
166
+ state = ParserState(mode="replay")
167
+ line = json.dumps(
168
+ {
169
+ "type": "assistant",
170
+ "message": {
171
+ "content": [
172
+ {"type": "text", "text": "Sure, I can help!"},
173
+ ],
174
+ },
175
+ }
176
+ )
177
+ result = parse_json_line(line, state)
178
+ output = result.get_output()
179
+
180
+ assert "Sure, I can help!" in output