- 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.
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
The complete(), delete(), and edit() functions each had an identical
loop searching for a task by ID across all lists. Extract a shared
find_task() helper that returns the list ID and task.
https://claude.ai/code/session_013ooJht2HrZUTXgNJFU79cV
Three components had duplicate date formatting functions. Extract
formatDateChip (for detail/input views with optional time) and
formatDateLabel (for compact list items) to a shared dateFormat module.
https://claude.ai/code/session_013ooJht2HrZUTXgNJFU79cV
storage.rs and google_tasks.rs had near-identical sanitize_filename
implementations. Extract the shared logic to a crate-level function
so both modules reuse it. The google_tasks version also gains Windows
reserved device name handling it previously lacked.
https://claude.ai/code/session_013ooJht2HrZUTXgNJFU79cV
The fields _client, _base_url, _username, _password were all actively
used throughout the struct's methods. The underscore prefix convention
signals unused fields, which was misleading for readers.
https://claude.ai/code/session_013ooJht2HrZUTXgNJFU79cV
The condition `len <= 3 && ends_with(":\\") || ends_with(":")` was
missing parentheses, causing the second ends_with check to run
regardless of path length due to && binding tighter than ||.
https://claude.ai/code/session_013ooJht2HrZUTXgNJFU79cV
- README.md: correct "group-by-due-date" to "group-by-date" (field is
group_by_date, not tied to due dates)
- docs/DEVELOPMENT.md: remove "Add migration logic if needed" step —
project is pre-alpha with no migration policy; remove CHANGELOG.md
references (file does not exist)
- docs/API.md: clarify load_credentials returns Zeroizing<String> not
plain String; fix legacy credential migration description (unscoped
format, not dot-separated format)
https://claude.ai/code/session_01KgXnipZEAHUwTp5wX4PFj9
- Fix debouncedSave in TaskDetailView losing edits when title and
description are changed within 400ms (shared timer only saved the
last-changed field)
- Return errors from Tauri commands when workspace ID doesn't exist
instead of silently succeeding (set_webdav_config, set_workspace_theme,
set_sync_interval, set_sync_interval_unfocused)
- Remove duplicate atomic_write_bytes in google_tasks.rs; reuse
pub(crate) atomic_write from storage.rs
- Fix failing test using wrong frontmatter field name (due → date)
- Add Audit.md log
https://claude.ai/code/session_0186pnnUJxj2uv1KhHjWoAGA
- Rename due_date → date and group_by_due_date → group_by_date throughout
all docs to match the renamed Rust fields (Task.date, TaskList.group_by_date,
repository set_group_by_date/get_group_by_date)
- Update YAML frontmatter examples: due: → date:, add has_time field
- Update .listdata.json examples: group_by_due_date → group_by_date
- Update CLI flag in examples: --due → --date
- Add WorkspaceMode::GoogleTasks variant and google_account / sync_interval_unfocused_secs
fields to WorkspaceConfig in all docs
- Document google_tasks.rs module (read-only Google Tasks API client, UUID v5 mapping)
- Document Ink theme and window decorations selector in CLAUDE.md
- Update PLAN.md Phase 7 Google Tasks checklist to reflect partial implementation
- Update current state date in CLAUDE.md (2026-04-06 → 2026-04-15)
- Update PLAN.md Last Updated date (2026-04-05 → 2026-04-15)
- Add google_tasks.rs to DEVELOPMENT.md project structure
https://claude.ai/code/session_01VmzpAB3PRY7bvSoVmzMRVm
- Renamed `TaskFrontmatter.due` → `TaskFrontmatter.date`; YAML key on disk is now `date:` instead of `due:`
- Added `#[serde(alias = "due")]` so existing task files with `due:` frontmatter still deserialize correctly
- Updated google_tasks.rs to write `date:` instead of `due:` in generated YAML
- Renamed CLI `--due` flag to `--date`; updated function signature and display string "Due:" → "Date:"