Files
my-pi/pi-permission-system/docs/plans/archive/0016-delete-vendored-zellij-modal.md
T

169 lines
11 KiB
Markdown

---
issue: 16
issue_title: "Delete vendored src/zellij-modal.ts; rebuild settings UI on pi-tui"
---
# Delete vendored zellij-modal and rebuild settings UI on pi-tui
## Problem Statement
`src/zellij-modal.ts` is 1,117 lines vendored from upstream's sibling `zellij-modal` package.
Its header comment instructs maintainers to "keep this module in sync when upstream zellij-modal primitives change" — the textbook maintenance trap AGENTS.md warns against.
The sole consumer is `src/config-modal.ts`, which uses `ZellijModal` and `ZellijSettingsModal` to render a 3-toggle settings dialog for `yoloMode`, `permissionReviewLog`, and `debugLog`.
The vendored code is unnecessary because `pi-tui` already exports `SettingsList` — a `Component`-conformant class with built-in navigation, value cycling, `onChange`/`onCancel` callbacks, and `updateValue()`.
The entire `ZellijModal` + `ZellijSettingsModal` layer exists only to add border rendering and title-bar chrome around `SettingsList`.
Since `ctx.ui.custom` already provides overlay positioning, the modal chrome can be dropped entirely or replaced with a few lines of `Box` wrapping.
### Why keep the interactive modal at all?
The three settings (`yoloMode`, `permissionReviewLog`, `debugLog`) are simple booleans that an agent could edit in `config.json` directly.
However, toggling a setting via the slash command is instant and free; asking the agent to do it costs a round-trip of token usage.
The interactive UI earns its keep as a zero-cost escape hatch for quick config changes, especially for `yoloMode` which users toggle frequently.
Issue #10 will later consolidate config paths, but the toggle UI remains useful regardless of where the file lives.
## Goals
- Delete `src/zellij-modal.ts` entirely (~1,117 lines removed).
- Rewrite `openSettingsModal()` in `src/config-modal.ts` to use `SettingsList` from `pi-tui` directly, removing the `ZellijModal` / `ZellijSettingsModal` abstraction layer.
- Keep the `/permission-system` slash command name, all subcommands (`show`, `path`, `reset`, `help`), and no-args interactive behaviour unchanged.
- Update `tests/config-modal.test.ts` to remove the `zellij-modal` mock surface and validate the new UI shape.
- Net result: ~1,100 lines removed, zero new vendored code.
## Non-Goals
- Changing the set of configurable runtime knobs (`yoloMode`, `permissionReviewLog`, `debugLog`).
- Changing the `/permission-system` slash command name or any on-disk identity (config directory, log filenames, event channel names).
- Consolidating config paths (#10) — that issue builds on this cleanup.
## Background
### Relevant modules
| File | Role |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `src/zellij-modal.ts` | Vendored modal framework (~1,117 lines). Wraps `pi-tui` `SettingsList`, `Container`, `Box`, `Text` to add border rendering, overlay sizing, title bar, and key handling. |
| `src/config-modal.ts` | Slash command handler. Registers `/permission-system`, dispatches subcommands, and calls `openSettingsModal()` for no-args invocation. |
| `src/extension-config.ts` | Loads/saves `config.json` (the 3-boolean runtime config). No changes needed. |
| `tests/config-modal.test.ts` | Mocks `pi-tui` and `pi-coding-agent` exports; tests completions, subcommands, headless guard, and custom-modal call count. |
### Permission surface
None — this change is purely UI/DX.
No policy semantics, permission surfaces, or merge precedence are affected.
### Key `pi-tui` / `pi-coding-agent` APIs
`SettingsList` from `pi-tui` is the critical primitive.
It already implements the `Component` interface (`render`, `handleInput`, `invalidate`) and accepts:
```typescript
constructor(
items: SettingItem[],
maxVisible: number,
theme: SettingsListTheme,
onChange: (id: string, newValue: string) => void,
onCancel: () => void,
options?: SettingsListOptions,
)
```
- `onChange` fires when a value is toggled — wired to `controller.setConfig()`.
- `onCancel` fires on Escape — wired to the `done()` callback from `ctx.ui.custom`.
- `updateValue(id, newValue)` lets us sync display after a persist round-trip.
`ctx.ui.custom<T>(factory, options)` renders a custom `Component` in an overlay.
The factory receives `(tui, theme, keybindings, done)` and returns the component.
`getSettingsListTheme()` from `pi-coding-agent` provides the `SettingsListTheme`.
## Design Overview
### Approach: `SettingsList` as the sole component
The `ZellijModal` + `ZellijSettingsModal` two-class indirection is replaced by passing a `SettingsList` instance directly to `ctx.ui.custom`.
No `Container`, `Box`, or `Text` wrapper is needed — `SettingsList` is already a self-contained `Component` that handles rendering, input, and invalidation.
The factory passed to `ctx.ui.custom`:
1. Instantiates `SettingsList` with items from `buildSettingItems()`, theme from `getSettingsListTheme()`, and callbacks.
2. Wires `onChange``applySetting()``controller.setConfig()``syncSettingValues()` (same logic as today).
3. Wires `onCancel``done()` (closes the overlay).
4. Returns the `SettingsList` instance directly as the component.
The overlay options on `ctx.ui.custom` handle positioning.
The elaborate border chrome from `ZellijModal` (title bar, help undertitle, rounded corners) is dropped — `SettingsList` already renders its own hint line and description, which is sufficient for 3 toggle items.
### What stays the same
- `registerPermissionSystemCommand()` signature and registration.
- `handleArgs()` — all subcommand handlers (`show`, `path`, `reset`, `help`, unknown).
- `getArgumentCompletions()` — completions for subcommands.
- `buildSettingItems()`, `applySetting()`, `summarizeConfig()` helpers — unchanged in logic.
- `PermissionSystemConfigController` interface.
### What changes
- `openSettingsModal()` body: rewritten (~60 lines → ~20 lines).
- Import block: removes `ZellijModal`, `ZellijSettingsModal`; adds `SettingsList` from `pi-tui` and `getSettingsListTheme` from `pi-coding-agent`.
- `SettingValueSyncTarget` interface: deleted (was an abstraction over `ZellijSettingsModal.updateValue`; `SettingsList.updateValue` is used directly).
- `syncSettingValues()`: parameter type changes from `SettingValueSyncTarget` to `SettingsList` (or inlined, since the function just calls `updateValue` three times).
## Module-Level Changes
### `src/zellij-modal.ts` — deleted
Entire file removed.
### `src/config-modal.ts` — simplified
- Remove import of `ZellijModal`, `ZellijSettingsModal` from `./zellij-modal.js`.
- Add imports: `SettingsList` from `@mariozechner/pi-tui`, `getSettingsListTheme` from `@mariozechner/pi-coding-agent`.
- Delete `SettingValueSyncTarget` interface.
- Rewrite `openSettingsModal()`:
- Create `SettingsList` with `buildSettingItems(current)`, `getSettingsListTheme()`, `onChange`, `onCancel: done`.
- Return it directly as the component from the `ctx.ui.custom` factory.
- Simplify or inline `syncSettingValues()` to call `settingsList.updateValue()` directly.
### `tests/config-modal.test.ts` — mock surface trimmed
- The `pi-tui` mock already stubs `SettingsList` — verify constructor signature matches.
- Add `getSettingsListTheme` to the `pi-coding-agent` mock (currently only mocks `getSettingsListTheme: () => ({})`; confirm this is present and sufficient).
- Existing assertions (completions, subcommand notifications, headless guard, `custom()` call count = 1) should pass without logic changes.
## TDD Order
1. **Baseline.**
Run `npm test -- tests/config-modal.test.ts` and `npm run build` to confirm green.
No commit (baseline verification only).
2. **Delete `zellij-modal.ts` and rewrite `openSettingsModal()`.**
Do both together — there is no useful intermediate state where the vendored file exists but nothing imports it.
- Delete `src/zellij-modal.ts`.
- Update imports in `src/config-modal.ts`.
- Delete `SettingValueSyncTarget` interface.
- Rewrite `openSettingsModal()` to use `SettingsList` directly.
- Simplify `syncSettingValues()`.
- Update test mocks if needed.
- Run full test suite and `npm run build`.
- Commit: `feat: replace vendored zellij-modal with direct pi-tui SettingsList (#16)`
3. **Docs.**
Scan `README.md`, `AGENTS.md`, `docs/` for references to `zellij-modal.ts` and remove them.
- Commit: `docs: remove zellij-modal references (#16)` (skip if none found)
## Risks and Mitigations
| Risk | Mitigation |
| ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Could this silently weaken a permission?** | No. This change is purely UI/DX for the `/permission-system` settings command. No permission decision logic is touched. |
| **`SettingsList` constructor signature mismatch.** | Verified from `pi-tui` type declarations above. The vendored code already uses `SettingsList` internally with the same constructor shape. |
| **Visual regression.** Losing the `ZellijModal` border chrome (title bar, rounded corners, help undertitle). | Acceptable. `SettingsList` renders its own hint line and item descriptions. With only 3 items the chrome was decorative, not functional. If needed later, `Box` can add a border in a few lines. |
| **Test mock drift.** | The `pi-tui` mock already stubs `SettingsList`. TDD step 2 verifies mocks before committing. |
| **On-disk identity change.** | None. Command name stays `/permission-system`; config directory, log filenames, and event channels are untouched. |
## Open Questions
None — the design is straightforward.
The only aesthetic question (border chrome vs. bare `SettingsList`) is answered by "start simple, add later if needed."