# File Explorer

> Browse the local filesystem like Windows Explorer: navigate folders, open files, filter by file type, and manage files (new folder, rename, delete, copy/cut/paste, upload, download).

| | |
|---|---|
| Version | 1.5.2 |
| Category | Productivity |
| Permissions | filesystem.read, filesystem.write |
| Depends on | ai-chat |

# File Explorer

Browse the local filesystem like Windows Explorer: navigate folders, open files, filter by file type, and manage files (new folder, rename, delete, copy/cut/paste, upload, download).

**Type:** `backend` — see `.claude/skills/kazuo-create-plugin/references/plugin-examples.md`

## Files

- `manifest.json` — metadata, `defaultSettings`, `defaultWidgetConfig`
- `widget.html` / `js/widget.js` / `css/widget.css` — main UI
  - has its own "⚙" button + settings overlay built into widget.html/widget.js (host no longer provides a per-widget gear icon — see references/plugin-runtime-api.md section 1b)
- `settings.html` / `js/settings.js` / `css/settings.css` — same fields as the widget overlay (intentionally duplicated), used by the Plugin Manager screen and the pop-out window
- `locales/en.json5` / `locales/vi.json5` — UI strings, `widget.*`/`settings.*` namespaces
- `main/index.ts` (+ compiled `main/index.js`) — runs in the Electron main process; `activate`/`deactivate` + optional `getMcpTools` (exposed to ai-chat's AI agent, Dev Mode only)

## Default settings

- `showHidden` — default: `false` — show dotfiles/dot-folders (names starting with `.`) in the list
- `allowedCallers` — default: `[]` — plugin IDs granted access to the `public:*` API below
- `pendingRequests` — default: `[]` — `{ pluginId, requestedAt }` entries awaiting Grant/Deny in Settings

## Backend channels (`plugin:file-explorer:*`, main/index.ts)

### Private (only this plugin's own widget/settings call these)

All take `pluginId` as the first arg (`ensureIsSelf` guard). Reached from the widget via `invoke()`:

- `home` → `{ path }` — the user's home directory (`os.homedir()`)
- `list(dirPath, showHidden)` → `{ path, parent, entries[] }` — directory listing; each entry is `{ name, path, isDir, size, mtime, ext }`, folders first then A→Z. Symlinked directories that fail an `opendirSync` probe (Windows' legacy XP-compat junctions like `SendTo`/`Cookies`/`Recent`, deny-ACL'd) are excluded outright, matching real Explorer. If the directory itself can't be read (`EPERM`/`EACCES`), throws a clear "Access denied" error instead of a raw Node error.
- `open(targetPath)` — open a file/folder with the OS default app (`shell.openPath`)
- `mkdir(parentPath, name)` — create a folder (auto "name (2)" on collision)
- `rename(targetPath, newName)` — rename in place
- `trash(paths[])` → `{ ok, count, failed: [{ path, reason: 'in-use' | 'other', message }] }` — move to the Recycle Bin
  (`shell.trashItem`, recoverable — never a permanent delete). Every item is tried even if an earlier one fails
  (a file locked by another program makes Windows answer "Operation was aborted"). `public:trash` still throws
  when anything failed, after trying them all.
- `paste(sources[], destDir, mode)` — `mode` is `'copy'` or `'cut'`; auto-renames on collision, guards against moving a folder into itself, handles cross-device moves (EXDEV) by copy+remove
- `prepare-download(paths[])` → `{ path, name }` — one plain file is returned as-is; a folder or several items are zipped (host `ctx.modules.archiver`) into `os.tmpdir()/kazuo-file-explorer/` (pruned after 1 hour). The widget then streams it via the bridge's `serveLocalFile(path)` + `?download=<name>`.
- `upload-begin(destDir, name)` → `{ dest, part, exists }` — `part` is a `<name>.<rand>.part` sibling the bytes go to; `exists` lets the widget ask before overwriting. Throws if a folder has that name.
- `upload-commit(part, dest)` — rename the finished part onto `dest` (replaces an existing file). `upload-abort(part)` — delete it.
- `info(path)` → `{ name, path, isDir, size, created, modified, accessed, readOnly, hidden, image: {width,height}|null }`
  — Properties panel. Hidden/read-only come from `attrib` on Windows; image size is read from the file header only.
- `dir-size-start(paths[])` → job id; `dir-size-poll(id)` → `{ size, files, dirs, skipped, done }` | null;
  `dir-size-cancel(id)`. Background async walk (no symlink following); stops by itself after 10s without a poll.
- `thumbnail(path)` → small data URL (longest side 160px) or `null` — used by the web dashboard, which can't load
  `app://local-file`. OS thumbnailer first (async, `nativeImage.createThumbnailFromPath`, Windows/macOS), then
  `nativeImage.createFromPath`.

Double-clicking an image opens an in-widget viewer (←/→ to step, Esc to close); right-click → "Open with default app"
still hands it to the OS.

### Upload / download

File bytes never go through `invoke()`. Download: `prepare-download` → `serveLocalFile(path)` → hidden iframe on
`<url>?download=<name>` (server answers `Content-Disposition: attachment`; desktop shows the OS Save dialog).
Upload (toolbar button, blank-area right-click, or drag-and-drop from the OS): `upload-begin` → bridge
`prepareUpload(part)` → POST the file in 512 KiB chunks to `<url>?offset=N` → `upload-commit`. Chunked because
the cloud relay carries a request body inline in one WebSocket frame. Needs a host with `prepareUpload` and
`ctx.modules.archiver` (added together with this plugin version).

### Public (other plugins may call these — cross-plugin filesystem API)

Same underlying logic as the private channels above (`doOpen`/`doMkdir`/`doRename`/`doTrash`/`doPaste`/`listDir`
are shared functions, not duplicated). Reached by any plugin via
`invoke('plugin:file-explorer:public:<action>', '<your-plugin-id>', ...args)`:

- `public:capabilities` → `{ ok, actions: [...] }` — always open, no gate; lists what's callable.
- `public:requestAccess(callerId)` → `{ ok, status: 'granted' | 'pending' }` — always open. If `callerId`
  isn't already allowed, queues it into `pendingRequests` and logs it (`ctx.log`) so it shows up in
  Dev Mode. The user must open File Explorer's own Settings and press **Grant** before that plugin can
  call anything below.
- `public:home`, `public:list`, `public:open`, `public:mkdir`, `public:rename`, `public:trash`, `public:paste`
  — same signatures as their private counterparts, but every call runs `ensureAuthorizedCaller(callerId)`
  first: throws unless `callerId` is in `allowedCallers`.

**Security note — this is a soft, opt-in gate, not a hard boundary.** `callerId` is whatever the calling
plugin's own renderer code chooses to pass to `invoke()` — the host does not verify it (same limitation
`ensureIsSelf` has, see `.claude/skills/kazuo-create-plugin/references/plugin-runtime-api.md` section 4c).
Granting access stops accidental/well-behaved callers from touching the filesystem before the user
approves them, and gives the user visibility into who's allowed — it does **not** stop a plugin that
deliberately lies about its own id to impersonate an already-granted one.

