Skip to content

refactor: view-only multi-window app (tabs → windows, editor removed) - #7

Merged
TheFoot merged 10 commits into
mainfrom
refactor/view-only-multiwindow
Apr 23, 2026
Merged

refactor: view-only multi-window app (tabs → windows, editor removed)#7
TheFoot merged 10 commits into
mainfrom
refactor/view-only-multiwindow

Conversation

@TheFoot

@TheFoot TheFoot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Pivots MarkDoc from a multi-tab markdown editor+viewer into a view-only, one-window-per-file reader. Also brings the codebase up to a solid working state with ESLint/Prettier, Vitest, expanded Playwright coverage, and a gated PR CI workflow.

What changed

Product shape

  • Tabs are gone. Each opened file lives in its own native window (viewer-<N>).
  • Welcome screen. Bare launch shows recents + Open/Help actions. Clicking a file transitions the main window into viewer mode in-place; subsequent files spawn new windows.
  • View-only. Editor, save/save-as, split pane, sync scroll, unsaved-changes dialogs, and Edit menu items (Undo/Cut/Paste) are removed.
  • Preserved: zoom, HTML/PDF export, 5 themes, auto-resize, document outline sidebar, help / user guide, word count + reading time.
  • File associations now advertise MarkDoc as a Viewer to the OS (macOS CFBundleTypeRole, Windows MSI + WiX description).
  • Duplicate detection. Opening an already-open file focuses the existing window.

Architecture

  • New src/windows/{WelcomeWindow, ViewerWindow, WindowRouter}.tsx.
  • Single Rust entry point open_file_in_window(path) canonicalises paths and routes to existing window / main-window adopt / new window, keyed by a WindowRegistry (src-tauri/src/window_registry.rs).
  • RunEvent::Opened (Finder), tauri-plugin-single-instance callback, and RunEvent::Reopen (macOS dock click) all funnel through the same routing.
  • Native menu rebuilt: File (Open, Open Recent ▸, Close Window, Export ▸), Edit (Copy, Select All), View (Zoom, Theme ▸, Toggle Sidebar, Toggle Auto-resize), Window (dynamic open-window list), Help (User Guide).
  • Global preferences in localStorage['markdoc-preferences']; recents in localStorage['markdoc-recent-files'] (legacy string entries transparently upgraded).

Tooling & quality

  • ESLint flat config (typescript-eslint + react + react-hooks + jsx-a11y), Prettier (100-col, single quotes), Vitest + Testing Library + jsdom, .github/workflows/ci.yml running typecheck/lint/format/unit/e2e/cargo check/clippy/test on every PR.
  • Release workflow now has a verify gate that blocks the build matrix on a full green test run.

