Disabling ext-toggle through its own /ext UI would rename the file to
ext-toggle.ts.off, which pi's auto-discovery skips, so the next /reload
silently drops the /ext command itself. The .ts.off rename also
persists across pi restarts and \u2014 in containerized setups
(opencode-devbox) where ~/.pi is mounted on the devbox-pi-config named
volume \u2014 across container recreate, so even nuking the container
doesn't recover the surface.
Add ext-toggle to the existing DISABLE_GUARDS map so the toggle is
refused at stage-time with an explanation pointing at the manual
recovery path:
mv ~/.pi/agent/extensions/ext-toggle.ts.off \
~/.pi/agent/extensions/ext-toggle.ts
Same shape as the existing ssh-controlmaster guard. Sibling slash
commands (/mcp from mcp-loader) don't need this guard because
settings.json is their source of truth and remains editable by hand
even if the loader is disabled \u2014 only ext-toggle's own UI is the
single point of management.
Replaces the single-pick + immediate-apply flow with a SettingsList
overlay where:
- ↑/↓ navigate
- space stages a toggle (●/○ flip in-place; not yet applied)
- enter commits all staged renames at once and triggers ctx.reload()
- esc cancels, no changes applied
Implementation: ctx.ui.custom() builds a Container with header, a
SettingsList (which cycles values on space), and a footer status line
showing pending changes (e.g. 'pending: notify→off, foo→on'). The
wrapper's handleInput intercepts Enter via matchesKey(data, Key.enter)
before SettingsList sees it — SettingsList would otherwise consume
Enter for cycling.
Disable guards still fire on the space-stage attempt: a refused toggle
is reverted via settingsList.updateValue and the reason shown in the
footer. ssh-controlmaster guard during --ssh therefore now refuses at
stage time, not commit time — clearer feedback.
Subdir extensions render as read-only rows (no , so SettingsList
will not cycle them).
Batches multiple toggles into a single ctx.reload() instead of one
reload per change, which was awkward when flipping several at once.
Disabling ssh-controlmaster mid --ssh session would tear down the
ControlMaster (if we own it) and silently redirect read/write/edit/bash
back to the local filesystem while the system prompt still claims we're
on the remote. Now blocked with an explanatory dialog.
Implementation: a DISABLE_GUARDS map keyed by bare extension name lets
specific extensions register a refusal predicate. ssh-controlmaster's
guard checks process.argv for --ssh and refuses if present. Easy to
extend with similar foot-guns later.
extensions/ext-toggle.ts:
/ext lists ~/.pi/agent/extensions/ with active/disabled markers
and toggles individual extensions by renaming between name.ts and
name.ts.off (pi only auto-discovers *.ts). Calls ctx.reload() so the
change takes effect without restarting pi.
Subdirectory-style extensions (name/index.ts) are listed read-only
in v1 — toggling a directory cleanly is more work than the rename
trick is worth.
install.sh:
--uninstall now matches both *.ts and *.ts.off symlinks pointing
into this repo, so a disabled extension is still cleaned up.
README.md / AGENTS.md:
Document ext-toggle alongside the others; AGENTS notes the API
surface used (registerCommand, ui.select/confirm/notify, reload)
and the rename-not-delete design decision.