ADR 0241: Ship the file view as a vendored, updatable plugin
- Status: Accepted (supersedes ADR 0105; issue #304)
- Date: 2026-09-13
- Deciders: PI-Desktop core
- Related: ADR 0104 · ADR 0105 · ADR 0109 · ADR 0111 · ADR 0169 · 07-plugins/07-plugin-marketplace
Context
The bundled pi.files view (ADR 0105) browsed and previewed the workspace and nothing else: no editing, no image, CSV, JSON, or SQLite surface, and a line cap on text preview. Anything more than looking at a file meant leaving the app.
pi.file-manager is a third-party plugin that covers exactly that surface on the same public channel — contributes.views, a sandboxed page over pluginBridge, ui.view plus fs.read with root workspace and scope **, no host-only capability. It is also published through the official marketplace.
Shipping it raises two questions this ADR answers: how the editor's writes are mediated, and how a copy inside the application stays consistent with the copy in the marketplace.
Decision
- The bundled file view is
pi.file-manager, vendored intoapps/desktop/resources/plugins/. The vendored files are the upstream release's own artifacts, byte for byte, except for alicensefield added to the manifest;UPSTREAM.mdrecords the repository, tag, commit, and checksums.pi.filesis removed, and a build that stops shipping a bundled plugin leaves no orphan registry row (ADR 0104). - It declares only
ui.viewandfs.read(root: workspace,scope: ["**"]) — the same permissions the view it replaces declared. The page is sandboxed and reaches the host only through the public bridge. - Editing writes are the plugin's own: its host process keeps the path jail, atomic writes,
mtime/size conflict detection, and the write audit. The host permission gateway does not mediate them. A manifest cannot express a whole-tree write, and this host does not sandbox a plugin process'sfsaccess to begin with, so bundling the plugin grants no capability a third-party plugin does not already have. The boundary is stated in the manifest'ssafetyNotesand is part of why this record exists. - Bundled means default and non-removable, not frozen. A bundled plugin cannot be uninstalled, but it can be updated from the marketplace, and that update survives the next launch:
sync_builtinkeeps an installed row whose source is notbuiltinfor as long as this build does not ship a strictly newer version. An app update still reaches a user who never installed anything, and a stale install cannot pin the plugin.uninstallrefuses by id against the set of plugins this build ships, rebuilt from disk on every launch. An updated bundled plugin stays uninstallable, and a plugin this build stops shipping becomes removable again.
- A catalog version is offered as an update only when it is strictly newer than what is installed. Equality is not an update, and an older catalog entry is not one either — it would be a downgrade wearing the update affordance.
- The panel title is the manifest's localized title, as for any plugin. The renderer does not carry a label for it, and no view id is hardcoded.
Consequences
- The work panel's file surface is a vendor copy, so the application's own release cycle no longer decides when the view improves; upstream releases do, and users can take them without waiting for an app update.
- Maintaining the copy is a deliberate act:
UPSTREAM.mdandapps/desktop/test/bundled-plugins.test.mjspin the vendored release, so a silent edit or a half-done re-sync fails the build. - "Cannot be uninstalled" is now separate from "came from
resources/plugins".PluginSummary.bundledcarries that distinction; it is recomputed on every launch and defaults to false in registries written before it existed. - The marketplace keeps listing a bundled plugin and offers its updates. That entry is the supported path for a newer version, not a confusing duplicate.
- A plugin the user updated keeps a marketplace/
localsource while remaining bundled. Anything that reasons aboutsource == "builtin"must not be used to answer "is this protected" — see the registry rules above. - The bundled view's filesystem reach is the plugin's own; a defect in the plugin's path jail is not caught by the host gateway. This is the same trust position as any installed plugin, taken deliberately in exchange for a capable, updatable file view.
Alternatives considered
Reimplement the missing viewers and editing in pi.files
Rejected: it duplicates a maintained third-party plugin, and the line cap, media viewers, structured viewers, and SQLite browsing are a large surface to keep in the host. The public channel exists so first-party surfaces do not have to live there.
Bundle it read-only and leave editing to the marketplace copy
Rejected: the same plugin cannot offer two capability levels without forking it, and a read-only default would leave the most common request — change this line — unanswered. The write path is unchanged either way.
Let the bundled copy always win, with no updates
Rejected: a user could then only get a newer upstream release by updating the application, which defeats the point of vendoring a published plugin. The strictly-newer rule keeps both directions honest.
Hide the marketplace entry for a bundled id
Rejected: the entry is where a newer upstream release is published. Hiding it would remove the update path this ADR establishes.