Files
my-pi/pi-permission-system/docs/retro/0573-inline-keybind-permission-dialog.md
T

11 KiB
Raw Blame History

issue, issue_title
issue issue_title
573 [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<PermissionPromptDecision> 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 Hex4Cgotgenes), 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<ExtensionUIContext, "select" | "input" | "custom"> 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 13 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 13 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 56"), and the batch-tail (#574) ship's stacked-issue check only scans <pkg-tag>..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 (<date>) 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.