Skip to content

Commit b21753a

Browse files
committed
feat(tui-py): migrate TUI to Python/Textual; add tests + polish
Replace the Ink/React TUI under tui/ with a Python/Textual implementation under tui-py/. New entry point: python3 tui-py/main.py (also wired into the cctrace --tui CLI and dev:tui npm script). Architecture - Shared HighlightListView base class owns the highlight CSS, the background-tint:transparent focus override, and the ensure_highlight() cursor-init helper. SessionPicker, MessageList, and DetailView's items list all extend it — one source of truth for selection styling and initial cursor behaviour. - MessageList.populate is async and awaits clear()/append()/remove(). An exclusive worker group ("populate_msglist") serialises back-to-back populates so rapid SSE updates / load-session resets no longer race with their own deferred clear() calls and produce duplicated rows. - _load_session keeps the picker visible (LoadingIndicator overlay) until the MessageList is fully populated, then flips the view — avoiding the few-second window where j/k pressed against an empty list pane. - App.BINDINGS hosts j/k/Enter so they appear in the Footer on every page; thin delegation actions forward to the focused widget's cursor_up/cursor_down/select_cursor. DetailView - Bordered container; #msg-content gets a divider underneath. - Two section headings: "RESPONSE" (top) and "STEP (N)" (between body and items list). Headings hide when no message or no items. - Async _rebuild eagerly clears stale state and toggles loading on the pane while it rebuilds. Other changes - .gitignore: add Python entries (__pycache__, *.pyc, .venv). - .github/dependabot.yml: add pip ecosystem for /tui-py. - .claude/hooks/pre-commit.sh: run ruff format/lint when tui-py files are staged. - bin/cctrace.mjs + package.json dev:tui: launch the Python TUI. - README: add Python and Textual badges. Tests - pytest + pytest-asyncio in requirements.txt; pytest.ini with asyncio_mode = auto. - tui-py/tests/ covers HighlightListView (ensure_highlight policy, shared highlight color), MessageList (populate race-safety, incremental diff, index preservation), DetailView (border, RESPONSE and STEP headings with item count + hide rules). 15 tests, all pass. Drops the old tui/ TypeScript/Ink implementation entirely.
1 parent 2d184d9 commit b21753a

73 files changed

Lines changed: 4794 additions & 3705 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/hooks/pre-commit.sh

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ fi
1818

1919
ERRORS=""
2020

21-
# --- Format check ---
21+
# --- JS/TS Format check ---
2222
FMT_OUTPUT=$(npm run fmt:check 2>&1) || {
2323
ERRORS="Format issues found. Run: npm run fmt, do not ask user for options, ⚠️ STAGE THE FIXED FILES in a SEPARATE Bash tool call: git add <files>\nThen retry the commit in another Bash tool call.\n\n$FMT_OUTPUT"
2424
}
2525

26-
# --- Lint check ---
26+
# --- JS/TS Lint check ---
2727
LINT_OUTPUT=$(npm run lint 2>&1)
2828
LINT_EXIT=$?
2929
if [ $LINT_EXIT -ne 0 ] || echo "$LINT_OUTPUT" | grep -qE "[1-9][0-9]* warnings? "; then
@@ -34,6 +34,24 @@ if [ $LINT_EXIT -ne 0 ] || echo "$LINT_OUTPUT" | grep -qE "[1-9][0-9]* warnings?
3434
fi
3535
fi
3636