**Grant/Deny/Revoke UI:** duplicated in both `widget.html`'s settings overlay and `settings.html` (same
`settings.pendingTitle`/`allowedTitle` locale keys), rendering `pendingRequests`/`allowedCallers` as plain
lists with Grant/Deny/Revoke buttons that call `setSettings(...)` directly — no extra IPC channel needed
for the human side, since granting is just editing this plugin's own settings. Because a backend-initiated
`ctx.setSettings` call (e.g. from another plugin's `requestAccess`) doesn't push a live update to an
already-open Settings panel, the panel re-fetches `getSettings()` every time it's opened, so new requests
show up next time it's reopened rather than instantly.

**Web Dashboard:** the backend channels above work fine there too — the Web Dashboard is a browser
tab connected via WebSocket to the SAME running desktop app (`electron/dashboard-server.ts`), so
`ctx.ipcMain.handle` still runs on that machine's real filesystem. The one thing that doesn't work
remotely is the native "Choose folder" dialog (`openFileDialog` — a browser can't pop a local OS
picker for a machine it isn't running on): the widget catches that rejection and falls back to typing
the path manually, same pattern as the git-manager plugin's "Open repository".

## Test

Open Kazuo AI → Plugin Manager — this plugin appears automatically (the host scans `plugins-library/` on its own, no registration needed) → Install → Enable.

After editing `main/index.ts`, recompile before testing: `npx tsc -p tsconfig.plugins.json` (run from the project root).
