1
0
Fork 0
No description
  • TypeScript 100%
Find a file
2026-09-28 18:39:24 +00:00
client feat: file uploads 2026-09-28 18:39:24 +00:00
server feat: file uploads 2026-09-28 18:39:24 +00:00
shared feat: file uploads 2026-09-28 18:39:24 +00:00
.dprint.json feat: basics 2026-09-25 20:42:27 +00:00
.gitignore feat: basics 2026-09-25 20:42:27 +00:00
index.client.tsx feat: file uploads 2026-09-28 18:39:24 +00:00
index.server.ts feat: file uploads 2026-09-28 18:39:24 +00:00
package-lock.json feat: file uploads 2026-09-28 18:39:24 +00:00
package.json feat: file uploads 2026-09-28 18:39:24 +00:00
paseo-plugin.json feat: file uploads 2026-09-28 18:39:24 +00:00
README.md feat: file uploads 2026-09-28 18:39:24 +00:00
tsconfig.json feat: file uploads 2026-09-28 18:39:24 +00:00
tsconfig.server.json feat: file uploads 2026-09-28 18:39:24 +00:00

Paseo Better Explorer

A Paseo plugin that improves the built-in Files explorer:

  • Collapse all — a button in the Files toolbar, to the left of the toolbar's own controls, that collapses every open folder.
  • Upload file — a button next to Collapse all that opens the client file picker and writes the picked file into the workspace root.
  • Reveal active file — when the selected tab becomes a file, the Files tree expands the file's ancestors, selects the file, and scrolls to it, even when its folder is collapsed.

What it does

Collapse all leaves only the workspace root open. It drives the explorer's own collapse handler, so the expanded-folder set and its persistence behave exactly as if each folder's context menu had been used.

Upload file puts the file in the workspace root, matching the toolbar's own New file / New folder actions. It never overwrites: a name collision becomes name (1).ext. After the write it refreshes the tree, so the file appears without a manual refresh.

Reveal active file runs on every mounted Files pane of the tab's workspace. It expands each missing ancestor (which loads that folder's listing), selects the file, and scrolls the tree to the row. Re-selecting the same file does not re-run it. On mobile, where an inactive Files tab is unmounted, reveal runs when the pane is back on screen only if its tab is still the selected one.

Why it reaches into the DOM

The plugin SDK has no file explorer toolbar contribution, no access to the explorer's expansion state, and no active-file input. A plugin cannot add a control to the built-in toolbar, reveal a file, or write into a workspace through the public client API.

This plugin therefore integrates one level lower, the same way paseo-localized-shortcuts integrates at the event level:

  • A throttled MutationObserver watches the app DOM, scoped by the files-pane-header and workspace-tab-file_* test ids.
  • It injects the Collapse all and Upload file buttons into the toolbar.
  • It walks the React tree behind the toolbar, reads the expanded-path set and the row list, and calls the pane's own onCollapseDirectory, onEntryPress, onSelectEntry, and handleRefresh handlers.

Collapse all and reveal trigger the same code paths the rows and context menus do. Upload cannot: the client has no filesystem access and the SDK exposes no file-write RPC, so the bytes are sent to the plugin's own index.server.ts, which runs in the daemon subprocess and writes them with Node fs.

Layout

File Owns
client/web.ts Entry point: the observer, the scan throttle, and feature wiring.
client/collapse.ts Collapse all: the collapse action.
client/upload.ts Upload file: the toolbar button and the RPC call.
client/upload-picker.ts The client file picker (<input type="file">).
client/reveal.ts Reveal active file: ancestor expansion, selection, and scroll.
client/toolbar-button.ts Shared toolbar-button chrome, measured from an existing control.
client/explorer.ts Locating the Files pane and its tree rows from the toolbar DOM.
client/fiber.ts React fiber access (getFiber, subtree and ancestor search).
client/dom.ts Browser globals (declared once) and typed accessors.
shared/upload.ts The upload RPC contract and size cap.
server/upload.ts The daemon-side write: path checks, unique name, writeFile.
index.server.ts Registers the upload handler.

Scope

  • Web and Electron only. On iOS and Android the plugin is a no-op; index.server.ts still registers, but nothing calls it.
  • Both buttons act only while a Files pane is mounted.
  • The toolbar buttons match the existing controls: the same 20x20 frame (32x32 on compact), 14px glyph (18px on compact), border radius, and hover highlight.
  • Upload caps at 10 MiB (MAX_UPLOAD_BYTES) per file. The RPC payload is JSON, so bytes travel base64-encoded; larger files are rejected instead of being buffered. The server re-checks the decoded size.
  • Upload assumes a single configured host. The button comes from the first plugin installation that scans a toolbar, and its rpc targets that installation's daemon; with several hosts the file can be written to the wrong one. Collapse all and reveal are client-only and have no such limitation.

Dependencies on Paseo internals

Neither the test ids nor the fiber shape are a public contract. A Paseo change that renames the explorer or tab test ids, changes the data-testid mapping, or moves the explorer state out of the pane component breaks this plugin. When a matching step fails the affected feature does nothing and logs a warning; the plugin never throws into Paseo's render or event loop.

Built against Paseo 0.9.1.

Install

The plugin is installed from Git:

paseo plugin install git:https://git.kosyrev.name/zoo/paseo-better-explorer.git
paseo plugin ls

Update later:

paseo plugin update paseo-better-explorer

Plugins are trusted, unsandboxed code. Read the source before installing.

Development

npm install
npm run typecheck
npm run format
paseo plugin reload paseo-better-explorer
paseo plugin logs paseo-better-explorer

npm run typecheck runs twice: the client config (tsconfig.json) and the server config (tsconfig.server.json). They are split because server code needs the Node types and client code must not see them.

Paseo compiles the TypeScript itself. The development dependencies exist only for local typechecking. Formatting uses dprint with the config in .dprint.json; dprint must be on PATH.