Skip to content

feat: comfort sweep (Next.js) - #531

Merged
fo0 merged 4 commits into
mainfrom
claude/confident-thompson-x0m3w4
Aug 24, 2026
Merged

fo0 merged 4 commits into
mainfrom
claude/confident-thompson-x0m3w4

Conversation

@fo0

@fo0 fo0 commented Aug 24, 2026

Copy link
Copy Markdown
Owner

Summary

Three comfort/quality features for the web GUI: import files from disk in the editor, a resizable sidebar that remembers its width, and a crash guard so a render error stops blanking the page.

# Feature Nutzen für User Aufwand Empfehlung
1 Import files from disk (picker + drag & drop) Getting an existing file into a stash meant opening it elsewhere, copying its content and pasting it into a blank row — one round trip per file, with the filename retyped. M auto-build
2 Resizable sidebar, width persisted The stash list was pinned at 260px, so long names truncated on every screen size with no way to widen the column. Now draggable (200–520px) and remembered. M auto-build
3 Crash guard instead of a blank page A render-time throw anywhere unmounted React to a white page: no message, no way back. Now a fallback naming the failure, with three real recoveries and the stack. M auto-build

Changes

  • feat(editor): import files from disk by picker or drag & drop — the Files section becomes a drop zone and gains an "Import Files" button. Reading and rejection live in the new src/utils/file-import.ts: binaries (NUL byte, or a U+FFFD share above 10% after the UTF-8 decode), content over the per-file limit the save path already enforces, and more than 20 files at once are each refused by name with a reason. Filenames are reduced to a bare basename with no path separators, no .. and no control characters — exactly what the server's isValidFilename() accepts, so an import cannot produce a stash that fails to save. A single untouched blank row is replaced rather than left above the import.
  • feat(sidebar): resizable width that survives a reload — --sidebar-width on the app root, dragged by a new <SidebarResizer>, clamped to 200–520px and persisted under clawstash-sidebar-width. It is a real separator widget: Arrow keys nudge, Home/End jump to the bounds, Enter and double-click restore the default, so a pointer-only resize cannot strand the sidebar. The stored width is adopted in a mount effect rather than the state initializer, because an initializer read would put a hydration mismatch on the root element. Mobile is untouched — fixed-width slide-in overlay, handle hidden.
  • feat(app): catch render crashes instead of blanking the page — <ErrorBoundary> wraps <App/> at both mount points (/ and the catch-all route). componentDidCatch logs to the browser console only; ClawStash ships no client error-reporting endpoint and adding one would be a new interface rather than a crash guard.
  • docs(agent_docs): record the comfort-sweep patterns in key-patterns.md — CLAUDE.md sits at 19.7k of its 20k offload threshold, so the detail went to the unbudgeted doc it already points at. README.md gained the two user-facing lines.

No new runtime dependency. No refactoring outside the three features.

Testing

Full local chain, in order, all exit 0:

  • npm run format:check — clean
  • npm run lint — clean
  • npx tsc --noEmit — clean
  • npm test — 56 files, 524 passed / 5 skipped
  • npm run build — succeeded

New tests: src/utils/__tests__/file-import.test.ts (7), src/utils/__tests__/sidebar-width.test.ts (5), src/components/__tests__/ErrorBoundary.test.tsx (4). Not verified in a real browser — the drag gesture and the drop highlight rest on the unit tests and the type checker only.

Checklist

  • Formatting passes (npm run format:check)
  • Code compiles without errors (npx tsc --noEmit)
  • Tests pass (npm test)
  • Build succeeds (npm run build)
  • Changes are documented (README.md, agent_docs/key-patterns.md)

Closes #530


Generated by Claude Code

claude added 4 commits August 24, 2026 02:14
Getting an existing file into a stash meant opening it elsewhere, copying
its content and pasting it into an empty file row — one round trip per
file, and the filename retyped by hand.

The editor's Files section is now a drop zone and carries an "Import
Files" button. Picked or dropped files become file rows with their own
name and content; a single untouched blank row is replaced rather than
left above the import, and the stash-name auto-fill stops overwriting
row 1 once a real filename has arrived.

The import refuses what a stash cannot hold and says so per file:
binaries (NUL bytes or a wall of U+FFFD from the UTF-8 decode), content
over the per-file limit the save path already enforces, and more than 20
files at once. Filenames are reduced to a bare basename with no path
separators, no "..", and no control characters — exactly what the
server's isValidFilename() accepts, so an import cannot produce a stash
that fails to save.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pb9QmXMRvpZJK5uEsGedav
The sidebar was pinned at 260px. It carries the stash list — names, tags
and file counts — so on a wide screen the column stayed narrow and long
names truncated, with nothing the user could do about it.

Its trailing edge is now a drag handle. The width is a CSS custom
property on the app root, clamped to 200-520px and persisted under
`clawstash-sidebar-width`, so it comes back on the next visit.

The handle is a real `separator` widget, not a bare div: Arrow keys nudge
it, Home/End jump to the bounds, Enter and a double-click restore the
default. That last part matters — a pointer-only resize can strand the
sidebar at a width with no way back for a keyboard user.

The width is adopted from localStorage after mount rather than in the
state initializer: doing it inline would put a hydration mismatch on the
app's root element. Mobile is untouched — the sidebar is a fixed-width
slide-in overlay there and the handle is hidden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pb9QmXMRvpZJK5uEsGedav
The SPA had no error boundary anywhere in the tree. A render-time
exception in any component — the viewer, a graph canvas, a Mermaid
diagram — unmounted React to a blank white page: no message, no way
back, and no hint that a reload was what was needed.

Both mount points (`/` and the catch-all route) now wrap `<App/>` in an
ErrorBoundary. The fallback says what happened, states that the stashes
themselves are on the server and unaffected, and offers the three
recoveries that actually exist: re-render the tree, reload the page, or
go back to the dashboard. The stack stays available behind a collapsed
`<details>` so a bug report can carry it.

The caught error is logged to the browser console — ClawStash ships no
client error-reporting endpoint, and adding one would be a new interface
rather than a crash guard.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pb9QmXMRvpZJK5uEsGedav
Editor file import, the persisted sidebar width and the crash guard, plus
the two new utils. CLAUDE.md is at 19.7k of its 20k offload threshold, so
the detail goes to the unbudgeted key-patterns doc it already points at.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pb9QmXMRvpZJK5uEsGedav
@fo0
fo0 merged commit 3256cea into main Aug 24, 2026
3 checks passed
@fo0
fo0 deleted the claude/confident-thompson-x0m3w4 branch September 2, 2026 21:14
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.

feat: comfort sweep (Next.js)

2 participants