gentle-pi 3.2.1 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +63 -59
  2. package/assets/orchestrator-delegation.md +1 -1
  3. package/docs/assets/brand/gentle-shell-banner.gif +0 -0
  4. package/docs/assets/diagrams/odd-workflow.svg +74 -0
  5. package/docs/assets/features/agents-view.png +0 -0
  6. package/docs/assets/features/changes-view.png +0 -0
  7. package/docs/assets/features/command-palette.png +0 -0
  8. package/docs/assets/features/profiles-routing.png +0 -0
  9. package/docs/gentle-shell.md +52 -15
  10. package/docs/readme-reference.md +67 -7
  11. package/docs/review-integration.md +22 -17
  12. package/extensions/ask-user-question.ts +210 -0
  13. package/extensions/gentle-agents.ts +93 -18
  14. package/extensions/gentle-ai.ts +180 -37
  15. package/extensions/gentle-shell.ts +476 -39
  16. package/extensions/gentle-todo.ts +19 -1
  17. package/extensions/quiet-tools.ts +28 -5
  18. package/extensions/startup-banner.ts +25 -10
  19. package/lib/agents-view.ts +41 -14
  20. package/lib/agents-widget.ts +84 -13
  21. package/lib/animation-policy.ts +52 -0
  22. package/lib/background-cache-warming.ts +38 -0
  23. package/lib/command-palette-catalog.ts +2 -0
  24. package/lib/double-esc-cancel-policy.ts +138 -0
  25. package/lib/inprocess-reviewer.ts +297 -0
  26. package/lib/native-review-cli.ts +50 -10
  27. package/lib/odd-runtime-delegation-gate.ts +88 -0
  28. package/lib/questionnaire/questionnaire-view.ts +603 -0
  29. package/lib/questionnaire/schema.ts +82 -0
  30. package/lib/questionnaire/validate.ts +141 -0
  31. package/lib/review-candidate-view-owner.ts +20 -5
  32. package/lib/review-candidate-view.ts +9 -2
  33. package/lib/review-host-relay.ts +256 -171
  34. package/lib/review-integration-v2.ts +114 -27
  35. package/lib/shell-bar.ts +163 -75
  36. package/lib/shell-card.ts +19 -9
  37. package/lib/shell-changes-view.ts +43 -5
  38. package/lib/shell-changes.ts +92 -5
  39. package/lib/shell-hover.ts +39 -0
  40. package/lib/shell-prompt.ts +10 -1
  41. package/lib/shell-sidebar-layout.ts +118 -16
  42. package/lib/shell-sidebar.ts +16 -0
  43. package/lib/shell-todo.ts +7 -1
  44. package/lib/shell-usage-view.ts +103 -12
  45. package/lib/shell-usage.ts +120 -6
  46. package/package.json +1 -1
  47. package/runtime/native-review-cli.mjs +49 -9
  48. package/runtime/review-integration-v2.mjs +114 -27
  49. package/scripts/gentle-ai-installer.mjs +10 -10
  50. package/scripts/maintainer/provider-relay-matrix.mjs +118 -47
  51. package/scripts/verify-package-files.mjs +3 -4
  52. package/tests/agents-grouping.test.ts +75 -18
  53. package/tests/agents-view.test.ts +28 -18
  54. package/tests/agents-widget.test.ts +100 -12
  55. package/tests/animation-policy.test.ts +42 -0
  56. package/tests/ask-user-question.test.ts +435 -0
  57. package/tests/background-cache-warming.test.ts +60 -0
  58. package/tests/background-subagents.test.ts +68 -0
  59. package/tests/command-palette.test.ts +10 -0
  60. package/tests/devbinary/pi-host-relay.devtest.ts +176 -138
  61. package/tests/double-esc-cancel-policy.test.ts +194 -0
  62. package/tests/gentle-agents.test.ts +599 -7
  63. package/tests/gentle-ai-binary.test.ts +1 -1
  64. package/tests/gentle-ai-installer.test.ts +47 -47
  65. package/tests/gentle-ai.test.ts +125 -9
  66. package/tests/gentle-shell.test.ts +1149 -24
  67. package/tests/gentle-todo.test.ts +17 -4
  68. package/tests/inprocess-reviewer.test.ts +460 -0
  69. package/tests/maintainer/provider-relay.maintest.ts +101 -143
  70. package/tests/native-review-capability-contract.test.ts +34 -1
  71. package/tests/native-review-parity.test.ts +19 -0
  72. package/tests/odd-runtime-delegation-gate.test.ts +212 -0
  73. package/tests/orchestrator-rdd-ownership.test.ts +3 -3
  74. package/tests/package-manifest.test.ts +6 -17
  75. package/tests/questionnaire-schema.test.ts +274 -0
  76. package/tests/questionnaire-view.test.ts +446 -0
  77. package/tests/rdd-status-line.test.ts +21 -4
  78. package/tests/review-candidate-owner-retry.test.ts +63 -0
  79. package/tests/review-candidate-view.test.ts +15 -0
  80. package/tests/review-controller-native-routing.test.ts +86 -0
  81. package/tests/review-host-relay-routing.test.ts +77 -0
  82. package/tests/review-host-relay.test.ts +297 -299
  83. package/tests/review-integration-v2-forward.test.ts +61 -0
  84. package/tests/review-integration-v2.test.ts +146 -1
  85. package/tests/review-ledger-contract.test.ts +1 -2
  86. package/tests/review-relay-transport-agent.test.ts +129 -26
  87. package/tests/review-risk-assessment.test.ts +104 -0
  88. package/tests/runtime-harness.mjs +11 -0
  89. package/tests/session-changes-shell.test.ts +27 -0
  90. package/tests/session-worktree-registry.test.ts +41 -0
  91. package/tests/shell-bar.test.ts +200 -124
  92. package/tests/shell-card.test.ts +5 -3
  93. package/tests/shell-changes-view.test.ts +47 -0
  94. package/tests/shell-changes.test.ts +177 -0
  95. package/tests/shell-hover.test.ts +19 -0
  96. package/tests/shell-prompt.test.ts +20 -0
  97. package/tests/shell-sidebar-fullscreen.test.ts +59 -0
  98. package/tests/shell-sidebar-layout.test.ts +301 -8
  99. package/tests/shell-sidebar.test.ts +25 -1
  100. package/tests/shell-todo.test.ts +36 -0
  101. package/tests/shell-usage-view.test.ts +120 -1
  102. package/tests/shell-usage.test.ts +129 -0
  103. package/tests/skill-collision-prefixes.test.ts +1 -1
  104. package/tests/startup-banner.test.ts +93 -2
  105. package/docs/assets/brand/gentle-pi-banner.png +0 -0
  106. package/lib/opaque-pi-reviewer-adapter.ts +0 -404
  107. package/skills/release/SKILL.md +0 -137
  108. package/tests/opaque-pi-reviewer-adapter.test.ts +0 -410
