feat: migrate to OpenCode SDK worktrees with per-project config

Add SDK-based worktree management that lists and starts SDK worktrees
Migrate per-project setup to ~/.config/openchamber/<projectId>.json
Deprecate .openchamber legacy paths and adapt UI to new config
This commit is contained in:
Bohdan Triapitsyn
2026-01-27 20:00:07 +02:00
parent 63a4c32dfa
commit 415b043326
32 changed files with 1514 additions and 897 deletions
@@ -605,6 +605,11 @@ async fn resolve_sandboxed_path(
.await
.map_err(FsCommandError::from)?;
// Allow OpenChamber per-project config under ~/.config/openchamber.
if is_within_openchamber_user_config(&canonicalized) {
return Ok(canonicalized);
}
if !workspace_roots.is_empty()
&& !workspace_roots
.iter()
@@ -637,11 +642,15 @@ async fn resolve_creatable_path(
fallback_root.join(candidate)
};
// Allow OpenChamber per-project config under ~/.config/openchamber.
// Needed because Desktop FS commands are sandboxed to workspace roots.
if is_within_openchamber_user_config(&absolute) {
return Ok(absolute);
}
let parent = absolute.parent().ok_or(FsCommandError::NotDirectory)?;
let canonical_parent = fs::canonicalize(parent)
.await
.map_err(FsCommandError::from)?;
let canonical_parent = canonicalize_existing_ancestor(&parent.to_path_buf()).await?;
if !workspace_roots.is_empty()
&& !workspace_roots
@@ -722,6 +731,30 @@ fn default_home_directory() -> PathBuf {
dirs::home_dir().unwrap_or_else(|| PathBuf::from("/"))
}
fn openchamber_user_config_root() -> PathBuf {
default_home_directory().join(".config").join("openchamber")
}
fn is_within_openchamber_user_config(path: &PathBuf) -> bool {
path.starts_with(&openchamber_user_config_root())
}
async fn canonicalize_existing_ancestor(path: &PathBuf) -> Result<PathBuf, FsCommandError> {
let mut current = Some(path.as_path());
while let Some(candidate) = current {
match fs::canonicalize(candidate).await {
Ok(canon) => return Ok(canon),
Err(err) => {
if err.kind() != std::io::ErrorKind::NotFound {
return Err(FsCommandError::from(err));
}
}
}
current = candidate.parent();
}
Err(FsCommandError::NotDirectory)
}
fn clamp_search_limit(value: Option<usize>) -> usize {
let limit = value.unwrap_or(DEFAULT_FILE_SEARCH_LIMIT);
limit.clamp(1, MAX_FILE_SEARCH_LIMIT)
@@ -1474,6 +1474,7 @@ pub async fn remove_git_worktree(
#[tauri::command]
pub async fn ensure_openchamber_ignored(
// LEGACY_WORKTREES: only needed for <project>/.openchamber era. Safe to remove after legacy support dropped.
directory: String,
state: State<'_, DesktopRuntime>,
) -> Result<(), String> {