Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions claude_code_log/tui.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
Tree,
)
from textual.reactive import reactive
from rich.markup import escape as escape_markup

from .cache import CacheManager, SessionCacheData, get_library_version
from .converter import (
Expand Down Expand Up @@ -1463,14 +1464,16 @@ def populate_table(self) -> None:
token_display = f"{total_tokens:,}" if total_tokens > 0 else "-"

# Get summary or first user message
preview = (
# Escape Rich markup to prevent MarkupError from square brackets
# in paths like [/Users/foo/bar] being parsed as closing tags
preview = escape_markup(
session_data.summary
or session_data.first_user_message
or "No preview available"
)
# Add [ARCHIVED] indicator for archived sessions
if is_archived:
preview = f"[ARCHIVED] {preview}"
preview = f"\\[ARCHIVED] {preview}"

table.add_row(
session_id[:8],
Expand Down
52 changes: 52 additions & 0 deletions test/test_tui.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,58 @@ async def test_populate_table(self, temp_project_dir):
# Check that columns exist (column access varies in Textual versions)
assert table.row_count == 2

@pytest.mark.asyncio
async def test_populate_table_with_bracket_content(self, temp_project_dir):
"""Test that session previews containing square brackets don't cause MarkupError.

Rich interprets square brackets as markup tags, so content like
'[/Users/foo/bar]' would be parsed as a closing tag and raise MarkupError.
Session summaries and first messages must be escaped before display.
"""
app = SessionBrowser(temp_project_dir)

# Use session IDs matching the JSONL files in temp_project_dir fixture
mock_session_data = {
"session-123": SessionCacheData(
session_id="session-123",
summary=None,
first_timestamp="2025-01-01T10:00:00Z",
last_timestamp="2025-01-01T10:01:00Z",
message_count=5,
first_user_message="10:06:02.383: [/Users/guowang/PycharmProjects/h5st_student] \ngit -c credential.helper=",
total_input_tokens=100,
total_output_tokens=200,
cwd="/test/project",
),
"session-456": SessionCacheData(
session_id="session-456",
summary="Working on [feature-branch] implementation",
first_timestamp="2025-01-02T14:30:00Z",
last_timestamp="2025-01-02T14:30:00Z",
message_count=3,
first_user_message="Normal message",
total_input_tokens=50,
total_output_tokens=75,
cwd="/test/other",
),
}

with (
patch.object(app.cache_manager, "get_cached_project_data") as mock_cache,
patch.object(app.cache_manager, "get_modified_files") as mock_modified,
):
mock_cache.return_value = Mock(
sessions=mock_session_data, working_directories=[str(temp_project_dir)]
)
mock_modified.return_value = []

async with app.run_test() as pilot:
await pilot.pause(0.1)

# Should not raise MarkupError - table should render successfully
table = cast(DataTable, app.query_one("#sessions-table"))
assert table.row_count == 2

@pytest.mark.asyncio
async def test_row_selection(self, temp_project_dir):
"""Test selecting a row in the sessions table."""
Expand Down
Loading