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
103 changes: 97 additions & 6 deletions src-tauri/crates/orgtrack-core/src/sources/claude_code/history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -400,16 +400,33 @@ fn load_claude_turn_range(
start_offset: u64,
end_offset: u64,
turn_id: &str,
) -> Result<Vec<ActivityChunk>, String> {
load_claude_turn_range_with_sequence(
file,
session_id,
start_offset,
end_offset,
usize::try_from(start_offset).unwrap_or(usize::MAX),
Some(turn_id),
)
}

fn load_claude_turn_range_with_sequence(
file: &mut fs::File,
session_id: &str,
start_offset: u64,
end_offset: u64,
start_sequence: usize,
forced_first_user_id: Option<&str>,
) -> Result<Vec<ActivityChunk>, String> {
file.seek(SeekFrom::Start(start_offset))
.map_err(|err| format!("Failed to seek Claude history: {err}"))?;
let take = file.take(end_offset.saturating_sub(start_offset));
let start_sequence = usize::try_from(start_offset).unwrap_or(usize::MAX);
load_claude_code_history_from_reader(
session_id,
BufReader::new(take),
start_sequence,
Some(turn_id),
forced_first_user_id,
)
}

Expand All @@ -427,13 +444,13 @@ pub fn load_claude_code_initial_window_for_session(
});
}

