Skip to content

Commit 42a1655

Browse files
committed
Fix workspace scope & full-scan backfill, add tests
Improve workspace startup scoping and harden full-history backfill. Removed the stale last-workspace restore when no workspace hint is provided and added resolve_workspace_path to ensure non-git --cwd/--project selections yield an "All workspaces" result (launches outside a repo now behave correctly). Strengthened compute_snapshot so a full scan with --scan-time-budget-ms 0 forces full reparse (ignores planner file/byte caps) and skips trusting cached rows when reparsing is required. Added comprehensive unit tests covering workspace selection precedence, full-scan stale-cache repair, append-only file resume via cached offsets, and other planner behaviors. Updated README/CHANGELOG and bumped package version to 0.3.2.
1 parent 1ea1f15 commit 42a1655

7 files changed

Lines changed: 428 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@
22

33
All notable changes to this project are documented in this file.
44

5+
## 0.3.2 - 2026-02-17
6+
7+
- Fixed workspace startup scoping:
8+
- Launching outside a git repo now always uses **All workspaces**.
9+
- A non-git `--project`/`--cwd` now disables repo filtering, even if launch dir is inside a repo.
10+
- `comon` no longer restores a stale last workspace filter when no workspace hint is detected.
11+
- Hardened long-history backfill behavior:
12+
- `--full-scan --scan-time-budget-ms 0` now forces full reparse instead of trusting unchanged cache rows.
13+
- Full scan now ignores planner file/byte caps.
14+
- Added regression tests for:
15+
- workspace selection precedence
16+
- full-scan stale-cache repair
17+
- append-only file resume via cached file offsets
18+
519
## 0.3.0 - 2026-02-16
620

721
- Added incremental session parsing with persisted offsets and parser state in `comon.db`.

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "comon"
3-
version = "0.3.0"
3+
version = "0.3.2"
44
edition = "2021"
55
license = "Apache-2.0"
66

README.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ See `CHANGELOG.md` for release history.
1111

1212
<img width="1647" height="861" alt="Dkh7CxLgx7" src="https://github.com/user-attachments/assets/edec765d-0924-493b-8c10-cb32bca867a9" />
1313

14-
## Release 0.3.0
14+
## Release 0.3.2
1515

1616
- Added incremental session parsing with persisted offsets and parser state in `comon.db`.
1717
- Reduced restart regressions: unchanged files outside current scan plan now stay visible via cache.
18+
- Fixed full backfill behavior: `--full-scan --scan-time-budget-ms 0` now forces a full reparse and cache refresh.
19+
- Fixed workspace startup behavior: non-repo launches show **All workspaces** and do not reuse a stale last workspace filter.
1820
- Added `--scan-time-budget-ms` for bounded per-refresh parse time (`0` disables budget).
1921
- Added `--max-jsonl-line-kib` to cap parsed line size without hard-dropping large files.
2022
- Added cache DB schema migration (`v1 -> v2`) for offset/parser-state fields.
@@ -40,6 +42,9 @@ If started outside a git repo, usage is shown as **All workspaces**.
4042
If you start outside a git repo but pass `--cwd` (or `--project`) pointing inside a git repo,
4143
`comon` will auto-detect the git root from that path.
4244

45+
If `--cwd` (or `--project`) points to a non-git directory, `comon` shows **All workspaces**
46+
even when launched from inside a git repo.
47+
4348
```bash
4449
# If installed (recommended):
4550
comon
@@ -64,7 +69,7 @@ Common flags:
6469
- `--max-session-files <n>`: max number of session files to scan per refresh (default from config)
6570
- `--max-jsonl-line-kib <n>`: max parsed JSONL line size in KiB (default from config)
6671
- `--scan-time-budget-ms <n>`: max parse time budget per refresh in ms (`0` disables budget)
67-
- `--full-scan`: scan all files under `CODEX_HOME/sessions`, including old months (ignores mtime cutoff)
72+
- `--full-scan`: scan all files under `CODEX_HOME/sessions`, including old months (ignores mtime cutoff and file/byte planning caps)
6873
- `--no-full-scan`: disable full scan for this run (overrides config)
6974
- `--scan-cache-max-entries <n>`: max entries kept in cache database (`comon.db`) (default from config)
7075
- `--rebuild-cache-on-start`: delete local scan cache DB files (`comon.db`, `comon.db-wal`, `comon.db-shm`) before first usage scan
@@ -102,7 +107,7 @@ comon --codex-home "C:\\Users\\You\\.codex" --cwd "C:\\Repos\\some-git-repo"
102107
Large-log recovery/tuning example:
103108

104109
```bash
105-
# One-time backfill for copied/old sessions:
110+
# One-time backfill for copied/old sessions (full reparse + cache refresh):
106111
comon --full-scan --scan-time-budget-ms 0
107112

