Skip to content
Open
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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

All notable changes to the Toolpath workspace are documented here.

## Fix: opencode's trait path now honors the configured resolver — 2026-07-08

- **`toolpath-opencode`** (0.5.1): `ConversationProvider::load_conversation`
called `to_view(&s)`, which builds a fresh default `PathResolver::new()`
instead of using the provider's own configured resolver
(`self.resolver()`). With any non-default opencode data directory (e.g.
`$XDG_DATA_HOME` overridden, or a resolver built via
`OpencodeConvo::with_resolver` for testing or a non-standard install),
the snapshot git repo was never found via the trait entrypoint, so every
file diff silently degraded to the structural-only tool-input fallback
(no `raw` perspective). Fixed by routing through
`to_view_with_resolver(&s, self.resolver())`. `list_conversations` and
`list_metadata` don't build views, so they weren't affected.

## Derive: resolve duplicate step ids — 2026-07-01

- **`toolpath-convo`** (0.11.1): `derive_path` now guarantees the derived
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ Tests live alongside the code (`#[cfg(test)] mod tests`), plus `path-cli` has in
- `toolpath-claude`: 229 unit + 18 integration + 6 doc tests (path resolution, conversation reading, query, chaining, watcher, derive, metadata first-user-message, group_id grouping + once-per-message usage totals)
- `toolpath-gemini`: 161 unit + 29 integration + 5 doc tests (path resolution, chat-file parsing, query, watcher, derive, provider, round-trip fidelity, thoughts-folded-into-output + reasoning breakdown round-trip)
- `toolpath-codex`: 80 unit + 51 integration + 2 doc tests (rollout parsing, provider assembly, patch-fidelity derive, real-session fixture, source→path fidelity invariants, JSON wire-level round-trip, per-turn token deltas from cumulative counters, reasoning breakdown)
- `toolpath-opencode`: 52 unit + 19 integration + 1 doc test (SQLite reader, JSON payload serde, provider assembly, snapshot-based derive, tool-input fallback for gitignored paths, reasoning breakdown)
- `toolpath-opencode`: 53 unit + 19 integration + 1 doc test (SQLite reader, JSON payload serde, provider assembly, snapshot-based derive, tool-input fallback for gitignored paths, reasoning breakdown, trait-path resolver configuration)
- `toolpath-cursor`: 78 unit + 8 integration round-trip + 1 real-DB sanity + 1 doc test (state.vscdb SQLite reader, bubble store + composer header parsing, content-addressed blob lookup, projector with full TOOL_TABLE coverage, JSONL transcript ingest in `examples/dump_fixture.rs`)
- `toolpath-pi`: 133 unit + 26 integration + 5 doc tests (types, paths, error, reader, io, provider)
- `toolpath-dot`: 30 unit + 2 doc tests (render, visual conventions, escaping)
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ toolpath-git = { version = "0.6.0", path = "crates/toolpath-git" }
toolpath-claude = { version = "0.12.0", path = "crates/toolpath-claude", default-features = false }
toolpath-gemini = { version = "0.6.0", path = "crates/toolpath-gemini", default-features = false }
toolpath-codex = { version = "0.6.0", path = "crates/toolpath-codex" }
toolpath-opencode = { version = "0.5.0", path = "crates/toolpath-opencode" }
toolpath-opencode = { version = "0.5.1", path = "crates/toolpath-opencode" }
toolpath-cursor = { version = "0.2.0", path = "crates/toolpath-cursor" }
toolpath-github = { version = "0.6.0", path = "crates/toolpath-github" }
toolpath-dot = { version = "0.5.0", path = "crates/toolpath-dot" }
Expand Down
2 changes: 1 addition & 1 deletion crates/toolpath-opencode/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "toolpath-opencode"
version = "0.5.0"
version = "0.5.1"
edition.workspace = true
license.workspace = true
repository = "https://github.com/empathic/toolpath"
Expand Down
97 changes: 96 additions & 1 deletion crates/toolpath-opencode/src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -726,7 +726,7 @@ impl ConversationProvider for OpencodeConvo {
let s = self
.read_session(conversation_id)
.map_err(|e| ConvoTraitError::Provider(e.to_string()))?;
Ok(to_view(&s))
Ok(to_view_with_resolver(&s, self.resolver()))
}

