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
57 changes: 48 additions & 9 deletions tools/ways-cli/src/cmd/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,41 @@ pub struct ContextInfo {
}

/// Get context info for the current session. Used by `ways context` and `ways list`.
///
/// When `session_id` is provided, the transcript is located by scanning
/// `~/.claude/projects/*/<session_id>.jsonl` — this is robust against
/// cwd/project mismatches (e.g. a session rooted in `~/.claude` while the
/// shell cwd is elsewhere). Falls back to `project_dir` + newest-transcript
/// lookup when no session id is given.
pub fn get_context(project_dir: Option<&str>) -> Result<ContextInfo> {
let project = project_dir
.map(|s| s.to_string())
.or_else(|| std::env::var("CLAUDE_PROJECT_DIR").ok())
.or_else(detect_project_dir)
.unwrap_or_else(|| ".".to_string());
get_context_inner(project_dir, None)
}

/// Like `get_context`, but pinned to a known session id. Locates the
/// transcript by session id across all project dirs rather than guessing
/// the project from cwd.
pub fn get_context_for_session(session_id: &str) -> Result<ContextInfo> {
get_context_inner(None, Some(session_id))
}

let project_slug = project.replace(['/', '.'], "-");
let conv_dir = home_dir().join(format!(".claude/projects/{project_slug}"));
fn get_context_inner(project_dir: Option<&str>, session_id: Option<&str>) -> Result<ContextInfo> {
let transcript = if let Some(sid) = session_id {
find_transcript_by_session(sid).ok_or_else(|| {
anyhow::anyhow!("No transcript found for session: {sid}")
})?
} else {
let project = project_dir
.map(|s| s.to_string())
.or_else(|| std::env::var("CLAUDE_PROJECT_DIR").ok())
.or_else(detect_project_dir)
.unwrap_or_else(|| ".".to_string());

let project_slug = project.replace(['/', '.'], "-");
let conv_dir = home_dir().join(format!(".claude/projects/{project_slug}"));

let transcript = find_newest_transcript(&conv_dir)
.ok_or_else(|| anyhow::anyhow!("No active transcript found for project: {project}"))?;
find_newest_transcript(&conv_dir)
.ok_or_else(|| anyhow::anyhow!("No active transcript found for project: {project}"))?
};

let session = transcript
.file_stem()
Expand Down Expand Up @@ -222,6 +245,22 @@ fn read_token_usage(content: &str) -> (u64, String) {
(estimated, "bytes".to_string())
}

/// Find a transcript by session id, searching every project dir under
/// `~/.claude/projects/`. Session ids are globally unique, so we don't
/// need to know which project the session is rooted in.
fn find_transcript_by_session(session_id: &str) -> Option<PathBuf> {
let projects_root = home_dir().join(".claude/projects");
let filename = format!("{session_id}.jsonl");
for entry in std::fs::read_dir(&projects_root).ok()? {
let entry = entry.ok()?;
let candidate = entry.path().join(&filename);
if candidate.is_file() {
return Some(candidate);
}
}
None
}

fn find_newest_transcript(dir: &Path) -> Option<PathBuf> {
if !dir.is_dir() {
return None;
Expand Down
8 changes: 6 additions & 2 deletions tools/ways-cli/src/cmd/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,12 @@ pub fn run(session: Option<&str>, sort: &str, json_out: bool) -> Result<()> {

let current_epoch = session::get_epoch(&session_id);

// Use accurate context data from transcript when available
let (current_tokens_k, context_window_k, context_window) = match context::get_context(None) {
// Use accurate context data from transcript when available.
// Pinning to session_id keeps detection correct when cwd is outside
// the session's project (e.g. a session rooted in ~/.claude while the
// shell has cd'd elsewhere) — otherwise the cwd-walk can land on
// ~/.claude (global config) and miss the real transcript.
let (current_tokens_k, context_window_k, context_window) = match context::get_context_for_session(&session_id) {
Ok(ctx) => (ctx.tokens_used / 1000, ctx.tokens_total / 1000, ctx.tokens_total),
Err(_) => {
// Fallback to session markers
Expand Down
Loading