Skip to content

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

Draft
roxi3906 wants to merge 16 commits into
aipoch:mainfrom
roxi3906:claude/beautiful-euclid-35cb9b
Draft

feat(files): add "This computer" local file browser#473
roxi3906 wants to merge 16 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

Add a local-filesystem sibling to the remote SSH browser, opening local
files as preview-workbench tabs through the shared preview pipeline.

- shared/local-fs: pure types + validation, sensitive-path detection,
  dir sorting, bookmark key
- main/local-fs: LocalFsService (list/preview/roots/reveal/openPath) over
  node:fs, entry-capped, realpath-canonicalized; reused by the managed
  preview resolver so local images/PDFs stream through validated paths
- 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
  bookmarks, sensitive-file/dir confirm), LocalFileHeaderActions (reveal /
  copy path / open-with), wired into the Files-panel Artifacts dropdown

Sensitive-path warning covers credential dirs (.ssh/.aws/.gnupg) on entry
plus suffix-less secret files (SSH keys, cloud credentials).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added the enhancement New feature or request label Jul 28, 2026
roxi3906 and others added 15 commits July 28, 2026 10:08
- SENSITIVE_BASENAMES: remove '.env' — startsWith('.env') already covers it
- buildMachineName: drop the owner='' reset in catch (already initialized)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Render the local file browser as a stable preview-workbench tab instead
of a modal, so it lives alongside file and notebook previews.

- add a 'local-browser' toolKind + createLocalBrowserPreviewItem factory
- refactor LocalFileBrowser from a Dialog to full-height tab content;
  load roots/bookmarks on mount, drop the open/onClose props and close
  button
- relabel the dropdown section "LOCAL" -> "this computer" and show the
  resolved device name (falls back to "This computer")
- make the address bar an always-on input (no click-to-edit, no folder
  icon)

Also fixes a stale ProjectFilesView test that asserted the old
pre-feature behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The local browser is no longer its own preview tab. The Files tab now
holds two containers and the source dropdown swaps between them: picking
an artifact scope shows the artifacts list, picking the device shows the
local file browser. Reverts the 'local-browser' toolKind and preview-item
factory added in the previous commit.

- ProjectFilesView owns a sourceMode ('artifacts' | 'local'); the trigger
  swaps its icon/label to the device and the entry gets a radio check
- the header file count follows the visible container, via an entry-count
  callback from the browser
- drop the browser's machine chip (the source picker carries that name)
  and give it the same rounded bordered container as the artifact cards
- hover rows/menu entries use the light-gray bg-200 surface instead of
  the gold accent token
- the address bar only re-lists when the path actually resolves somewhere
  new, so blurring an untouched field costs no IPC call; equivalent
  spellings snap back to the canonical path

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The local browser toolbar had two arrows side by side — back and up — which
read as a duplicate of the same action. Keep one arrow that goes to the parent
folder and drop the history stack it was the only consumer of, so navigation is
a single axis (parent arrow, address bar, Go-to, entry click).

Also:
  - the address bar sat on bg-muted, which is darker than the bg-200 used for
    row hover; move it to bg-200 so the field reads as a lighter inset while
    keeping its border against the white container
  - every toolbar control is icon-only or icon + a two-word label, so add
    delayed (200ms) tooltips saying what each one does, including one on the
    pinned-folder ✕ clarifying that it unpins rather than deletes
  - the preview header's "…" button gains the same treatment, and Reveal's
    tooltip now describes the action instead of repeating its aria-label
  - title-case the "This computer" source label to match "Artifacts"

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Toolbar and Go-to refinements on the local browser, from a pass over the
rendered UI:
  - the address bar moves to bg-000; bg-200 is what the listing rows use for
    hover, so the field only reads as an input once it is lighter than that.
    Its border still separates it from the container
  - "Go to" drops its leading MapPin: the two-word label already says what the
    control is, and the trailing chevron carries the dropdown affordance
  - the parent arrow, refresh and pin icons thin out to strokeWidth 1.5, which
    sits better next to the 12px address text than lucide's default 2
  - bookmarks switch from Bookmark to a pushpin: Pin when unpinned, PinOff once
    pinned, so the icon shows what a click does next. The Go-to menu follows,
    including its per-row unpin control, which was an ✕
  - Go-to rows now show the absolute path under the label. "Home" said nothing
    about where it lands, and two folders pinned from different trees were
    indistinguishable when their basenames matched
  - the per-row unpin button gets a real 24px hover square filled with
    surface-control-hover; at bg-300 it was 2% off the row's own hover fill and
    effectively invisible
  - title-case the Remote source label to match Artifacts / This computer

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Go-to list was a hand-rolled absolutely-positioned div behind a gotoOpen
boolean, with no dismiss path: clicking anywhere else left it open, and its
role="listbox" wrapping plain buttons was not a valid menu tree. Move it onto
the shadcn DropdownMenu, so click-outside, Escape, focus handling and the
trigger's aria-expanded all come from Radix, and the rows become real
menuitems.

