gentle-pi 3.7.0 → 4.0.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 (316) hide show
  1. package/README.md +37 -6
  2. package/assets/agents/gentle-ai-explore.md +4 -4
  3. package/assets/agents/gentle-ai-verify.md +6 -4
  4. package/assets/agents/gentle-ai-worker.md +7 -9
  5. package/assets/orchestrator-delegation.md +40 -41
  6. package/assets/orchestrator-memory.md +1 -22
  7. package/assets/orchestrator-skills.md +1 -1
  8. package/assets/orchestrator.md +12 -24
  9. package/assets/support/strict-tdd-verify.md +4 -266
  10. package/assets/support/strict-tdd.md +8 -360
  11. package/bin/gentle-shell.mjs +254 -29
  12. package/docs/delegated-verification.md +26 -1
  13. package/docs/gentle-agents-activity.md +24 -0
  14. package/docs/gentle-shell.md +92 -28
  15. package/docs/native-authority-architecture.md +2 -2
  16. package/docs/prompt-history.md +280 -0
  17. package/docs/readme-reference.md +112 -206
  18. package/docs/telemetry.md +1 -1
  19. package/docs/yolo-mode.md +86 -0
  20. package/extensions/child-context.ts +26 -0
  21. package/extensions/child-safety.ts +23 -0
  22. package/extensions/gentle-agents.ts +429 -262
  23. package/extensions/gentle-ai.ts +781 -683
  24. package/extensions/gentle-shell.ts +1375 -88
  25. package/extensions/gentle-stats.ts +101 -0
  26. package/extensions/gentle-todo.ts +18 -7
  27. package/extensions/history/atomic-write.ts +38 -0
  28. package/extensions/history/hide-prompts.ts +183 -0
  29. package/extensions/history/index.ts +1419 -0
  30. package/extensions/history/load-shared-history.ts +39 -0
  31. package/extensions/history/selector-helpers.ts +538 -0
  32. package/extensions/history/session-scan.ts +233 -0
  33. package/extensions/history/store.ts +1119 -0
  34. package/extensions/nan-provider.ts +6 -0
  35. package/extensions/quiet-tools.ts +179 -87
  36. package/extensions/resume-hint.ts +60 -0
  37. package/extensions/skill-registry.ts +16 -12
  38. package/extensions/startup-banner.ts +60 -31
  39. package/lib/agent-assets.ts +604 -0
  40. package/lib/agent-profile-pin.ts +12 -0
  41. package/lib/agents-message-delivery.ts +181 -0
  42. package/lib/agents-protocol.ts +60 -0
  43. package/lib/agents-runner.ts +96 -98
  44. package/lib/agents-view.ts +26 -3
  45. package/lib/agents-widget.ts +16 -9
  46. package/lib/append-system-prompt.ts +21 -0
  47. package/lib/bounded-writer-admission.ts +147 -0
  48. package/lib/card-style-policy.ts +60 -0
  49. package/lib/child-context-files.ts +166 -0
  50. package/lib/codemode-renderer.ts +185 -0
  51. package/lib/command-palette-catalog.ts +3 -9
  52. package/lib/command-palette.ts +25 -14
  53. package/lib/destructive-command-guard.ts +144 -0
  54. package/lib/gentle-ai-elapsed-store.ts +87 -0
  55. package/lib/gentle-ai-renderer.ts +196 -39
  56. package/lib/gentle-shell-launcher.ts +24 -13
  57. package/lib/gentle-shell-resume-hint.ts +176 -0
  58. package/lib/history-capture-policy.ts +95 -0
  59. package/lib/model-routing-authority.ts +5 -1
  60. package/lib/nan-provider.ts +227 -0
  61. package/lib/native-review-cli.ts +49 -108
  62. package/lib/odd-phase-inference.ts +231 -0
  63. package/lib/odd-phase.ts +141 -0
  64. package/lib/overlay-repaint.ts +26 -0
  65. package/lib/pi-tui-keys.ts +53 -0
  66. package/lib/review-candidate-view-owner.ts +67 -17
  67. package/lib/review-candidate-view.ts +112 -25
  68. package/lib/review-reminder-receipt.ts +48 -8
  69. package/lib/review-risk-assessment.ts +156 -11
  70. package/lib/review-sidebar-state.ts +223 -0
  71. package/lib/selection-engine.ts +515 -0
  72. package/lib/session-messaging-grants.ts +135 -0
  73. package/lib/session-worktree-registry.ts +14 -2
  74. package/lib/shell-bar.ts +179 -24
  75. package/lib/shell-card.ts +287 -18
  76. package/lib/shell-changes-view.ts +2 -1
  77. package/lib/shell-prompt.ts +102 -6
  78. package/lib/shell-sidebar-layout.ts +78 -14
  79. package/lib/shell-sidebar.ts +15 -1
  80. package/lib/shell-todo.ts +23 -13
  81. package/lib/shell-usage-view.ts +9 -4
  82. package/lib/shell-usage.ts +66 -10
  83. package/lib/stats-collector.ts +381 -0
  84. package/lib/stats-view.ts +431 -0
  85. package/lib/theme-customization.ts +52 -0
  86. package/lib/vim-editor-adapter.ts +379 -0
  87. package/lib/vim-normal-engine.ts +154 -0
  88. package/lib/vim-operator-engine.ts +416 -0
  89. package/lib/vim-policy.ts +49 -0
  90. package/lib/vim-visual-engine.ts +107 -0
  91. package/lib/visual-customization-policy.ts +108 -0
  92. package/lib/visual-customize-view.ts +330 -0
  93. package/lib/visual-profiles.ts +228 -0
  94. package/lib/yolo-session-policy.ts +240 -0
  95. package/package.json +20 -8
  96. package/runtime/gentle-shell-launcher.mjs +23 -12
  97. package/runtime/gentle-shell-resume-hint.mjs +177 -0
  98. package/runtime/native-review-cli.mjs +49 -108
  99. package/runtime/review-risk-assessment.mjs +154 -9
  100. package/scripts/build-runtime-modules.mjs +1 -0
  101. package/scripts/gentle-ai-installer.mjs +14 -13
  102. package/scripts/mirror-odd-routing.mjs +2 -2
  103. package/scripts/run-test-suite.mjs +76 -0
  104. package/scripts/test-packed-runner.mjs +31 -14
  105. package/scripts/verify-package-files.mjs +11 -22
  106. package/skills/branch-pr/SKILL.md +24 -52
  107. package/skills/chained-pr/SKILL.md +31 -15
  108. package/skills/chained-pr/references/chaining-details.md +31 -20
  109. package/skills/gentle-ai/SKILL.md +9 -15
  110. package/skills/issue-creation/SKILL.md +8 -2
  111. package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
  112. package/skills/work-unit-commits/SKILL.md +4 -3
  113. package/tests/agent-profiles.test.ts +18 -0
  114. package/tests/agents-fake-child.ts +2 -2
  115. package/tests/agents-message-delivery.test.ts +106 -0
  116. package/tests/agents-protocol.test.ts +40 -0
  117. package/tests/agents-runner.test.ts +378 -89
  118. package/tests/agents-view-thread-identity.test.ts +169 -0
  119. package/tests/agents-view.test.ts +8 -2
  120. package/tests/agents-widget.test.ts +154 -15
  121. package/tests/append-system-prompt-route.test.ts +160 -0
  122. package/tests/append-system-prompt.test.ts +46 -0
  123. package/tests/artifact-language.test.ts +19 -213
  124. package/tests/ask-user-question.test.ts +44 -1
  125. package/tests/asset-installation-runtime.test.ts +5 -16
  126. package/tests/autonomous-guard.test.ts +69 -1
  127. package/tests/bounded-writer-admission.test.ts +95 -0
  128. package/tests/branch-pr-skill.test.ts +43 -0
  129. package/tests/card-style-policy.test.ts +55 -0
  130. package/tests/chained-pr-skill.test.ts +124 -0
  131. package/tests/child-context-files.test.ts +255 -0
  132. package/tests/child-safety.test.ts +82 -0
  133. package/tests/codemode-rendering.test.ts +491 -0
  134. package/tests/command-palette.test.ts +39 -3
  135. package/tests/delegated-key-learnings-contract.test.ts +0 -76
  136. package/tests/destructive-command-guard.test.ts +84 -0
  137. package/tests/devbinary/native-review-parity.devtest.ts +170 -2
  138. package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
  139. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
  140. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
  141. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
  142. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
  143. package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
  144. package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
  145. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
  146. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
  147. package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
  148. package/tests/generic-agent-tools.test.ts +54 -0
  149. package/tests/gentle-agents.test.ts +1047 -252
  150. package/tests/gentle-ai-binary.test.ts +3 -3
  151. package/tests/gentle-ai-elapsed-store.test.ts +68 -0
  152. package/tests/gentle-ai-installer.test.ts +68 -54
  153. package/tests/gentle-ai-renderer.test.ts +487 -8
  154. package/tests/gentle-ai.test.ts +288 -65
  155. package/tests/gentle-card-text.ts +2 -1
  156. package/tests/gentle-shell-bin.test.ts +651 -114
  157. package/tests/gentle-shell-launcher.test.ts +99 -52
  158. package/tests/gentle-shell-resume-hint.test.ts +270 -0
  159. package/tests/gentle-shell.test.ts +3839 -187
  160. package/tests/gentle-stats.test.ts +152 -0
  161. package/tests/gentle-theme.test.ts +4 -1
  162. package/tests/gentle-todo.test.ts +80 -8
  163. package/tests/history-atomic-write.test.ts +57 -0
  164. package/tests/history-capture-policy.test.ts +102 -0
  165. package/tests/history-command-registration.test.ts +164 -0
  166. package/tests/history-dedupe-entries.test.ts +123 -0
  167. package/tests/history-delete-backfill.test.ts +190 -0
  168. package/tests/history-delete-confirm.test.ts +460 -0
  169. package/tests/history-dispatch.test.ts +180 -0
  170. package/tests/history-drain-hidden.test.ts +110 -0
  171. package/tests/history-drain-order.test.ts +98 -0
  172. package/tests/history-expanded-globals.test.ts +62 -0
  173. package/tests/history-gc.test.ts +832 -0
  174. package/tests/history-header-layout.test.ts +265 -0
  175. package/tests/history-hide-prompts.test.ts +275 -0
  176. package/tests/history-lazy-windowing.test.ts +508 -0
  177. package/tests/history-legacy-migrate-v2.test.ts +297 -0
  178. package/tests/history-load-shared-history.test.ts +53 -0
  179. package/tests/history-max-results-cap.test.ts +76 -0
  180. package/tests/history-multi-reader.test.ts +203 -0
  181. package/tests/history-off-path.test.ts +170 -0
  182. package/tests/history-openflow-integration.test.ts +173 -0
  183. package/tests/history-overlay-margin.test.ts +326 -0
  184. package/tests/history-preview-layout.test.ts +93 -0
  185. package/tests/history-registry.test.ts +143 -0
  186. package/tests/history-scope-delete.test.ts +411 -0
  187. package/tests/history-search-caret-keys.test.ts +142 -0
  188. package/tests/history-seed-bootstrap.test.ts +170 -0
  189. package/tests/history-seed-regen.test.ts +129 -0
  190. package/tests/history-selector-windowing.test.ts +94 -0
  191. package/tests/history-session-scan-directory.test.ts +87 -0
  192. package/tests/history-session-scan-extract.test.ts +583 -0
  193. package/tests/history-session-writer.test.ts +351 -0
  194. package/tests/history-store-paths.test.ts +79 -0
  195. package/tests/history-tombstone-exact.test.ts +139 -0
  196. package/tests/history-wheel-mouse.test.ts +242 -0
  197. package/tests/inprocess-reviewer.test.ts +29 -19
  198. package/tests/issue-creation-skill.test.ts +61 -0
  199. package/tests/model-routing-authority.test.ts +16 -0
  200. package/tests/nan-provider.test.ts +471 -0
  201. package/tests/native-review-capability-contract.test.ts +7 -1
  202. package/tests/native-review-cli.test.ts +6 -120
  203. package/tests/native-review-parity-runtime.test.ts +100 -3
  204. package/tests/odd-integration.test.ts +67 -0
  205. package/tests/odd-phase-inference.test.ts +213 -0
  206. package/tests/odd-phase-loader.test.ts +253 -0
  207. package/tests/odd-phase.test.ts +307 -0
  208. package/tests/odd-routing-canonical-ratchet.test.ts +11 -6
  209. package/tests/odd-routing-contract.test.ts +86 -35
  210. package/tests/orchestrator-budget.test.ts +14 -39
  211. package/tests/orchestrator-rdd-ownership.test.ts +3 -3
  212. package/tests/overlay-repaint.test.ts +74 -0
  213. package/tests/package-manifest.test.ts +252 -115
  214. package/tests/packed-runner-owned-path.test.ts +46 -0
  215. package/tests/persona-single-channel.test.ts +6 -6
  216. package/tests/provider-defect-handoff.test.ts +3 -11
  217. package/tests/quiet-bash-runtime.test.ts +76 -0
  218. package/tests/quiet-tool-rendering.test.ts +409 -184
  219. package/tests/rdd-aware-verification-contract.test.ts +76 -1
  220. package/tests/rdd-status-line.test.ts +9 -4
  221. package/tests/resume-hint-extension.test.ts +122 -0
  222. package/tests/review-agent-end-preflight.test.ts +176 -12
  223. package/tests/review-candidate-owner-retry.test.ts +22 -1
  224. package/tests/review-candidate-view.test.ts +298 -0
  225. package/tests/review-contract-prompt.test.ts +108 -43
  226. package/tests/review-controller-lock-status.test.ts +0 -1
  227. package/tests/review-controller-native-routing.test.ts +611 -5
  228. package/tests/review-controller-workspace-root.test.ts +163 -4
  229. package/tests/review-host-relay-routing.test.ts +338 -2
  230. package/tests/review-integration-v2-forward.test.ts +200 -0
  231. package/tests/review-ledger-contract.test.ts +10 -34
  232. package/tests/review-reminder-receipt.test.ts +47 -1
  233. package/tests/review-risk-assessment.test.ts +498 -6
  234. package/tests/review-sidebar-state.test.ts +402 -0
  235. package/tests/run-test-suite.test.ts +124 -0
  236. package/tests/runtime-harness.mjs +145 -786
  237. package/tests/runtime-metrics-children.test.ts +16 -23
  238. package/tests/selection-engine.test.ts +421 -0
  239. package/tests/session-messaging-grants.test.ts +255 -0
  240. package/tests/session-worktree-registry.test.ts +77 -0
  241. package/tests/shell-bar.test.ts +382 -1
  242. package/tests/shell-card.test.ts +353 -1
  243. package/tests/shell-changes-view.test.ts +52 -0
  244. package/tests/shell-prompt.test.ts +94 -2
  245. package/tests/shell-sidebar-layout.test.ts +325 -21
  246. package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
  247. package/tests/shell-todo.test.ts +87 -1
  248. package/tests/shell-usage-view.test.ts +27 -0
  249. package/tests/shell-usage.test.ts +73 -0
  250. package/tests/skill-registry.test.ts +50 -1
  251. package/tests/startup-banner.test.ts +130 -2
  252. package/tests/stats-collector.test.ts +195 -0
  253. package/tests/stats-view.test.ts +202 -0
  254. package/tests/telemetry-trigger.test.ts +81 -20
  255. package/tests/theme-customization.test.ts +72 -0
  256. package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
  257. package/tests/vim-editor-adapter.test.ts +804 -0
  258. package/tests/vim-normal-engine.test.ts +101 -0
  259. package/tests/vim-operator-engine.test.ts +215 -0
  260. package/tests/vim-policy.test.ts +19 -0
  261. package/tests/vim-visual-engine.test.ts +52 -0
  262. package/tests/visual-customization-policy.test.ts +110 -0
  263. package/tests/visual-customize-view.test.ts +418 -0
  264. package/tests/visual-profiles.test.ts +87 -0
  265. package/tests/yolo-customize.test.ts +256 -0
  266. package/tests/yolo-mode-runtime.test.ts +161 -0
  267. package/tests/yolo-mode.test.ts +261 -0
  268. package/tests/yolo-session-policy.test.ts +59 -0
  269. package/themes/Gentle.json +2 -1
  270. package/themes/Gentleman-Cute.json +2 -1
  271. package/themes/Gentleman-Sexy.json +2 -1
  272. package/assets/agents/sdd-apply.md +0 -159
  273. package/assets/agents/sdd-archive.md +0 -228
  274. package/assets/agents/sdd-design.md +0 -49
  275. package/assets/agents/sdd-explore.md +0 -48
  276. package/assets/agents/sdd-init.md +0 -56
  277. package/assets/agents/sdd-onboard.md +0 -52
  278. package/assets/agents/sdd-proposal.md +0 -64
  279. package/assets/agents/sdd-remediate.md +0 -37
  280. package/assets/agents/sdd-research.md +0 -49
  281. package/assets/agents/sdd-spec.md +0 -192
  282. package/assets/agents/sdd-status.md +0 -54
  283. package/assets/agents/sdd-tasks.md +0 -108
  284. package/assets/agents/sdd-verify.md +0 -124
  285. package/assets/chains/sdd-full.chain.md +0 -83
  286. package/assets/chains/sdd-plan.chain.md +0 -56
  287. package/assets/chains/sdd-verify.chain.md +0 -43
  288. package/assets/sdd-orchestrator-workflow.md +0 -319
  289. package/assets/support/sdd-status-contract.md +0 -77
  290. package/docs/assets/diagrams/sdd-cycle.svg +0 -14
  291. package/extensions/sdd-init.ts +0 -816
  292. package/lib/openspec-deltas.ts +0 -156
  293. package/lib/sdd-preflight.ts +0 -1066
  294. package/lib/sdd-research-capabilities.ts +0 -94
  295. package/lib/sdd-status.ts +0 -26
  296. package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
  297. package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
  298. package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
  299. package/tests/openspec-deltas.test.ts +0 -209
  300. package/tests/sdd-agent-tools.test.ts +0 -156
  301. package/tests/sdd-archive-replay.test.ts +0 -82
  302. package/tests/sdd-classical-continuation.test.ts +0 -74
  303. package/tests/sdd-execution-routing-contract.test.ts +0 -44
  304. package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
  305. package/tests/sdd-native-managed-uptake.test.ts +0 -243
  306. package/tests/sdd-no-attempts-contract.test.ts +0 -15
  307. package/tests/sdd-odd-integration.test.ts +0 -33
  308. package/tests/sdd-optional-research.test.ts +0 -124
  309. package/tests/sdd-planning-routing-contract.test.ts +0 -45
  310. package/tests/sdd-preflight-rpc-input.test.ts +0 -125
  311. package/tests/sdd-preflight.test.ts +0 -541
  312. package/tests/sdd-research-capabilities.test.ts +0 -114
  313. package/tests/sdd-research-live.test.ts +0 -241
  314. package/tests/sdd-selection-transport.test.ts +0 -653
  315. package/tests/sdd-status.test.ts +0 -9
  316. package/tests/sdd-task-truth.test.ts +0 -43
