Skip to content

Commit eacdef9

Browse files
simonwclaude
andcommitted
Show repo first in web session picker and add --repo filter
- Add enrich_sessions_with_repos() to detect repos by fetching session details - Add filter_sessions_by_repo() to filter sessions by repo - Update format_session_for_display() to show repo first, then date, then title - Modify --repo option to filter sessions list (also still sets commit link default) - Add comprehensive tests for new functionality Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 6ee2372 commit eacdef9

File tree

2 files changed

+199
-11
lines changed

2 files changed

+199
-11
lines changed

src/claude_code_transcripts/__init__.py

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,49 @@ def detect_github_repo(loglines):
619619
return None
620620

621621

622+
def enrich_sessions_with_repos(sessions, token, org_uuid, fetch_fn=None):
623+
"""Enrich sessions with repo information by fetching each session's details.
624+
625+
Args:
626+
sessions: List of session dicts from the API
627+
token: API access token
628+
org_uuid: Organization UUID
629+
fetch_fn: Optional function to fetch session data (for testing)
630+
631+
Returns:
632+
List of session dicts with 'repo' key added
633+
"""
634+
if fetch_fn is None:
635+
fetch_fn = fetch_session
636+
637+
enriched = []
638+
for session in sessions:
639+
session_copy = dict(session)
640+
try:
641+
session_data = fetch_fn(token, org_uuid, session["id"])
642+
loglines = session_data.get("loglines", [])
643+
session_copy["repo"] = detect_github_repo(loglines)
644+
except Exception:
645+
session_copy["repo"] = None
646+
enriched.append(session_copy)
647+
return enriched
648+
649+
650+
def filter_sessions_by_repo(sessions, repo):
651+
"""Filter sessions by repo.
652+
653+
Args:
654+
sessions: List of session dicts with 'repo' key
655+
repo: Repo to filter by (owner/name), or None to return all
656+
657+
Returns:
658+
Filtered list of sessions
659+
"""
660+
if repo is None:
661+
return sessions
662+
return [s for s in sessions if s.get("repo") == repo]
663+
664+
622665
def format_json(obj):
623666
try:
624667
if isinstance(obj, str):
@@ -1691,15 +1734,19 @@ def resolve_credentials(token, org_uuid):
16911734
def format_session_for_display(session_data):
16921735
"""Format a session for display in the list or picker.
16931736
1737+
Shows repo first (if available), then date, then title.
16941738
Returns a formatted string.
16951739
"""
1696-
session_id = session_data.get("id", "unknown")
16971740
title = session_data.get("title", "Untitled")
16981741
created_at = session_data.get("created_at", "")
1742+
repo = session_data.get("repo")
16991743
# Truncate title if too long
1700-
if len(title) > 60:
1701-
title = title[:57] + "..."
1702-
return f"{session_id} {created_at[:19] if created_at else 'N/A':19} {title}"
1744+
if len(title) > 50:
1745+
title = title[:47] + "..."
1746+
# Format: repo (or placeholder) date title
1747+
repo_display = repo if repo else "(no repo)"
1748+
date_display = created_at[:19] if created_at else "N/A"
1749+
return f"{repo_display:30} {date_display:19} {title}"
17031750

17041751

