Skip to content

fix(memory) [BRNS-DESK-022]: a missing CLAUDE.md is a first-run empty state, not an error - #80

Open
sebastian-ssvlabs wants to merge 1 commit into
mainfrom
fix/desk-022-memory-empty-state
Open

fix(memory) [BRNS-DESK-022]: a missing CLAUDE.md is a first-run empty state, not an error#80
sebastian-ssvlabs wants to merge 1 commit into
mainfrom
fix/desk-022-memory-empty-state

Conversation

@sebastian-ssvlabs

Copy link
Copy Markdown

Summary

  • A fresh install has no ~/.claude/CLAUDE.md, and /memory auto-selects exactly that path. Users now get an empty editor plus "No memory file yet. Start typing and Save to create <path>" instead of backend error text — and Save creates the file, ~/.claude/ included.
  • The ticket's "observed" text is already half-fixed on main, and the ticket doesn't say why the remaining half matters. main suppresses the ENOENT banner by string-sniffing the OS message for "No such file" / "not found", which only holds where the C library says that: Windows' ENOENT reads "The system cannot find the file specified. (os error 2)" and matches neither, so the raw string still reached the banner there. Classifying at the source removes the guess.
  • Genuine failures now name the io kind rather than the errno — Couldn't read <path>: permission denied, not (os error 13) — for reads, writes, and the create_dir_all that makes ~/.claude/.
  • Closes BRNS-DESK-022 (P3, effort XS, onboarding / desktop / ux).

Mechanism

Absence was being recovered from a rendered error string; it is now carried as state from the point where the kernel reports it.

Layer Before After
commands/files.rs read_text_fileErr("Failed to read <path>: No such file or directory (os error 2)") new read_memory_fileOk(MemoryFileRead { content: "", missing: true }) on ErrorKind::NotFound; all other kinds still Err
Error copy io::Error Display, errno tail included io_reason() names NotFound / PermissionDenied / InvalidData, falls back to the raw message for anything else
routes/memory/+page.svelte catchmsg.includes("No such file") reads missing off the response; catch is now only real failures
UI editor chrome + red error banner editor chrome + muted first-run strip naming the path; cleared by a successful Save

missing is also set on the sidebar's "switch to a file that doesn't exist yet" path, which skips the IPC round-trip entirely, so both routes into the empty state agree.

read_memory_file is a sibling of read_text_file rather than a change to it: read_text_file has three other frontend callers (FilePreviewPane, /settings, the keybindings store) for which a missing file is an error, and widening its return type would have churned all of them. It is registered in both lib.rs and web_server/dispatch.rs, so remote access keeps parity (cf. BRNS-DESK-019).

One deliberate deviation from plan step 4: the friendly failure copy is composed in Rust and stays English. This repo has no i18n on the backend — every Err string from src-tauri/ is English and rendered verbatim — so localizing it would have meant either shipping a parseable error code and splitting the string frontend-side, or duplicating the copy. The two new UI strings are localized in both locales.

Verification

  • npx prettier --check on all four touched frontend files — clean.
  • npx eslint on the touched frontend files — clean (0 errors; the repo-wide run reports 35 pre-existing warnings in files this PR doesn't touch).
  • node scripts/i18n-check.mjs — 0 errors, 18 pre-existing warnings; both new keys present in en.json and zh-CN.json with matching {path} placeholders.
  • cargo fmt --manifest-path src-tauri/Cargo.toml — applied.
  • Three Rust tests added (read_memory_file_reports_absent_file_as_missing with neither the file nor its .claude parent present, read_memory_file_returns_existing_content, io_reason_drops_the_os_error_tail).
  • CI only: no local build env in this worktree (no .svelte-kit, no cargo target), so svelte-check, cargo clippy/build, npm test and the new Rust tests were written but not run — npm test can't even load tsconfig.json without .svelte-kit. The pre-commit hook was bypassed for that reason alone (its lint and format steps passed).
  • Not verified by hand: no GUI run. The acceptance walk-through — fresh open with no ~/.claude/ at all, type + Save + Reload round-trip, and the chmod 000 permission banner — is unexercised; only the unit tests cover the classification.

Locale note: sibling DESK PRs may also touch messages/en.json / messages/zh-CN.json; whichever lands second needs a rebase.

… state, not an error

/memory auto-selects ~/.claude/CLAUDE.md, which a fresh install has never
created. The read failure was classified by sniffing the OS error text for
"No such file", so the raw backend string still reached the banner on any
platform whose ENOENT message reads differently, and no path offered the
user a way to create the file.

The new read_memory_file command matches io::ErrorKind::NotFound at the
source and returns Ok with empty content and missing: true; every other
kind still fails. The route renders a create affordance while missing, and
Save (which already create_dir_all's the parent) clears it. Remaining read
and write failures now name the io kind instead of "(os error N)".

@Chris-ssvlabs Chris-ssvlabs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@nir-ssvlabs nir-ssvlabs left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Classifying at the source instead of sniffing the rendered string is the right fix, and the Windows argument is the part that justifies it over leaving the existing suppression alone — ENOENT there reads "The system cannot find the file specified", which matches neither substring, so the raw error really was still reaching the banner on that platform. That's a good catch to make while the ticket only described the macOS half.

Two things I checked because a new file-reading command is where they go missing: read_memory_file still runs validate_file_path before touching the filesystem, so the traversal guard is not weakened by the new entry point, and it is registered in the invoke handler rather than being unreachable. The create_dir_all on save covers the fresh-install case where .claude/ itself doesn't exist.

Checked: the path validation and command registration on the new seam, the NotFound-only branch (every other kind still errors), and the save path's parent creation. Not read: the route's rendering of the empty state.

Merge: ✅ into main.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants