Skip to content

feat(files): add "This computer" local file browser - #473

Merged
ewen-poch merged 5 commits into
aipoch:mainfrom
roxi3906:claude/beautiful-euclid-35cb9b
Aug 3, 2026
Merged

feat(files): add "This computer" local file browser#473
ewen-poch merged 5 commits into
aipoch:mainfrom
roxi3906:claude/beautiful-euclid-35cb9b

Conversation

@roxi3906

Copy link
Copy Markdown
Contributor

Summary

Adds a local-filesystem file browser ("This computer") as a sibling to the existing remote SSH browser. Local files open as standard preview-workbench tabs and render through the shared preview pipeline. Entry point is a new LOCAL section in the Files-panel Artifacts dropdown.

What's included

  • Shared core (src/shared/local-fs.ts + tests): pure types, validateLocalPath, resolveLocalPath, sortLocalEntries, isSensitiveLocalPath, and the reserved LOCAL_BOOKMARKS_KEY.
  • Main process (src/main/local-fs/): LocalFsService (list / preview / roots / reveal / openPath) backed by node:fs, entry-capped and realpath-canonicalized. The same instance also feeds the managed-preview resolver, so local images/PDFs stream through the same validated path.
  • Bridge: window.api.localFs (5 methods); 'local' added to PreviewFileSource / ManagedPreviewSource; regenerated web api map.
  • UI: LocalFileBrowser (back/up/refresh, editable address bar, Go-to with fixed Home + pin/unpin bookmarks, sensitive-file/dir confirm) and LocalFileHeaderActions (Reveal in Finder + Copy path / Open-with), replacing "Download" in the header and fallback surfaces for local files.

Security model

Deliberately "Home start, full-disk navigable" — paths are not confined to a storage root; validation rejects only malformed input (non-absolute / control chars) and realpath canonicalizes symlinks/...

The arbitrary-path read via source: 'local' is reachable only by the trusted main renderer: the open-science-preview:// protocol resolves opaque, owner-scoped resource IDs (never renderer-supplied paths), the HTML preview sandbox runs under default-src 'none'; connect-src 'none' with no preload, and local Office files skip the OOPIF pipeline. acquire/readRange/release are isolated per ownerId (webContents).

Sensitive-path warning covers credential directories (.ssh / .aws / .gnupg) on entry, dotenv files, and suffix-less secret files (SSH private keys, cloud credentials, .pgpass, .p12/.pfx).

Testing

  • tsc --noEmit on both node and web projects — clean.
  • ESLint on all changed files — clean.
  • Vitest: shared local-fs + main local-fs/service suites pass (20 tests, incl. new suffix-less-secret cases); previously verified managed-preview / office-preview suites unaffected.
  • Renderer components (LocalFileBrowser, LocalFileHeaderActions) have no unit tests — verified via typecheck/lint only.

Deferred / not included

  • Save as artifact from a local preview (mirrors the still-unwired remote "Add to project").
  • "Add local folder…" entry is present but disabled ("Soon").
  • Local Office files open in the OS default app rather than rendering inline (the in-app LibreOffice pipeline resolves managed paths only); text/code/CSV/JSON/Markdown/PDF/images render inline.
  • Header is a compact 32px strip, not the taller machine-chip + breadcrumb layout from docs/local-artifacts-mockup.html.
  • "Reveal in Finder" label is macOS-specific.

🤖 Generated with Claude Code

@github-actions github-actions Bot added the enhancement New feature or request label Jul 28, 2026
roxi3906 added a commit to roxi3906/open-science that referenced this pull request Aug 3, 2026
…clid-35cb9b

Resolves the conflicts blocking PR aipoch#473 against 103 new upstream commits.

Conflicts and how they were resolved:

- src/main/ipc.ts — upstream moved every handler registration behind
  declareElectronAdapter() and already had an 'uploads' adapter, so the
  duplicate bare registerUploadIpcHandlers() call was dropped and the
  onStandaloneUploadSaved broadcast folded into upstream's adapter. Same for
  session-persistence and project-files, whose adapters upstream already owns.
  The genuinely new local-fs registration is wrapped in its own adapter.

- src/main/uploads/ipc.ts — kept the extracted runLocalStaging() helper and the
  stage-local-path handler that upstream had inlined/absent, and migrated all 11
  registrations from ipcMain.handle to the new typed ipcMainHandle.

