--- 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(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."