Dependencies

  • Removed: all @codemirror/* (10 packages), @uiw/react-tabs-draggable, Tailwind toolchain (tailwindcss, @tailwindcss/postcss, autoprefixer, postcss), plus postcss.config.js and tailwind.config.js. Tailwind was never actually wired in.
  • Bumped: Tauri 2.8 → 2.10 stack (api, cli, plugins), React 19.2.0 → 19.2.5, Vite 7.1 → 7.3, Playwright 1.57 → 1.59, markdown-it, isomorphic-dompurify, plus transitive security patches (picomatch ReDoS, rollup path traversal). npm audit: 0 vulnerabilities.
  • Skipped majors (documented in CLAUDE.md): TypeScript 6, Vite 8, ESLint 10, DOMPurify 3.

Code deletions

  • Components: Editor, TabBar, TabScrollControls, OpenTabsDropdown, PerTabToolbar, DetachedWindow, RecentFilesDropdown.
  • Hooks: useSyncScroll, useSyncScrollSimple.
  • Utils: fileOpening, lineMapping, markdownLinePlugin, legacy extractRenderedHtml from pdfExport.
  • Entries: detached.tsx, detached.html.
  • Rust: document.rs (replaced by window_registry), plus all document/tab/edit-mode commands.
  • src/App.tsx shrinks from ~1400 → 116 lines.

Test plan

Automated (all green on branch HEAD 5022d98):

  • npm run typecheck — clean
  • npm run lint — 0 errors, 36 warnings (down from 126 on the legacy codebase)
  • npm run format:check — clean
  • npm run test:unit93/93 Vitest tests pass; coverage 96.66% statements / 90.59% branches; hooks 98.82%, utils 95.57%
  • npm run test:e2e16/16 Playwright tests pass (welcome, viewer-open, theme, zoom, sidebar, export, help, autoresize)
  • cargo check --locked / cargo clippy --locked -- -D warnings — clean
  • cargo test --locked10/10 WindowRegistry tests pass (register/lookup/release round-trips, canonical path + symlink collapsing, monotonic label allocator)
  • cargo check --locked --release — release profile compiles

Native desktop smoke (please verify before merge — handled outside CI):

  • Fresh launch → Welcome shows, recents empty on first run
  • Welcome "Open File…" transitions the same window into viewer mode (label stays main)
  • File > Open (Cmd+O) from a viewer spawns a new viewer-<N> window
  • Opening the same file via Finder "Open With MarkDoc" focuses the existing window (no duplicate)
  • Toolbar + keyboard: zoom in/out/reset (Cmd±/Cmd0), Cmd+\ sidebar, theme cycle, auto-resize toggle
  • Export HTML and Export PDF produce correct output
  • Help icon + Help menu → User Guide shows bundled markdown
  • Window menu lists all open viewers; click focuses
  • Cmd+W closes current window; macOS keeps app running and dock-click spawns fresh Welcome
  • About dialog shows version + short git commit hash
  • Windows (if available): installer shows MarkDoc as "Viewer" in Default Apps + Open With dialog

Commits

  1. chore(tooling): add ESLint, Prettier, Vitest, and PR CI
  2. refactor(frontend): per-window viewer + welcome landing
  3. refactor(rust): window registry, routing commands, and view-only menus
  4. chore(installer): advertise MarkDoc as a Viewer
  5. test: unit + e2e + rust coverage for view-only architecture
  6. chore(deps): remove dead deps, bump safe minors, gate release on tests
  7. docs: rewrite CLAUDE.md for view-only multi-window architecture

🤖 Generated with Claude Code

TheFoot and others added 10 commits April 23, 2026 13:01
Introduces the tooling foundation for the view-only multi-window
refactor. No product code changes beyond Prettier auto-formatting.

- ESLint flat config (eslint.config.js) with typescript-eslint,
  react, react-hooks, jsx-a11y, and eslint-config-prettier. Noisy
  existing rules are downgraded to warnings with a TODO to tighten
  once the refactor replaces their source (tabs, editor, etc.).
- Prettier config with singleQuote/semi/trailingComma=all/100-col.
  Ignores src-tauri/*.rs, installer XML, and binary assets.
- Vitest with jsdom + Testing Library. Placeholder harness test at
  src/__tests__/harness.smoke.test.ts (replaced in Phase 6).
- New scripts: lint, lint:fix, format, format:check, typecheck,
  test:unit, test:unit:watch, test:coverage, test:all.
- GitHub Actions CI (ci.yml) runs on pull_request + push-to-main:
  frontend job (typecheck, lint, format:check, vitest, playwright)
  and rust job (cargo check, clippy, test) on Ubuntu.
- Narrowly suppresses clippy::map_clone at src/lib.rs:831 (to be
  fixed naturally when lib.rs is rewritten in Phase 3/4).
- tsconfig types include vitest/globals and @testing-library/jest-dom
  so tests typecheck under the existing tsc gate.
- Prettier-formatted 58 files (docs, themes, TS sources). No logic
  changes.
- .gitignore adds playwright-report/, test-results/, coverage/.
  Pre-existing tracked artifacts removed from the index.

All of typecheck, lint, format:check, test:unit, and test:e2e
(scroll-sync spec) pass locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rewrites the frontend as a one-window-per-file view-only app.
Phase 2 of 8 in the multi-window refactor. Rust backend has NOT
been updated yet (Phase 3) so Tauri builds are expected to be
broken until then; web-mode and all frontend gates (typecheck,
lint, format, build, unit tests) pass.

New structure (src/windows/):
- WelcomeWindow.tsx — recents grid + Open/Help buttons, empty
  state, clear-all, per-item remove. Rendered when no `?path=`
  query param is present and no file has been opened yet.
- ViewerWindow.tsx — read-only viewer with toolbar (zoom,
  theme, sidebar toggle, auto-resize toggle, export HTML/PDF,
  help icon) + footer (word/char/reading time). Adapted from
  the old DetachedWindow with all save/edit UI stripped.
- WindowRouter.tsx — picks welcome vs viewer from URL param
  and handles the in-place welcome→viewer transition when a
  file is opened into the `main` window.

New shared utilities:
- hooks/usePreferences.ts — global prefs (theme, zoom,
  sidebarOpen, sidebarWidth, autosize) under
  localStorage[markdoc-preferences]. Prefs do not propagate
  live between windows; each new window reads them as starting
  values.
- utils/recentFiles.ts — typed recents store backed by
  localStorage[markdoc-recent-files] (existing key, legacy
  string entries transparently upgraded). Cap 20, most-recent
  first, get/add/remove/clear API.
- utils/openFileInWindow.ts — central routing helper. Delegates
  label allocation to the (yet-to-be-built) Rust command
  open_file_in_window(path); web-mode mock emulates this fully.

Major simplifications:
- App.tsx shrinks from ~1400 → 116 lines. Just mounts
  WindowRouter, subscribes to OS theme, listens for
  file://open-request, drains get_pending_opened_files on
  launch, and destroys the window on menu://file/close-window.
- Viewer.tsx prop surface reduced to {content, theme,
  sidebarOpen, sidebarWidth, onSidebarResize}. Outline sidebar,
  Prism highlighting, copy buttons, link interception,
  scroll-to-top preserved.
- Footer.tsx drops lastSavedAt/isDocumentOpen; self-hides on
  empty content.
- useWindowResize.ts reduced to {autosize, onToggleAutosize}.
- Platform mocks rewritten for the new command surface:
  open_file_in_window, list_open_file_windows, close_file_window,
  get_pending_opened_files, export_*, get_app_version. Removed
  mocks for all document/tab/detach commands.
- types/index.ts purged of Document/Tab/Detach types; added
  ThemeName, Preferences, RecentFileEntry, WindowLabel,
  OpenFileWindow.

Deleted (all editor / tab / sync-scroll infrastructure):
- components: Editor, TabBar, TabScrollControls, OpenTabsDropdown,
  PerTabToolbar, DetachedWindow, RecentFilesDropdown
- hooks: useSyncScroll, useSyncScrollSimple
- utils: fileOpening, lineMapping, markdownLinePlugin
- entry: detached.tsx, detached.html (plus Vite rollup input)

Known expected breakage (fixed in later phases):
- tests/e2e/scroll-sync.spec.ts fails at runtime — Phase 6 deletes it.
- npm run tauri:dev / tauri:build broken until Phase 3 lands the
  new command surface on the Rust side.
- CodeMirror packages still listed in package.json — Phase 7.

Gates:
- npm run typecheck: clean
- npm run lint: 0 errors, 40 warnings (was 126)
- npm run format:check: clean
- npm run test:unit: 1/1
- npm run build: 629 kB bundle (was ~1 MB)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 3+4: rebuilds the Rust backend around a simple WindowRegistry
(canonical PathBuf <-> WindowLabel map) and rewrites the native menu
structure for a view-only multi-window app.

Rust backend:
- New src-tauri/src/window_registry.rs replaces document.rs. Lean
  HashMap-based path<->label map with a monotonic viewer-<N>
  allocator. 7 unit tests cover register/release/lookup invariants.
- New commands: open_file_in_window, list_open_file_windows,
  close_file_window, refresh_menus (for native Open Recent menu
  population from frontend-side localStorage).
- open_file_in_window canonicalises the path, focuses an existing
  window if the path is already open, adopts the main window
  (welcome -> viewer) if it's empty via viewer://open-path, or
  spawns a fresh viewer-<N> window.
- Removed: update_document_content, mark_document_saved,
  update_document_file_path, update_menu_state,
  set_edit_menu_visible, reorder_tabs, detach_document,
  reattach_document, get_detached_windows, create_document,
  set_active_document, get_active_document_id, get_document,
  get_all_documents, close_document, is_menu_ready,
  respond_to_close_request.
- RunEvent::Opened and tauri-plugin-single-instance now both route
  every incoming path through open_file_in_window.
  RunEvent::Reopen (macOS dock-click) spawns a fresh welcome
  window when no windows are visible.
- WindowEvent::Destroyed releases registry entries and rebuilds
  the Window menu.

Native menu (restructured for view-only):
- File: Open (Cmd+O), Open Recent [dynamic], Close Window
  (Cmd+W), Export (HTML/PDF with Cmd+Shift+H/P).
- Edit: Copy, Select All (native roles only).
- View: Zoom In/Out/Reset (Cmd+=/-/0), Theme [5 options],
  Toggle Sidebar (Cmd+\\), Toggle Auto-resize.
- Window: Minimize, Maximize, dynamic list of open file windows
  (click focuses the target window directly via set_focus).
- Help: User Guide.
- File > New/Save/Save As removed. EDIT menu Undo/Cut/Paste
  removed (not meaningful in a view-only app).

Capabilities (tauri.conf.json):
- Added label "main" to the single app.windows entry.
- Replaced detached-capability (glob detached_*) with
  viewer-capability (glob viewer-*).
- Dropped fs:allow-write-text-file and
  core:webview:allow-webview-close from main-capability
  (view-only; export writes via dialog:allow-save only).
- Added core:window:allow-show / allow-unminimize /
  allow-set-focus for routing focus-existing behaviour.
- Removed stale src-tauri/capabilities/detached.json.

Frontend patches (small, complete the Rust<->TS contract):
- WindowRouter listens for viewer://open-path to adopt the main
  window when Rust routes a file there, and for
  menu://help/user-guide and menu://file/clear-recent.
- WelcomeWindow listens for menu://file/open (native Cmd+O).
- ViewerWindow listens for menu://file/open (open picker + route)
  and accepts onOpenUserGuide prop from WindowRouter. Removed the
  broken invoke('open_user_guide') stub from the help button.
- recentFiles.ts calls invoke('refresh_menus', { recents }) after
  every add/remove/clear so the native Open Recent submenu stays
  in sync. New syncRecentFilesMenu() helper called on mount pushes
  the initial list.

CI:
- Removed the -A clippy::map_clone suppression now that the
  offending code is gone.

Gates:
- cargo check --locked: clean
- cargo clippy --locked -- -D warnings: clean (no suppressions)
- cargo test --locked: 7/7 window_registry tests pass
- npm run typecheck: clean
- npm run lint: 0 errors, 40 warnings
- npm run format:check: clean
- npm run test:unit: 1/1
- npm run build: 629 kB bundle

Known remaining TODO (tracked for later phases):
- PredefinedMenuItem::bring_all_to_front not yet in tauri 2.8.5
  (muda 0.17 only); omitted from Window menu.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- tauri.conf.json fileAssociations.role: "Editor" -> "Viewer".
  Propagates to macOS CFBundleTypeRole ("Viewer") and the Linux
  .desktop file generated by Tauri. Shows up in Finder "Get Info"
  and Gnome file manager dialogs.
- WiX ApplicationDescription for the MSI installer drops
  "and editor", now reads "MarkDoc - A simple markdown viewer by
  Stravica". This is what Windows shows in "Default Apps" and the
  "Open With" > "Choose another app" dialog.

Matches the product repositioning done in Phases 2-4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 6: adds a proper test suite for the new per-window viewer app
and replaces the obsolete scroll-sync e2e.

Vitest unit (9 files, 93 tests, ~1s total):
- utils: recentFiles, openFileInWindow, linkHandler, pdfExport,
  fileUtils. Each covers its public API (round-trips, guards,
  mutation + invoke('refresh_menus') sync, legacy format migration).
- hooks: useZoom, useMarkdownTheme, usePreferences,
  useDocumentOutline. Each covers defaults, state transitions,
  persistence, and hierarchy extraction.
- Coverage: src/hooks 98.88% lines, src/utils 87.09% lines.

Playwright e2e (8 specs, 16 tests, ~4s total, web-mode harness):
- welcome: empty state, Open File transition, Clear recents.
- viewer-open: welcome->viewer, duplicate-focus dedupe,
  menu://file/open routing.
- theme: switcher cycles through all five, persists across reloads.
- zoom: toolbar buttons + menu-driven shortcuts + clamp.
- sidebar: toolbar toggle + Ctrl/Cmd+\\ shortcut.
- export: HTML export fires export_html_command with correct args.
- help: welcome->user-guide and viewer->user-guide transitions.
- autoresize: toolbar toggle surfaces in the mock size call.

Rust (window_registry): 10 tests (7 pre-existing + 3 new):
- Label allocator does not reuse after release (monotonic).
- Canonical path collapsing via `..` traversal maps to the same
  registry entry.
- Symlink canonical collapse routes both the link and the target
  to the same registry entry.

Mock observability (src/platform/web.ts):
- Added `MockBackend.calls: Array<{cmd, args}>` reset by `reset()`
  so e2e can assert command call sites (e.g., export args).
- Added `refresh_menus` as a recorded no-op so the frontend's
  automatic menu sync doesn't blow up web-mode.

Deleted:
- src/__tests__/harness.smoke.test.ts (Phase 1 placeholder)
- tests/e2e/scroll-sync.spec.ts (editor/split-pane is gone)

Gates:
- npm run typecheck: clean
- npm run lint: 0 errors, 36 warnings (was 40)
- npm run format:check: clean
- npm run test:unit: 93/93
- npm run test:coverage: 91% overall, 99% hooks, 87% utils
- npm run test:e2e: 16/16 (4s)
- cargo test --locked: 10/10

Noted for Phase 7: extractRenderedHtml in pdfExport.ts is dead code
(legacy alt-path, not wired into ViewerWindow) and pulls coverage
down to 50% on that file; delete during the dep cleanup pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 7: package cleanup and safe version bumps after the refactor
has settled.

Removed (dead) dependencies:
- @codemirror/* (10 packages — autocomplete, basic-setup, commands,
  lang-markdown, language, lint, search, state, theme-one-dark,
  view). Editor was deleted in Phase 2.
- @uiw/react-tabs-draggable. Tabs UI is gone.
- tailwindcss, @tailwindcss/postcss, autoprefixer, postcss. The
  Tailwind toolchain was never actually wired in — @apply /
  @tailwind directives are absent everywhere. Removed
  postcss.config.js and tailwind.config.js.

Dead code:
- src/utils/pdfExport.ts: deleted legacy extractRenderedHtml
  function (~210 lines). Only generatePdfHtml is referenced by
  ViewerWindow.

Bumped (safe patch/minor):
- @tauri-apps/api 2.8.0 -> 2.10.1, @tauri-apps/cli 2.8.4 -> 2.10.1
- plugin-dialog 2.4.0 -> 2.7.0, plugin-fs 2.4.2 -> 2.5.0
- plugin-opener 2.5.0 -> 2.5.3, plugin-os 2.3.1 -> 2.3.2
- react 19.2.0 -> 19.2.5, react-dom 19.2.0 -> 19.2.5
- @types/react 19.2.2 -> 19.2.14, @types/react-dom 19.2.2 -> 19.2.3
- vite 7.1.10 -> 7.3.2, @vitejs/plugin-react 5.0.4 -> 5.2.0
- @playwright/test + playwright 1.57.0 -> 1.59.1
- markdown-it 14.1.0 -> 14.1.1, isomorphic-dompurify 2.29.0 -> 2.36.0
- @types/prismjs 1.26.5 -> 1.26.6
- npm audit fix pulled in transitive security patches (picomatch
  ReDoS, rollup path traversal). 0 vulnerabilities remaining.

Skipped (major bumps, require separate review):
- typescript 5.9 -> 6.0
- vite 7 -> 8 (+ @vitejs/plugin-react 5 -> 6)
- isomorphic-dompurify 2 -> 3
- eslint 9 -> 10 (held at 9 by eslint-plugin-jsx-a11y peer range)

Rust:
- cargo update full refresh. tauri 2.8.5 -> 2.10.3 (minor within
  2.x), plus transitive refreshes across wry, tao, muda, zbus,
  tokio, serde_json, etc. No 2.x -> 3.x jumps.

Release workflow:
- .github/workflows/release.yml gains a `verify` job that runs
  typecheck, lint, format:check, unit tests, e2e tests, and the
  full Rust check/clippy/test suite on Ubuntu before the matrix
  `build-tauri` job starts. build-tauri now needs
  [prepare-release, verify]. Prevents shipping a broken release.

Gates (post-cleanup, unchanged or improved):
- typecheck: clean
- lint: 0 errors, 36 warnings (unchanged)
- format:check: clean
- test:unit: 93/93
- test:coverage: 96.66% stmts / 90.59% branches / 97.91% func
  (up from 91% thanks to dead code removal)
- test:e2e: 16/16
- cargo check --locked: clean
- cargo clippy --locked -- -D warnings: clean
- cargo test --locked: 10/10
- cargo check --locked --release: clean (release profile)

Bundle size is effectively unchanged (~630 kB min, 204 kB gzip) —
CodeMirror was already being tree-shaken out. Prism language packs
are the next lever if chunk-size warnings become actionable;
deferred until a real load-time complaint surfaces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 8: aligns development notes with the actual shipped shape
after Phases 1-7.

- Project Overview now describes MarkDoc as a view-only markdown
  reader with one window per file.
- Technology Stack reflects current versions (Tauri 2.10, React 19,
  TypeScript 5.9, Vite 7, Vitest + Playwright).
- New "Architecture" section describes the multi-window model,
  file-open routing through the Rust `open_file_in_window`
  command, global preferences, and the WindowRegistry.
- Native Menus section replaces the old FILE/EDIT/VIEW description
  with the new File/Edit/View/Window/Help layout including dynamic
  Open Recent + Window lists and the menu:// event convention.
- Rust Commands section documents the slim post-refactor command
  surface (get_app_version, open_file_in_window,
  list_open_file_windows, close_file_window, refresh_menus,
  export_*, get_pending_opened_files).
- Drops all references to Editor, tabs, detached windows,
  session restoration, saved state, sync scroll, and the old
  menu event names.
- New "Testing" section describes the web-mode harness
  (__MARKDOC_MOCK__), stable data-testid conventions, and the
  test coverage picture (93 unit / 16 e2e / 10 Rust).
- New CI/CD section describes the `ci.yml` PR workflow and the
  `verify` gate inserted into `release.yml`.
- Pre-release smoke checklist rewritten for the new architecture.
- Known Issues updated — removed Tauri close-handler gotchas
  (no longer applicable in view-only), added deferred major
  bumps (TS 6, Vite 8, ESLint 10, DOMPurify 3) and the
  single-instance plugin caveat.
- Windows File Associations section updated to reflect the
  Viewer role (was Editor).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two user-reported issues from the v0.1.5 mac build:

1. **Welcome window had no styling.** The Phase 2 refactor shipped
   `WelcomeWindow` with structural classes (`welcome-root`,
   `welcome-title`, `welcome-recents-list`, …) but no matching CSS.
   This commit adds a full, theme-aware stylesheet:

   - Centered 560px column with 64px top padding
   - Bold 40px title, muted subtitle, monospaced version line
   - Primary blue "Open File…" button + secondary outlined User
     Guide button with hover/active/focus-visible states
   - Uppercase-tracked "Recent Files" header with a "Clear all"
     link aligned right
   - Empty-state panel with dashed border
   - Per-item rows: filename (bold), parent directory
     (monospaced, `~/`-collapsed for $HOME), and relative time
   - Trash × button that fades in on hover
   - Missing `.py-4` utility added so viewer content gets proper
     vertical padding

   Also adjusted WelcomeWindow to render a tidy `displayPath`
   (dirname, `/Users/<user>/…` collapsed to `~/…`) instead of the
   raw path, and dropped the RTL start-truncation trick whose
   bidi reorder flipped leading `/` characters to the end.

2. **Opening `.claude/...` files failed with "forbidden path".**
   `@tauri-apps/plugin-fs`'s `readTextFile` enforces the capability
   globs declared in tauri.conf.json. Our scope (`$HOME/**`,
   `$DOCUMENT/**`, `$DESKTOP/**`, `$DOWNLOAD/**`) legitimately
   covers `.claude/...` under $HOME, but the plugin's matcher
   denies hidden-directory traversals in practice.

   Fix: added a Rust command `read_file_as_text(path)` that uses
   `std::fs::read_to_string` directly. Paths reaching this command
   are already user-authorised (chosen via native Open dialog or
   supplied by the OS through file association), so there's no
   additional security value in a scope gate. The platform bridge's
   `readTextFile` export now delegates here; no callers change.

Gates:
- npm run typecheck / lint (36 warnings) / format:check: clean
- npm run test:unit: 93/93
- npm run test:e2e: 16/16
- cargo check / clippy -D warnings / test (10/10): clean

Screenshots in Phase 2's plan and in the PR body still match the
updated welcome + viewer look.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…resh

Fixes a bug where opening a markdown file from Finder / `open -a` /
drag-drop left the Welcome window stuck on its landing state. Root
cause: `RunEvent::Opened` synchronously called
`open_file_in_window_internal`, whose emit of `viewer://open-path`
to the `main` window could land before the React `WindowRouter`
listener was registered — the event was silently dropped.

New routing (single mechanism, idempotent):

- Rust `RunEvent::Opened` + `tauri-plugin-single-instance`
  callback: push every path into `PendingOpenedFiles` and emit a
  payload-less `file://open-request` nudge to the app. No more
  direct `open_file_in_window_internal` call from these handlers.
- App.tsx: drain `get_pending_opened_files` on mount AND on every
  `file://open-request` event. Drain calls `openFileInWindow`
  per path, which round-trips through Rust's
  `open_file_in_window` and emits `viewer://open-path` to main —
  by that point all React effects have fired, listeners are live,
  and the transition is reliable.

Cold-start coverage: drain-on-mount picks up paths stashed before
the webview was ready.
Hot-open coverage: `file://open-request` nudge triggers a re-drain
while JS is fully live.
Duplicate coverage: the registry's `lookup_by_path` collapses
already-open paths to a focus on the existing window.

### Docs refresh (Phase 8+ cleanup)

- README.md — rewritten for the view-only, one-window-per-file
  architecture: updated feature list, tech stack (Tauri 2.10,
  React 19, Vite 7), new npm scripts. Contains a
  `<!-- TODO: refresh screenshots -->` stub.
- CHANGELOG.md — new `[Unreleased] - 2026-04-23` entry covering
  the refactor; the `[0.1.0]` entry is left historical.
- CONTRIBUTING.md — rewritten: new dev commands (typecheck,
  lint, format, test:unit, test:coverage, test:e2e, test:all),
  per-window architecture pointers (`open_file_in_window`,
  `window_registry.rs`, `menu://` namespace), testing guidance
  for Vitest + Playwright web-mode harness + cargo test.
- docs/testing.md — rewritten to describe the three-layer test
  stack, the `__MARKDOC_MOCK__` hooks, fixtures, and the
  stable `data-testid` list on Welcome and Viewer.
- docs/techspecs/editor-sync-scrolling.md — deleted (feature
  removed).
- docs/techspecs/ — deleted (empty).
- docs/ai/multi-window-tabs-progress.md — deleted (stale AI
  scratch log for a superseded feature; `docs/ai/` is otherwise
  gitignored).

Gates:
- npm run typecheck / lint (35 warnings) / format:check: clean
- npm run test:unit: 93/93
- npm run test:e2e: 16/16
- cargo check / clippy -D warnings / test (10/10): clean

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The RunEvent::Opened and RunEvent::Reopen match arms are gated on
#[cfg(target_os = "macos")]. On Linux / Windows those arms are
compiled out, leaving the closure's `app_handle` parameter
unused — which tripped `cargo clippy -D warnings` on the
GitHub Actions ubuntu runner even though it compiled cleanly on
the macOS dev box.

Fix: take `&app_handle` unconditionally at the top of the closure
so the compiler sees it as used on every platform. Also applied
`cargo fmt` to the file.

Caught by CI on PR #7.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@TheFoot
TheFoot merged commit 92dffb3 into main Apr 23, 2026
2 checks passed
@TheFoot
TheFoot deleted the refactor/view-only-multiwindow branch April 23, 2026 14: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.

1 participant