- TypeScript 100%
| client | ||
| server | ||
| shared | ||
| .dprint.json | ||
| .gitignore | ||
| index.client.tsx | ||
| index.server.ts | ||
| package-lock.json | ||
| package.json | ||
| paseo-plugin.json | ||
| README.md | ||
| tsconfig.json | ||
| tsconfig.server.json | ||
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
MutationObserverwatches the app DOM, scoped by thefiles-pane-headerandworkspace-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, andhandleRefreshhandlers.
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.tsstill 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
rpctargets 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.