@@ -1,11 +1,13 @@
1
1
  import assert from "node:assert/strict";
2
- import { realpathSync } from "node:fs";
3
2
  import test from "node:test";
4
3
  import { initTheme, keyHint } from "@earendil-works/pi-coding-agent";
5
4
  import { imageFallback, visibleWidth } from "@earendil-works/pi-tui";
6
5
  import { cardBody, cardHint, cardTitle, cardTone } from "./gentle-card-text.ts";
6
+ import { CARD_STYLE, cardStyle, setCardStyle } from "../lib/shell-card.ts";
7
+ import { stripAnsi } from "../lib/terminal-theme.ts";
7
8
  import piPretty from "../extensions/pi-pretty.ts";
8
- import quietTools, {
9
+ import productionQuietTools, {
10
+ createQuietToolRenderer,
9
11
  countNonEmptyLines,
10
12
  extractTextContent,
11
13
  formatToolResultOutput,
@@ -50,10 +52,37 @@ function routineRenderContext(overrides: Record<string, unknown> = {}) {
50
52
  };
51
53
  }
52
54
 
53
- function renderLines(component: { render(width: number): string[] }, width = 120): string[] {
55
+ // Content assertions read the quiet tool card content: the top rule becomes its
56
+ // call text (glyph, fill, and hint dropped), sides are stripped, and the closing
57
+ // rule is dropped. Gentle AI cards keep their raw frame for the card-text
58
+ // helpers, and the quiet card tests assert the raw frame via frameLines.
59
+ // Legacy bash argument assertions drop its new identity prefix here; distinct
60
+ // tool identity is asserted separately against the actual unmodified frame.
61
+ // A pending call's `running…` row is frame chrome too, asserted separately.
62
+ const TAG = "(?:</?[a-zA-Z]+>)*";
63
+ const CARD_TOP = new RegExp(`^${TAG}╭─ ${TAG}[✿≡$⌕⌖☷✎+]${TAG} (.*?) ${TAG}─+${TAG}(?: .*? )?${TAG}╮${TAG}$`);
64
+ const CARD_SIDE = new RegExp(`^${TAG}│${TAG} (.*) ${TAG}│${TAG}$`);
65
+ const CARD_BOTTOM = new RegExp(`^${TAG}╰${TAG}─*${TAG}╯${TAG}$`);
66
+
67
+ function frameLines(component: { render(width: number): string[] }, width = 120): string[] {
54
68
  return component.render(width).map((line) => line.trimEnd());
55
69
  }
56
70
 
71
+ const QUIET_CARD_COMPONENTS = new Set(["ToolCardTop", "ToolCardBody"]);
72
+
73
+ function renderLines(component: { render(width: number): string[] }, width = 120): string[] {
74
+ if (!QUIET_CARD_COMPONENTS.has(component.constructor.name)) return component.render(width).map((line) => line.trimEnd());
75
+ const call = component.constructor.name === "ToolCardTop";
76
+ return component.render(width).flatMap((line) => {
77
+ if (CARD_BOTTOM.test(line)) return [];
78
+ const top = CARD_TOP.exec(line);
79
+ if (top) return [top[1]!.replace(/^(?:<[^>]+>)*bash(?:<[^>]+>)* /, "").trimEnd()];
80
+ const side = CARD_SIDE.exec(line);
81
+ const text = (side ? side[1]! : line).trimEnd();
82
+ return call && side && text.replace(/<\/?[a-zA-Z]+>/g, "") === "running…" ? [] : [text];
83
+ });
84
+ }
85
+
57
86
  function renderToString(component: { render(width: number): string[] }, width = 120): string {
58
87
  return renderLines(component, width).join("\n");
59
88
  }
@@ -141,6 +170,15 @@ async function withEnvAsync<T>(updates: Record<string, string | undefined>, run:
141
170
  }
142
171
  }
143
172
 
173
+ // Standalone rendering-only fixture for historical Bash formatting contracts.
174
+ // No executor, shellPath, or claim of production native-Bash UI parity.
175
+ function quietTools(pi: any, resolveOverride?: any) {
176
+ productionQuietTools(pi, resolveOverride);
177
+ if (process.env.GENTLE_PI_QUIET_TOOLS !== "0") {
178
+ pi.registerTool({ name: "bash", ...createQuietToolRenderer("bash", resolveOverride) });
179
+ }
180
+ }
181
+
144
182
  function registeredQuietTools() {
145
183
  const { pi, tools } = createPi();
146
184
  withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any));
