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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "continue-transcripts"
version = "0.10.0"
version = "0.11.0"
edition = "2021"
description = "Convert continue.dev session files to readable HTML transcripts"
license = "MIT"
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Install globally so the `continue-transcripts` command is always available:
```sh
uv tool install continue-transcripts \
--no-index \
--find-links https://github.com/curtisalexander/continue-transcripts/releases/expanded_assets/v0.10.0
--find-links https://github.com/curtisalexander/continue-transcripts/releases/expanded_assets/v0.11.0
```

The `continue-transcripts` command is then available on your `PATH`.
Expand All @@ -83,7 +83,7 @@ To upgrade later (update the version in the URL):
```sh
uv tool install --upgrade continue-transcripts \
--no-index \
--find-links https://github.com/curtisalexander/continue-transcripts/releases/expanded_assets/v0.10.0
--find-links https://github.com/curtisalexander/continue-transcripts/releases/expanded_assets/v0.11.0
```

To uninstall:
Expand All @@ -100,7 +100,7 @@ Run without installing:
uvx \
--no-index \
--from continue-transcripts \
--find-links https://github.com/curtisalexander/continue-transcripts/releases/expanded_assets/v0.10.0 \
--find-links https://github.com/curtisalexander/continue-transcripts/releases/expanded_assets/v0.11.0 \
continue-transcripts ./sessions
```

Expand All @@ -115,7 +115,7 @@ uv venv .venv
source .venv/bin/activate
uv pip install continue-transcripts \
--no-index \
--find-links https://github.com/curtisalexander/continue-transcripts/releases/expanded_assets/v0.10.0
--find-links https://github.com/curtisalexander/continue-transcripts/releases/expanded_assets/v0.11.0
```

### Building from source
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "maturin"

[project]
name = "continue-transcripts"
version = "0.10.0"
version = "0.11.0"
description = "Convert continue.dev session files to readable HTML transcripts"
readme = "README.md"
license = { text = "MIT" }
Expand Down
2 changes: 1 addition & 1 deletion python/continue_transcripts/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""continue-transcripts - Convert continue.dev session files to readable HTML transcripts."""

__version__ = "0.10.0"
__version__ = "0.11.0"
60 changes: 60 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use html_escape::encode_text;
use pulldown_cmark::{CodeBlockKind, Event, Options, Parser as MdParser, Tag, TagEnd};
use rayon::prelude::*;
use serde::Deserialize;
use std::collections::HashMap;
use std::fs;
use std::io::{BufWriter, Write};
use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -2809,13 +2810,60 @@ fn discover_session_files(path: &Path) -> Vec<PathBuf> {
// Recursively find .json files
let pattern = format!("{}/**/*.json", path.display());
for p in glob::glob(&pattern).expect("Failed to read glob pattern").flatten() {
// Skip sessions.json — it contains session metadata, not a session transcript
if p.file_name().map_or(false, |n| n == "sessions.json") {
continue;
}
files.push(p);
}
}
files.sort();
files
}

/// Metadata entry from sessions.json.
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct SessionMeta {
#[serde(default)]
session_id: String,
/// dateCreated in sessions.json is a Unix timestamp (seconds since epoch).
#[serde(default)]
date_created: Option<f64>,
}

/// Load sessions.json from a directory and return a map of session_id → ISO 8601 date string.
/// Returns an empty map if the file doesn't exist or can't be parsed.
fn load_sessions_metadata(dir: &Path) -> HashMap<String, String> {
let meta_path = dir.join("sessions.json");
let raw = match fs::read_to_string(&meta_path) {
Ok(s) => s,
Err(_) => return HashMap::new(),
};

let entries: Vec<SessionMeta> = match serde_json::from_str(&raw) {
Ok(v) => v,
Err(_) => return HashMap::new(),
};

let mut map = HashMap::new();
for entry in entries {
if entry.session_id.is_empty() {
continue;
}
if let Some(ts) = entry.date_created {
let secs = ts as i64;
if let Some(dt) = chrono::DateTime::from_timestamp(secs, 0) {
map.insert(
entry.session_id,
dt.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
);
}
}
}
map
}

/// Parse a date string (YYYY-MM-DD) into a chrono NaiveDate.
fn parse_date_filter(s: &str) -> Option<chrono::NaiveDate> {
chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d").ok()
Expand Down Expand Up @@ -3088,6 +3136,13 @@ fn main() {
let files_discovered = files.len();
eprintln!("Discovered {} JSON file(s)", files_discovered);

// Load sessions.json metadata for date fallback (only when input is a directory)
let sessions_meta = if input.is_dir() {
load_sessions_metadata(input)
} else {
HashMap::new()
};

fs::create_dir_all(output_dir).expect("Failed to create output directory");

// Process files in parallel: read, parse, filter, and render HTML concurrently
Expand Down Expand Up @@ -3171,6 +3226,7 @@ fn main() {
let date = session
.date_created
.clone()
.or_else(|| sessions_meta.get(&session.session_id).cloned())
.or_else(|| file_modified_date(file.as_path()))
.unwrap_or_default();

Expand Down Expand Up @@ -3258,6 +3314,10 @@ fn main() {
}
}

// Sort index entries by date descending (newest first).
// Dates are ISO 8601 strings, so lexicographic sort works correctly.
index_entries.sort_by(|a, b| b.2.cmp(&a.2));

// Generate index.html (also skip if unchanged)
let mut index_written = false;
if !index_entries.is_empty() {
Expand Down