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.
The connectivity-vs-real-error classifier tested the message against
/timeout|connect|network|unreachable|refused/i, matching any error
whose text happened to include one of those words. A server-side
permission error like 'network share access refused' was silently
classified as transient, updating only the status dot — the user
never saw the actual problem.
Tighten the regex to well-known connectivity phrases and lowercase
error codes (ENOTFOUND/ECONNREFUSED/etc), using word boundaries so
substrings in unrelated messages don't match.
The new-task bottom sheet called createTask then, if a date was set,
made a follow-up updateTask to attach the date. If the update failed
(e.g. filesystem error between the two writes) the user was left with
a dateless task and, because transient sync errors are already
suppressed, often no visible error either.
Extend the create_task Tauri command to accept optional date/has_time
fields and pass them through. The frontend now creates the task in one
round-trip. No separate update path needed.
Two coupled issues in workspace settings:
1. The credentials-loading effect re-ran whenever ws.webdav_url changed,
so any config mutation (e.g. changing sync interval) would trigger a
re-load of the stored username/password, overwriting whatever the
user was typing into those fields. Gate with a one-shot credsLoaded
flag.
2. Save would persist whatever was in the URL input even if the user
had never tested it — a typo'd host silently pointed the workspace
at a dead server. Now saveWebdav auto-runs the connection test and
bails if it fails; any edit to the three inputs clears the "ok"
status via markDirty() so the next Save is forced to re-verify.
Also replaces the ASCII "Failed -- Retry" with an em dash.
Sync's conflict-resolution and download paths wrote the local file with
plain fs::write. A crash or I/O error mid-write left a truncated .md
or .listdata.json that would then fail YAML/JSON parsing on the next
list_tasks. All other callers in this crate use atomic_write; route
the four sync call sites through it for consistency and crash safety.
create_task used a plain += on the in-memory version returned to the
caller while FileSystemStorage uses saturating_add when serialising
the frontmatter. The two would disagree at u64::MAX, and in debug
builds the + operator would panic on overflow. Match the storage
behaviour.
delete_task only collected direct children when a parent was deleted,
so grandchildren (and deeper descendants — the data model allows any
depth even though the UI is two-level today) would be left with a
parent_id pointing at a deleted task. Walk the parent-child graph to
collect the full descendant set and delete children before the parent
so a mid-cascade failure can't strand descendants.
The frontend currently calls init_workspace before add_workspace, but
the Tauri command itself is trivially breakable by any caller that
skips the pre-step or a future frontend refactor: add_workspace would
save the workspace entry pointing at a non-existent directory, and
every subsequent command would then fail with 'Path does not exist'
via TaskRepository::new. Call TaskRepository::init inside the command
so it is self-contained and idempotent.
write_task used plain fs::write for the .md payload even though every
other write path in this module (metadata files, sync state, offline
queue, config) uses atomic_write. A crash mid-write left a truncated
.md file whose malformed YAML frontmatter then failed list_tasks for
the entire list. Route through atomic_write so a failed write either
leaves the old file intact or produces the full new file.
The day cell class used `selectedDay === day`, ignoring the currently
viewed month/year. After picking e.g. April 15, flipping to May still
painted May 15 as the selected day; committing with Done would shift
the task's date to whatever month the user happened to be viewing.
Track selectedYear/selectedMonth alongside selectedDay, update them
only on actual day click, and construct the committed ISO from the
selection (not the view). The pre-existing isSelected() helper is now
wired into the cell template.
When RUST_BACKTRACE was set in the environment, every user-facing error
dumped a 20-line Rust backtrace at the user — e.g. running 'onyx list
show' with no workspace gave them a stack trace through anyhow, clap,
and libc start. Replace 'fn main() -> Result' with an explicit error
printer that walks the anyhow cause chain using Display, and exits 1.
Programming-bug panics still surface through the default panic handler.
Three related CLI bugs found during smoke testing:
1. `get_repository` used `config.get_workspace(name)` which expects the
UUID string, so `onyx list create -w dev` or `onyx task add -w dev`
always failed with "Workspace 'dev' not found". Unified CLI resolution
into a single `resolve_workspace()` helper that accepts either the
display name or the UUID; removed sync.rs's duplicated local copy.
2. `workspace switch`/`remove`/`retarget`/`migrate` only accepted the
display name — the error message even suggested "Use the workspace ID
instead" on ambiguous names, but IDs were then rejected. Updated
`resolve_name` to try the map key first.
3. `onyx workspace add` never set `current_workspace`, so the very next
command failed with "No workspace set. Use 'onyx init'..." even
though a workspace was just created. Now sets the new workspace as
current whenever none was previously selected, and reports the fact.
Updated the error message to point at the correct `workspace add` /
`workspace switch` commands instead of `init`.
removeWorkspace already switches to the next available workspace (or falls
back to setup). forgetMissingWorkspace can just delegate, dropping the
duplicate branch that previously never ran anyway because current_workspace
was always null after removal.
When a user deletes the current workspace from settings, the backend
clears current_workspace and the frontend's hasWorkspace derived fell
through to the setup screen — even if the user still had other healthy
workspaces configured. Mirror the forgetMissingWorkspace flow: switch
to the next available workspace automatically.
- README.md: update Phase 4 status to reflect Android preliminaries done
(file-watcher gating, tauri-plugin-credentials, safe area insets, Android
targets configured) but init/build not yet run; add tauri-plugin-credentials
to project structure; expand docs/ tree; add newer GUI features (workspace
rename, safe area insets, accessibility); add setup screen screenshot;
update What's Next to note Phase 4 is in progress
- PLAN.md: fix Phase 4 checkboxes — android init and build-succeeds were
marked [x] but gen/android/ does not exist; correct cfg gate annotation
from #[cfg(not(mobile))] to #[cfg(not(target_os = "android"))]; update
dependency snippet to reflect actual keyring/zeroize/sha2/quick-xml usage;
bump Last Updated to 2026-04-17
- docs/DEVELOPMENT.md: add WEBKIT_DISABLE_DMABUF_RENDERER=1 Wayland note
to tauri dev command
https://claude.ai/code/session_01MypN7wPNqeSgw8b5DYpMc1
Resolve conflicts against latest main:
- PLAN.md: keep main's updated Settings/theme list (window decorations,
Black and Gold) while adopting PR's "Move to..." inline phrasing.
- README.md: keep main's theme list including Black and Gold.
- docs/API.md: keep main's atomic move_task documentation.
https://claude.ai/code/session_01NCtJ5PNhaDh21kYnDZXYsN
Extracts two pure helpers out of Svelte components so they can be
exercised without the reactive runtime, and adds component tests for
ConfirmDialog's Escape-handling behavior.
- apps/tauri/src/lib/grouping.ts (new): `groupTasksByDate` lifted out of
the `groupedPendingTasks` $derived in the app store.
- apps/tauri/src/lib/paths.ts (new): `workspaceNameFromPath` lifted out
of SetupScreen.handleOpen.
- apps/tauri/src/lib/grouping.test.ts: 8 cases — "No Date" placed last
(regression), full bucket ordering, empty input, within-bucket
stable sort, earlier-today stays in Today, multi-task same-day,
No Date preserves insertion order.
- apps/tauri/src/lib/paths.test.ts: 8 cases — POSIX/Windows/mixed
separators, trailing slash regression ("…/Tasks/" → "Tasks"), empty
and root-only fallback, names with spaces.
- apps/tauri/src/lib/components/ConfirmDialog.test.ts: 6 cases —
renders message/detail/custom confirm text, Cancel/Confirm fire the
right callbacks, Escape calls oncancel and does NOT reach an outer
window listener (regression), non-Escape keys are ignored, and the
module-level open-count increments/decrements correctly (including
when two dialogs are mounted at once).
Test harness: Vitest + jsdom + @testing-library/svelte. `npm test`
runs the suite; `resolve.conditions` is set to "browser" under VITEST
so Svelte resolves its client entry and mount() works.
23/23 tests pass. cargo check, cargo test -p onyx-core (162/162),
and npm run build all still green.
- crates/onyx-core/src/webdav.rs: rename `getpassword`/`setpassword`
(7 call sites) to `get_password`/`set_password` so `cargo build`
and the CLI compile again under the default `keyring-storage` feature.
- ConfirmDialog.svelte: intercept Escape at window capture phase and
expose a module-level open-count so TasksScreen's Escape handler can
defer; previously Escape on a dialog both dismissed the dialog AND
popped the task-detail view behind it. Cancel is also focused on
mount for keyboard users.
- TasksScreen.svelte: extend the taskStack cleanup effect to collapse
back to parent detail when only the subtask is gone (was leaving a
blank third panel); focus the new-list input when it appears; reset
the Completed section's expand state when switching lists.
- TaskDetailView.svelte: re-sync local title/description state when
the task prop's content changes (unless the user is editing), so a
sync pull doesn't get silently overwritten on next save. Bail out of
the parent delete if a subtask delete fails instead of orphaning.
- app.svelte.ts: deleteTask now returns a success boolean; move the
"No Date" group to the end of the grouped-by-date view so Overdue
and Today surface first.
- SetupScreen.svelte: strip trailing separators before splitting the
picked folder path so "…/MyTasks/" yields "MyTasks" instead of the
literal fallback "workspace".
Verified live under Xvfb for the three user-visible cases (ConfirmDialog
Escape, orphan subtask collapse, new-list autofocus). Screenshots in
screenshots/smoke-test/. cargo test --lib -p onyx-core is green
(162/162); npm run build succeeds.
Screenshots captured from a seeded local workspace loaded under Xvfb.
Includes working flows (task list, drawer, detail view, group by date)
and four bug demonstrations: Escape on ConfirmDialog pops navigation,
subtask panel orphaned after external delete, new-list input lacks
autofocus.
Captured by running the Tauri GUI under Xvfb (1024x768x24) with
WEBKIT_DISABLE_DMABUF_RENDERER=1 and WEBKIT_DISABLE_COMPOSITING_MODE=1,
then using ImageMagick `import` against the Onyx X window id.
- CLAUDE.md: add last_sync field to WorkspaceConfig description
- README.md: update Phase 4 status, replace dark mode with multi-theme
system, add has_time/parent fields to data format example
- PLAN.md: add last_sync to Phase 1 WorkspaceConfig, update dark mode
entries to reflect theme selector, fix Google Tasks Tauri command
names, add rename_list/move_task to Core Library API, fix "Move to..."
description (inline, not submenu)
- docs/API.md: document rename_list and move_task repository methods
- docs/DEVELOPMENT.md: add dateFormat.ts to frontend file structure
https://claude.ai/code/session_01NCtJ5PNhaDh21kYnDZXYsN