Skip to content

Commit 3f3d42c

Browse files
committed
Prioritize CC sessions from CWD and display them first
1 parent 854a4f8 commit 3f3d42c

File tree

2 files changed

+274
-25
lines changed

2 files changed

+274
-25
lines changed

src/claude_code_transcripts/__init__.py

Lines changed: 123 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Convert Claude Code session JSON to a clean mobile-friendly HTML page with pagination."""
22

3-
import json
43
import html
4+
import json
55
import os
66
import platform
77
import re
@@ -13,11 +13,11 @@
1313
from pathlib import Path
1414

1515
import click
16-
from click_default_group import DefaultGroup
1716
import httpx
18-
from jinja2 import Environment, PackageLoader
1917
import markdown
2018
import questionary
19+
from click_default_group import DefaultGroup
20+
from jinja2 import Environment, PackageLoader
2121

2222
# Set up Jinja2 environment
2323
_jinja_env = Environment(
@@ -158,6 +158,20 @@ def _get_jsonl_summary(filepath, max_length=200):
158158
return "(no summary)"
159159

160160

161+
def _should_include_session(filepath, include_agents=False):
162+
"""Check if a session file should be included in listings.
163+
164+
Returns (True, summary) if the session should be included,
165+
(False, None) if it should be skipped.
166+
"""
167+
if not include_agents and filepath.name.startswith("agent-"):
168+
return False, None
169+
summary = get_session_summary(filepath)
170+
if summary.lower() == "warmup" or summary == "(no summary)":
171+
return False, None
172+
return True, summary
173+
174+
161175
def find_local_sessions(folder, limit=10):
162176
"""Find recent JSONL session files in the given folder.
163177
@@ -170,11 +184,8 @@ def find_local_sessions(folder, limit=10):
170184

171185
results = []
172186
for f in folder.glob("**/*.jsonl"):
173-
if f.name.startswith("agent-"):
174-
continue
175-
summary = get_session_summary(f)
176-
# Skip boring/empty sessions
177-
if summary.lower() == "warmup" or summary == "(no summary)":
187+
include, summary = _should_include_session(f)
188+
if not include:
178189
continue
179190
results.append((f, summary))
180191

@@ -183,6 +194,52 @@ def find_local_sessions(folder, limit=10):
183194
return results[:limit]
184195

185196

197+
def find_sessions_for_project(projects_folder, project_path, limit=10):
198+
"""Find sessions for a specific project directory.
199+
200+
Returns (sessions_list, project_folder_exists) where:
201+
- sessions_list: list of (Path, summary) tuples sorted by modification time
202+
- project_folder_exists: True if the project folder exists, False otherwise
203+
"""
204+
projects_folder = Path(projects_folder)
205+
encoded = encode_path_to_folder_name(project_path)
206+
project_folder = projects_folder / encoded
207+
208+
if not project_folder.exists():
209+
return [], False
210+
211+
results = []
212+
for f in project_folder.glob("*.jsonl"):
213+
include, summary = _should_include_session(f)
214+
if not include:
215+
continue
216+
results.append((f, summary))
217+
218+
results.sort(key=lambda x: x[0].stat().st_mtime, reverse=True)
219+
return results[:limit], True
220+
221+
222+
def find_sessions_excluding_project(projects_folder, exclude_project_path, limit=10):
223+
"""Find recent sessions from all projects except the specified one.
224+
225+
Returns a list of (Path, summary) tuples sorted by modification time.
226+
"""
227+
projects_folder = Path(projects_folder)
228+
exclude_encoded = encode_path_to_folder_name(exclude_project_path)
229+
230+
results = []
231+
for f in projects_folder.glob("**/*.jsonl"):
232+
if f.parent.name == exclude_encoded:
233+
continue
234+
include, summary = _should_include_session(f)
235+
if not include:
236+
continue
237+
results.append((f, summary))
238+
239+
results.sort(key=lambda x: x[0].stat().st_mtime, reverse=True)
240+
return results[:limit]
241+
242+
186243
def get_project_display_name(folder_name):
187244
"""Convert encoded folder name to readable project name.
188245
@@ -242,6 +299,29 @@ def get_project_display_name(folder_name):
242299
return folder_name
243300

244301

302+
def encode_path_to_folder_name(path):
303+
"""Encode a filesystem path to Claude's project folder naming convention.
304+
305+
This is the inverse of how Claude Code stores project folders:
306+
- /Users/foo/bar -> -Users-foo-bar
307+
- /Users/foo/.hidden -> -Users-foo--hidden
308+
309+
Note: This is NOT the inverse of get_project_display_name(), which is
310+
a one-way beautifier that extracts meaningful project names for display.
311+
"""
312+
# Convert to absolute path but don't resolve symlinks
313+
# (resolve() doesn't work well with non-existent paths)
314+
p = Path(path)
315+
if not p.is_absolute():
316+
p = Path.cwd() / p
317+
path = str(p)
318+
# Hidden directories: /. becomes --
319+
encoded = path.replace("/.", "--")
320+
# All other slashes become dashes
321+
encoded = encoded.replace("/", "-")
322+
return encoded
323+
324+
245325
def find_all_sessions(folder, include_agents=False):
246326
"""Find all sessions in a Claude projects folder, grouped by project.
247327
@@ -260,13 +340,8 @@ def find_all_sessions(folder, include_agents=False):
260340
projects = {}
261341