@@ -161,17 +199,18 @@ function assertGenericBash(tool: any, command: string): void {
161
199
  const call = renderToString(tool.renderCall({ command }, passthroughTheme, { args: { command } }));
162
200
  const output = renderToolResult(tool, textResult("original command output"), { expanded: true, isPartial: false }, { args: { command } });
163
201
  assert.equal(call.trimEnd(), `$ ${command}`, command);
164
- assert.doesNotMatch(call, /🌹︎ Gentle AI/);
202
+ assert.doesNotMatch(call, /🌹/);
165
203
  assert.match(output, /original command output/);
166
204
  }
167
205
 
168
- test("quiet tool rendering registers noisy built-in tools", () => {
206
+ test("quiet tools register six overrides and codemode, never Bash", () => {
169
207
  withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => {
170
208
  const { pi, tools } = createPi();
171
209
 
172
- quietTools(pi as any);
210
+ productionQuietTools(pi as any);
173
211
 
174
- for (const toolName of ["read", "bash", "grep", "find", "ls", "edit", "write"]) {
212
+ assert.equal(tools.has("bash"), false);
213
+ for (const toolName of ["read", "grep", "find", "ls", "edit", "write", "codemode"]) {
175
214
  const tool = tools.get(toolName);
176
215
  assert.ok(tool, `missing quiet renderer for ${toolName}`);
177
216
  assert.equal(tool.renderShell, "self", `${toolName} must opt out of Pi's painted Box`);
@@ -181,24 +220,19 @@ test("quiet tool rendering registers noisy built-in tools", () => {
181
220
  });
182
221
  });
183
222
 
184
- test("quiet tool execution uses the tool-call cwd", async () => {
185
- const tool = registeredQuietTools().get("bash");
186
- const context = {
187
- cwd: "/tmp",
188
- sessionManager: {
189
- getSessionId: () => "quiet-tool-test",
190
- getSessionFile: () => undefined,
191
- },
192
- };
193
- const output = extractTextContent(
194
- await tool.execute("tool-call", { command: "pwd" }, new AbortController().signal, undefined, context),
195
- ).trim();
196
- // pwd prints the physical directory: on macOS /tmp is a symlink to /private/tmp.
197
- assert.equal(output, realpathSync("/tmp"));
198
- assert.notEqual(output, process.cwd());
223
+ test("quiet tools never replace an existing native Bash tool", () => {
224
+ withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => {
225
+ const { pi, tools } = createPi({ throwOnToolConflict: true });
226
+ const native = createSdkTool("bash");
227
+ pi.registerTool(native);
228
+ productionQuietTools(pi as any);
229
+ assert.strictEqual(tools.get("bash"), native);
230
+ });
199
231
  });
200
232
 
201
- test("quiet Bash rendering leaves configured shellPath outside its scope (#107)", () => {
233
+ // Actual configured Bash execution is covered in quiet-bash-runtime.test.ts.
234
+
235
+ test("standalone Bash rendering fixture does not interpret shellPath", () => {
202
236
  const tool = registeredQuietTools().get("bash");
203
237
  const render = (context: Record<string, unknown> = {}) => renderToString(tool.renderCall({ command: "printf output" }, passthroughTheme, routineRenderContext(context)));
204
238
  const configured = render({ shellPath: "/configured/bash" });
@@ -210,7 +244,7 @@ test("quiet tool rendering can be disabled by env", () => {
210
244
  withEnv({ GENTLE_PI_QUIET_TOOLS: "0" }, () => {
211
245
  const { pi, tools } = createPi();
212
246
 
213
- quietTools(pi as any);
247
+ productionQuietTools(pi as any);
214
248
 
215
249
  assert.equal(tools.size, 0);
216
250
  });
@@ -223,9 +257,10 @@ test("pi-pretty suppresses overlapping tools before quiet tools register", async
223
257
  const { pi, tools } = createPi({ throwOnToolConflict: true });
224
258
 
225
259
  await piPretty(pi as any, fakePiPrettyDeps as any);
226
- quietTools(pi as any);
260
+ productionQuietTools(pi as any);
227
261
 
228
- for (const toolName of ["read", "bash", "grep", "find", "ls", "edit", "write"]) {
262
+ assert.equal(tools.has("bash"), false);
263
+ for (const toolName of ["read", "grep", "find", "ls", "edit", "write"]) {
229
264
  assert.ok(tools.has(toolName), `missing quiet tool ${toolName}`);
230
265
  }
231
266
  assert.equal(process.env.PRETTY_DISABLE_TOOLS, "multi_grep,read,bash,ls,find,grep");
@@ -288,9 +323,10 @@ test("quiet tool rendering uses bounded previews while preserving search summari
288
323
  );
289
324
  assert.match(read, /first line\nsecond line/);
290
325
  assert.match(bash, /stdout line\nstderr line/);
326
+ // The expand key rides the call card's top rule, never the result body.
291
327
  const configuredHint = keyHint("app.tools.expand", "to expand");
292
- assert.ok(read.includes(configuredHint));
293
- assert.ok(bash.includes(configuredHint));
328
+ assert.ok(!read.includes(configuredHint));
329
+ assert.ok(!bash.includes(configuredHint));
294
330
 
295
331
  for (const [tool, text, label] of [
296
332
  ["grep", "src/a.ts:1:match\nsrc/b.ts:2:match", "2 matches"],
@@ -304,7 +340,7 @@ test("quiet tool rendering uses bounded previews while preserving search summari
304
340
  tools.get(tool).renderResult(textResult(text), { expanded: true, isPartial: false }, passthroughTheme, {}),
305
341
  );
306
342
  assert.match(collapsed, new RegExp(label));
307
- assert.doesNotMatch(collapsed, /src\/a\.ts|file-a\.ts/);
343
+ assert.match(collapsed, /src\/a\.ts|file-a\.ts/);
308
344
  assert.match(expanded, new RegExp(text.split("\n")[1]!));
309
345
  }
310
346
  });
@@ -343,18 +379,18 @@ test("quiet search and placeholder summaries preserve exact-result behavior", as
343
379
  const tools = registeredQuietTools();
344
380
  const hint = keyHint("app.tools.expand", "to expand");
345
381
  const cases = [
346
- ["grep", "context rows count only match rows", "grep", contextText, { context: 1 }, " → 5 matches", undefined, /5 matches/, /15 matches/, 1],
382
+ ["grep", "context rows count only match rows", "grep", contextText, { context: 1 }, " → 5 matches", undefined, /5 matches[\s\S]*first match/, /15 matches/, 1],
347
383
  ["grep", "no-context output counts every non-empty row", "grep", "src/a.ts:1:match\nsrc/b.ts:2:match", {}, " → 2 matches", undefined, /2 matches/, undefined, 1],
348
384
  ["exact", "current empty-directory marker", "ls", "(empty directory)", {}, "", undefined, /^$/, /entries|to expand/, 0],
349
385
  ["exact", "legacy empty-directory marker", "ls", "Directory is empty", {}, "", undefined, /^$/, /entries|to expand/, 0],
350
386
  ["exact", "no-output marker", "bash", "(no output)", { command: "true" }, "\n(no output)", undefined, /^\(no output\)$/, /to expand/, 0],
351
- ["collision", "empty-directory marker with a real entry", "ls", lsCollisionText, {}, " → 2 entries", `\n${lsCollisionText}`, /2 entries/, /empty directory|real-entry\.txt/, 1],
387
+ ["collision", "empty-directory marker with a real entry", "ls", lsCollisionText, {}, " → 2 entries", `\n${lsCollisionText}`, /2 entries[\s\S]*real-entry\.txt/, undefined, 1],
352
388
  ["collision", "no-output marker with real output", "bash", bashCollisionText, { command: "printf output" }, `\n${bashCollisionText}`, `\n${bashCollisionText}`, /\(no output\)\nreal output/, undefined, 1],
353
389
  ] as const;
354
390
  assert.equal(cases.length, 7);
355
391
 
356
392
  const assertSummaryCase = (current: (typeof cases)[number]) => {
357
- const [, name, toolName, text, args, collapsed, expanded, rendered, forbidden, hintCount] = current;
393
+ const [, name, toolName, text, args, collapsed, expanded, rendered, forbidden] = current;
358
394
  const result = textResult(text) as any;
359
395
  const tool = tools.get(toolName);
360
396
  assert.ok(tool, `${name}: missing ${toolName} renderer`);
@@ -363,7 +399,7 @@ test("quiet search and placeholder summaries preserve exact-result behavior", as
363
399
  const output = renderToolResult(tool, result, { expanded: false, isPartial: false }, { args });
364
400
  assert.match(output, rendered, name);
365
401
  if (forbidden) assert.doesNotMatch(output, forbidden, name);
366
- assert.equal(output.split(hint).length - 1, hintCount, `${name}: hint count`);
402
+ assert.equal(output.split(hint).length - 1, 0, `${name}: the expand key rides the call card, not the result`);
367
403
  };
368
404
 
369
405
  for (const [section, count] of [["grep", 2], ["exact", 3], ["collision", 2]] as const) await t.test(section, () => {
@@ -408,9 +444,9 @@ test("quiet Bash errors preserve meaningful rows for both public output ordering
408
444
  assert.ok(collapsedLines.length <= 4, `${name}: expected at most 4 visual rows, got ${collapsedLines.length}`);
409
445
  assert.deepEqual(collapsedLines.slice(0, 3), previewRows, `${name}: preview rows`);
410
446
  assert.equal(collapsedLines.filter((line) => line.trim().length === 0).length, 0, `${name}: no blank preview rows`);
411
- assert.equal(collapsedLines.filter((line) => line.includes(hint)).length, 1, `${name}: one expansion hint`);
447
+ assert.equal(collapsedLines.filter((line) => line.includes(hint)).length, 0, `${name}: the expand key rides the call card`);
412
448
  const expanded = renderToString(renderResult(result, true), 1000);
413
- assert.equal(expanded, `\n${renderedExpandedText}`, `${name}: complete expanded sanitized text`);
449
+ assert.equal(expanded, renderedExpandedText, `${name}: complete expanded sanitized text`);
414
450
  assert.doesNotMatch(expanded, /\x1b\[/, `${name}: expanded output is ANSI-free`);
415
451
  }
416
452
  });
@@ -439,20 +475,38 @@ test("quiet Bash normalizes compact JSON for bounded previews while keeping expa
439
475
  assert.equal(previewRows.length, 3);
440
476
  assert.ok(collapsedLines.length <= 4, `expected at most 4 visual rows, got ${collapsedLines.length}`);
441
477
  assert.deepEqual(collapsedLines.slice(0, 3), previewRows);
442
- assert.equal(collapsedLines.filter((line) => line.includes(keyHint("app.tools.expand", "to expand"))).length, 1);
478
+ assert.equal(collapsedLines.filter((line) => line.includes(keyHint("app.tools.expand", "to expand"))).length, 0);
443
479
  assert.doesNotMatch(collapsed, /longTail|this must stay out of the collapsed preview|deeper/);
444
480
 
445
481
  const expanded = renderToString(tool.renderResult(result, { expanded: true, isPartial: false }, passthroughTheme, { args: commandArgs }), 1000);
446
- assert.equal(expanded, `\n${compactJson}`);
482
+ assert.equal(expanded, compactJson);
483
+ });
484
+
485
+ test("retired SDD commands use generic bash rendering while review remains private", () => {
486
+ const { pi, tools } = createPi();
487
+ withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any));
488
+ const bash = tools.get("bash");
489
+ for (const command of [
490
+ "gentle-ai sdd-status rose --json",
491
+ "env FOO=bar gentle-ai sdd-continue rose",
492
+ "gentle-ai sdd-attempt grant --authorization-root /root",
493
+ "./.gentle-ai/v2.2.0/gentle-ai sdd-status rose --json",
494
+ "C:\\package\\.gentle-ai\\v2.2.0\\gentle-ai.exe sdd-continue rose",
495
+ ]) {
496
+ assert.equal(gentleAiRoutineCommand({ command }), undefined, command);
497
+ const call = renderToString(bash.renderCall({ command }, passthroughTheme, { args: { command } }));
498
+ assert.equal(call, `$ ${command}`);
499
+ const result = renderToString(bash.renderResult(textResult("ordinary output"), { expanded: false, isPartial: false }, passthroughTheme, { args: { command } }));
500
+ assert.match(result, /ordinary output/);
501
+ }
502
+ const review = "gentle-ai review status --next-transition";
503
+ assert.equal(gentleAiRoutineCommand({ command: review }), "review");
504
+ assert.equal(cardTitle(renderToString(bash.renderCall({ command: review }, passthroughTheme, { args: { command: review } }))), "🌹 rdd running · status");
447
505
  });
448
506
 
449
- test("quiet tool rendering identifies only routine Gentle AI SDD and RDD commands", () => {
450
- assert.equal(gentleAiRoutineCommand({ command: "gentle-ai sdd-status fix-rose --json" }), "sdd-status");
451
- assert.equal(gentleAiRoutineCommand({ command: "env FOO=bar gentle-ai sdd-continue fix-rose" }), "sdd-continue");
507
+ test("quiet tool rendering identifies only routine Gentle AI review commands", () => {
452
508
  assert.equal(gentleAiRoutineCommand({ command: "/package/.gentle-ai/v2.2.0/gentle-ai review status --next-transition" }), "review");
453
- assert.equal(gentleAiRoutineCommand({ command: "./.gentle-ai/v2.2.0/gentle-ai sdd-status rose --json" }), "sdd-status");
454
509
  assert.equal(gentleAiRoutineCommand({ command: ".\\.gentle-ai\\v2.2.0\\gentle-ai.exe review status --next-transition" }), "review");
455
- assert.equal(gentleAiRoutineCommand({ command: "C:\\package\\.gentle-ai\\v2.2.0\\gentle-ai.exe sdd-continue rose" }), "sdd-continue");
456
510
  assert.equal(gentleAiRoutineCommand({ command: "gentle-ai sdd-attempt acquire --change fix-rose" }), undefined);
457
511
  assert.equal(gentleAiRoutineCommand({ command: "gentle-ai sdd-attempt settle --change fix-rose" }), undefined);
458
512
  assert.equal(gentleAiRoutineCommand({ command: "gentle-ai review status --next-transition" }), "review");
@@ -492,11 +546,11 @@ test("quiet tool rendering refuses to compact composed Gentle AI shell commands"
492
546
  }
493
547
  });
494
548
 
495
- test("quiet tool rendering hides every collapsed direct Gentle AI result and preserves expanded errors", () => {
549
+ test("quiet tool rendering previews direct Gentle AI results and preserves expanded errors", () => {
496
550
  const { pi, tools } = createPi();
497
551
  withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any));
498
552
  const command = "gentle-ai review status --next-transition";
499
- const textRose = "\u{1F339}\uFE0E";
553
+ const rose = "\u{1F339}";
500
554
  const tool = tools.get("bash");
501
555
  const failureText = "review status failed: authority unavailable\x1b[31m\nlineage=secret";
502
556
  const expandHint = keyHint("app.tools.expand", "to expand");
@@ -509,16 +563,15 @@ test("quiet tool rendering hides every collapsed direct Gentle AI result and pre
509
563
  const empty = renderToString(tool.renderResult(textResult(""), { expanded: false, isPartial: false }, passthroughTheme, { args: { command } }));
510
564
  const nonText = renderToString(tool.renderResult({ content: [{ type: "image", data: "opaque", mimeType: "image/png" }] }, { expanded: false, isPartial: false }, passthroughTheme, { args: { command } }));
511
565
 
512
- assert.equal([...textRose].length, 2);
513
- assert.equal(cardTitle(call), `🌹︎ Gentle AI · running · review status`);
514
- assert.match(cardBody(collapsed), /\d+ lines?\b/);
515
- assert.match(cardBody(collapsed), /\d+ lines?\b/);
516
- assert.doesNotMatch(collapsed, /next_transition|stop/);
517
- assert.equal(cardBody(expanded).split("\n")[0], '{"next_transition":"stop"}');
518
- assert.match(expanded, /"next_transition":"stop"/);
566
+ assert.equal([...rose].length, 1, "the rose is a plain emoji, no presentation selector");
567
+ assert.equal(visibleWidth(rose), 2);
568
+ assert.equal(cardTitle(call), `🌹 rdd running · status`);
569
+ assert.equal(cardBody(collapsed), "stop", "a JSON envelope collapses to its readable summary");
570
+ assert.doesNotMatch(collapsed, /\{"|next_transition/);
571
+ assert.equal(cardBody(expanded), '{\n "next_transition": "stop"\n}', "expanded JSON is pretty-printed");
519
572
  assert.doesNotMatch(expanded, /to expand/);
520
- assert.match(cardBody(failure), /\d+ lines?\b/);
521
- assert.doesNotMatch(failure, /review status failed|authority unavailable|lineage=secret/);
573
+ assert.match(cardBody(failure), /review status failed.*authority unavailable/);
574
+ assert.match(failure, /lineage=secret/);
522
575
  assert.match(expandedFailure, /review status failed: authority unavailable/);
523
576
  assert.match(expandedFailure, /lineage=secret/);
524
577
  assert.doesNotMatch(expandedFailure, /to expand/);
@@ -566,16 +619,16 @@ test("quiet tool rendering transitions one Gentle AI header through lifecycle st
566
619
  assert.strictEqual(initial, running);
567
620
  assert.strictEqual(running, completed);
568
621
  assert.strictEqual(completed, failed);
569
- assert.equal(cardTitle(initialText), "🌹︎ Gentle AI · running · review status"); assert.equal(cardTone(initialText), "warning");
570
- assert.equal(cardTitle(runningText), "🌹︎ Gentle AI · running · review status"); assert.equal(cardTone(runningText), "warning");
571
- assert.equal(cardTitle(completedText), "🌹︎ Gentle AI · completed · review status"); assert.equal(cardTone(completedText), "success");
572
- assert.equal(cardTitle(failedText), "🌹︎ Gentle AI · failed · review status"); assert.equal(cardTone(failedText), "error");
622
+ assert.equal(cardTitle(initialText), "🌹 rdd running · status"); assert.equal(cardTone(initialText), "warning");
623
+ assert.equal(cardTitle(runningText), "🌹 rdd running · status"); assert.equal(cardTone(runningText), "warning");
624
+ assert.equal(cardTitle(completedText), "🌹 rdd status"); assert.equal(cardTone(completedText), "success");
625
+ assert.equal(cardTitle(failedText), "🌹 rdd failed · status"); assert.equal(cardTone(failedText), "error");
573
626
  assert.doesNotMatch(failedText, /private-change/);
574
627
  });
575
628
 
576
629
  test("quiet Bash keeps direct Gentle AI calls seamless while streaming", () => {
577
630
  const tool = registeredQuietTools().get("bash");
578
- const rose = /🌹︎ Gentle AI/;
631
+ const rose = /🌹/;
579
632
  const render = (command: string, state: Record<string, unknown>, overrides: Record<string, unknown> = {}) => {
580
633
  const component = tool.renderCall({ command }, passthroughTheme, routineRenderContext({ args: { command }, state, argsComplete: false, ...overrides }));
581
634
  return [component, renderToString(component)] as const;
@@ -583,14 +636,14 @@ test("quiet Bash keeps direct Gentle AI calls seamless while streaming", () => {
583
636
  const command = "gentle-ai review status --token 'lineage-secret";
584
637
  const state = {};
585
638
  const [preparing, collapsed] = render(command, state);
586
- assert.equal(cardTitle(collapsed), "🌹︎ Gentle AI · preparing · review status");
639
+ assert.equal(cardTitle(collapsed), "🌹 rdd preparing · status");
587
640
  assert.doesNotMatch(collapsed, /token|lineage-secret/);
588
641
  const expanded = render(command, state, { expanded: true, lastComponent: preparing })[1];
589
- assert.equal(cardTitle(expanded), "🌹︎ Gentle AI · preparing · review status"); assert.equal(cardBody(expanded), "$ gentle-ai review status --token 'lineage-secret");
642
+ assert.equal(cardTitle(expanded), "🌹 rdd preparing · status"); assert.equal(cardBody(expanded), "$ gentle-ai review status --token 'lineage-secret");
590
643
 
591
644
  const direct = "gentle-ai review status";
592
645
  const [running, runningText] = render(direct, state, { argsComplete: true, executionStarted: true, lastComponent: preparing });
593
- assert.strictEqual(running, preparing); assert.equal(cardTitle(runningText), "🌹︎ Gentle AI · running · review status");
646
+ assert.strictEqual(running, preparing); assert.equal(cardTitle(runningText), "🌹 rdd running · status");
594
647
  const genericState = {};
595
648
  const [generic, genericText] = render("gentle-ai review status | cat", genericState);
596
649
  assert.doesNotMatch(genericText, rose); assert.match(genericText, /\| cat/);
@@ -607,19 +660,14 @@ test("quiet Bash keeps direct Gentle AI calls seamless while streaming", () => {
607
660
  assert.equal(genericResult.split("\n")[0], "generic result");
608
661
  const directState = {}; render(direct, directState, { argsComplete: true });
609
662
  const directResult = renderToolResult(tool, textResult("direct result"), { expanded: false, isPartial: false }, { args: { command: direct }, state: directState, argsComplete: true });
610
- assert.match(cardBody(directResult), /\d+ lines?\b/);
663
+ assert.match(cardBody(directResult), /direct result/);
611
664
  });
612
665
 
613
666
  test("quiet tool rendering displays only finite safe Gentle AI operation paths", () => {
614
667
  const { pi, tools } = createPi();
615
668
  withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any));
616
669
  const tool = tools.get("bash");
617
- const rose = "🌹︎";
618
670
  const cases = [
619
- ["gentle-ai sdd-status change-123 --cwd /repo/private", "sdd status"],
620
- ["gentle-ai sdd-continue change-123 --json", "sdd continue"],
621
- ["gentle-ai sdd-attempt acquire --change change-123", "sdd attempt"],
622
- ["gentle-ai sdd-attempt settle --change change-123 --actor maintainer", "sdd attempt"],
623
671
  ["gentle-ai review capabilities --cwd /repo/private", "review capabilities"],
624
672
  ["gentle-ai review start --target sha256:secret --path src/private.ts", "review start"],
625
673
  ["gentle-ai review finalize --lineage lineage-secret --payload '{\"secret\":true}'", "review finalize"],
@@ -673,7 +721,7 @@ test("quiet tool rendering displays only finite safe Gentle AI operation paths",
673
721
  routineRenderContext({ args: { command } }),
674
722
  ),
675
723
  );
676
- assert.equal(cardTitle(rendered), `🌹︎ Gentle AI · running · ${path}`, command);
724
+ assert.equal(cardTitle(rendered), path === "review" ? "🌹 rdd running" : `🌹 rdd running · ${path.slice("review ".length)}`, command);
677
725
  assert.doesNotMatch(rendered, /change-123|private|secret|lineage|sha256|result\.json|incident\.json/);
678
726
  }
679
727
  });
@@ -682,22 +730,20 @@ test("quiet tool rendering covers version and future standalone Gentle AI comman
682
730
  const { pi, tools } = createPi();
683
731
  withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any));
684
732
  const tool = tools.get("bash");
685
- const rose = "🌹︎";
686
733
  const cases = [
687
- ["gentle-ai version", "version"],
688
- ["gentle-ai future-command --authorization-root /repo/private", "command"],
689
- ["gentle-ai sdd-attempt future-verb --change secret-change", "sdd attempt"],
690
- ["gentle-ai review future-operation --path /repo/private", "review"],
691
- ["/package/.gentle-ai/v2.2.0/gentle-ai version", "version"],
692
- ["C:\\package\\.gentle-ai\\v2.2.0\\gentle-ai.exe future-command --root C:\\private", "command"],
734
+ ["gentle-ai version", "gentle-ai running · version"],
735
+ ["gentle-ai future-command --authorization-root /repo/private", "gentle-ai running · command"],
736
+ ["gentle-ai review future-operation --path /repo/private", "rdd running"],
737
+ ["/package/.gentle-ai/v2.2.0/gentle-ai version", "gentle-ai running · version"],
738
+ ["C:\\package\\.gentle-ai\\v2.2.0\\gentle-ai.exe future-command --root C:\\private", "gentle-ai running · command"],
693
739
  ] as const;
694
740
 
695
741
  for (const [command, path] of cases) {
696
742
  const rendered = renderToString(
697
743
  tool.renderCall({ command }, passthroughTheme, routineRenderContext({ args: { command } })),
698
744
  );
699
- assert.equal(cardTitle(rendered), `🌹︎ Gentle AI · running · ${path}`, command);
700
- assert.doesNotMatch(rendered, /authorization-root|secret-change|private|C:\\\\private/);
745
+ assert.equal(cardTitle(rendered), `🌹 ${path}`, command);
746
+ assert.doesNotMatch(rendered, /authorization-root|private|C:\\\\private/);
701
747
  }
702
748
  });
703
749
 
@@ -724,13 +770,13 @@ test("quiet tool rendering recognizes exact quoted, escaped, Windows, and comman
724
770
 
725
771
  for (const command of routineCommands) {
726
772
  const call = renderToString(tool.renderCall({ command }, passthroughTheme, { args: { command } }));
727
- assert.equal(cardTitle(call), "🌹︎ Gentle AI · running · version", command);
773
+ assert.equal(cardTitle(call), "🌹 gentle-ai running · version", command);
728
774
  }
729
775
 
730
776
  for (const command of ["'gentle-ai-copy' version", '"gentle-ai-copy.exe" version', "'gentle-ai' version && echo done"] as const) {
731
777
  const call = renderToString(tool.renderCall({ command }, passthroughTheme, { args: { command } }));
732
778
  assert.equal(call.trimEnd(), `$ ${command}`);
733
- assert.doesNotMatch(call, /🌹︎ Gentle AI/);
779
+ assert.doesNotMatch(call, /🌹/);
734
780
  }
735
781
  });
736
782
 
@@ -743,35 +789,33 @@ test("quiet tool rendering recognizes only the exact resolved dev binary", () =>
743
789
  });
744
790
  const tool = tools.get("bash");
745
791
  const cases = [
746
- [`${devPath} review validate --gate pre-commit --cwd /repo/private`, "review validate pre commit"],
747
- [`'${devPath}' review status --lineage secret`, "review status"],
748
- [`"${devPath}" version`, "version"],
749
- [`env FOO='a b' ${devPath} review capabilities`, "review capabilities"],
750
- [`command -- "${devPath}" sdd-status hidden`, "sdd status"],
792
+ [`${devPath} review validate --gate pre-commit --cwd /repo/private`, "rdd running · validate pre commit"],
793
+ [`'${devPath}' review status --lineage secret`, "rdd running · status"],
794
+ [`"${devPath}" version`, "gentle-ai running · version"],
795
+ [`env FOO='a b' ${devPath} review capabilities`, "rdd running · capabilities"],
751
796
  ] as const;
752
797
  for (const [command, operationPath] of cases) {
753
798
  const call = renderToString(tool.renderCall({ command }, statusTheme, routineRenderContext({ args: { command } })));
754
- assert.equal(cardTitle(call), `🌹︎ Gentle AI · running · ${operationPath}`, command); assert.equal(cardTone(call), "warning", command);
799
+ assert.equal(cardTitle(call), `🌹 ${operationPath}`, command); assert.equal(cardTone(call), "warning", command);
755
800
  assert.doesNotMatch(call, /gentle-ai-main|private|secret|hidden/);
756
801
  }
757
802
  const command = `${devPath} review status --prompt hidden-prompt --lineage lineage-secret --body private-body`;
758
803
  const lifecycleContext = routineRenderContext({ args: { command }, state: {} });
759
- assert.match(renderToString(tool.renderCall({ command }, passthroughTheme, lifecycleContext)), /Gentle AI · running · review status/);
804
+ assert.match(renderToString(tool.renderCall({ command }, passthroughTheme, lifecycleContext)), /rdd running · status/);
760
805
  devPath = "/new/dev/gentle-ai-main";
761
806
  const refreshedCommand = `${devPath} review status --token token-secret`;
762
- assert.match(renderToString(tool.renderCall({ command: refreshedCommand }, passthroughTheme, routineRenderContext({ args: { command: refreshedCommand } }))), /Gentle AI · running · review status/);
807
+ assert.match(renderToString(tool.renderCall({ command: refreshedCommand }, passthroughTheme, routineRenderContext({ args: { command: refreshedCommand } }))), /rdd running · status/);
763
808
  assertGenericBash(tool, cases[2][0]);
764
809
  const text = "error: private failure\nlineage=secret body=hidden\x1b[31m";
765
810
  const collapsed = renderToolResult(tool, textResult(text), { expanded: false, isPartial: false, isError: true }, lifecycleContext);
766
811
  const expanded = renderToolResult(tool, textResult(text), { expanded: true, isPartial: false, isError: true }, lifecycleContext);
767
812
  const refreshed = renderToolResult(tool, textResult("result-secret"), { expanded: false, isPartial: false }, { args: { command: refreshedCommand } });
768
813
  const hint = keyHint("app.tools.expand", "to expand");
769
- assert.match(cardBody(collapsed), /\d+ lines?\b/);
814
+ assert.match(cardBody(collapsed), /private failure/);
770
815
  assert.equal(cardBody(collapsed).split(hint).length - 1, 0);
771
- assert.doesNotMatch(collapsed, /private|lineage|secret|hidden|error/);
772
816
  assert.match(expanded, /private failure|lineage=secret body=hidden/);
773
817
  assert.doesNotMatch(cardBody(expanded), /to expand|\x1b\[/);
774
- assert.doesNotMatch(refreshed, /result-secret/);
818
+ assert.match(refreshed, /result-secret/);
775
819
  assert.ok(resolutions > 1);
776
820
  });
777
821
  test("quiet tool rendering keeps unregistered dev lookalikes and composed calls generic", () => {
@@ -792,7 +836,7 @@ test("quiet tool rendering keeps unregistered dev lookalikes and composed calls
792
836
  assertGenericBash(throwing!.get("bash"), unresolved);
793
837
  assertGenericBash(registeredQuietToolsWithResolver(() => ({ path: "relative/gentle-ai-main" })).get("bash"), unresolved);
794
838
  });
795
- test("quiet tool rendering hides the routine partial result because the header owns state", () => {
839
+ test("quiet tool rendering previews routine partial content while the header owns state", () => {
796
840
  const { pi, tools } = createPi();
797
841
  withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any));
798
842
  const tool = tools.get("bash");
@@ -832,12 +876,12 @@ test("quiet tool rendering hides the routine partial result because the header o
832
876
  ),
833
877
  );
834
878
 
835
- assert.match(cardBody(partial), /\d+ lines?\b/);
879
+ assert.equal(cardBody(partial), "running");
836
880
  assert.equal(cardBody(partial).split(expandHint).length - 1, 0);
837
- assert.match(partialExpanded, /"status":"running"/);
881
+ assert.match(partialExpanded, /"status": "running"/);
838
882
  assert.doesNotMatch(partialExpanded, /to expand/);
839
- assert.match(cardBody(partialFailure), /\d+ lines?\b/);
840
- assert.match(cardBody(completed), /\d+ lines?\b/);
883
+ assert.match(cardBody(partialFailure), /authority unavailable/);
884
+ assert.equal(cardBody(completed), "running");
841
885
 
842
886
  const partialExpandedFailure = renderToString(
843
887
  tool.renderResult(
@@ -851,62 +895,7 @@ test("quiet tool rendering hides the routine partial result because the header o
851
895
  assert.doesNotMatch(cardBody(partialExpandedFailure), /\x1b\[/);
852
896
  });
853
897
 
854
- test("quiet tool rendering collapses grant calls to action and authorization-root cardinality", () => {
855
- const { pi, tools } = createPi();
856
- withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any));
857
- const tool = tools.get("bash");
858
- const command = "gentle-ai sdd-attempt grant --authorization-root /repo/root --authorization-root /other/root --change secret-change\x1b[31m";
859
-
860
- const initial = tool.renderCall(
861
- { command },
862
- statusTheme,
863
- routineRenderContext({ args: { command } }),
864
- );
865
- const running = tool.renderCall(
866
- { command },
867
- statusTheme,
868
- routineRenderContext({ args: { command }, executionStarted: true, lastComponent: initial }),
869
- );
870
- const completed = tool.renderCall(
871
- { command },
872
- statusTheme,
873
- routineRenderContext({ args: { command }, executionStarted: true, isPartial: false, lastComponent: running }),
874
- );
875
- const failed = tool.renderCall(
876
- { command },
877
- statusTheme,
878
- routineRenderContext({ args: { command }, executionStarted: true, isPartial: false, isError: true, lastComponent: completed }),
879
- );
880
-
881
- assert.strictEqual(initial, running);
882
- assert.strictEqual(running, completed);
883
- assert.strictEqual(completed, failed);
884
- const rendered = renderToString(failed);
885
- assert.equal(cardTitle(rendered), "🌹︎ Gentle AI · failed · sdd attempt grant · 2 roots"); assert.equal(cardTone(rendered), "error");
886
- assert.doesNotMatch(rendered.split(keyHint("app.tools.expand", "to expand")).join(""), /authorization-root|secret-change|repo\/root|other\/root|audit:|\x1b\[/);
887
- });
888
-
889
- test("quiet tool rendering counts grant authorization roots without rendering values", () => {
890
- const { pi, tools } = createPi();
891
- withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any));
892
- const tool = tools.get("bash");
893
- const cases = [
894
- ["gentle-ai sdd-attempt grant --change change", "sdd attempt grant"],
895
- ["gentle-ai sdd-attempt grant --authorization-root /repo/root", "sdd attempt grant · 1 root"],
896
- ["gentle-ai sdd-attempt grant --authorization-root=/repo/root", "sdd attempt grant · 1 root"],
897
- ["gentle-ai sdd-attempt grant --authorization-root --change change", "sdd attempt grant"],
898
- ["gentle-ai sdd-attempt grant --authorization-root /one --authorization-root=/two --authorization-root /three", "sdd attempt grant · 3 roots"],
899
- ["gentle-ai sdd-attempt grant --authorization-root= --authorization-root /one", "sdd attempt grant · 1 root"],
900
- ] as const;
901
-
902
- for (const [command, expected] of cases) {
903
- const rendered = renderToString(tool.renderCall({ command }, passthroughTheme, { args: { command } }));
904
- assert.equal(cardTitle(rendered), `🌹︎ Gentle AI · running · ${expected}`, command);
905
- assert.doesNotMatch(rendered, /authorization-root|\/repo\/root|\/one|\/two|\/three|change/);
906
- }
907
- });
908
-
909
- test("quiet tool rendering hides invocation secrets from collapsed Gentle AI calls and results", () => {
898
+ test("quiet tool rendering hides invocation arguments but previews result content without redaction", () => {
910
899
  const { pi, tools } = createPi();
911
900
  withEnv({ GENTLE_PI_QUIET_TOOLS: undefined }, () => quietTools(pi as any));
912
901
  const tool = tools.get("bash");
@@ -921,11 +910,11 @@ test("quiet tool rendering hides invocation secrets from collapsed Gentle AI cal
921
910
  ),
922
911
  );
923
912
 
924
- assert.equal(cardTitle(call), "🌹︎ Gentle AI · running · review finalize");
925
- assert.match(cardBody(collapsed), /\d+ lines?\b/);
913
+ assert.equal(cardTitle(call), "🌹 rdd running · finalize");
914
+ assert.match(cardBody(collapsed), /audit:/);
926
915
  for (const forbidden of ["hidden-prompt", "lineage-secret", "private-body", "/private/root", "audit:"]) {
927
916
  assert.doesNotMatch(call, new RegExp(forbidden));
928
- assert.doesNotMatch(collapsed, new RegExp(forbidden));
917
+ assert.match(collapsed, new RegExp(forbidden), "result preview is not secret redaction");
929
918
  }
930
919
  });
931
920
 
@@ -952,7 +941,7 @@ test("quiet tool rendering keeps shell compositions, expansions, and lookalikes
952
941
  tool.renderResult(textResult("original command output"), { expanded: true, isPartial: false }, passthroughTheme, { args: { command } }),
953
942
  );
954
943
  assert.equal(call.trimEnd(), `$ ${command}`);
955
- assert.doesNotMatch(call, /🌹︎ Gentle AI/);
944
+ assert.doesNotMatch(call, /🌹/);
956
945
  assert.match(output, /original command output/);
957
946
  }
958
947
  });
@@ -977,11 +966,11 @@ test("quiet Bash previews semantic lines for generic JSON output", () => {
977
966
  const collapsed = renderToolResult(tool, textResult(object), { expanded: false, isPartial: false }, { args: { command } });
978
967
  const expanded = renderToolResult(tool, textResult(object), { expanded: true, isPartial: false }, { args: { command } });
979
968
  assert.equal(call.trimEnd(), `$ ${command}`);
980
- assert.doesNotMatch(call, /🌹︎ Gentle AI/);
969
+ assert.doesNotMatch(call, /🌹/);
981
970
  assert.doesNotMatch(collapsed, /^\n\s*[}\]]/);
982
971
  const expandHint = keyHint("app.tools.expand", "to expand");
983
- assert.equal(collapsed.split(expandHint).length - 1, 1);
984
- assert.equal(expanded, `\n${object}`);
972
+ assert.equal(collapsed.split(expandHint).length - 1, 0);
973
+ assert.equal(expanded, object);
985
974
  assert.doesNotMatch(expanded, /to expand/);
986
975
  });
987
976
 
@@ -1044,19 +1033,21 @@ test("quiet tool rendering limits collapsed visual rows at the actual width", ()
1044
1033
  const tools = registeredQuietTools();
1045
1034
  const bash = tools.get("bash");
1046
1035
  const hint = keyHint("app.tools.expand", "to expand");
1047
- const narrowWidth = 12;
1036
+ // Twelve content columns inside the four-column card sides.
1037
+ const narrowWidth = 16;
1048
1038
  const cases = [
1049
- [textResult("x".repeat(80)), { expanded: false, isPartial: true }, { args: { command: "printf output" } }, 4],
1039
+ // Preview rows plus the closing rule; a partial result adds its label row.
1040
+ [textResult("x".repeat(80)), { expanded: false, isPartial: true }, { args: { command: "printf output" } }, 5],
1050
1041
  [textResult("界".repeat(40)), { expanded: false, isPartial: false }, { args: { command: "printf output" } }, 4],
1051
1042
  [textResult("e\u0301".repeat(40)), { expanded: false, isPartial: false, isError: true }, { args: { command: "false" }, isError: true }, 4],
1052
1043
  ] as const;
1053
1044
  for (const [result, options, context, maxRows] of cases) {
1054
- const lines = renderLines(bash.renderResult(result, options, passthroughTheme, context), narrowWidth);
1045
+ const lines = frameLines(bash.renderResult(result, options, passthroughTheme, context), narrowWidth);
1055
1046
  assert.ok(lines.length <= maxRows, `expected at most ${maxRows} rows, got ${lines.length}`);
1056
1047
  assert.ok(lines.every((line) => visibleWidth(line) <= narrowWidth));
1057
1048
  }
1058
- const completed = renderLines(bash.renderResult(textResult("😀".repeat(40)), { expanded: false, isPartial: false }, passthroughTheme, { args: { command: "printf output" } }), narrowWidth);
1059
- assert.equal(completed.filter((line) => line.includes(hint)).length, 1);
1049
+ const completed = frameLines(bash.renderResult(textResult("😀".repeat(40)), { expanded: false, isPartial: false }, passthroughTheme, { args: { command: "printf output" } }), narrowWidth);
1050
+ assert.equal(completed.filter((line) => line.includes(hint)).length, 0);
1060
1051
  assert.ok(completed.every((line) => visibleWidth(line) <= narrowWidth));
1061
1052
  });
1062
1053
 
@@ -1069,8 +1060,8 @@ test("quiet tool rendering classifies quoted and escaped literal shell metachara
1069
1060
  ]) {
1070
1061
  const call = renderToString(tool.renderCall({ command }, passthroughTheme, { args: { command } }));
1071
1062
  const collapsed = renderToolResult(tool, textResult("private output"), { expanded: false, isPartial: false }, { args: { command } });
1072
- assert.match(call, /🌹︎ Gentle AI · running · review status/, command);
1073
- assert.doesNotMatch(collapsed, /private output/, command);
1063
+ assert.match(call, /🌹 rdd running · status/, command);
1064
+ assert.match(collapsed, /private output/, command);
1074
1065
  }
1075
1066
  });
1076
1067
 
@@ -1102,9 +1093,9 @@ test("quiet tool rendering recognizes a quoted exact dev override path containin
1102
1093
  const command = `"${devPath}" review status "literal \\$|#;"`;
1103
1094
  const call = renderToString(tool.renderCall({ command }, passthroughTheme, { args: { command } }));
1104
1095
  const collapsed = renderToolResult(tool, textResult("private result"), { expanded: false, isPartial: false }, { args: { command } });
1105
- assert.equal(cardTitle(call), "🌹︎ Gentle AI · running · review status");
1096
+ assert.equal(cardTitle(call), "🌹 rdd running · status");
1106
1097
  assert.doesNotMatch(call, /\/opt\/Gentle AI\/gentle-ai|literal/);
1107
- assert.doesNotMatch(collapsed, /private result/);
1098
+ assert.match(collapsed, /private result/);
1108
1099
  });
1109
1100
 
1110
1101
  test("quiet tool rendering bounds and sanitizes partial text without a completion hint", () => {
@@ -1142,20 +1133,20 @@ test("quiet tool rendering bounds completed previews, errors, and edit/write sum
1142
1133
  const rendered = renderToolResult(tools.get(toolName), textResult(text), { expanded: false, isPartial: false }, context);
1143
1134
  assert.doesNotMatch(rendered, new RegExp(hidden));
1144
1135
  assert.match(rendered, new RegExp(visible));
1145
- assert.match(rendered, /to expand/);
1136
+ assert.doesNotMatch(rendered, /to expand/);
1146
1137
  }
1147
1138
 
1148
1139
  for (const toolName of ["read", "bash", "grep", "find", "ls", "edit", "write"] as const) {
1149
1140
  const rendered = renderToolResult(tools.get(toolName), textResult("error one\nerror two\nerror three\nerror four"), { expanded: false, isPartial: false, isError: true }, { args: toolName === "bash" ? { command: "false" } : {} });
1150
1141
  assert.doesNotMatch(rendered, /error one/);
1151
1142
  assert.match(rendered, /error two\nerror three\nerror four/);
1152
- assert.match(rendered, /to expand/);
1143
+ assert.doesNotMatch(rendered, /to expand/);
1153
1144
  }
1154
1145
 
1155
1146
  const edit = renderToolResult(tools.get("edit"), textResult("Applied", { diff: "@@\n-old\n+new" }), { expanded: false, isPartial: false }, { args: { path: "file.ts", edits: [] } });
1156
1147
  const write = renderToolResult(tools.get("write"), textResult("Successfully wrote 4 bytes\nbody"), { expanded: false, isPartial: false }, { args: { path: "file.ts", content: "body" } });
1157
1148
  assert.match(edit, /\+1 \/ -1/);
1158
- assert.doesNotMatch(edit, /Applied|old|new/);
1149
+ assert.match(edit, /-old[\s\S]*\+new/);
1159
1150
  assert.match(write, /wrote 4 bytes/);
1160
1151
  assert.doesNotMatch(write, /body/);
1161
1152
  });
@@ -1184,7 +1175,8 @@ test("quiet tool rendering preserves directional visual preview rows at narrow w
1184
1175
  const tools = registeredQuietTools();
1185
1176
  const bash = tools.get("bash");
1186
1177
  const readTool = tools.get("read");
1187
- const narrowWidth = 12;
1178
+ // Twelve content columns inside the four-column card sides.
1179
+ const narrowWidth = 16;
1188
1180
  const renderCollapsed = (
1189
1181
  text: string,
1190
1182
  options: { expanded: false; isPartial: boolean; isError?: boolean },
@@ -1213,7 +1205,7 @@ test("quiet tool rendering preserves directional visual preview rows at narrow w
1213
1205
  assert.match(lines.join("\n"), new RegExp(marker));
1214
1206
  }
1215
1207
  assert.match(partial[0] ?? "", /… bash/);
1216
- assert.match(completedBash.join("\n"), /to expand/);
1208
+ assert.doesNotMatch(completedBash.join("\n"), /to expand/);
1217
1209
 
1218
1210
  const read = renderRead(`READ-FIRST\n${oldText}\nREAD-NEW`);
1219
1211
  const semanticJson = renderCollapsed(
@@ -1259,8 +1251,8 @@ test("quiet tool rendering fails closed on unquoted shell expansions", () => {
1259
1251
  const call = renderToString(bash.renderCall({ command }, passthroughTheme, { args: { command } }));
1260
1252
  const collapsed = renderToolResult(bash, textResult("dummy-secret"), { expanded: false, isPartial: false }, { args: { command } });
1261
1253
  assert.equal(gentleAiRoutineCommand({ command }), "review", command);
1262
- assert.match(call, /🌹︎ Gentle AI · running · review status/, command);
1263
- assert.doesNotMatch(collapsed, /dummy-secret/, command);
1254
+ assert.match(call, /🌹 rdd running · status/, command);
1255
+ assert.match(collapsed, /dummy-secret/, command);
1264
1256
  }
1265
1257
  });
1266
1258
 
@@ -1275,9 +1267,9 @@ test("quiet tool rendering redacts only exact package-local executable paths wit
1275
1267
  const call = renderToString(bash.renderCall({ command }, passthroughTheme, { args: { command } }));
1276
1268
  const collapsed = renderToolResult(bash, textResult("dummy-secret"), { expanded: false, isPartial: false }, { args: { command } });
1277
1269
  assert.equal(gentleAiRoutineCommand({ command }), "review", command);
1278
- assert.match(call, /🌹︎ Gentle AI · running · review status/, command);
1270
+ assert.match(call, /🌹 rdd running · status/, command);
1279
1271
  assert.doesNotMatch(call, /Gentle Package|secret/, command);
1280
- assert.doesNotMatch(collapsed, /dummy-secret/, command);
1272
+ assert.match(collapsed, /dummy-secret/, command);
1281
1273
  }
1282
1274
 
1283
1275
  for (const command of [
@@ -1304,8 +1296,8 @@ test("quiet tool rendering respects command and env wrapper order", () => {
1304
1296
  const call = renderToString(bash.renderCall({ command }, passthroughTheme, { args: { command } }));
1305
1297
  const collapsed = renderToolResult(bash, textResult("dummy-secret"), { expanded: false, isPartial: false }, { args: { command } });
1306
1298
  assert.equal(gentleAiRoutineCommand({ command }), "review", command);
1307
- assert.match(call, /🌹︎ Gentle AI · running · review status/, command);
1308
- assert.doesNotMatch(collapsed, /dummy-secret/, command);
1299
+ assert.match(call, /🌹 rdd running · status/, command);
1300
+ assert.match(collapsed, /dummy-secret/, command);
1309
1301
  }
1310
1302
 
1311
1303
  const command = "env FOO=bar command -- gentle-ai review status";
@@ -1325,20 +1317,20 @@ test("quiet tool rendering lets a final result promote a replayed call card to i
1325
1317
  const replayed = routineRenderContext({ args: { command }, state, argsComplete: false, executionStarted: false, isPartial: false, invalidate: () => { invalidations += 1; } });
1326
1318
 
1327
1319
  const call = tool.renderCall({ command }, statusTheme, replayed);
1328
- assert.equal(cardTitle(renderToString(call)), "🌹︎ Gentle AI · preparing · review status");
1320
+ assert.equal(cardTitle(renderToString(call)), "🌹 rdd preparing · status");
1329
1321
  renderToString(tool.renderResult(textResult("done"), { expanded: false, isPartial: false }, statusTheme, replayed));
1330
1322
  assert.equal(invalidations, 0, "never reentrant");
1331
1323
  await flush();
1332
1324
  assert.equal(invalidations, 1);
1333
1325
  const promoted = tool.renderCall({ command }, statusTheme, { ...replayed, lastComponent: call });
1334
1326
  assert.strictEqual(promoted, call);
1335
- assert.equal(cardTitle(renderToString(promoted)), "🌹︎ Gentle AI · completed · review status");
1327
+ assert.equal(cardTitle(renderToString(promoted)), "🌹 rdd status");
1336
1328
  assert.equal(cardTone(renderToString(promoted)), "success");
1337
1329
 
1338
1330
  renderToString(tool.renderResult(textResult("boom"), { expanded: false, isPartial: false }, statusTheme, { ...replayed, isError: true }));
1339
1331
  await flush();
1340
1332
  assert.equal(invalidations, 2);
1341
- assert.equal(cardTitle(renderToString(tool.renderCall({ command }, statusTheme, { ...replayed, lastComponent: call }))), "🌹︎ Gentle AI · failed · review status");
1333
+ assert.equal(cardTitle(renderToString(tool.renderCall({ command }, statusTheme, { ...replayed, lastComponent: call }))), "🌹 rdd failed · status");
1342
1334
  renderToString(tool.renderResult(textResult("boom"), { expanded: false, isPartial: false }, statusTheme, { ...replayed, isError: true }));
1343
1335
  await flush();
1344
1336
  assert.equal(invalidations, 2, "an unchanged outcome must not request another render");
@@ -1353,11 +1345,244 @@ test("quiet tool rendering puts the expand key in the finished call card's top r
1353
1345
  assert.equal(cardHint(running), undefined, "a running call has nothing to expand yet");
1354
1346
  const completed = renderToString(tool.renderCall({ command }, passthroughTheme, routineRenderContext({ args: { command }, executionStarted: true, isPartial: false, expanded: false })));
1355
1347
  assert.match(cardHint(completed) ?? "", /to expand$/);
1356
- assert.equal(cardTitle(completed), "🌹︎ Gentle AI · completed · review status");
1348
+ assert.equal(cardTitle(completed), "🌹 rdd status");
1357
1349
  const expanded = renderToString(tool.renderCall({ command }, passthroughTheme, routineRenderContext({ args: { command }, executionStarted: true, isPartial: false, expanded: true })));
1358
1350
  assert.match(cardHint(expanded) ?? "", /to collapse$/);
1359
1351
  const collapsedResult = renderToolResult(tool, textResult("{\"next_transition\":\"stop\"}"), { expanded: false, isPartial: false }, { args: { command } });
1360
- assert.equal(cardBody(collapsedResult), "1 line");
1361
- assert.equal(cardBody(renderToolResult(tool, textResult("a\nb\nc"), { expanded: false, isPartial: false }, { args: { command } })), "3 lines");
1352
+ assert.equal(cardBody(collapsedResult), "stop");
1353
+ assert.equal(cardBody(renderToolResult(tool, textResult("a\nb\nc"), { expanded: false, isPartial: false }, { args: { command } })), "a\nb\nc");
1362
1354
  assert.match(collapsedResult.split("\n").pop() ?? "", /╰─+╯$/);
1363
1355
  });
1356
+
1357
+ test("long finished quiet headings retain the applicable expansion hint and complete command", () => {
1358
+ const tool = registeredQuietTools().get("bash");
1359
+ const command = "printf alpha bravo charlie delta echo foxtrot golf hotel india juliet kilo lima mike november oscar papa quebec romeo sierra tango";
1360
+ const args = { command };
1361
+ for (const expanded of [false, true]) {
1362
+ const hint = keyHint("app.tools.expand", expanded ? "to collapse" : "to expand").replace(/\x1b\[[\d;]*m/g, "");
1363
+ const ctx = routineRenderContext({ args, executionStarted: true, isPartial: false, expanded });
1364
+ for (const width of [80, 120]) {
1365
+ const rows = frameLines(tool.renderCall(args, passthroughTheme, ctx), width);
1366
+ assert.ok(rows[0].endsWith(` ${hint} ╮`), `width ${width}: applicable hint must survive heading wrapping`);
1367
+ assert.ok(rows.length > 1);
1368
+ assert.ok(rows.every((row) => visibleWidth(row) <= width));
1369
+ const first = rows[0].slice("╭─ $ ".length).split(" ─")[0];
1370
+ const rest = rows.slice(1).map((row) => row.replace(/^│ /, "").replace(/ +│$/, ""));
1371
+ assert.equal([first, ...rest].join(" ").replace(/\s+/g, " ").trim(), `bash $ ${command}`);
1372
+ }
1373
+ }
1374
+ });
1375
+
1376
+ test("quiet components repaint after theme invalidation and retain transparent error frames", (t) => {
1377
+ const found = cardStyle();
1378
+ t.after(() => setCardStyle(found));
1379
+ setCardStyle(CARD_STYLE.NEON);
1380
+ const bash = registeredQuietTools().get("bash");
1381
+ let paint = "\x1b[31m";
1382
+ const theme = { bold: (text: string) => text, fg: (_role: string, text: string) => `${paint}${text}\x1b[0m`, bg: (_role: string, text: string) => `\x1b[44m${text}\x1b[49m` };
1383
+ const args = { command: "printf failure" };
1384
+ const context = routineRenderContext({ args, isError: true, isPartial: false });
1385
+ const components = [bash.renderCall(args, theme, context), bash.renderResult(textResult("denied\nreason"), { expanded: false, isPartial: false }, theme, context)];
1386
+ const first = components.flatMap((component) => component.render(80)).join("\n");
1387
+ paint = "\x1b[35m";
1388
+ for (const component of components) component.invalidate();
1389
+ const changed = components.flatMap((component) => component.render(80)).join("\n");
1390
+ assert.match(first, /\x1b\[31m/);
1391
+ assert.doesNotMatch(changed, /\x1b\[31m|\x1b\[44m/);
1392
+ assert.match(changed, /\x1b\[35m/);
1393
+ assert.equal(first.replace(/\x1b\[[\d;]*m/g, ""), changed.replace(/\x1b\[[\d;]*m/g, ""));
1394
+ });
1395
+
1396
+ test("six quiet cards and standalone Bash fixture retain distinct identity and bounded results", () => {
1397
+ const tools = registeredQuietTools();
1398
+ for (const [name, glyph] of [["read", "≡"], ["bash", "$"], ["grep", "⌕"], ["find", "⌖"], ["ls", "☷"], ["edit", "✎"], ["write", "+"]] as const) {
1399
+ const tool = tools.get(name);
1400
+ const args = { command: "printf hello", path: "src/example.ts", pattern: "needle" };
1401
+ const ctx = routineRenderContext({ args, isPartial: false, executionStarted: true });
1402
+ const header = frameLines(tool.renderCall(args, passthroughTheme, ctx), 120).join("\n");
1403
+ assert.ok(header.startsWith(`╭─ ${glyph} ${name} `), `${name} retains its distinct icon and actual identity`);
1404
+ assert.match(header, /to expand/);
1405
+ const value = textResult("useful row\nsecond row", name === "edit" ? { diff: "-before\n+after" } : undefined);
1406
+ const body = frameLines(tool.renderResult(value, { expanded: false, isPartial: false }, passthroughTheme, ctx), 120).join("\n");
1407
+ if (name !== "write") assert.match(body, name === "edit" ? /before|after/ : /useful row/);
1408
+ for (const width of [0, 1, 2, 3, 4, 5, 6, 7, 8, 24]) {
1409
+ const rows = frameLines(tool.renderResult(textResult("界e\u0301🌹".repeat(80)), { expanded: false, isPartial: false }, passthroughTheme, ctx), width);
1410
+ assert.ok(rows.length <= 4);
1411
+ assert.ok(rows.every((row) => visibleWidth(row) <= width));
1412
+ assert.equal(rows.filter((row) => row.startsWith("╰")).length, width ? 1 : 0);
1413
+ }
1414
+ }
1415
+ });
1416
+
1417
+ test("quiet tool calls draw a rounded petal card in the lifecycle tone", () => {
1418
+ const tools = registeredQuietTools();
1419
+ const bash = tools.get("bash");
1420
+ const args = { command: "printf output" };
1421
+ const width = 30;
1422
+
1423
+ const header = bash.renderCall(args, passthroughTheme, routineRenderContext({ args, isPartial: false })).render(width);
1424
+ assert.deepEqual(header, [`╭─ $ bash $ printf output ${"─".repeat(3)}╮`]);
1425
+ const running = bash.renderCall(args, statusTheme, routineRenderContext({ args, executionStarted: true, isPartial: true })).render(width);
1426
+ assert.match(running[0] ?? "", /^<warning>╭<\/warning><warning>─ <\/warning><warning>\$ (?:<toolTitle>)?bash/);
1427
+ assert.match(bash.renderCall(args, statusTheme, routineRenderContext({ args, isPartial: false, isError: true })).render(width)[0] ?? "", /^<error>╭<\/error>/);
1428
+
1429
+ const expanded = bash.renderResult(textResult("alpha\nbeta"), { expanded: true, isPartial: false }, passthroughTheme, { args }).render(width);
1430
+ assert.deepEqual(expanded, [
1431
+ `│ alpha${" ".repeat(21)} │`,
1432
+ `│ beta${" ".repeat(22)} │`,
1433
+ `╰${"─".repeat(28)}╯`,
1434
+ ]);
1435
+ const failed = bash.renderResult(textResult("boom"), { expanded: true, isPartial: false, isError: true }, statusTheme, { args, isError: true }).render(width);
1436
+ assert.match(failed.at(-1) ?? "", /^<error>╰<\/error><error>─+╯<\/error>$/);
1437
+ assert.ok(failed.slice(0, -1).every((line) => line.startsWith("<error>│</error> ")), failed.join("\n"));
1438
+
1439
+ const empty = tools.get("grep").renderResult(textResult("No matches found"), { expanded: false, isPartial: false }, passthroughTheme, {}).render(width);
1440
+ assert.deepEqual(empty, [`╰${"─".repeat(28)}╯`], "an empty result still closes the card");
1441
+
1442
+ // The expand key rides the top rule once the call finished, like the Gentle AI card.
1443
+ const wide = 80;
1444
+ const expandKey = keyHint("app.tools.expand", "to expand").replace(/\x1b\[[0-9;]*m/g, "");
1445
+ const finishedTop = bash.renderCall(args, passthroughTheme, routineRenderContext({ args, executionStarted: true, isPartial: false })).render(wide)[0] ?? "";
1446
+ assert.ok(finishedTop.endsWith(` ${expandKey} ╮`), finishedTop);
1447
+ const expandedTop = bash.renderCall(args, passthroughTheme, routineRenderContext({ args, executionStarted: true, isPartial: false, expanded: true })).render(wide)[0] ?? "";
1448
+ assert.match(expandedTop, /to collapse ╮$/);
1449
+ const runningTop = bash.renderCall(args, passthroughTheme, routineRenderContext({ args, executionStarted: true, isPartial: true })).render(wide)[0] ?? "";
1450
+ assert.ok(!runningTop.includes("to expand"), runningTop);
1451
+ const collapsedBody = bash.renderResult(textResult("alpha\nbeta"), { expanded: false, isPartial: false }, passthroughTheme, { args }).render(wide);
1452
+ assert.ok(!collapsedBody.some((line) => line.includes("to expand")), "the body no longer carries the expand key");
1453
+
1454
+ for (const narrow of [1, 2, 3, 4, 5, 8]) {
1455
+ const lines = [
1456
+ ...bash.renderCall({ command: "printf " + "x".repeat(80) }, passthroughTheme, routineRenderContext({ args, isPartial: false })).render(narrow),
1457
+ ...bash.renderResult(textResult("😀".repeat(40)), { expanded: true, isPartial: false }, passthroughTheme, { args }).render(narrow),
1458
+ ];
1459
+ for (const line of lines) assert.ok(visibleWidth(line) <= narrow, `width ${narrow}: ${JSON.stringify(line)}`);
1460
+ }
1461
+ });
1462
+
1463
+ // Pi builds the call component, then the result component once a result
1464
+ // exists, and only then renders them; both share one row state.
1465
+ function piToolRow(tool: any, args: Record<string, unknown>, value: unknown, context: Record<string, unknown>, width: number, theme: object = passthroughTheme): string[] {
1466
+ const call = tool.renderCall(args, theme, context);
1467
+ const body = value ? tool.renderResult(value, { expanded: context.expanded === true, isPartial: context.isPartial === true }, theme, context) : undefined;
1468
+ return [...call.render(width), ...(body?.render(width) ?? [])];
1469
+ }
1470
+
1471
+ test("a running quiet card closes its own frame with one running row until a result exists", () => {
1472
+ const tools = registeredQuietTools();
1473
+ const read = tools.get("read");
1474
+ const args = { path: "a.md" };
1475
+ const pending = () => routineRenderContext({ args, executionStarted: true, isPartial: true, state: {} });
1476
+ const running = piToolRow(read, args, undefined, pending(), 40);
1477
+ assert.deepEqual(running, [
1478
+ `╭─ ≡ read a.md ${"─".repeat(24)}╮`,
1479
+ `│ running…${" ".repeat(28)} │`,
1480
+ `╰${"─".repeat(38)}╯`,
1481
+ ]);
1482
+ const painted = piToolRow(read, args, undefined, pending(), 40, statusTheme).join("\n");
1483
+ assert.match(painted, /<muted>running…<\/muted>/);
1484
+ assert.match(painted, /<warning>╰<\/warning>/);
1485
+ for (const width of [0, 1, 2, 3, 4, 5, 6, 7, 8]) {
1486
+ const rows = piToolRow(read, args, undefined, pending(), width);
1487
+ assert.ok(rows.every((row) => visibleWidth(row) <= width), `width ${width}`);
1488
+ // Narrow headings may continue on rows below the top rule; the frame still closes once.
1489
+ assert.equal(rows.filter((row) => row.startsWith("╰")).length, width === 0 ? 0 : 1);
1490
+ if (width > 0) assert.ok(rows.at(-1)!.startsWith("╰"), `width ${width}: ${rows.join("\n")}`);
1491
+ }
1492
+
1493
+ // A partial result below the call closes the frame instead: one bottom rule, no running row.
1494
+ const bash = tools.get("bash");
1495
+ const bashArgs = { command: "printf output" };
1496
+ const partial = piToolRow(bash, bashArgs, textResult("out"), routineRenderContext({ args: bashArgs, executionStarted: true, isPartial: true, state: {} }), 40);
1497
+ assert.equal(partial.filter((row) => row.startsWith("╭")).length, 1);
1498
+ assert.equal(partial.filter((row) => row.startsWith("╰")).length, 1);
1499
+ assert.match(partial.at(-1)!, /^╰─+╯$/);
1500
+ assert.doesNotMatch(partial.join("\n"), /running…/);
1501
+
1502
+ // A finished call keeps its open top rule; the result closes the card.
1503
+ const finished = piToolRow(read, args, textResult("one"), routineRenderContext({ args, executionStarted: true, isPartial: false, state: {} }), 40);
1504
+ assert.equal(finished.length, 3);
1505
+ assert.match(finished[0]!, /^╭─ ≡ read a\.md .*╮$/);
1506
+ assert.match(finished[1]!, /^│ one +│$/);
1507
+ assert.match(finished[2]!, /^╰─+╯$/);
1508
+ });
1509
+
1510
+
1511
+
1512
+ // Float style: the call and result components of one quiet card read as one
1513
+ // borderless panel. The heading owns the blank row above it; the result owns
1514
+ // the separator below the heading and the blank closing row.
1515
+ const floatToolTheme = {
1516
+ bold(value: string) {
1517
+ return value;
1518
+ },
1519
+ fg(_color: string, value: string) {
1520
+ return `\x1b[38;5;114m${value}\x1b[39m`;
1521
+ },
1522
+ bg(color: string, value: string) {
1523
+ return `\x1b[48;5;${color === "toolErrorBg" ? 52 : color === "toolPendingBg" ? 58 : 22}m${value}\x1b[49m`;
1524
+ },
1525
+ };
1526
+
1527
+ function withFloatCards<T>(run: () => T): T {
1528
+ setCardStyle(CARD_STYLE.FLOAT);
1529
+ try {
1530
+ return run();
1531
+ } finally {
1532
+ setCardStyle(CARD_STYLE.NEON);
1533
+ }
1534
+ }
1535
+
1536
+ test("float quiet cards render the call and result as one panel with a single separator", () => {
1537
+ const tools = registeredQuietTools();
1538
+ const read = tools.get("read");
1539
+ const context = routineRenderContext({ args: { path: "package.json" }, isPartial: false, executionStarted: true });
1540
+ const width = 60;
1541
+ const rows = withFloatCards(() => [
1542
+ ...read.renderCall({ path: "package.json" }, floatToolTheme, context).render(width),
1543
+ ...read.renderResult(textResult("{\n \"name\": \"gentle-pi\""), { expanded: false, isPartial: false }, floatToolTheme, context).render(width),
1544
+ ]);
1545
+ const plain = rows.map(stripAnsi);
1546
+ for (const line of rows) {
1547
+ assert.equal(visibleWidth(line), width);
1548
+ assert.ok(line.startsWith(" \x1b[48;5;22m"), JSON.stringify(line));
1549
+ assert.ok(line.endsWith("\x1b[49m "), JSON.stringify(line));
1550
+ }
1551
+ assert.doesNotMatch(plain.join("\n"), /[╭╮╰╯│─]/);
1552
+ assert.match(plain[0]!, /^ ▎ +$/);
1553
+ assert.match(plain[1]!, /^ ▎ ≡ read package\.json +.*to expand {4}$/);
1554
+ assert.match(plain[2]!, /^ ▎ +$/);
1555
+ assert.match(plain[3]!, /^ ▎ \{ +$/);
1556
+ assert.match(plain[4]!, /^ ▎ {3}"name": "gentle-pi" +$/);
1557
+ assert.match(plain.at(-1)!, /^ ▎ +$/);
1558
+ assert.equal(plain.length, 6);
1559
+ assert.equal(plain[1]!.indexOf("≡"), plain[3]!.indexOf("{"), "the glyph starts in the body text column");
1560
+ });
1561
+
1562
+ test("float quiet cards close a running call as one panel and add no separator without result rows", () => {
1563
+ const tools = registeredQuietTools();
1564
+ const read = tools.get("read");
1565
+ const running = withFloatCards(() => read.renderCall({ path: "a.md" }, floatToolTheme, routineRenderContext({ args: { path: "a.md" }, state: {} })).render(40)).map(stripAnsi);
1566
+ assert.equal(running.length, 5, "blank row, heading, separator, running row, closing row");
1567
+ assert.match(running[0]!, /^ ▎ +$/);
1568
+ assert.match(running[1]!, /^ ▎ ≡ read a\.md +$/);
1569
+ assert.match(running[2]!, /^ ▎ +$/);
1570
+ assert.match(running[3]!, /^ ▎ running… +$/);
1571
+ assert.match(running[4]!, /^ ▎ +$/);
1572
+ assert.doesNotMatch(running.join("\n"), /[╭╮╰╯│─]/);
1573
+ const empty = withFloatCards(() => read.renderResult(textResult(""), { expanded: false, isPartial: false }, floatToolTheme, routineRenderContext({ isPartial: false })).render(40)).map(stripAnsi);
1574
+ assert.deepEqual(empty, [` ▎${" ".repeat(37)} `], "an empty result only closes the panel");
1575
+ });
1576
+
1577
+ test("quiet cards stay outlined in the neon style with a background-capable theme", () => {
1578
+ const tools = registeredQuietTools();
1579
+ const read = tools.get("read");
1580
+ const context = routineRenderContext({ args: { path: "a.md" }, isPartial: false, executionStarted: true });
1581
+ const rows = [
1582
+ ...read.renderCall({ path: "a.md" }, floatToolTheme, context).render(40),
1583
+ ...read.renderResult(textResult("one"), { expanded: false, isPartial: false }, floatToolTheme, context).render(40),
1584
+ ].map(stripAnsi);
1585
+ assert.match(rows[0]!, /^╭─ ≡ read a\.md/);
1586
+ assert.match(rows.at(-1)!, /^╰─+╯$/);
1587
+ assert.doesNotMatch(rows.join("\n"), /▎/);
1588
+ });