108113
# Normal usage with bounded incremental refresh:
@@ -293,7 +298,7 @@ CI also runs this check on each push and pull request via `.github/workflows/asc
293298
- comon stores local app state in `~/.comon/state.json` by default (or `$COMON_HOME`, or `--comon-home`).
294299
- comon stores scan cache in `~/.comon/comon.db` to avoid rereading unchanged session files.
295300
- Large session logs are parsed incrementally with persisted parser offsets in `comon.db`; unchanged files are reused from cache.
296-
- If historical days look incomplete after adding old session files, run once with `--full-scan --scan-time-budget-ms 0` to complete backfill.
301+
- If historical days look incomplete after adding old session files, run once with `--full-scan --scan-time-budget-ms 0` to force a full reparse and refresh cached summaries.
297302
- comon uses embedded SQLite (`rusqlite` with bundled SQLite); no system `sqlite3` CLI is required at runtime.
298303
- comon stores user-editable runtime settings in `~/.comon/config.json` by default.
299304
- Privacy: comon stores metadata (workspace paths, timestamps, token/run/time aggregates) and does not persist prompt/completion text.

src/app/mod.rs

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -574,9 +574,6 @@ fn load_persisted_ui_state(
574574
state.orientation = orientation;
575575
}
576576
}
577-
if state.workspace_path.is_none() {
578-
state.workspace_path = store.global.last_workspace_path.map(PathBuf::from);
579-
}
580577

581578
if let Some(workspace_path) = state.workspace_path.as_ref() {
582579
let workspace_key = workspace_path.to_string_lossy();
@@ -708,3 +705,63 @@ impl AppState {
708705
Some(crate::ui::format_updated_label(updated_at))
709706
}
710707
}
708+
709+
#[cfg(test)]
710+
mod tests {
711+
use super::*;
712+
use std::sync::atomic::{AtomicU64, Ordering};
713+
714+
static TEMP_ID_COUNTER: AtomicU64 = AtomicU64::new(0);
715+
716+
fn make_temp_dir(prefix: &str) -> PathBuf {
717+
let unique = format!(
718+
"{}-{}-{}",
719+
std::process::id(),
720+
SystemTime::now()
721+
.duration_since(UNIX_EPOCH)
722+
.map(|duration| duration.as_nanos())
723+
.unwrap_or(0),
724+
TEMP_ID_COUNTER.fetch_add(1, Ordering::Relaxed)
725+
);
726+
let dir = std::env::temp_dir().join(format!("comon-app-{prefix}-{unique}"));
727+
let _ = std::fs::remove_dir_all(&dir);
728+
std::fs::create_dir_all(&dir).expect("create temp dir");
729+
dir
730+
}
731+
732+
#[test]
733+
fn load_persisted_ui_state_does_not_restore_last_workspace_without_hint() {
734+
let comon_home = make_temp_dir("state-no-hint");
735+
let mut store = StateStore::default();
736+
store.global.last_workspace_path = Some("/tmp/old-workspace".to_string());
737+
write_state_store(&comon_home, &store).expect("write state store");
738+
739+
let loaded = load_persisted_ui_state(&comon_home, None).expect("load persisted ui state");
740+
assert_eq!(loaded.workspace_path, None);
741+
742+
let _ = std::fs::remove_dir_all(comon_home);
743+
}
744+
745+
#[test]
746+
fn load_persisted_ui_state_uses_workspace_hint_and_workspace_state() {
747+
let comon_home = make_temp_dir("state-hint");
748+
let workspace_path = PathBuf::from("/tmp/repo-workspace");
749+
let mut store = StateStore::default();
750+
store.global.last_workspace_path = Some("/tmp/other-workspace".to_string());
751+
store.workspaces.insert(
752+
workspace_path.to_string_lossy().into_owned(),
753+
StoredWorkspaceState {
754+
no_sessions_confirm_dismissed: true,
755+
updated_at: 1,
756+
},
757+
);
758+
write_state_store(&comon_home, &store).expect("write state store");
759+
760+
let loaded = load_persisted_ui_state(&comon_home, Some(workspace_path.as_path()))
761+
.expect("load persisted ui state");
762+
assert_eq!(loaded.workspace_path, Some(workspace_path));
763+
assert!(loaded.no_sessions_confirm_dismissed);
764+
765+
let _ = std::fs::remove_dir_all(comon_home);
766+
}
767+
}

src/main.rs

Lines changed: 94 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,15 @@ fn detect_git_root(start: &std::path::Path) -> Option<PathBuf> {
7878
None
7979
}
8080

81+
fn resolve_workspace_path(
82+
launch_dir: &Path,
83+
cwd_override: Option<&Path>,
84+
project_override: Option<&Path>,
85+
) -> Option<PathBuf> {
86+
let project_candidate = project_override.or(cwd_override).unwrap_or(launch_dir);
87+
detect_git_root(project_candidate).map(|path| std::fs::canonicalize(&path).unwrap_or(path))
88+
}
89+
8190
fn resolve_comon_home(override_home: Option<PathBuf>) -> Option<PathBuf> {
8291
if let Some(path) = override_home {
8392
return Some(path);
@@ -238,14 +247,12 @@ async fn main() -> Result<()> {
238247
.map(|p| validate_dir(&p, "--project"))
239248
.transpose()?;
240249

241-
let project_candidate = project_override
242-
.as_deref()
243-
.or(cwd_override.as_deref())
244-
.unwrap_or(launch_dir.as_path());
245-
246250
// Only treat something as a "project" if it is inside a git work tree.
247-
let project =
248-
detect_git_root(project_candidate).map(|p| std::fs::canonicalize(&p).unwrap_or(p));
251+
let project = resolve_workspace_path(
252+
launch_dir.as_path(),
253+
cwd_override.as_deref(),
254+
project_override.as_deref(),
255+
);
249256

250257
// `cwd` controls where `codex app-server` is launched.
251258
let cwd = cwd_override
@@ -333,3 +340,83 @@ async fn main() -> Result<()> {
333340

334341
app::run(config).await
335342
}
343+
344+
#[cfg(test)]
345+
mod tests {
346+
use super::*;
347+
use std::sync::atomic::{AtomicU64, Ordering};
348+
use std::time::SystemTime;
349+
350+
static TEMP_ID_COUNTER: AtomicU64 = AtomicU64::new(0);
351+
352+
fn make_temp_dir(prefix: &str) -> PathBuf {
353+
let unique = format!(
354+
"{}-{}-{}",
355+
std::process::id(),
356+
SystemTime::now()
357+
.duration_since(SystemTime::UNIX_EPOCH)
358+
.map(|duration| duration.as_nanos())
359+
.unwrap_or(0),
360+
TEMP_ID_COUNTER.fetch_add(1, Ordering::Relaxed)
361+
);
362+
let dir = std::env::temp_dir().join(format!("comon-main-{prefix}-{unique}"));
363+
let _ = std::fs::remove_dir_all(&dir);
364+
std::fs::create_dir_all(&dir).expect("create temp dir");
365+
dir
366+
}
367+
368+
fn make_git_repo(path: &Path) -> PathBuf {
369+
std::fs::create_dir_all(path.join(".git")).expect("create .git directory");
370+
std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
371+
}
372+
373+
#[test]
374+
fn resolve_workspace_path_uses_all_workspaces_outside_repo() {
375+
let root = make_temp_dir("non-repo");
376+
let workspace = resolve_workspace_path(root.as_path(), None, None);
377+
assert!(workspace.is_none());
378+
let _ = std::fs::remove_dir_all(root);
379+
}
380+
381+
#[test]
382+
fn resolve_workspace_path_uses_launch_repo_when_no_overrides() {
383+
let root = make_temp_dir("launch-repo");
384+
let repo = root.join("repo");
385+
std::fs::create_dir_all(&repo).expect("create repo dir");
386+
let expected = make_git_repo(&repo);
387+
388+
let workspace = resolve_workspace_path(repo.as_path(), None, None);
389+
assert_eq!(workspace, Some(expected));
390+
let _ = std::fs::remove_dir_all(root);
391+
}
392+
393+
#[test]
394+
fn resolve_workspace_path_prefers_project_override_repo() {
395+
let root = make_temp_dir("project-override-repo");
396+
let launch = root.join("launch");
397+
std::fs::create_dir_all(&launch).expect("create launch dir");
398+
let repo = root.join("repo");
399+
let nested = repo.join("nested");
400+
std::fs::create_dir_all(&nested).expect("create nested repo dir");
401+
let expected = make_git_repo(&repo);
402+
403+
let workspace = resolve_workspace_path(launch.as_path(), None, Some(nested.as_path()));
404+
assert_eq!(workspace, Some(expected));
405+
let _ = std::fs::remove_dir_all(root);
406+
}
407+
408+
#[test]
409+
fn resolve_workspace_path_uses_all_when_project_override_not_repo() {
410+
let root = make_temp_dir("project-override-non-repo");
411+
let launch_repo = root.join("launch-repo");
412+
std::fs::create_dir_all(&launch_repo).expect("create launch repo dir");
413+
let _ = make_git_repo(&launch_repo);
414+
let non_repo = root.join("plain-dir");
415+
std::fs::create_dir_all(&non_repo).expect("create non-repo dir");
416+
417+
let workspace =
418+
resolve_workspace_path(launch_repo.as_path(), None, Some(non_repo.as_path()));
419+
assert!(workspace.is_none());
420+
let _ = std::fs::remove_dir_all(root);
421+
}
422+
}

0 commit comments

Comments
 (0)