fn load_metadata(
Expand Down Expand Up @@ -886,6 +886,7 @@ mod tests {
use super::*;
use rusqlite::Connection;
use std::fs;
use std::path::Path;
use tempfile::TempDir;

fn setup(body_sql: &str) -> (TempDir, OpencodeConvo) {
Expand Down Expand Up @@ -1152,4 +1153,98 @@ mod tests {
let v = ConversationProvider::load_conversation(&mgr, "", "ses_x").unwrap();
assert_eq!(v.turns.len(), 2);
}

/// Build a bare snapshot git repo for `(project_id, worktree)` with two
/// commits — an empty tree ("before") and a tree containing
/// `main.cpp` ("after") — and return their commit SHAs. Mirrors how
/// opencode's real snapshot repos are laid out (see `paths.rs`).
fn build_snapshot_repo(
resolver: &PathResolver,
project_id: &str,
worktree: &std::path::Path,
) -> (String, String) {
let gitdir = resolver.snapshot_gitdir(project_id, worktree).unwrap();
fs::create_dir_all(&gitdir).unwrap();
let repo = git2::Repository::init_bare(&gitdir).unwrap();
let sig = git2::Signature::now("test", "test@example.com").unwrap();

let empty_tree_id = repo.treebuilder(None).unwrap().write().unwrap();
let empty_tree = repo.find_tree(empty_tree_id).unwrap();
let snap_a = repo
.commit(None, &sig, &sig, "snap_a", &empty_tree, &[])
.unwrap();

let blob_id = repo.blob(b"int main(){}\n").unwrap();
let mut tb = repo.treebuilder(None).unwrap();
tb.insert("main.cpp", blob_id, 0o100_644).unwrap();
let tree_b = repo.find_tree(tb.write().unwrap()).unwrap();
let snap_b = repo
.commit(None, &sig, &sig, "snap_b", &tree_b, &[])
.unwrap();

(snap_a.to_string(), snap_b.to_string())
}

#[test]
fn trait_load_conversation_uses_configured_resolver_for_snapshots() {
let temp = TempDir::new().unwrap();
let data = temp.path().join(".local/share/opencode");
fs::create_dir_all(&data).unwrap();
let resolver = PathResolver::new()
.with_home(temp.path())
.with_data_dir(&data);

// BASIC_SQL's session lives at project "proj" / worktree "/tmp/proj".
let (snap_a, snap_b) = build_snapshot_repo(&resolver, "proj", Path::new("/tmp/proj"));
let sql = BASIC_SQL
.replace("snap_a", &snap_a)
.replace("snap_b", &snap_b);

let conn = Connection::open(data.join("opencode.db")).unwrap();
conn.execute_batch(&format!(
r#"
CREATE TABLE project (
id text PRIMARY KEY, worktree text NOT NULL, vcs text, name text,
icon_url text, icon_color text,
time_created integer NOT NULL, time_updated integer NOT NULL,
time_initialized integer, sandboxes text NOT NULL, commands text
);
CREATE TABLE session (
id text PRIMARY KEY, project_id text NOT NULL, parent_id text,
slug text NOT NULL, directory text NOT NULL, title text NOT NULL,
version text NOT NULL, share_url text,
summary_additions integer, summary_deletions integer,
summary_files integer, summary_diffs text, revert text, permission text,
time_created integer NOT NULL, time_updated integer NOT NULL,
time_compacting integer, time_archived integer, workspace_id text
);
CREATE TABLE message (
id text PRIMARY KEY, session_id text NOT NULL,
time_created integer NOT NULL, time_updated integer NOT NULL,
data text NOT NULL
);
CREATE TABLE part (
id text PRIMARY KEY, message_id text NOT NULL, session_id text NOT NULL,
time_created integer NOT NULL, time_updated integer NOT NULL,
data text NOT NULL
);
{sql}
"#
))
.unwrap();
drop(conn);

// Constructed with a resolver pointed at the temp data dir — this
// is the configuration the trait path must honor.
let convo = OpencodeConvo::with_resolver(resolver);

let view = ConversationProvider::load_conversation(&convo, "", "ses_x").unwrap();
assert!(
view.turns
.iter()
.flat_map(|t| &t.file_mutations)
.any(|m| m.raw_diff.is_some()),
"trait path must resolve snapshot diffs with the configured resolver"
);
}
}
2 changes: 1 addition & 1 deletion site/_data/crates.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
},
{
"name": "toolpath-opencode",
"version": "0.5.0",
"version": "0.5.1",
"description": "Derive from opencode SQLite databases",
"docs": "https://docs.rs/toolpath-opencode",
"crate": "https://crates.io/crates/toolpath-opencode",
Expand Down
Loading