262342
for session_file in folder.glob("**/*.jsonl"):
263-
# Skip agent files unless requested
264-
if not include_agents and session_file.name.startswith("agent-"):
265-
continue
266-
267-
# Get summary and skip boring sessions
268-
summary = get_session_summary(session_file)
269-
if summary.lower() == "warmup" or summary == "(no summary)":
343+
include, summary = _should_include_session(session_file, include_agents)
344+
if not include:
270345
continue
271346

272347
# Get project folder
@@ -1432,15 +1507,17 @@ def local_cmd(output, output_auto, repo, gist, include_json, open_browser, limit
14321507
return
14331508

14341509
click.echo("Loading local sessions...")
1435-
results = find_local_sessions(projects_folder, limit=limit)
1510+
cwd = os.getcwd()
14361511

1437-
if not results:
1438-
click.echo("No local sessions found.")
1439-
return
1512+
current_sessions, project_exists = find_sessions_for_project(
1513+
projects_folder, cwd, limit=limit
1514+
)
1515+
1516+
other_sessions = find_sessions_excluding_project(projects_folder, cwd, limit=limit)
14401517

1441-
# Build choices for questionary
14421518
choices = []
1443-
for filepath, summary in results:
1519+
1520+
def format_session(filepath, summary, include_project=False):
14441521
stat = filepath.stat()
14451522
mod_time = datetime.fromtimestamp(stat.st_mtime)
14461523
size_kb = stat.st_size / 1024
@@ -1449,7 +1526,31 @@ def local_cmd(output, output_auto, repo, gist, include_json, open_browser, limit
14491526
if len(summary) > 50:
14501527
summary = summary[:47] + "..."
14511528
display = f"{date_str} {size_kb:5.0f} KB {summary}"
1452-
choices.append(questionary.Choice(title=display, value=filepath))
1529+
if include_project:
1530+
project_name = get_project_display_name(filepath.parent.name)
1531+
display = f"{display} [{project_name}]"
1532+
return display
1533+
1534+
if current_sessions:
1535+
choices.append(questionary.Separator("── Current Project ──"))
1536+
for filepath, summary in current_sessions:
1537+
display = format_session(filepath, summary, include_project=False)
1538+
choices.append(questionary.Choice(title=display, value=filepath))
1539+
elif project_exists:
1540+
choices.append(questionary.Separator("── Current Project ──"))
1541+
choices.append(questionary.Separator(" (no sessions found)"))
1542+
else:
1543+
choices.append(questionary.Separator("── No sessions for this project ──"))
1544+
1545+
if other_sessions:
1546+
choices.append(questionary.Separator("── Other Projects ──"))
1547+
for filepath, summary in other_sessions:
1548+
display = format_session(filepath, summary, include_project=True)
1549+
choices.append(questionary.Choice(title=display, value=filepath))
1550+
1551+
if not current_sessions and not other_sessions:
1552+
click.echo("No local sessions found.")
1553+
return
14531554

14541555
selected = questionary.select(
14551556
"Select a session to convert:",

0 commit comments

Comments
 (0)