@@ -24,6 +24,7 @@ ODD is the predefined workflow: it runs by default on every request, without the
24
24
  - **One feature document:** `odd/tasks/<feature-name>.md` holds objective, problem, why, scope, constraints, actionable checklist with stable IDs and acceptance criteria, verification evidence, progress, and next step. Project-scoped Engram topic `odd/<feature-name>/tasks` mirrors the full document and repository-relative locator. Keep concise rationale for meaningful accepted changes here, not a separate plan or exhaustive journal. Accepted user, review, or verification changes update intent and tasks together; preserve valid completed work, add new tasks or reopen invalidated items with reasons. Findings alone do not authorize expansion or acceptance. Routine corrections stay with their tasks; checkoffs require observed proof.
25
25
  - **Recovery:** write local progress first and read back both copies; writes are not atomic. Unavailable Engram leaves an explicit pending mirror, not invented success or a block on unrelated safe work. Before implementation or resume, the parent reads full feature memory and the actual task file, reconciles code and evidence, and preserves conflicting versions. Pass the locator and relevant context; workers read the document before edits. The existing Todo UI is a projection, not another authority.
26
26
  - **Task size:** about 400 authored changed lines (additions plus deletions) is advisory only, not a cap, acceptance criterion, automatic stop, forced split, or RDD trigger. Keep coherent behavior with tests and docs, explain natural overages, and continue under existing PR policy. Forward this instruction to workers; never remove whitespace, comments, or tests, minify, invent abstractions, or split artificially for cosmetic savings.
27
+ - **Runtime boundary:** in a primary turn, direct `edit`/`write` calls may successfully mutate one repository file and repeat that path. A second distinct file is refused before mutation and must go through `subagent_run`; `odd/tasks/**` bookkeeping and delegated child actors are exempt, failed calls consume nothing, and the next primary start resets the boundary.
27
28
  - **Research:** optional research addresses a named uncertainty. Establish problem, intended outcome, constraints, and current evidence; inspect code and adapt depth to consequence, not fixed questionnaires or rounds. The parent asks one focused product question only when needed, then waits; workers return gaps. Use available authorized documentation/web tools, prefer primary sources, and attribute claims to URLs/code locations. Distinguish facts, assumptions, contradictions, freshness, and gaps; return a recommendation, tradeoffs, open questions, and implementation implications. Forward these instructions to an existing fresh general worker, not a specialized agent or `sdd-research`. Unavailable evidence pauses only unsafe dependent decisions. Research stays read-only with no new persistence/readiness machinery; a brief proposal is needed only for a real decision.
28
29
  - **Assumptions:** at most one scoped independent read-only challenge for a high-consequence unproven premise, including a small security-critical change. Deterministic failures need fixes, not debate. Native RDD claims stay with its refuter.
29
30
  - **TDD:** resolve on/off from existing project/session configuration or explicit user choice; retain source and exact runner in the feature document when present and forward all three on every implementation delegation, refreshing on resume. Test presence does not enable TDD. Enabled requires observed RED before implementation → GREEN → REFACTOR; disabled still requires ordinary functional checks. Unknown/conflicting mode or a missing runner needs only the clarification affecting the next action, never invented precedence, commands, or `sdd-init`.
@@ -105,7 +106,7 @@ This is guidance through existing tools, not a new CLI, phase, state engine, or
105
106
  | **Skill creation workflow** | Provides the `gentle-ai-skill-creator`/`gentle-ai-skill-improver` skills, `/skill-creation` prompt, and packaged style guide for LLM-first skills. |
106
107
  | **Delivery skills** | Includes issue-first PRs, chained PRs, work-unit commits, cognitive docs, comment writing, and Judgment Day review. |
107
108
  | **Bounded native review** | Freezes one candidate, dispatches only controller-selected lenses, and records native authority. Review outcomes are informational; delivery follows ordinary repository policy. |
108
- | **Verified native runtime** | The current source checkout provisions the exact package-local Gentle AI v3.2.1 runtime: signed, SHA-256-pinned release archives on Darwin/Linux and a Go SumDB-verified source build on Windows x64/arm64. It validates package-local integrity and rejects PATH, global, sibling, symlink, and mode fallbacks. |
109
+ | **Verified native runtime** | The current source checkout provisions the exact package-local Gentle AI v3.5.0 runtime: signed, SHA-256-pinned release archives on Darwin/Linux and a Go SumDB-verified source build on Windows x64/arm64. It validates package-local integrity and rejects PATH, global, sibling, symlink, and mode fallbacks. |
109
110
  | **Runtime safety** | Blocks destructive shell commands, asks for confirmation for sensitive operations, and blocks direct read/write/edit access to sensitive paths. |
110
111
 
111
112
  ## Native pointer regions