let file_len = fs::metadata(&path)
let file_len = fs::metadata(path.as_path())
.map_err(|err| format!("Failed to stat Claude history {}: {err}", path.display()))?
.len();
let first_loaded_turn = indexed
.len()
.saturating_sub(recent_turn_count.max(1).min(indexed.len()));
let mut file = fs::File::open(&path)
let mut file = fs::File::open(path.as_path())
.map_err(|err| format!("Failed to open Claude history {}: {err}", path.display()))?;
let mut chunks = Vec::with_capacity(indexed.len().saturating_mul(2));
for (index, turn) in indexed.iter().enumerate() {
Expand Down Expand Up @@ -475,15 +492,15 @@ pub fn load_claude_code_turn_windows_for_session(
let file_stem = claude_file_stem_from_session_id(session_id)?;
let path = resolve_claude_session_path(conn, file_stem)?;
let indexed = index_claude_user_turns(session_id, &path)?;
let file_len = fs::metadata(&path)
let file_len = fs::metadata(path.as_path())
.map_err(|err| format!("Failed to stat Claude history {}: {err}", path.display()))?
.len();
let positions = indexed
.iter()
.enumerate()
.map(|(index, turn)| (turn.start_offset, index))
.collect::<HashMap<_, _>>();
let mut file = fs::File::open(&path)
let mut file = fs::File::open(path.as_path())
.map_err(|err| format!("Failed to open Claude history {}: {err}", path.display()))?;

turn_ids
Expand Down Expand Up @@ -518,6 +535,68 @@ pub fn load_claude_code_turn_windows_for_session(
.collect()
}

pub fn load_claude_code_cloud_turn_windows_for_session(
conn: &Connection,
session_id: &str,
turn_ids: &[String],
start_sequence: usize,
) -> Result<Vec<imported_history::window::ImportedHistoryTurnWindow>, String> {
let file_stem = claude_file_stem_from_session_id(session_id)?;
let path = resolve_claude_session_path(conn, file_stem)?;
load_claude_code_cloud_turn_windows_from_path(session_id, &path, turn_ids, start_sequence)
}

fn load_claude_code_cloud_turn_windows_from_path(
session_id: &str,
path: &Path,
turn_ids: &[String],
start_sequence: usize,
) -> Result<Vec<imported_history::window::ImportedHistoryTurnWindow>, String> {
let file_len = fs::metadata(path)
.map_err(|err| format!("Failed to stat Claude history {}: {err}", path.display()))?
.len();
let offsets = turn_ids
.iter()
.map(|turn_id| {
claude_window_turn_offset(turn_id)
.ok_or_else(|| format!("Invalid Claude cloud turn id: {turn_id}"))
})
.collect::<Result<Vec<_>, _>>()?;
if offsets
.windows(2)
.any(|pair| pair[0] >= pair[1] || pair[1] >= file_len)
|| offsets.first().is_some_and(|offset| *offset >= file_len)
{
return Err("Claude cloud turn offsets are out of order or out of bounds".to_string());
}
let mut file = fs::File::open(path)
.map_err(|err| format!("Failed to open Claude history {}: {err}", path.display()))?;
let mut next_sequence = start_sequence;

turn_ids
.iter()
.enumerate()
.map(|(index, turn_id)| {
let offset = offsets[index];
let end_offset = offsets.get(index + 1).copied().unwrap_or(file_len);
let chunks = load_claude_turn_range_with_sequence(
&mut file,
session_id,
offset,
end_offset,
next_sequence,
None,
)?;
next_sequence = next_sequence.saturating_add(chunks.len());
Ok(imported_history::window::ImportedHistoryTurnWindow {
loaded_event_count: chunks.len(),
chunks,
turn_id: turn_id.clone(),
})
})
.collect()
}

pub fn load_claude_code_turn_index_for_session(
conn: &Connection,
session_id: &str,
Expand All @@ -534,6 +613,18 @@ pub fn load_claude_code_turn_index_for_session(
Ok(projected)
}

pub fn load_claude_code_turn_ids_for_session(
conn: &Connection,
session_id: &str,
) -> Result<Vec<String>, String> {
let file_stem = claude_file_stem_from_session_id(session_id)?;
let path = resolve_claude_session_path(conn, file_stem)?;
Ok(index_claude_user_turns(session_id, &path)?
.into_iter()
.map(|turn| turn.user_chunk.chunk_id)
.collect())
}

/// Cheap freshness probe for one session's transcript: `(mtime_ms, size_bytes)`.
/// Auto-refresh callers compare it against the previous probe and skip the
/// full read/parse/merge pipeline when the source file has not changed —
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,23 @@ fn byte_index_discovers_rounds_without_parsing_tool_result_bodies() {
assert!(!rendered.contains("third"));
assert!(!rendered.contains(&large_output));

let full = load_claude_code_history_from_path("claudecodeapp-window", &path)
.expect("load full transcript");
let turn_ids = indexed
.iter()
.map(|turn| claude_window_turn_id(turn.start_offset))
.collect::<Vec<_>>();
let cloud =
load_claude_code_cloud_turn_windows_from_path("claudecodeapp-window", &path, &turn_ids, 0)
.expect("load exact cloud turns")
.into_iter()
.flat_map(|window| window.chunks)
.collect::<Vec<_>>();
assert_eq!(
serde_json::to_value(cloud).expect("serialize cloud chunks"),
serde_json::to_value(full).expect("serialize full chunks")
);

// Body-size surrogate: round 1 is followed by tool_use + tool_result +
// text (3 lines); rounds 2 and 3 by one assistant line each. Placeholder
// rounds surface these as bodyEventCount — without them the flat-view
Expand Down
25 changes: 23 additions & 2 deletions src-tauri/crates/orgtrack-core/src/sources/codex/app/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ use super::meta::{
resume_codex_session_meta_with_title, session_meta_to_cache_input, CodexSessionMetaParse,
};
use super::transcript::{
load_codex_app_from_path, load_codex_app_initial_window_from_path,
load_codex_app_turn_from_path, CodexAppInitialWindow, CodexAppTurnWindow,
load_codex_app_cloud_turn_from_path, load_codex_app_from_path,
load_codex_app_initial_window_from_path, load_codex_app_turn_from_path,
load_codex_app_turn_ids_from_path, CodexAppInitialWindow, CodexAppTurnWindow,
};
use super::{
CodexAppRecentPath, CodexAppSessionPage, CodexAppSourceMetadata,
Expand Down Expand Up @@ -116,6 +117,26 @@ pub fn load_codex_app_turn_for_session(
Ok(window)
}

pub fn load_codex_app_turn_ids_for_session(
conn: &Connection,
session_id: &str,
) -> Result<Vec<String>, String> {
let file_stem = codex_file_stem_from_session_id(session_id)?;
let path = resolve_codex_session_path(conn, file_stem)?;
load_codex_app_turn_ids_from_path(&path)
}

pub fn load_codex_app_cloud_turn_for_session(
conn: &Connection,
session_id: &str,
turn_id: &str,
start_sequence: usize,
) -> Result<Vec<ActivityChunk>, String> {
let file_stem = codex_file_stem_from_session_id(session_id)?;
let path = resolve_codex_session_path(conn, file_stem)?;
load_codex_app_cloud_turn_from_path(session_id, &path, turn_id, start_sequence)
}

#[derive(Debug, Clone)]
struct CodexChildSessionLink {
session_id: String,
Expand Down
5 changes: 3 additions & 2 deletions src-tauri/crates/orgtrack-core/src/sources/codex/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ mod transcript;
pub use index::{
codex_thread_id_from_file_stem, list_codex_app_recent_paths,
list_codex_app_reconciliation_sessions, list_codex_app_sessions_paginated,
load_codex_app_for_session, load_codex_app_initial_window_for_session,
load_codex_app_turn_for_session,
load_codex_app_cloud_turn_for_session, load_codex_app_for_session,
load_codex_app_initial_window_for_session, load_codex_app_turn_for_session,
load_codex_app_turn_ids_for_session,
};
pub use meta::{resolve_codex_transcript_for_thread_id_near_path, CodexTranscriptLocator};
pub(crate) use normalize::normalize_codex_tool_calls;
Expand Down
Loading
Loading