--- issue: 573 issue_title: "[FEATURE REQUEST] Keybinds for approve/deny/deny with reason" --- # Retro: #573 — Inline keybind permission dialog ## Stage: Planning (2026-07-13T00:00:00Z) ### Session summary Planned Phase 11 Step 4: an inline `ctx.ui.custom` permission dialog with `y`/`s`/`n`/`r` hotkeys, gated on `ctx.mode === "tui"` with the `select`/`input` flow preserved for RPC (the #519 constraint). The design splits a pure decision model (`permission-prompt-decision.ts`) from a thin TUI component (`permission-prompt-component.ts`), adds a default-on `doublePressToConfirm` config toggle, and makes deny-with-reason a mandatory inline editor sub-step with back-navigation. Committed the plan; no follow-up issues filed (deferred items are unfiled by design). ### Observations - This is a third-party issue (author `Hex4C` ≠ `gotgenes`), but it was already scheduled in the operator's own roadmap (Phase 11 Step 4), so the `ask-user` direction gate largely confirmed the roadmap design. - The operator extended the raw request in two rounds of `ask-user`: (1) double-press-to-confirm on **all four** hotkeys, a **default-on** config toggle, modeled on pi-ask's pure `resolveReviewShortcutDoublePress`; (2) deny-with-reason reason is **mandatory** (empty rejected), `esc` = back to the decision list. - Keybinding scheme resolved to the roadmap's `y`/`s`/`n`/`r` (over the issue's `y`/`A`/`n`/`tab`) — avoids `tab` (a navigation key) and uses mnemonic lowercase. - Tension noted and accepted: #573's motivation was *fewer* keypresses, but the operator chose double-press default-on for safety; the operator's call overrides the original motivation. - Enter semantics decided by the planner (not re-asked): enter confirms the highlighted option in a single press; the double-press toggle governs only the **letter-hotkey** fast path. Navigate-then-enter is already two deliberate keystrokes. - Classified **non-breaking** (`feat:`, not `feat!:`): the TUI interaction changes on upgrade, but the `PermissionPromptDecision` contract, gate outcomes, and config surface are additive/unchanged; the new toggle defaults on and is disableable. - SDK verification: `ctx.ui.custom` renders inline by default and works only in TUI (`hasUI` is also true for RPC), so the gate is `ctx.mode === "tui"`, not `hasUI`. `config-modal.ts` is the in-package `ctx.ui.custom` precedent; `@earendil-works/pi-tui` is already a `devDependency`. - `requestPermissionDecisionFromUi` is **retained** as the RPC fallback (lift-and-shift), so no consumer/doc breaks — only the injected seam re-points to a new `requestPermissionDecision` dispatcher. - `doublePressToConfirm` threaded as a live **getter** (not an activation snapshot) so the `/permission-system` settings-modal toggle takes effect on the next prompt. ## Stage: Implementation — TDD (2026-07-13T20:20:00Z) ### Session summary Implemented the inline keybind permission dialog across 5 TDD cycles: a pure decision model (`permission-prompt-decision.ts`), the `doublePressToConfirm` config toggle, the inline `ctx.ui.custom` component (`permission-prompt-component.ts`), the mode-dispatch wiring, and the architecture-doc completion. Test count grew from 2418 to 2455 (+37); full suite, `pnpm run check`, root lint, and `pnpm fallow dead-code` all green. ### Observations - **Design deviation (ISP + cycle avoidance):** the plan said to widen `PermissionDecisionUi` with `custom` and put the dispatcher in `permission-dialog.ts`. Instead I kept `PermissionDecisionUi` narrow (`select`/`input`) and added a separate `PermissionPromptUi = Pick` plus the `requestPermissionDecision` dispatcher in `permission-prompt-component.ts`. This avoids a `permission-dialog.ts` ↔ component import cycle and is more ISP-faithful; `permission-dialog.ts` was consequently **not** modified. - **Tidy-first prep skipped:** the assessor's one recommended commit (a `makeStubUi` fixture) was predicated on widening the shared `PermissionDecisionUi` — the ISP deviation above made it low-value (only the 2 `local-user-authorizer.test.ts` sites broke, and they were rewritten in the wiring step anyway). - **Config field forced call-site updates early:** making `doublePressToConfirm` a required field of `PermissionSystemExtensionConfig` broke three literal constructions (`config-modal.ts` `cloneDefaultConfig`, `config-modal.test.ts`, `config-reporter.test.ts`), fixed in the same step-2 commit per the required-field rule. - **Options are always `[y, s, n, r]`:** the plan's speculative "s absent" edge case does not occur — the fallback dialog always offers the session option, so the model mirrors that. - **`matchesKey` works on raw strings** (verified via a throwaway probe): arrows `\u001b[A/B`, `\r` enter, `\u001b` escape, `\u007f` backspace all resolve, so the component maps keystrokes without a live terminal, and the pi-ask fake-`tui`/`plainTheme`/`done` harness tests the component end-to-end. - **Pre-completion reviewer: WARN** — one non-blocking finding: the plan asked to bump the `Inline prompt component files` health-metric row `0 → 1`. Deliberately **not** done: that column is the dated `Baseline (2026-07-12)` snapshot, and the already-landed Phase 11 steps 1–3 left their achieved rows' baselines untouched too (the table is recomputed at phase close, not per-step). Editing only this row would make the baseline column internally inconsistent; the `✅` step heading + Mermaid node are the completion markers. ## Stage: Final Retrospective (2026-07-14T01:30:00Z) ### Session summary One continuous session carried #573 from `/plan-issue` through `/tdd-plan`, `/ship-issue`, and this retro: an inline keybind permission dialog for TUI sessions, released as `pi-permission-system` `20.7.0`. Five TDD cycles landed with zero rework commits and no test failures (+37 tests, 2418 → 2455); the whole plan→ship arc was clean. ### Observations #### What went well - **Test-Driven Design paid off.** Splitting the branch-heavy interaction logic into a pure `reducePrompt` model (no SDK/TUI imports) and a thin `ctx.ui.custom` adapter made every decision path (double-press arming, reason validation, scope step, esc transitions) unit-testable without a live terminal. A throwaway `matchesKey`-on-raw-strings probe confirmed the pi-ask fake-`tui`/`plainTheme`/`done` harness would work before committing to it. - **The `ask-user` gate surfaced real scope.** The raw third-party issue asked only for `y`/`n`/`tab`/reason keybinds; three planning `ask-user` rounds drew out the operator's double-press-to-confirm affordance (default-on toggle) and the mandatory-reason rule — substantial behavior the issue never stated. The operator explicitly appended "I also want to improve the flow" to an answer, which the interactive flow invited. - **An implementation-time design refinement improved on the plan.** The plan's "widen `PermissionDecisionUi` + dispatcher in `permission-dialog.ts`" would have created a `permission-dialog.ts` ↔ component import cycle; catching it during TDD and refining to a narrow `PermissionDecisionUi` + a `Pick`-based `PermissionPromptUi` (ISP) left `permission-dialog.ts` untouched and shrank the diff. - **The ship `UNSTABLE` guidance worked as written.** `release_pr_merge` refused with `merge_state: UNSTABLE`; the `statusCheckRollup` check was `IN_PROGRESS` (not the empty-rollup `GITHUB_TOKEN` case), so waiting two polls for it to finish and then retrying `release_pr_merge` was exactly the prompt's prescribed path — no premature `gh pr merge` fallback. #### What caused friction (agent side) - `missing-context` — the plan's Module-Level Changes instructed "update the health-metric row from `0` to `1`," but that column is the dated `Baseline (2026-07-12)` snapshot, recomputed at phase close, not a per-step live value (the already-landed Phase 11 Steps 1–3 left their achieved rows' baselines untouched). Following the instruction would have made the baseline column internally inconsistent. Impact: one pre-completion-reviewer WARN and a judgment call to decline the edit; no rework, but the plan (self-authored) committed to a doc edit without checking the table's own convention. - `other` (path slip) — a `README.md` `Edit` used the absolute path `/Users/chris/development/pi/pi-permission-system/README.md`, dropping the `pi-packages/packages` segment, and returned `ENOENT`. Impact: one wasted tool call, self-corrected immediately with the relative path. #### What caused friction (user side) - None. The operator's extended requirements emerged naturally through the planning `ask-user` rounds — the right place for a third-party issue where the operator's own preferences (double-press, mandatory reason) are not in the issue body. ### Diagnostic details - **Model-performance correlation** — both subagent dispatches (`tidy-first-assessor`, `pre-completion-reviewer`) ran on `anthropic/claude-sonnet-5`, appropriate for their judgment-heavy work (preparatory-refactor assessment, quality review); no reasoning-weak-on-judgment or high-cost-on-mechanical mismatch. - **Escalation-delay tracking** — no `rabbit-hole` friction; the longest same-error sequence was 1 retry (the `README.md` path slip). - **Feedback-loop gap analysis** — verification ran incrementally: each TDD step ran its affected test file (red→green), `pnpm run check` after every shared-interface/config change, and the full suite + root lint + `pnpm fallow dead-code` after the last step. No end-only verification gap. ### Follow-up: orphaned mid-batch issue #580 During this retro the operator noticed #580 (Phase 11 Step 2, `shellTools` config model) was still **open** despite being released in `pi-permission-system-v20.5.0`. It was not #573's responsibility to close it — the two issues are unrelated and #580's commits predate #573's `v20.6.0` ship baseline. Root cause: #580 was a **mid-batch** member of the "shell-tool-aliases" batch (tail #574); `/ship-issue` step 4b couples the issue **close** to the release **defer** decision ("leave the issue open and skip steps 5–6"), and the batch-tail (#574) ship's stacked-issue check only scans `..HEAD`, which excludes a sibling already released below the tag. So the issue orphaned. Closed #580 manually with a summary; filed #586 to fix the ship-flow gap (two candidate fixes: decouple close from release in step 4b, or have the batch-tail ship enumerate and close its batch siblings). ### Changes made 1. `.pi/skills/package-pi-permission-system/SKILL.md` — clarified that a dated `Baseline ()` health-metric column is a fixed phase-open snapshot recomputed at phase close, not a per-step value (resolves the pre-completion WARN's root cause; `Refs #573`). 2. Closed #580 (orphaned mid-batch issue) with an implemented-in/released-in summary. 3. Filed #586 — the `/ship-issue` mid-batch-close gap that orphaned #580. 4. `packages/pi-permission-system/docs/retro/0573-inline-keybind-permission-dialog.md` — this Final Retrospective stage entry.