@@ -142,7 +143,7 @@ The stable release is [`v2.6.0`](https://github.com/Gentleman-Programming/gentle
142
143
 
143
144
  ### Source checkout
144
145
 
145
- This checkout prepares `gentle-pi` `3.2.1`; it is source state, not a published release. Its package-local native runtime pin is Gentle AI `v3.2.1`, distinct from the published `v2.6.0` pairing.
146
+ This checkout prepares `gentle-pi` `3.4.0`; it is source state, not a published release. Its package-local native runtime pin is Gentle AI `v3.5.0`, distinct from the published `v2.6.0` pairing.
146
147
 
147
148
  The native SDD status consumer accepts both the pinned producer's legacy
148
149
  `apply`/`verify`/`remediate`/`archive` instruction record and the classical
@@ -153,7 +154,7 @@ Unknown or incomplete instruction records still fail closed.
153
154
  The Pi runtime now uses native status exclusively for SDD and retires standalone
154
155
  sync. The full chain follows completed apply to archive, where applicable delta
155
156
  specs are composed; verification remains explicitly invokable. With the current
156
- 3.2.1 pin, native still requires verification and its emitted evidence requirements;
157
+ 3.5.0 pin, native still requires verification and its emitted evidence requirements;
157
158
  a plain practical PASS report does not satisfy that legacy native gate. Pi forwards
158
159
  those exact instructions without overriding readiness or inventing legacy evidence.
159
160
  Classical direct-archive behavior is compatibility-tested with an identified
@@ -187,7 +188,7 @@ pi install npm:gentle-pi@2.6.0
187
188
 
188
189
  RDD remains opt-in. Enable it only through an explicit user decision with `/gentle:review-mode enable`; `status` lets you inspect the mode without changing it.
189
190
 
190
- The source checkout's RDD integration installs Gentle AI only into its private `.gentle-ai/` directory. Darwin and Linux use pinned release assets with asset and executable SHA-256 verification (signed archives for source pin `v3.2.1`; raw prerelease binaries only under a prerelease pin). Windows x64 and arm64 build the exact `v3.2.1` source tag with a local Go 1.25.10+ toolchain, a sealed Go environment, `GOTOOLCHAIN=local`, and `GOSUMDB=sum.golang.org`; it does not download Go automatically. Windows provenance is Go-toolchain plus SumDB evidence and postinstall tamper detection, **not** Authenticode or protection against a malicious joint binary-and-manifest replacement. Package-private locks coordinate cooperative concurrent or crashed installers; their tombstones fail closed. A malicious same-user process with write access to package-private `node_modules` is outside that protocol because it can already replace package code, binary, or manifest, and portable Node has no pathname-delete CAS. It never uses `PATH` or a global `gentle-ai` installation. For development or offline installs only, set `GENTLE_PI_SKIP_GENTLE_AI_INSTALL=1`; native review operations then fail closed with an actionable `package-local-binary-missing` error. To recover explicitly, if `GENTLE_PI_SKIP_GENTLE_AI_INSTALL` is set, remove or unset it before changing to the installed `gentle-pi` package directory. Then run `node scripts/install-gentle-ai.mjs`. This invokes the package-owned installer without relying on a global binary or npm configuration change. A missing binary can result from skipped lifecycle scripts, but does not prove that lifecycle scripts were disabled.
191
+ The source checkout's RDD integration installs Gentle AI only into its private `.gentle-ai/` directory. Darwin and Linux use pinned release assets with asset and executable SHA-256 verification (signed archives for source pin `v3.5.0`; raw prerelease binaries only under a prerelease pin). Windows x64 and arm64 build the exact `v3.5.0` source tag with a local Go 1.25.10+ toolchain, a sealed Go environment, `GOTOOLCHAIN=local`, and `GOSUMDB=sum.golang.org`; it does not download Go automatically. Windows provenance is Go-toolchain plus SumDB evidence and postinstall tamper detection, **not** Authenticode or protection against a malicious joint binary-and-manifest replacement. Package-private locks coordinate cooperative concurrent or crashed installers; their tombstones fail closed. A malicious same-user process with write access to package-private `node_modules` is outside that protocol because it can already replace package code, binary, or manifest, and portable Node has no pathname-delete CAS. It never uses `PATH` or a global `gentle-ai` installation. For development or offline installs only, set `GENTLE_PI_SKIP_GENTLE_AI_INSTALL=1`; native review operations then fail closed with an actionable `package-local-binary-missing` error. To recover explicitly, if `GENTLE_PI_SKIP_GENTLE_AI_INSTALL` is set, remove or unset it before changing to the installed `gentle-pi` package directory. Then run `node scripts/install-gentle-ai.mjs`. This invokes the package-owned installer without relying on a global binary or npm configuration change. A missing binary can result from skipped lifecycle scripts, but does not prove that lifecycle scripts were disabled.
191
192
 
192
193
  Recommended companion packages:
193
194
 
@@ -236,6 +237,7 @@ An orphan branch with commits and no parent has no branch point to name as `base
236
237
  /gentle:persona Switch between gentleman and neutral persona modes.
237
238
  /gentle:background-subagents Show or set the managed background-subagents policy, with its deciding source.
238
239
  /gentle:review-mode Show or set the receipt-driven development mode (status|enable|disable).
240
+ /gentle:animations Show or set global animations: quality, performance, or potato.
239
241
  /gentle:banner Configure startup rose, text logo, and color preset.
240
242
  ```
241
243
 
@@ -361,13 +363,13 @@ flowchart TD
361
363
 
362
364
  VALIDATE is informational. Commit, push, PR, and release commands follow ordinary repository policy; RDD never authorizes, rewrites, consumes review state for, or blocks them. Dangerous-command safety and destructive-review consent remain independent.
363
365
 
364
- For the source checkout, native contract pairing is exact: this adapter resolves only the integrity-verified package-local Gentle AI v3.2.1 executable, independently hashes it, then negotiates `gentle-ai.review-integration/v2` outside the repository. Capabilities are cached by that executable digest. Every START, target status, FINALIZE, validate, and BIND-SDD request passes the same contract identifier. Negotiated envelopes decode exactly against the vendored schemas; `recover` routes only the provider-selected `action_disposition`, and optional additions require a future compatible schema/minor that the provider explicitly advertises and the consumer negotiates.
366
+ For the source checkout, native contract pairing is exact: this adapter resolves only the integrity-verified package-local Gentle AI v3.5.0 executable, independently hashes it, then negotiates `gentle-ai.review-integration/v2` outside the repository. Capabilities are cached by that executable digest. Every START, target status, FINALIZE, validate, and BIND-SDD request passes the same contract identifier. Negotiated envelopes decode exactly against the vendored schemas; `recover` routes only the provider-selected `action_disposition`, and optional additions require a future compatible schema/minor that the provider explicitly advertises and the consumer negotiates.
365
367
 
366
368
  Contract `/v2` replaces the Base64 `candidate_diff` reviewer transport of `/v1` with immutable `base_tree`/`candidate_tree` plus an ordered `changed_path_manifest` and never an inline patch. `gentle-pi` negotiates `/v2` only, with no dual-lane fallback; the cutover landed as one atomic commit against gentle-ai v2.2.2 (tracked by the `migrate-review-integration-v2` change), and the `/v1` schemas stay packaged because the `/v2` schemas `$ref` into their fragments. This provider contract version is unrelated to Pi's own internal "compact-v2" review-authority naming used below — the shared digit is coincidental, not a version pairing.
367
369
 
368
370
  Target status owns `current_target`, `unrelated`, `ambiguous`, and `corrupted` applicability and returns one native action. Pi does not reconstruct ordinary authority from provider-private files or choose a lineage from repository-wide history. Restart recovery rebuilds only the derived candidate view from the native Git/content projection, including intended-untracked paths, symlinks, and immutable gitlink identities. Native failure envelopes retain their exact mutation outcome, replayability, required inputs, request digest, and next action. After an unknown or lost mutating result, Pi calls target status before any replay decision and returns only the provider-declared action.
369
371
 
370
- Once the source checkout's pinned gentle-ai runtime (currently v3.2.1) has written review authority, rollback MUST preserve every native store and receipt and MUST NOT run a downgraded binary against that repository. Disable the Pi route or roll forward to a compatible authority-aware release instead; deleting authority data or reinstalling an older binary is not a rollback path.
372
+ Once the source checkout's pinned gentle-ai runtime (currently v3.5.0) has written review authority, rollback MUST preserve every native store and receipt and MUST NOT run a downgraded binary against that repository. Disable the Pi route or roll forward to a compatible authority-aware release instead; deleting authority data or reinstalling an older binary is not a rollback path.
371
373
 
372
374
  ### FINALIZE wrapper input
373
375
 
@@ -730,7 +732,7 @@ Profiles are named, switchable snapshots of the global agent-model routing from
730
732
 
731
733
  Applying a profile writes `~/.pi/gentle-ai/models.json`, then reconciles agent frontmatter and `subagents.json` the same way `/gentle:models` does. A profile is a complete snapshot: every discoverable agent it omits returns to inherit, so routing materialized by a previous profile, by `/gentle:models`, or by a migration never survives a switch silently. The reconciliation happens on the next subagent launch, and that launch still routes with the previous routing — expect one launch of lag after switching. The active profile is persisted so `/gentle:profiles` reopens with the applied profile marked.
732
734
 
733
- A profile also carries the orchestrator under the reserved routing key `orchestrator`. Applying a profile that defines it writes `defaultProvider`, `defaultModel`, and `defaultThinkingLevel` to Pi's global `settings.json` (preserving every other key; an unreadable `settings.json` aborts that part and is reported instead of being overwritten). Applying a profile without an `orchestrator` entry never moves the orchestrator, and `s` snapshots the currently effective orchestrator together with the routing. `orchestrator` is reserved: it is not a subagent name, is never written to `subagents.json`, and is not counted as a role.
735
+ A profile also carries the orchestrator under the reserved routing key `orchestrator`. Applying a profile that defines it writes `defaultProvider`, `defaultModel`, and `defaultThinkingLevel` to Pi's global `settings.json` (preserving every other key; an unreadable `settings.json` aborts that part and is reported instead of being overwritten) and switches the session you are in to that model and thinking level right away, so the orchestrator answers with the profile's model from the next turn. When the model is not in Pi's catalog or its provider has no authentication, the default for new sessions is still recorded and the apply note says this session kept its current model. Applying a profile without an `orchestrator` entry never moves the orchestrator, and `s` snapshots the currently effective orchestrator together with the routing. `orchestrator` is reserved: it is not a subagent name, is never written to `subagents.json`, and is not counted as a role.
734
736
 
735
737
  The panel's current routing, the `current` seed, and `s` all read the routing in effect: `models.json` where it has an entry, and otherwise the `subagents.json` model profile or frontmatter routing the runtime actually resolves for that agent. A sparse `models.json` therefore never hides routing that is still live. When `profiles.json` is missing, the command seeds one profile named `current` captured from that effective routing, marked active only when it has routing entries. Profiles or routing entries dropped by normalization are named in a warning instead of being lost silently.
736
738
 
@@ -822,6 +824,8 @@ One limitation is worth stating. When a pinned profile omits an agent, that agen
822
824
  | `/gentle:commands` | Opens the command palette (default `alt+k`): a curated, grouped menu (Configuration, Session, Diagnostics, SDD, Skills) of registered Gentle commands; search and run by label. |
823
825
  | `/gentle:persona` | Switches global persona mode, with project override support. |
824
826
  | `/gentle:background-subagents` | Shows or sets the managed background-subagents policy (`status\|enable\|disable`), naming the source that decided it. |
827
+ | `/gentle:double-esc-cancel` | Shows or sets the double-esc-cancel preference (`status\|enable\|disable`); no argument toggles it. |
828
+ | `/gentle:animations` | Shows or sets global animations (`status\|quality\|performance\|potato`); no argument reports status. |
825
829
  | `/gentle:telemetry` | Shows or changes the local Gentle AI telemetry trigger (`status\|enable\|disable\|preview`). |
826
830
  | `/gentle:review-mode` | Shows or sets the receipt-driven development mode (`status\|enable\|disable`); user-initiated only, Pi automation never toggles it. |
827
831
  | `/gentle:banner` | Configures startup banner rose, text logo, and color preset. |
@@ -838,6 +842,16 @@ One limitation is worth stating. When a pinned profile omits an agent, that agen
838
842
 
839
843
  Startup installs and refreshes only delegation and review assets. SDD assets are installed/refreshed on demand; status and doctor report never-installed SDD assets as informational, while missing or stale assets from an existing installation identify their owner-specific repair command. User and project overrides are reported separately from package drift. Package refresh preserves overrides; explicit saved model settings may still update existing SDD or custom-agent routing at startup.
840
844
 
845
+ ### Native cache warming (Pi 0.86.1+)
846
+
847
+ To allow warming while the parent waits for background results, explicitly set `"cacheWarming": "idle"` in Pi's `settings.json` (user scope: `~/.pi/agent/settings.json`, or project scope: `.pi/settings.json`). Gentle Shell never changes this setting. Native `"streaming"` mode stops when the agent settles: no idle decision is offered for this hook to override. `"off"` remains an opt-out.
848
+
849
+ Pi owns provider cache-lifetime eligibility, safe replay, scheduling, and the fixed 30-minute idle / one-hour streaming horizons. Unknown provider lifetimes do not get inferred. Real provider requests replace Pi's schedule; Gentle Shell adds no timer or maintenance message. Refresh usage stays outside model context. Warming is best-effort, not a guarantee of a future cache hit.
850
+
851
+ Ordinary idle decisions retain Pi's 15% continuation assumption. When the active parent owns queued or running background tasks, Gentle Shell treats continuation probability as 1, but still requires estimated cache-miss savings minus refresh cost to be at least $0.05. Restored, foreign-session, foreground, waiting-for-input, and finished tasks do not strengthen that decision. This only overrides candidates Pi actually offers; it never starts, inspects, polls, steers, or duplicates children.
852
+
853
+ Completion remains push-driven through `gentle-agents.result`. Retain the task ID, end the parent turn when independent work is done, and never sleep or periodically poll status/results to maintain cache or detect completion. Status inspection is for a concrete orchestration decision, not a heartbeat.
854
+
841
855
  ### Background subagents policy
842
856
 
843
857
  Background delegation requires a live interactive/RPC parent and is rejected in `pi -p`, even when the policy is on. Use task mode for bounded print-mode work.
@@ -865,6 +879,52 @@ Both files use the strict shape `{"schema":"gentle-pi.background-subagents/v1","
865
879
 
866
880
  Because the project file outranks the global one, `enable` still writes the global file but reports plainly when a project file keeps the effective policy unchanged. The resolved capability (`ready` or `absent`) reports whether `subagent_run` is actually callable in this session; a policy of `on` with capability `absent` means Gentle Agents is disabled or the retired subagents package is still installed.
867
881
 
882
+ ### Esc behavior
883
+
884
+ The Gentle prompt matches Claude Code's Esc model on top of Pi's own. Four flows share the frame's single hint slot on the bottom rule, each decided by its own state:
885
+
886
+ 1. **Working: cancel keeps the queue moving.** Esc aborts the running turn (a single Esc by default, or the confirming second Esc when double-esc-cancel below is enabled). Any steer or follow-up messages queued while the turn ran are sent as the next turn once the abort settles, instead of being dumped back into the editor for the user to notice and resend by hand; if another turn starts first (for example the user sends the restored draft before the abort has fully settled), the queued text simply waits and is sent once that turn settles, so it is never lost and never injected mid-turn. The user's own unsent draft stays in the editor untouched. Completed work before the abort is preserved, as Pi already does. Images inside a queued message are dropped, because Pi's own restore already drops them before this code ever sees the text. If Pi ever restores a shape this code does not recognize, Pi's own text is left exactly as written and nothing is dispatched, rather than guessing.
887
+ 2. **Working: double-esc-cancel (opt-in, off by default).** While the prompt is working (autocomplete hidden), a single Esc still aborts the turn immediately by default, exactly like Pi's own escape. Once enabled, the first Esc is swallowed and the prompt frame shows `esc again to cancel`; a second Esc within 1000ms falls through so Pi's own `onEscape` performs the abort (and flow 1 above still applies to that second Esc). Letting the window expire treats the next Esc as a first press again.
888
+ 3. **Idle with a draft: double Esc clears it.** With the prompt idle, autocomplete hidden, and non-empty editor text, the first Esc shows `esc again to clear` instead of doing nothing; a second Esc within 500ms, on the exact same text, adds the draft to history (recoverable with the Up arrow) and clears it. Editing the draft between the two presses starts a fresh first press on the new text instead of clearing the edit away. Letting the window expire treats the next Esc as a first press again. A bash-mode draft (starting with `!`) is never touched by this gate; Pi's own bash-mode Esc keeps deciding it.
889
+ 4. **Idle, empty editor: unchanged.** Pi's own idle double-Esc (`/tree` or `/fork`, 500ms) keeps deciding this case entirely; the prompt never intercepts it.
890
+
891
+ Overlays, autocomplete cancel, and bash mode all consume the first Esc locally and are unaffected by any of the four flows above.
892
+
893
+ Double-esc-cancel's policy is user-owned: only an explicit `/gentle:double-esc-cancel enable` or `disable` writes it, and Pi automation never toggles it.
894
+
895
+ ```text
896
+ /gentle:double-esc-cancel Toggle the effective policy (on -> off, off -> on).
897
+ /gentle:double-esc-cancel status Report the effective policy and the deciding source.
898
+ /gentle:double-esc-cancel enable Write "on" to the global file.
899
+ /gentle:double-esc-cancel disable Write "off" to the global file.
900
+ ```
901
+
902
+ Unlike `/gentle:background-subagents`, no argument here reports status; it toggles the effective policy instead, since this preference has only one file layer and nothing else can outrank a write.
903
+
904
+ Three sources can decide the policy, and the first hit wins:
905
+
906
+ | Priority | Source | Notes |
907
+ | -------- | ------------------------------------------- | ------------------------------------------------------------ |
908
+ | 1 | `<configHome>/double-esc-cancel.json` | Global file, written by `enable`/`disable`. `configHome` honors `GENTLE_PI_CONFIG_HOME` and defaults to `~/.pi/gentle-ai`. There is no project-level override: this preference changes what a keypress does, and per-project overrides would make the same key do two different things depending on which repo is open. |
909
+ | 2 | `GENTLE_PI_DOUBLE_ESC_CANCEL` | Exactly `on` or `off`. Any other value is ignored, and it decides only when the global file does not exist. |
910
+ | 3 | Built-in default | `off`. |
911
+
912
+ The file uses the strict shape `{"schema":"gentle-pi.double-esc-cancel/v1","policy":"on"}`. A file that is present but malformed fails closed to `off` instead of falling through to the environment variable, and the command reports that case as a warning instead of an ordinary `off`. The extension resolves the policy once at startup and updates it in memory when the command runs; the prompt never re-reads the file on every keypress.
913
+
914
+ ### Animation modes
915
+
916
+ Use `/gentle:animations performance` to reduce redraw frequency, or `/gentle:animations potato` to stop Gentle-owned periodic animation. Find **Animation mode** under the command palette's **Configuration** group. `/gentle:animations` and `/gentle:animations status` report the effective mode and deciding source without writing; `/gentle:animations quality` restores the default.
917
+
918
+ | Mode | Working prompt | Startup banner |
919
+ |------|----------------|----------------|
920
+ | `quality` (default) | Existing frames every 80ms | Existing animation every 25ms |
921
+ | `performance` | One animation pulse every 1000ms | One paint every 250ms, advancing 10 logical ticks to retain approximately the original duration |
922
+ | `potato` | Static idle/working/queued state, no animation interval | Completed static artwork immediately, no animation interval |
923
+
924
+ The selection is global: `<configHome>/animations.json`, where `configHome` honors `GENTLE_PI_CONFIG_HOME` and defaults to `~/.pi/gentle-ai`. The strict file shape is `{"schema":"gentle-pi.animations/v1","policy":"quality"}`. There is no project or environment mode override. Missing files use `quality`; malformed or unreadable files also fall back to `quality`, with an attributable warning in status, and are not silently rewritten.
925
+
926
+ A successful command applies to the live prompt immediately, including while working. Starting and settling still request immediate renders. Pi owns enqueue repaint scheduling; Gentle shows the current queued state on the next host render without requiring an animation tick. A running startup banner retains its creation-time policy; the new selection applies at the next banner creation. Operational polling, refresh/debounce timers, Pi core animations, and install-time `tuiMode` are unchanged.
927
+
868
928
  Startup banner settings remain global in `banner.json` under `GENTLE_PI_CONFIG_HOME` (default `~/.pi/gentle-ai`). Existing `showRose` and `showTextLogo` opt-outs independently control the main startup artwork; both default to enabled. Changes apply on the next session or `/reload`. Color presets are `pink` (default), `cyan`, `yellow`, and `green`. The static sidebar heading is independent of these preferences and follows the active theme.
869
929
 
870
930
  Startup flag:
@@ -8,27 +8,32 @@ Gentle Pi is a transport consumer, not a review authority. Gentle AI generates t
8
8
 
9
9
  | Component | Responsibility |
10
10
  | --- | --- |
11
- | Pi reviewer adapter | A pure opaque adapter: `Buffer → Buffer/error`. It accepts a Go-materialized prompt as bytes, invokes Pi in JSON event mode, and returns the event stream's final assistant text or a typed transport error carrying what the child's own stream revealed. |
12
- | Host coordinator | Executes the exact Go-issued materialize/submission tokens, launches the adapter, and submits its result only through the supplied token. It validates and forwards two optional caller-owned launch selections: the lens's reviewer model (`--model`) and an explicit extension allowlist (`-e` paths). |
11
+ | In-process reviewer completion | A pure completion (`lib/inprocess-reviewer.ts`): resolves the lens's "provider/id" selection through the live model registry, authenticates through the registry's own resolver, and completes the Go-materialized prompt as one frozen user message — no systemPrompt, no tools, no session, no extension hooks, no child process. |
12
+ | Host coordinator | Executes the exact Go-issued materialize/submission tokens, runs the completion through the caller-supplied model registry, and submits its result only through the supplied token. |
13
13
  | Gentle AI (Go) | Go owns worktree, lineage, candidate freeze, lens selection, correction, validator, approval burn, and review semantics. Delivery commands remain ordinary repository-policy operations. |
14
14
 
15
- The adapter does not parse bindings, select work, rebuild prompts, inspect repository state, retry, classify results, or create authority. The coordinator does not infer a command or replace a provider-issued token. The package has no durable receipt or policy authority.
15
+ The completion does not parse bindings, select work, rebuild prompts, inspect repository state, retry, classify results, or create authority. The coordinator does not infer a command or replace a provider-issued token. The package has no durable receipt or policy authority.
16
16
 
17
17
  ## Transport behavior
18
18
 
19
19
  1. Gentle AI emits an opaque materialization or submission token for the selected Pi runtime.
20
- 2. The host coordinator executes that exact token and gives only the materialized bytes to the adapter.
21
- 3. The adapter runs the child with `--mode json`, extracts the final assistant text from the pi event stream, and returns those bytes to the coordinator. A run that produced no assistant text fails typed with evidence: the stream kind, the reviewer selection the child itself reported, and whether a tool call was attempted (a text-mode run used to exit 0 with zero bytes and no diagnosable cause).
22
- 4. The coordinator sends those bytes only through the exact Go-issued submission token.
20
+ 2. The host coordinator executes that exact token and gives only the materialized bytes to the completion, as the single user message of one frozen prompt.
21
+ 3. The completion resolves its "provider/id" selection through the live model registry, authenticates, and returns the completion's text to the coordinator. A run that produced no text, attempted a tool call, or exceeded its bound fails typed with evidence (the completion's stop reason, its aborted/timed-out signal, or its reviewer model, depending on the code); a run over the fixed output bound also fails typed.
22
+ 4. The coordinator sends that text only through the exact Go-issued submission token.
23
23
 
24
- ## Reviewer launch selection (user-owned)
24
+ ## Reviewer completion selection (user-owned)
25
25
 
26
- The default reviewer launch is selection-free: no model flag, no extensions, no ambient inheritance of the session's model. Two optional, user-owned selections ride the request and are validated before any process launches; a broken value is refused typed as `reviewer-config-invalid`, never a mid-review transport failure:
26
+ There is no model flag, no extension allowlist, and no ambient default model: the in-process completion resolves entirely through the caller-supplied model registry (`ctx.modelRegistry`), and a missing registry or an unconfigured lens is refused typed as `reviewer-config-invalid` before materialize ever runs, never a mid-review transport failure.
27
27
 
28
- - **Lens model** — the capture path reads the lens's entry from the agent model routing config (`review-risk`, `review-resilience`, `review-readability`, `review-reliability`) and forwards it as `--model <provider/id>`.
29
- - **Extension allowlist** — `GENTLE_PI_REVIEW_RELAY_EXTENSIONS` holds absolute extension file paths separated by the platform path delimiter. They are loaded through explicit `-e` paths, which pi honors even under `--no-extensions`; this is how a subscription provider's OAuth billing adapter rides along without re-enabling extension discovery.
28
+ - **Lens model and thinking level** — the capture path reads the lens's entry from the agent model routing config (`review-risk`, `review-resilience`, `review-readability`, `review-reliability`) and forwards its `model` and `thinking` fields verbatim to the completion. A routing entry with no configured model is refused typed, naming the routing key — the completion never falls back to another provider or an ambient default.
30
29
 
31
- A typed Pi transport refusal fails closed. The coordinator reports the refusal — including the reviewer evidence and a bounded stderr excerpt on an empty-output failure — without an agentless lifecycle fallback, local retry policy, synthetic result, or alternate approval path.
30
+ A typed reviewer refusal fails closed. The coordinator reports the refusal — including the completion's evidence — without an agentless lifecycle fallback, local retry policy, synthetic result, or alternate approval path.
31
+
32
+ ## Refuter and targeted validator
33
+
34
+ The refuter and targeted-validator roles are host-mediated in-process completions too, on a provider that advertises the v9 role contract: the collect input carries `--materialize=true` and a provider-owned submission descriptor, exactly like a lens capture-result materialize slot — materialize, complete in-process, then submit through the exact `--input` form. Their entries in the agent model routing config, `review-refuter` and `review-validator`, select the model and thinking level the same way `review-<lens>` does for a lens; a missing entry is refused typed, naming that key, before materialize ever runs.
35
+
36
+ An older provider that has not advertised the v9 role contract still renders each role as a self-contained vector (binding tokens plus `--agent=pi --execute=true`, no submission): executing that exact vector makes Go materialize the role prompt, run its own locked-down pi subprocess, and admit the verdict itself. The host still accepts this compatibility form unchanged.
32
37
 
33
38
  ## Dynamic contract delivery
34
39
 
@@ -36,19 +41,19 @@ Package static assets intentionally omit lifecycle instructions, candidate routi
36
41
 
37
42
  ## Integration constraints
38
43
 
39
- - Keep Pi transport opaque: raw prompt bytes in, the event stream's assistant text or a typed, evidenced error out.
44
+ - Keep the completion frozen: raw prompt bytes in as one user message, the completion's text or a typed, evidenced error out.
40
45
  - Preserve Go-issued materialize and submission tokens exactly; they are the only authority-bearing inputs the host may execute.
41
- - Keep the reviewer launch selection user-owned: the relay never invents a model and never enables extension discovery; it only forwards the validated caller-owned selections.
42
- - Treat a transport failure as unavailable evidence, never as an approval, completion, or permission to substitute a local workflow.
46
+ - Keep the reviewer completion selection user-owned: it never invents a model or falls back to another provider or an ambient default; it only forwards the validated caller-owned selection and thinking level, or refuses typed.
47
+ - Treat a reviewer refusal as unavailable evidence, never as an approval, completion, or permission to substitute a local workflow.
43
48
  - Keep command safety and user interaction in the host, without interpreting provider authority state.
44
49
  - Keep durable review state, admissions, correction accounting, and approvals in Gentle AI. Keep delivery decisions in ordinary repository policy.
45
50
 
46
51
  ## Review checklist
47
52
 
48
- - [ ] The adapter surface is still `Buffer → Buffer/error` (the output is the pi event stream's assistant text; failures carry typed evidence).
53
+ - [ ] The completion still takes raw prompt bytes as one frozen user message and returns text or a typed, evidenced error — no child process, no extension allowlist.
49
54
  - [ ] The coordinator executes only exact Go-issued materialize/submission tokens.
50
- - [ ] The reviewer launch stays selection-free unless the caller-owned selection and extension allowlist validate.
51
- - [ ] Typed transport refusal remains fail-closed.
55
+ - [ ] The reviewer completion selection stays refused typed unless the caller-owned model registry and a configured lens selection validate.
56
+ - [ ] Typed reviewer refusal remains fail-closed.
52
57
  - [ ] No package code or static prompt uses review authority to decide, authorize, rewrite, or block delivery commands.
53
58
 
54
59
  ← [Back to README](../README.md)
@@ -0,0 +1,210 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { DynamicBorder } from "@earendil-works/pi-coding-agent";
3
+ import { Text } from "@earendil-works/pi-tui";
4
+ import { createNativeFullscreenInteraction } from "../lib/native-fullscreen-interaction.ts";
5
+ import { type QuestionParams, QuestionParamsSchema } from "../lib/questionnaire/schema.ts";
6
+ import {
7
+ QuestionnaireView,
8
+ type AnswerRow,
9
+ type QuestionnaireResult,
10
+ } from "../lib/questionnaire/questionnaire-view.ts";
11
+ import { validateQuestionnaire, type QuestionnaireError } from "../lib/questionnaire/validate.ts";
12
+
13
+ const QUESTION_TOOL_NAME = "ask_user_question";
14
+ const ASK_USER_QUESTION_BLOCKED_EVENT = "gentle-pi:ask-user-question:blocked";
15
+
16
+ /** Maximum characters kept from a renderCall question summary. */
17
+ const CALL_SUMMARY_LIMIT = 120;
18
+
19
+ /** Structured details returned by the tool for UI rendering and callers. */
20
+ interface QuestionnaireDetails {
21
+ cancelled?: boolean;
22
+ answers?: AnswerRow[];
23
+ error?: QuestionnaireError;
24
+ errorKind?: string;
25
+ }
26
+
27
+ /** Content plus details returned by `execute`. */
28
+ interface QuestionnaireToolResult {
29
+ content: Array<{ type: "text"; text: string }>;
30
+ details: QuestionnaireDetails;
31
+ }
32
+
33
+ /**
34
+ * Invalid-parameter result. `AgentToolResult` has no `isError` field, so this
35
+ * follows the repository convention for rejected tool input: a leading error
36
+ * sentence in `content` plus a machine-readable payload in `details`
37
+ * (`extensions/gentle-todo.ts` returns `Error: ...` with `details.error`).
38
+ */
39
+ function invalidQuestionnaireResult(error: QuestionnaireError): QuestionnaireToolResult {
40
+ return {
41
+ content: [{ type: "text", text: `Invalid questionnaire: ${error.message}` }],
42
+ details: { error, errorKind: error.code },
43
+ };
44
+ }
45
+
46
+ /** Non-interactive result; parity with ask_user_choice's TUI-only guard. */
47
+ function unavailableResult(): QuestionnaireToolResult {
48
+ return {
49
+ content: [{ type: "text", text: "Error: ask_user_question is unavailable outside the interactive TUI" }],
50
+ details: { errorKind: "unavailable_outside_tui" },
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Human-readable body for one answer. A custom answer on a multiSelect
56
+ * question keeps the toggled options, so the text must name them explicitly:
57
+ * the free-text value alone would silently drop the user's selections. Plain
58
+ * custom answers (no selections) stay concise.
59
+ */
60
+ function answerBody(answer: AnswerRow): string {
61
+ if (answer.kind === "multi") return `selected: ${(answer.selected ?? []).join(", ")}`;
62
+ if (answer.kind === "custom") {
63
+ const body = `(custom) ${answer.answer ?? ""}`;
64
+ const selected = answer.selected ?? [];
65
+ return selected.length > 0 ? `${body} — selected: ${selected.join(", ")}` : body;
66
+ }
67
+ return answer.answer ?? "";
68
+ }
69
+
70
+ /**
71
+ * Compact LLM-facing transcript of the committed answers. Each row keeps the
72
+ * original one-based question index so a partially answered questionnaire
73
+ * (the last question committed early) still reads in order.
74
+ */
75
+ function answersText(answers: AnswerRow[]): string {
76
+ if (answers.length === 0) return "The user answered the questionnaire.";
77
+ const lines: string[] = [];
78
+ for (const answer of answers) {
79
+ const prefix = `${answer.questionIndex + 1}. ${answer.question}`;
80
+ lines.push(`${prefix} — ${answerBody(answer)}`);
81
+ if (answer.preview !== undefined) lines.push(` selected preview: ${answer.preview}`);
82
+ }
83
+ return lines.join("\n");
84
+ }
85
+
86
+ /** Single-line summary of one question for the collapsed tool call row. */
87
+ function callSummary(question: unknown, index: number): string {
88
+ const source = typeof question === "object" && question !== null ? question as { header?: unknown; options?: unknown } : {};
89
+ const header = typeof source.header === "string" ? source.header : "";
90
+ const labels = Array.isArray(source.options)
91
+ ? source.options
92
+ .map((option) => (typeof option === "object" && option !== null && typeof (option as { label?: unknown }).label === "string"
93
+ ? (option as { label: string }).label
94
+ : ""))
95
+ .filter((label) => label.length > 0)
96
+ : [];
97
+ const labelsPart = labels.length > 0 ? ` (${labels.join(", ")})` : "";
98
+ return `${index + 1}. ${header}${labelsPart}`;
99
+ }
100
+
101
+ function truncate(text: string, limit: number): string {
102
+ return text.length <= limit ? text : `${text.slice(0, Math.max(0, limit - 1))}…`;
103
+ }
104
+
105
+ /**
106
+ * Register the first-party questionnaire tool.
107
+ *
108
+ * Name-collision semantics (live-verified against the installed Pi runtime):
109
+ * - Tool names are exclusive across extensions. Pi has no precedence, override,
110
+ * or silent shadowing: loading two extensions that register the same tool
111
+ * name fails the whole load with a hard error
112
+ * (`Tool "ask_user_question" conflicts with <other extension>`; the runtime
113
+ * exits non-zero). The name is either free or fatal, full stop.
114
+ * - `registerTool` writes into the calling extension's own tool map keyed by
115
+ * name, so re-registering inside one extension overwrites that entry
116
+ * (`loader.js:240`). That same-name write is the only one Pi tolerates.
117
+ * - This first-party tool ships as THE `ask_user_question` provider. A competing
118
+ * provider such as the third-party `@juicesharp/rpiv-ask-user-question`
119
+ * package fails the load by design and must be removed from the user's Pi
120
+ * settings; that deletion is the documented migration path, not a runtime
121
+ * precedence choice.
122
+ */
123
+ export default function askUserQuestion(pi: ExtensionAPI): void {
124
+ pi.registerTool({
125
+ name: QUESTION_TOOL_NAME,
126
+ renderShell: "self",
127
+ label: "Ask User Question",
128
+ description: "Ask one to four structured questions in a single call, each with two to four ordered options, and read the user's answers back in one result.",
129
+ promptGuidelines: [
130
+ "Use ask_user_question to collect decisions in one batch: ask one to four questions at a time, each with two to four options.",
131
+ "Keep each header a short chip of at most 16 characters and each option label at most 60 characters.",
132
+ "Add a preview to an option when the user needs to compare rich detail side-by-side with the options.",
133
+ "Set multiSelect when the choices are not mutually exclusive.",
134
+ "The free-text \"Type something.\" row is always available and is also how the user bails out into a normal conversation; never rely on it as a hidden escape hatch.",
135
+ "Never use this tool for decisions that must not be delegated to the user.",
136
+ ],
137
+ parameters: QuestionParamsSchema,
138
+ executionMode: "sequential",
139
+ async execute(
140
+ _toolCallId: string,
141
+ params: QuestionParams,
142
+ _signal: AbortSignal | undefined,
143
+ _onUpdate: undefined,
144
+ ctx,
145
+ ): Promise<QuestionnaireToolResult> {
146
+ const error = validateQuestionnaire(params);
147
+ if (error) return invalidQuestionnaireResult(error);
148
+ if (ctx.mode !== "tui") return unavailableResult();
149
+
150
+ let selection: QuestionnaireResult | undefined;
151
+ try {
152
+ pi.events.emit(ASK_USER_QUESTION_BLOCKED_EVENT, { active: true });
153
+ selection = await ctx.ui.custom<QuestionnaireResult>((tui, theme, keybindings, done) => {
154
+ const view = new QuestionnaireView({
155
+ questions: params.questions,
156
+ theme,
157
+ keybindings,
158
+ onComplete: (result) => done(result),
159
+ });
160
+ // Native dock swap, never an overlay: the transcript stays scrollable
161
+ // while the questionnaire owns focus. No `overlay` option is passed.
162
+ const container = createNativeFullscreenInteraction({
163
+ keyboardTarget: view,
164
+ requestRender: () => tui.requestRender(),
165
+ });
166
+ container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
167
+ container.addChild(view);
168
+ container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
169
+ return container;
170
+ });
171
+ }
172
+ finally {
173
+ pi.events.emit(ASK_USER_QUESTION_BLOCKED_EVENT, { active: false });
174
+ }
175
+
176
+ if (selection === undefined || selection.cancelled) {
177
+ return {
178
+ content: [{ type: "text", text: "User cancelled the questionnaire" }],
179
+ details: { cancelled: true },
180
+ };
181
+ }
182
+ return {
183
+ content: [{ type: "text", text: answersText(selection.answers) }],
184
+ details: { answers: selection.answers },
185
+ };
186
+ },
187
+ renderCall(args: QuestionParams, theme) {
188
+ const questions = Array.isArray(args.questions) ? args.questions : [];
189
+ const summary = truncate(questions.map((question, index) => callSummary(question, index)).join(" "), CALL_SUMMARY_LIMIT);
190
+ return new Text(
191
+ theme.fg("toolTitle", theme.bold("ask_user_question ")) +
192
+ theme.fg("muted", summary),
193
+ 0,
194
+ 0,
195
+ );
196
+ },
197
+ renderResult(result, _options, theme) {
198
+ const details = result.details as QuestionnaireDetails | undefined;
199
+ if (details?.cancelled === true) return new Text(theme.fg("warning", "Cancelled"), 0, 0);
200
+ const answers = Array.isArray(details?.answers) ? details.answers : [];
201
+ if (answers.length === 0) return new Text(theme.fg("warning", "No answers"), 0, 0);
202
+ const lines = answers.map((answer) => {
203
+ if (answer.kind === "multi") return theme.fg("success", `✓ ${answer.question} — ${(answer.selected ?? []).join(", ")}`);
204
+ if (answer.kind === "custom") return theme.fg("success", `✓ ${answer.question} — ${answerBody(answer)}`);
205
+ return theme.fg("success", `✓ ${answer.question} — ${answer.answer ?? ""}`);
206
+ });
207
+ return new Text(lines.join("\n"), 0, 0);
208
+ },
209
+ });
210
+ }