start_watcher takes handle: tauri::AppHandle by value, then immediately did let handle = handle.clone() before moving it into the file-watcher closure. The intermediate clone has no purpose: the parameter is unused outside the closure, so the move can take it directly.
https://claude.ai/code/session_01Vk2NBZGFP3YVshDj1CwDjt
SyncState::save duplicated the temp+rename+cleanup-on-failure pattern that storage::atomic_write already provides. Replace the inline implementation with a call to atomic_write, completing the consolidation begun for AppConfig::save_to_file and OfflineQueue::save.
https://claude.ai/code/session_01Vk2NBZGFP3YVshDj1CwDjt
OfflineQueue::save had its own copy of the temp-file + rename + cleanup-on-failure dance, even though the storage::atomic_write helper is pub(crate) and already used by AppConfig::save_to_file and google_tasks. Replace the inline implementation with a call to atomic_write so the crate has one canonical atomic write path.
https://claude.ai/code/session_01Vk2NBZGFP3YVshDj1CwDjt
The Storage trait itself does not declare `Send + Sync` bounds — only the
boxed instance held by `TaskRepository` does. Reword to describe what's
actually required of an implementation, and call out that
`FileSystemStorage` does not coordinate writes across processes outside
the `.sync.lock`-protected WebDAV flow.
https://claude.ai/code/session_01LweYBKMFbnTen7pCTdeQKq
The dedup loop wrapped its winner in `Option<Task>` and then mapped the
`None` case to `Error::InvalidData("Empty dedup entries for task")`.
That branch is unreachable: `by_id` is built by pushing every entry of
`file_tasks` into the vector for its UUID, so every group has at least
one entry, and the `len() > 1` branch keeps the first element after
`drain(1..)`.
Replace the spurious error with `expect` calls that document the
invariant and let the dedup loop yield `Task` directly instead of
`Option<Task>`.
`sync_google_workspace` silently dropped errors from `.listdata.json`
and `.onyx-workspace.json` atomic writes via `let _ = ...`, so a sync
could report `downloaded: N` while the list/workspace ordering had not
been persisted. Push those errors into the `errors` vec returned by
`GoogleSyncResult` so callers see the failure.
`SyncAction::Upload` cloned the file bytes solely so it could later read
`data.len()` for the sync-state record. Capture the length up front and
move the buffer into `put_file`.
- PLAN.md: uncheck push/pull sync mode selector (backend supports it
via SyncMode enum, but no UI exists in SettingsScreen; always full sync)
- PLAN.md: bump Last Updated to 2026-04-27, Document Version to 4.5
- CLAUDE.md: update Current state date to 2026-04-27
https://claude.ai/code/session_01C7jV6wrzJVhHRKWsq87XwB
`AppConfig::save_to_file` had its own copy of the temp-file + rename +
cleanup-on-failure dance. `storage::atomic_write` is already
`pub(crate)` and does exactly that — `google_tasks.rs` was migrated to
use it earlier. Drop the duplicate so there's one canonical atomic
write path in the crate.
`delete_task`'s descendant walk re-scanned the full task list on every
frontier pop, so the cost was O(n * depth) where n is the list size.
For a list of a few hundred tasks with even moderate nesting that's
already noticeable.
Index `parent_id -> [child_id]` once up-front; the BFS then visits each
descendant in O(1) amortised, dropping the total to O(n).
The deletion-detection loop in `get_sync_status` scanned `local_files`
linearly for every tracked path in `sync_state.files`, making the cost
quadratic in the file count. The earlier "pending change" loop just
above already does the inverse direction via `sync_state.files.get`
(O(1)). Build a `HashSet<&str>` of local paths once and check it
the same way to make the function O(n).
This is called by the GUI status indicator, so the win shows up as
soon as a workspace tracks more than a handful of files.
`FileSystemStorage::sanitize_filename` was a one-line forwarder to
`crate::sanitize_filename` with a single call site in
`task_file_path`. The extra method added a layer of indirection
without value. Inline the crate-level call.
`compute_sync_actions` emits no action for files that are missing from
both local and remote but still tracked in the sync base (the
`(None, None, Some(_))` arm). Nothing else cleaned those entries, so
`.syncstate.json` grew forever every time a file was deleted both
locally and remotely — and on each subsequent sync the same
no-op match fired again.
Add a `prune_orphan_bases` pass that runs before `compute_sync_actions`
in `sync_workspace_inner`, dropping any base entry whose path is in
neither the local nor remote scan. Unit-tested in isolation.
The `(None, Some(_), Some(b))` arm re-checked the already-matched
`remote` via `remote.is_some_and(...)`, which obscures intent and
compiles to redundant None-branch code. Bind `Some(r)` in the match
and use `r` directly.
No behavior change.
17 Tauri commands repeated `Uuid::parse_str(&s).map_err(|e| e.to_string())`
for each UUID argument. Collapse the pattern into a `parse_uuid`
helper so callers read as `let id = parse_uuid(&list_id)?;`.
When a parent task was toggled, `update_task` failures on child tasks
were silently swallowed with `let _ = ...`, leaving subtasks out of
sync with the parent's status and giving the user no feedback. Map the
error and propagate so the UI can show it and the user can retry.
Descendant walking in delete_task called Vec::contains in the inner
loop, making the traversal O(n^2) in the number of tasks. Swap the
visited set to HashSet so membership tests are O(1); HashSet::insert
also folds the contains-check and record-new steps into one call.
- Remove BottomSheet.svelte from PLAN.md file structure (deleted in
efb4cca — NewTaskInput hand-rolls its own sheet)
- Expand workspace path validation description in API.md and CLAUDE.md
to include filesystem root "/" alongside system directories, matching
the forbidden list added in fix(tauri): reject "/" root path
https://claude.ai/code/session_015BSAnuhvMBLk7s4g7dSE53
selectedYear/selectedMonth were declared below selectDay, which writes
to them, and below isToday, which is declared nearby. Runtime worked
because the assignments only run on user click (after script init), but
the split made the initialization order confusing. Group all $state
fields at the top of the script.
Three call sites repeated the same "empty base -> child, otherwise
trim_end + slash + child" pattern. Pull it into a helper to keep the
join convention consistent across list_remote_folder, inspect, and
create_remote_workspace.
sort_by may call the comparator many times, so the previous tiebreaker
re-read each duplicate file's metadata on every comparison. With N
duplicates that's O(N log N) stat calls, and the ordering could flip
mid-sort if a file was touched concurrently. Snapshot mtime per file up
front and sort on the cached values.
trim_end_matches('/') collapses "/" to "", which then isn't matched by
the forbidden list, so a root-filesystem workspace slipped through. Keep
"/" as the canonical form when the stripped value is empty.
- CLAUDE.md: add `sync` to the CLI commands list (commands/sync.rs exists)
- PLAN.md: remove BottomSheet.svelte (deleted in efb4cca)
- DEVELOPMENT.md: add grouping.ts and paths.ts to the lib directory listing
https://claude.ai/code/session_01YbcpJqmwpEW5tCJFFkMSPZ
Add regression tests for the bugs found in this smoke test:
- resolve_workspace: by-name, by-UUID, unknown-identifier, current-fallback,
actionable no-workspace message.
- DateTimePicker: selected-day highlight must be month-scoped; committing
after navigating months uses the selected month, not the viewed one.
- create_task: version is saturating_add on u64::MAX (doesn't panic/wrap).
Also fixes the three pre-existing clippy warnings (WorkspaceMode now uses
#[derive(Default)] + #[default], repository test drops unused binding,
sync test uses struct-update syntax instead of field-reassign-default).
BottomSheet.svelte is not imported anywhere — NewTaskInput hand-rolls
its own sheet. SetupScreen had a standalone testConnection() function
that was only ever reachable through connectAndBrowse which calls
test_webdav_connection directly; the standalone variant had no
callers.
onyx task edit wrote the task body to /tmp/onyx-<uuid>.md with the
default umask, leaving it world-readable on shared multi-user systems
for the duration of the editor session. Open with O_CREAT|O_TRUNC +
mode 0600 via OpenOptionsExt on unix; Windows keeps the existing
behaviour since unix-style mode bits don't apply.
The no-lists empty state said 'Tap the list name above to create one' —
but there is no list name above, just a static 'Tasks' label. The
actual affordance (+ New list) lives in the drawer, which may not be
open. Add a primary-button shortcut that opens the drawer and puts
focus in the new-list input in one click. Google Tasks workspaces are
read-only so they still get the explanatory text instead.
Svelte's native autofocus attribute is unreliable for inputs rendered
via conditional blocks (prior smoke-test fixed this for the new-list
input). Apply the same bind:this + $effect pattern to the list-rename
input (TasksScreen) and the workspace-rename input (SettingsScreen),
and select() the existing text so typing replaces the old name
cleanly.
The module-scope fs-changed listener fired unconditionally, calling
loadLists even when the user was on the setup or missing-workspace
screens (where no current workspace exists). The invoke would fail
silently and a WebDAV debounced sync could kick off against an
incomplete state. Bail when there's no active workspace or the tasks
screen isn't mounted.
executeDeleteCompleted and executeDeleteCompletedSubtasks iterated over
the reactive completedTasks/completedSubtasks lists with no error
handling: the array shrinks with every successful delete, skipping
subsequent entries, and a failed delete silently left a half-deleted
state. Snapshot the target list up front and abort as soon as a delete
returns false — matching the subtask-cascade path.