The per-row unpin button now stops propagation so clicking it doesn't select
(navigate to) the row it sits in; the menu stays open, which lets several
folders be unpinned in one pass. Covered by a new test that opens the menu and
dismisses it with an outside pointerdown/click.

Also tighten the browser's bottom gap from mb-4 to mb-1.5 so the container sits
closer to the panel edge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A mistyped path rendered the raw IPC rejection in destructive red — "Error
invoking remote method 'local-fs:list-dir': Error: ENOENT: no such file or
directory, realpath '/x'". Typing a path that isn't there is ordinary, not a
fault, so map the errno to a sentence and show it in body text: a short summary
line plus the offending path in mono beneath it. describeLocalListingError is
pure and covers ENOENT / ENOTDIR / EACCES / EPERM / ELOOP / ENAMETOOLONG and
both validation rejections, keeping unrecognized text rather than inventing a
reason.

Icons all go neutral: the sky-500 folder, the primary-tinted pins and the
emerald copy checkmark carried color that no longer meant anything once the
glyphs and labels said the same thing.

The three toolbar icon buttons drop to text-300, darkening only on hover, and
their stroke thins to 1.25 via one shared pair of constants. "Go to" moves up to
text-sm: it is the only worded control in that row and was rendering as the
smallest type in it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"Go to" moves to text-000 so the row's only worded control is its most legible
element. The address bar goes the other way, to text-100: the current path is
context rather than content, and it darkens to text-000 on focus, when it is
about to be edited.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…clid-35cb9b

# Conflicts:
#	src/main/ipc.ts
#	src/renderer/src/pages/workspace/PreviewFileSurface.tsx
#	src/renderer/src/pages/workspace/ProjectFilesView.tsx
#	src/renderer/src/pages/workspace/previews/preview-file-reader.ts
#	src/renderer/src/stores/preview-workbench-store.ts
#	src/shared/preview-resources.ts
…clid-35cb9b

# Conflicts:
#	src/renderer/src/pages/workspace/ProjectFilesView.test.tsx
#	src/renderer/src/pages/workspace/ProjectFilesView.tsx
Keep tool-type preview panels mounted (hidden) when inactive so
ProjectFilesView and LocalFileBrowser do not unmount when the user
opens a file preview. On return to the Files tab the component is
already mounted: cwd, sourceMode, and scroll position are intact.

File panels are unaffected — they continue to re-create on each
activation via a key encoding path + mtime.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
After stageLocalPath copied the file into .pending/, the handler
returned without writing any SQL rows. listFiles({ collection:
'uploads' }) queries ManagedFile with source='upload', so the file
never appeared in the Your uploads panel.

Fix: after runLocalStaging succeeds, call
finalizePendingSessionUploads(STANDALONE_UPLOAD_SESSION_ID,
[attachment]) which runs publishAttachment → completeStagingUpload,
writing uploadFile + uploadVersion + ManagedFile in one transaction.
A new onStandaloneUploadSaved callback lets ipc.ts broadcast
project-files:changed so the renderer refreshes immediately.

Also add STANDALONE_UPLOAD_SESSION_ID = 'standalone-uploads' constant
in shared/uploads.ts for the sessionless publish path.

Tests: 103 passing (all uploads + project-files + ipc suites)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Show the full path with a "This computer" pill under the filename in
  the local preview header.
- Replace the standalone Reveal in Finder button with a reload action
  that remounts the preview so the file is re-read from disk.
- Regroup the "..." menu per the local-file design: a file identity
  header, Copy path, and an "On this machine" group holding Download
  and Save as artifact.
- Let local files download through the existing file:save-managed
  pipeline by accepting the 'local' source.
- Keep header tooltips above the full-screen preview modal (z-[70]) and
  stop the More actions tooltip from reappearing after the menu closes:
  it shows once per hover and stays suppressed until both pointer and
  focus have left the trigger.
- After Save as artifact succeeds, show an in-memory Saved chip at the
  start of the action cluster and hide the menu item.

Tests: 70 passing (file-save, PreviewFileSurface, PreviewPanel,
FilePreviewDialog lifecycle suites)
Save-as-artifact uploads were stored under 'default-project' regardless of
which project the workspace had open. useProjectFilesIndex filters project-
files:changed events by projectId (line 392), so the broadcast was silently
dropped and no reload triggered. listFiles also queries by projectId, so even
a manual refresh found nothing.

Fix: add optional projectId to StageLocalPathUploadRequest; LocalFileHeaderActions
reads activeProjectId from the navigation store and passes it; the IPC handler
uses request.projectId ?? DEFAULT_UPLOAD_PROJECT_NAME so fallback behaviour is
preserved. Broadcast and SQL write now use the same resolved projectId.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The tooltip is no longer wanted on the local preview header's More
actions button, so the button keeps only its aria-label. Removing it
also drops the entire suppression machinery that kept the tooltip from
reappearing after the menu closed (menu open state, suppressed/open
tooltip state, and the pointer/focus handlers on the trigger).

Tests: 13 passing (PreviewFileSurface suite)
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.

1 participant