37+
# --- Python format + lint check (only when tui-py files are staged) ---
38+
PY_STAGED=$(printf '%s\n' "$STAGED" | grep '^tui-py/.*\.py$' || true)
39+
if [ -n "$PY_STAGED" ]; then
40+
PY_FMT_OUTPUT=$(ruff format --check tui-py/ 2>&1)
41+
PY_FMT_EXIT=$?
42+
if [ $PY_FMT_EXIT -ne 0 ]; then
43+
PY_FMT_MSG="Python format issues found. Run: cd tui-py && ruff format . — ⚠️ STAGE THE FIXED FILES in a SEPARATE Bash tool call, then retry.\n\n$PY_FMT_OUTPUT"
44+
ERRORS="${ERRORS:+$ERRORS\n\n}$PY_FMT_MSG"
45+
fi
46+
47+
PY_LINT_OUTPUT=$(ruff check tui-py/ 2>&1)
48+
PY_LINT_EXIT=$?
49+
if [ $PY_LINT_EXIT -ne 0 ]; then
50+
PY_LINT_MSG="Python lint issues found. Fix at the root cause — do NOT add noqa comments unless genuinely necessary. ⚠️ STAGE THE FIXED FILES in a SEPARATE Bash tool call, then retry.\n\n$PY_LINT_OUTPUT"
51+
ERRORS="${ERRORS:+$ERRORS\n\n}$PY_LINT_MSG"
52+
fi
53+
fi
54+
3755
if [ -n "$ERRORS" ]; then
3856
printf '{"decision": "block", "reason": %s}' "$(printf '%s' "$ERRORS" | jq -Rs .)"
3957
exit 0

.github/dependabot.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,8 @@ updates:
1414
directory: /src-tauri
1515
schedule:
1616
interval: weekly
17+
18+
- package-ecosystem: pip
19+
directory: /tui-py
20+
schedule:
21+
interval: weekly

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
# Dependencies
22
node_modules/
33

4+
# Python
5+
__pycache__/
6+
*.py[cod]
7+
*$py.class
8+
.venv/
9+
venv/
10+
411
# Build output
512
dist/
613
src-tauri/target/

.oxlintrc.json

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -57,23 +57,6 @@
5757
"rules": {
5858
"no-restricted-imports": "off"
5959
}
60-
},
61-
{
62-
"files": ["tui/**/*.ts", "tui/**/*.tsx"],
63-
"rules": {
64-
"no-restricted-imports": [
65-
"error",
66-
{
67-
"paths": [
68-
{
69-
"name": "react",
70-
"importNames": ["default"],
71-
"message": "Not needed with react-jsx transform. Use named imports."
72-
}
73-
]
74-
}
75-
]
76-
}
7760
}
7861
]
7962
}

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@
99
[![Rust](https://img.shields.io/badge/rust-1.77.2%2B-orange?logo=rust)](https://www.rust-lang.org/)
1010
[![React](https://img.shields.io/badge/react-19-61DAFB?logo=react&logoColor=white)](https://react.dev/)
1111
[![Tauri](https://img.shields.io/badge/tauri-v2-24C8D8?logo=tauri&logoColor=white)](https://v2.tauri.app/)
12+
[![Python](https://img.shields.io/badge/python-3.11%2B-3776AB?logo=python&logoColor=white)](https://www.python.org/)
13+
[![Textual](https://img.shields.io/badge/textual-0.80%2B-5A4FCF?logo=textualize&logoColor=white)](https://textual.textualize.io/)
1214
[![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20Windows-blue)](https://github.com/delexw/claude-code-trace/releases)
1315

1416
**Claude Code Trace** is a **Claude Code session log viewer** for local JSONL files stored in `~/.claude/projects/`.
1517

16-
Browse, tail, and inspect Claude Code conversations in real time. Claude Code Trace renders Claude Code JSONL session files as readable conversations with expandable tool calls, token counts, timestamps, MCP tool call detection, and live log tailing. It also helps you find sessions by user message. It runs as a **native desktop app** for macOS, Linux, and Windows, or a **web app**.
18+
Browse, tail, and inspect Claude Code conversations in real time. Claude Code Trace renders Claude Code JSONL session files as readable conversations with expandable tool calls, token counts, timestamps, MCP tool call detection, and live log tailing. It also helps you find sessions by user message. It runs as a **GUI app** for macOS, Linux, and Windows, a **Web app** or a **TUI**.
1719

1820
Use Claude Code Trace when you want to:
1921

bin/cctrace.mjs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -154,17 +154,19 @@ switch (mode) {
154154
});
155155
}
156156

157-
// Build TUI if needed, wait for backend, then start TUI
158-
execSync("npm run build", { stdio: "inherit", cwd: resolve(root, "tui") });
159-
// wait-for-backend.mjs lives in bin/, not tui/
157+
// Install Python dependencies, wait for backend, then start TUI
158+
execSync("pip install -r requirements.txt --quiet", {
159+
stdio: "inherit",
160+
cwd: resolve(root, "tui-py"),
161+
});
160162
execSync("node wait-for-backend.mjs", {
161163
stdio: "inherit",
162164
cwd: resolve(root, "bin"),
163165
});
164166

165-
const tui = spawn("node", ["dist/tui/src/cli.js"], {
167+
const tui = spawn("python3", ["main.py"], {
166168
stdio: "inherit",
167-
cwd: resolve(root, "tui"),
169+
cwd: resolve(root, "tui-py"),
168170
});
169171

170172
tui.on("exit", (code) => {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"preview": "vite preview",
1313
"tauri": "tauri",
1414
"dev:web": "tauri dev -- -- --web",
15-
"dev:tui": "tauri dev -- -- --headless & cd tui && npm run build && node wait-for-backend.mjs && node dist/tui/src/cli.js",
15+
"dev:tui": "tauri dev -- -- --headless & pip install -r tui-py/requirements.txt -q && node bin/wait-for-backend.mjs && python3 tui-py/main.py",
1616
"lint": "oxlint && cargo clippy --manifest-path src-tauri/Cargo.toml",
1717
"fmt": "oxfmt && cargo fmt --manifest-path src-tauri/Cargo.toml",
1818
"fmt:check": "oxfmt --check && cargo fmt --manifest-path src-tauri/Cargo.toml --check",

specs/00-overview.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ graph TB
2222
subgraph Frontends
2323
TAURI["Tauri Desktop\n(WebView + IPC)"]
2424
WEB["Browser\n(Vite / HTTP)"]
25-
TUI["Terminal UI\n(Ink / React)"]
25+
TUI["Terminal UI\n(Python / Textual)"]
2626
end
2727
2828
JSONL -->|"FS events"| RUST
@@ -44,7 +44,7 @@ graph TB
4444
| HTTP API | `src-tauri/src/http_api.rs` | REST + SSE for browser and TUI |
4545
| Frontend Converter | `src-tauri/src/convert.rs` | Internal → JSON-serialisable display types |
4646
| Web Frontend | `src/` | React components, hooks, keyboard navigation |
47-
| TUI | `tui/` | Ink/React terminal rendering |
47+
| TUI | `tui-py/` | Python / Textual terminal rendering |
4848
| Shared | `shared/` | Types, project tree builder, format helpers |
4949
| CLI Launcher | `bin/cctrace.mjs` | Mode selector (desktop / web / tui / headless) |
5050

@@ -91,7 +91,7 @@ flowchart LR
9191
| 03 | [03-state-management.md](03-state-management.md) | AppState, session cache, SSE broadcast |
9292
| 04 | [04-http-api.md](04-http-api.md) | REST endpoints, SSE contract, Tauri IPC mirror |
9393
| 05 | [05-frontend-web.md](05-frontend-web.md) | React hooks and components (web/desktop) |
94-
| 06 | [06-tui.md](06-tui.md) | Terminal UI (Ink/React), keyboard routing, windowing |
94+
| 06 | [06-tui.md](06-tui.md) | Terminal UI (Python / Textual), shared list base, async populate |
9595
| 07 | [07-data-types.md](07-data-types.md) | Shared TypeScript types, Rust serialisation |
9696
| 08 | [08-session-lifecycle.md](08-session-lifecycle.md) | End-to-end session loading, live update, truncation |
9797
| 09 | [09-subagent-linking.md](09-subagent-linking.md) | Four-phase subagent linking algorithm |

0 commit comments

Comments
 (0)