17051752
def generate_html_from_session_data(session_data, output_dir, github_repo=None):
@@ -1891,7 +1938,7 @@ def generate_html_from_session_data(session_data, output_dir, github_repo=None):
18911938
)
18921939
@click.option(
18931940
"--repo",
1894-
help="GitHub repo (owner/name) for commit links. Auto-detected from git push output if not specified.",
1941+
help="GitHub repo (owner/name). Filters session list and sets default for commit links.",
18951942
)
18961943
@click.option(
18971944
"--gist",
@@ -1945,16 +1992,21 @@ def web_cmd(
19451992
if not sessions:
19461993
raise click.ClickException("No sessions found.")
19471994

1995+
# Enrich sessions with repo information
1996+
click.echo("Fetching session details to detect repos...")
1997+
sessions = enrich_sessions_with_repos(sessions, token, org_uuid)
1998+
1999+
# Filter by repo if specified
2000+
if repo:
2001+
sessions = filter_sessions_by_repo(sessions, repo)
2002+
if not sessions:
2003+
raise click.ClickException(f"No sessions found for repo: {repo}")
2004+
19482005
# Build choices for questionary
19492006
choices = []
19502007
for s in sessions:
19512008
sid = s.get("id", "unknown")
1952-
title = s.get("title", "Untitled")
1953-
created_at = s.get("created_at", "")
1954-
# Truncate title if too long
1955-
if len(title) > 50:
1956-
title = title[:47] + "..."
1957-
display = f"{created_at[:19] if created_at else 'N/A':19} {title}"
2009+
display = format_session_for_display(s)
19582010
choices.append(questionary.Choice(title=display, value=sid))
19592011

19602012
selected = questionary.select(

tests/test_all.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,3 +530,139 @@ def test_json_command_still_works_with_local_file(self, output_dir):
530530

531531
assert result.exit_code == 0
532532
assert (html_output / "index.html").exists()
533+
534+
535+
class TestWebCommandRepoFiltering:
536+
"""Tests for the web command repo display and filtering."""
537+
538+
def test_detect_github_repo_from_session(self):
539+
"""Test that detect_github_repo extracts repo from session loglines."""
540+
from claude_code_transcripts import detect_github_repo
541+
542+
loglines = [
543+
{
544+
"type": "assistant",
545+
"message": {
546+
"role": "assistant",
547+
"content": [
548+
{
549+
"type": "tool_result",
550+
"content": "remote: Create a pull request for 'my-branch' on GitHub by visiting:\nremote: https://github.com/simonw/datasette/pull/new/my-branch",
551+
}
552+
],
553+
},
554+
}
555+
]
556+
repo = detect_github_repo(loglines)
557+
assert repo == "simonw/datasette"
558+
559+
def test_detect_github_repo_returns_none_when_not_found(self):
560+
"""Test that detect_github_repo returns None when no repo found."""
561+
from claude_code_transcripts import detect_github_repo
562+
563+
loglines = [
564+
{
565+
"type": "user",
566+
"message": {"role": "user", "content": "Hello"},
567+
}
568+
]
569+
repo = detect_github_repo(loglines)
570+
assert repo is None
571+
572+
def test_enrich_sessions_with_repos(self):
573+
"""Test enriching sessions with repo information."""
574+
from claude_code_transcripts import enrich_sessions_with_repos
575+
576+
# Mock sessions from the API list
577+
sessions = [
578+
{"id": "sess1", "title": "Session 1", "created_at": "2025-01-01T10:00:00Z"},
579+
{"id": "sess2", "title": "Session 2", "created_at": "2025-01-02T10:00:00Z"},
580+
]
581+
582+
# Mock fetch function that returns session data with loglines
583+
def mock_fetch(token, org_uuid, session_id):
584+
if session_id == "sess1":
585+
return {
586+
"loglines": [
587+
{
588+
"type": "assistant",
589+
"message": {
590+
"role": "assistant",
591+
"content": [
592+
{
593+
"type": "tool_result",
594+
"content": "https://github.com/simonw/datasette/pull/new/branch",
595+
}
596+
],
597+
},
598+
}
599+
]
600+
}
601+
else:
602+
return {"loglines": []}
603+
604+
enriched = enrich_sessions_with_repos(
605+
sessions, "token", "org", fetch_fn=mock_fetch
606+
)
607+
608+
assert enriched[0]["repo"] == "simonw/datasette"
609+
assert enriched[1]["repo"] is None
610+
611+
def test_filter_sessions_by_repo(self):
612+
"""Test filtering sessions by repo."""
613+
from claude_code_transcripts import filter_sessions_by_repo
614+
615+
sessions = [
616+
{"id": "sess1", "title": "Session 1", "repo": "simonw/datasette"},
617+
{"id": "sess2", "title": "Session 2", "repo": "simonw/llm"},
618+
{"id": "sess3", "title": "Session 3", "repo": None},
619+
]
620+
621+
filtered = filter_sessions_by_repo(sessions, "simonw/datasette")
622+
assert len(filtered) == 1
623+
assert filtered[0]["id"] == "sess1"
624+
625+
def test_filter_sessions_by_repo_none_returns_all(self):
626+
"""Test that filtering with None repo returns all sessions."""
627+
from claude_code_transcripts import filter_sessions_by_repo
628+
629+
sessions = [
630+
{"id": "sess1", "title": "Session 1", "repo": "simonw/datasette"},
631+
{"id": "sess2", "title": "Session 2", "repo": None},
632+
]
633+
634+
filtered = filter_sessions_by_repo(sessions, None)
635+
assert len(filtered) == 2
636+
637+
def test_format_session_for_display_with_repo(self):
638+
"""Test formatting session display with repo first."""
639+
from claude_code_transcripts import format_session_for_display
640+
641+
session = {
642+
"id": "sess1",
643+
"title": "Fix the bug",
644+
"created_at": "2025-01-15T10:30:00.000Z",
645+
"repo": "simonw/datasette",
646+
}
647+
648+
display = format_session_for_display(session)
649+
# Repo should appear first
650+
assert display.startswith("simonw/datasette")
651+
assert "2025-01-15T10:30:00" in display
652+
assert "Fix the bug" in display
653+
654+
def test_format_session_for_display_without_repo(self):
655+
"""Test formatting session display without repo."""
656+
from claude_code_transcripts import format_session_for_display
657+
658+
session = {
659+
"id": "sess1",
660+
"title": "Fix the bug",
661+
"created_at": "2025-01-15T10:30:00.000Z",
662+
"repo": None,
663+
}
664+
665+
display = format_session_for_display(session)
666+
# Should show (no repo) placeholder
667+
assert "(no repo)" in display
668+
assert "Fix the bug" in display

0 commit comments

Comments
 (0)