- src/renderer/.../PreviewPanel.tsx — took upstream's PreviewPanelSurface
  extraction, then hand-ported the keep-tool-panels-mounted logic into the new
  component. Upstream's extraction rendered an empty <section />, which would
  have dropped the local browser's current directory on every tab switch.

Also regenerated, not textually merged:
- src/shared/web-api-map.generated.ts (path moved from src/renderer/web/)
- Prisma client, for the new UnreadTaskSession model

And registered the six new preload channels (localFs.* and
uploads.stageLocalPath) in the public-surface inventory test upstream added.

Verification: typecheck clean; lint 0 errors, 0 warnings on changed files; full
suite 682/699 files pass. The two reds (notebook/kernel-executor.test.ts and
skills/literature-review/kernel.test.ts) are byte-identical to upstream here and
fail for environment reasons — the latter needs Python 3.10+ for `X | None`.
@roxi3906
roxi3906 force-pushed the claude/beautiful-euclid-35cb9b branch 2 times, most recently from 4d7ea0b to cb5ac34 Compare August 3, 2026 05:53
roxi3906 and others added 5 commits August 3, 2026 14:14
Add a "This computer" mode to the Files panel so users can browse the local
filesystem, preview files, and pull them into a project without leaving the app.

- local-fs main service + IPC (getRoots, listDir, readPreview, openPath, reveal)
  with path validation shared through src/shared/local-fs.ts
- LocalFileBrowser listing with breadcrumb navigation, plus LocalFileHeaderActions
  for "Save as artifact"
- route the preview surface through a shared preview-file-item shape so managed
  files and local paths render with the same renderers
- save-as-artifact reuses the composer upload pipeline: it publishes a ManagedFile
  with source='upload' so the file appears under "Your uploads", and threads the
  active projectId through so the project-files:changed event is not dropped

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The local-fs handlers were the only ones in the repo calling ipcMain.handle
directly, so the registry never tracked them: adapter teardown saw zero added
channels and left the handlers installed, and the Web RPC router rejected every
call with "Unregistered Web RPC channel".

Routing them through ipcMainHandle also makes them reachable over the Web seam,
which they should not be for a paired remote browser. Reading and opening host
paths is native-host authority, so the five local-fs channels join
REMOTE_LOCAL_ONLY_RPC_CHANNELS alongside compute:reveal-in-folder, together with
uploads:stage-local-path, whose sibling stage-local-file is already web-blocked.
The conflict resolution meant to preserve tool-panel state (the local file
browser's current directory) across a switch to a file preview, but it returned
a <section> wrapper for the inactive case and <PreviewToolPanel> for the active
one. React reconciles by element type at a given position, so every activation
change unmounted and re-created the subtree, discarding the state the wrapper
existed to protect.

Tool items now always render <PreviewToolPanel>, which toggles `hidden` itself,
so the element type at that position never changes. Expansion is gated on the
panel being active so a background tab cannot open a modal over another tab.
resolveManagedFilePath ended in a bare `return localFsService.resolveFilePath`,
making arbitrary host-path resolution the default branch for anything that is
not artifact/upload/notebook-input. Any source added to ManagedPreviewSource
later would have inherited that path silently.

The 'local' case is now named explicitly and the tail asserts `never`, so an
unhandled source is a compile error and, if one reaches runtime anyway, a
rejected promise instead of a host-path lookup.
stage-local-path checked only that sourcePath was a non-blank string, then
passed it straight to stat(). It now runs the same validateLocalPath check the
local-fs browser applies, so relative paths and control characters are rejected
at the IPC boundary instead of surfacing as raw ENOENT from Node.

Staging commits bytes into .pending/ and releases the writer lease before the
separate SQLite publish, so a throw there left a file with no Version row and no
sweep to reclaim it. A failed publish now discards the staged copy and rethrows.
@roxi3906
roxi3906 force-pushed the claude/beautiful-euclid-35cb9b branch from cb5ac34 to 6d302e4 Compare August 3, 2026 06:21
@roxi3906
roxi3906 marked this pull request as ready for review August 3, 2026 06:43
@ewen-poch
ewen-poch merged commit ef8de2b into aipoch:main Aug 3, 2026
27 checks passed
@roxi3906
roxi3906 deleted the claude/beautiful-euclid-35cb9b branch August 3, 2026 07:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants