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
package/lib/shell-bar.ts CHANGED
@@ -2,7 +2,13 @@ import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works
2
2
  import { GAUGE_CELLS, gaugeTone, paintGauge, renderGauge, type GaugeTone } from "./shell-gauge.ts";
3
3
  import { renderUsageBar, selectUsageLimit, type ProviderUsage, type UsageWindow } from "./shell-usage.ts";
4
4
  import { sanitizeTerminalText } from "./terminal-theme.ts";
5
- import { CARD_TONE, cardInnerWidth, renderCard } from "./shell-card.ts";
5
+ import { CARD_TONE, floatRows, panelInnerWidth, renderCard } from "./shell-card.ts";
6
+ import { REVIEW_SCOPE_UNAVAILABLE, REVIEW_SIDEBAR_LABELS, type ReviewSidebarSnapshot } from "./review-sidebar-state.ts";
7
+ import type { VisualSettings } from "./visual-customization-policy.ts";
8
+ import { renderChangesWidget, type ChangesModel } from "./shell-changes.ts";
9
+
10
+ type Presentation = Pick<VisualSettings, "density" | "visibility">;
11
+ type HeaderPresentation = Presentation & Partial<Pick<VisualSettings, "headerPlacement" | "statusPlacement">>;
6
12
 
7
13
  export { gaugeTone, renderGauge, type GaugeTone };
8
14
 
@@ -11,6 +17,7 @@ export { gaugeTone, renderGauge, type GaugeTone };
11
17
  // verified without a live TUI.
12
18
 
13
19
  export interface ShellBarModel {
20
+ review?: ReviewSidebarSnapshot;
14
21
  profile?: string;
15
22
  changes?: { files: number; added: number; deleted: number; notice?: string };
16
23
  cwd: string;
@@ -64,6 +71,12 @@ export interface ShellHeaderResult {
64
71
  usageSpan?: HeaderUsageSpan;
65
72
  }
66
73
 
74
+ export interface ShellHeaderChrome {
75
+ rows: string[];
76
+ headerRow: number;
77
+ usageSpan?: HeaderUsageSpan;
78
+ }
79
+
67
80
  export interface ShellBarTheme {
68
81
  fg(color: string, text: string): string;
69
82
  bold(text: string): string;
@@ -105,7 +118,7 @@ export function formatTokens(count: number): string {
105
118
  return `${Math.round(count / 1_000_000)}M`;
106
119
  }
107
120
 
108
- export function formatCost(total: number, subscription: boolean): string {
121
+ export function formatCost(total: number, subscription = false): string {
109
122
  const amount = total >= 1 ? total.toFixed(2) : total.toFixed(3);
110
123
  return subscription ? `$${amount} sub` : `$${amount}`;
111
124
  }
@@ -141,14 +154,20 @@ function costSegment(costTotal: number, subscription: boolean, theme: ShellBarTh
141
154
  return theme.fg(ROLE.VALUE, formatCost(costTotal, subscription));
142
155
  }
143
156
 
144
- function buildSegments(model: ShellBarModel, theme: ShellBarTheme): string[] {
157
+ function buildSegments(model: ShellBarModel, theme: ShellBarTheme, presentation?: Presentation): string[] {
145
158
  const location = locationSegment(model, theme);
146
159
  const modelSegment = executionSegment(model.modelId, model.effort, theme);
147
160
  const context = contextSegment(model.contextPercent, theme);
148
161
  const cost = costSegment(model.costTotal, model.subscription, theme);
149
162
  const usage = model.usage ? renderUsageBar(model.usage, theme, model.modelId) : undefined;
150
163
  const statuses = model.statuses.map((status) => theme.fg(ROLE.STATUS, sanitizeStatus(status)));
151
- return [theme.fg(ROLE.BRAND, SHELL_BAR_BRAND), location, modelSegment, context, cost, ...(usage ? [usage] : []), ...statuses];
164
+ return [
165
+ ...(presentation?.density === "minimal" ? [] : [theme.fg(ROLE.BRAND, SHELL_BAR_BRAND)]),
166
+ location,
167
+ ...(presentation?.visibility.modelDetails === false ? [] : [modelSegment]),
168
+ ...(presentation?.visibility.usageCost === false ? [] : [context, cost, ...(usage ? [usage] : [])]),
169
+ ...statuses,
170
+ ];
152
171
  }
153
172
 
154
173
  // When the line overflows, the location gives way first: the path shrinks to
@@ -177,14 +196,14 @@ function joinSegments(segments: string[], theme: ShellBarTheme): string {
177
196
 
178
197
  // Sidebar groups use structured fields, never positional compact-bar segments
179
198
  // or inferred meanings from opaque extension status strings.
180
- export function renderShellSidebarBar(model: ShellBarModel, theme: ShellBarTheme, width: number): string[] {
199
+ export function renderShellSidebarBar(model: ShellBarModel, theme: ShellBarTheme, width: number, presentation?: Presentation): string[] {
181
200
  const value = (text: string) => theme.fg(ROLE.VALUE, theme.bold(text));
182
201
  const label = (text: string) => theme.fg(ROLE.LABEL, text);
183
202
  const changes = model.changes;
184
203
  const branch = model.branch ? `${label("Branch")} ${value(model.branch)}` : "";
185
204
  // Pre-wrap values before indenting so Unicode/ANSI continuation lines keep
186
205
  // the same inset without consuming the card's right border.
187
- const innerWidth = cardInnerWidth(width);
206
+ const innerWidth = panelInnerWidth(theme, width);
188
207
  const inset = Math.min(1, innerWidth - 1);
189
208
  // Model, effort, context, cost, and the per-model usage table now live in
190
209
  // the always-visible header row (and /gentle:usage for the full table);
@@ -200,7 +219,7 @@ export function renderShellSidebarBar(model: ShellBarModel, theme: ShellBarTheme
200
219
  ...(model.profile ? [`${label("Profile")} ${value(sanitizeStatus(model.profile))}`] : []),
201
220
  ],
202
221
  },
203
- {
222
+ ...(presentation?.visibility.changes === false ? [] : [{
204
223
  title: "Changes",
205
224
  lines: [
206
225
  changes?.files
@@ -209,7 +228,15 @@ export function renderShellSidebarBar(model: ShellBarModel, theme: ShellBarTheme
209
228
  ...(changes?.notice ? [theme.fg("warning", sanitizeStatus(changes.notice))] : []),
210
229
  label("/gentle:changes"),
211
230
  ],
212
- },
231
+ }]),
232
+ ...(model.review && presentation?.visibility.rdd !== false ? [{
233
+ title: "🌹 RDD",
234
+ lines: [
235
+ value(REVIEW_SIDEBAR_LABELS[model.review.state]),
236
+ // Unknown scope is an internal sentinel, not something the user acts on.
237
+ ...(model.review.scope !== REVIEW_SCOPE_UNAVAILABLE ? [label(sanitizeStatus(model.review.scope))] : []),
238
+ ],
239
+ }] : []),
213
240
  { title: "Integrations", lines: model.statuses.length
214
241
  ? model.statuses.map((status) => theme.fg(ROLE.STATUS, sanitizeStatus(status)))
215
242
  : [label("No status reported")] },
@@ -217,11 +244,11 @@ export function renderShellSidebarBar(model: ShellBarModel, theme: ShellBarTheme
217
244
  // Wrap and indent every group line before it reaches the card, so Unicode and
218
245
  // ANSI continuation lines keep the same inset without consuming the right border.
219
246
  const body = groups.flatMap((group, index) => [
220
- ...(index ? [""] : []),
221
- label(group.title),
247
+ ...(index && (!presentation || presentation.density === "comfortable") ? [""] : []),
248
+ ...(presentation?.density === "minimal" ? [] : [label(group.title)]),
222
249
  ...group.lines.flatMap((line) => wrapTextWithAnsi(line, innerWidth - inset).map((part) => " ".repeat(inset) + part)),
223
250
  ]);
224
- return renderCard({ title: "Status", body, tone: CARD_TONE.INFO }, theme, width, { expanded: true });
251
+ return renderCard({ title: "Status", body, tone: CARD_TONE.INFO }, theme, width, { expanded: true, panel: true });
225
252
  }
226
253
 
227
254
  const HEADER_BRAND = "✿ Gentle Shell";
@@ -230,9 +257,10 @@ const HEADER_BRAND = "✿ Gentle Shell";
230
257
  // effort, then the whole location keeps the brand and the bare model id
231
258
  // alive as long as anything can still share the row with the right-aligned
232
259
  // counters.
233
- function headerLeftStages(model: ShellHeaderModel, theme: ShellBarTheme): string[][] {
260
+ function headerLeftStages(model: ShellHeaderModel, theme: ShellBarTheme, showModelDetails: boolean): string[][] {
234
261
  const brand = theme.fg(ROLE.BRAND, theme.bold(HEADER_BRAND));
235
262
  const location = locationSegment(model, theme);
263
+ if (!showModelDetails) return [[brand, location], [brand]];
236
264
  const withEffort = executionSegment(model.modelId, model.effort, theme);
237
265
  const modelOnly = executionSegment(model.modelId, undefined, theme);
238
266
  const withProfile = model.profile ? `${withEffort} ${theme.fg(ROLE.LABEL, "·")} ${theme.fg(ROLE.MODEL, sanitizeStatus(model.profile))}` : withEffort;
@@ -271,12 +299,72 @@ function usageSegmentText(windows: UsageWindow[], theme: ShellBarTheme, stage: U
271
299
  return hint ? `${head} ${theme.fg(ROLE.LABEL, "·")} ${theme.fg(USAGE_HINT_ROLE, hint)}` : head;
272
300
  }
273
301
 
274
- export function renderShellHeaderBar(model: ShellHeaderModel, theme: ShellBarTheme, width: number, usageHint?: string): ShellHeaderResult {
302
+ // The float header is a full-width INFO background bar with a two-column
303
+ // inset on each side, closed below by the header edge line. floatRows paints
304
+ // the background inside one-column transparent margins; those margin cells
305
+ // are painted too, so the bar reaches both edges. Undefined for neon, a width
306
+ // under the float minimum, or a theme without a background.
307
+ function floatHeaderRow(theme: ShellBarTheme, width: number, content: (width: number) => string): string | undefined {
308
+ const painted = floatHeaderPaint(theme, width, content);
309
+ if (painted === undefined) return undefined;
310
+ const { inner, open } = painted;
311
+ // The background spans the full width: the margin cells are painted too, so
312
+ // the bar reaches both edges with no frame on its sides.
313
+ const side = `${open} `;
314
+ return `${side}${inner.slice(open.length, -BG_RESET.length)}${side}${BG_RESET}`;
315
+ }
316
+
317
+ function fitHeaderContent(text: string, width: number): string {
318
+ const clipped = visibleWidth(text) <= width ? text : truncateToWidth(text, width, "…");
319
+ return clipped + " ".repeat(Math.max(0, width - visibleWidth(clipped)));
320
+ }
321
+
322
+ export function renderShellHeaderBar(model: ShellHeaderModel, theme: ShellBarTheme, width: number, usageHint?: string, presentation?: Presentation): ShellHeaderResult {
323
+ const targetWidth = Math.max(0, Math.floor(width));
324
+ let content: ShellHeaderResult | undefined;
325
+ let contentWidth = targetWidth;
326
+ const row = floatHeaderRow(theme, targetWidth, (inner) => {
327
+ contentWidth = inner;
328
+ content = headerContent(model, theme, inner, usageHint, presentation);
329
+ return content.text;
330
+ });
331
+ if (row === undefined || !content) return headerContent(model, theme, width, usageHint, presentation);
332
+ const { usageSpan } = content;
333
+ // The inset splits evenly around the content, so its left share is where
334
+ // the content's columns start.
335
+ const offset = (targetWidth - contentWidth) / 2;
336
+ return usageSpan ? { text: row, usageSpan: { start: usageSpan.start + offset, end: usageSpan.end + offset } } : { text: row };
337
+ }
338
+
339
+ /** Shared geometry for the rail header and the below-editor header widget. */
340
+ export function renderShellHeaderChrome(model: ShellHeaderModel, theme: ShellBarTheme, width: number, usageHint?: string, presentation?: HeaderPresentation): ShellHeaderChrome {
341
+ const { text, usageSpan } = renderShellHeaderBar(model, theme, width, usageHint, presentation);
342
+ const targetWidth = Math.max(0, Math.floor(width));
343
+ const painted = floatHeaderPaint(theme, targetWidth, () => "");
344
+ const rule = renderShellHeaderRule(theme, targetWidth);
345
+ if (painted === undefined) return { rows: [text, rule], headerRow: 0, usageSpan };
346
+ const padding = `${painted.open}${" ".repeat(targetWidth)}${BG_RESET}`;
347
+ if (presentation?.headerPlacement === "below-input") {
348
+ // A lower one-eighth block hugs the painted padding in the next row.
349
+ const upperEdge = theme.fg(HEADER_RULE_ROLE, "▁".repeat(targetWidth));
350
+ return { rows: [upperEdge, padding, text, padding], headerRow: 2, usageSpan };
351
+ }
352
+ return { rows: [padding, text, padding, rule], headerRow: 1, usageSpan };
353
+ }
354
+
355
+ /** The usage segment is interactive only on the content row, never padding or the edge. */
356
+ export function shellHeaderUsageHit(chrome: ShellHeaderChrome, x: number, y: number): boolean {
357
+ const { usageSpan } = chrome;
358
+ return y === chrome.headerRow && usageSpan !== undefined && x >= usageSpan.start && x < usageSpan.end;
359
+ }
360
+
361
+ function headerContent(model: ShellHeaderModel, theme: ShellBarTheme, width: number, usageHint?: string, presentation?: Presentation): ShellHeaderResult {
275
362
  const targetWidth = Math.max(0, Math.floor(width));
276
363
  const ctxCost = joinSegments([contextSegment(model.contextPercent, theme), costSegment(model.costTotal, model.subscription, theme)], theme);
277
364
  const windows = model.usage ? (selectUsageLimit(model.usage, model.modelId)?.windows ?? []) : [];
278
- const leftStages = headerLeftStages(model, theme);
279
- const minimalLeft = leftStages.length - 2; // brand + bare model id, before dropping the model too
365
+ const leftStages = headerLeftStages(model, theme, presentation?.visibility.modelDetails !== false)
366
+ .map((stage) => presentation?.density === "minimal" ? stage.slice(1) : stage);
367
+ const minimalLeft = Math.max(0, leftStages.length - 2); // brand + bare model id, before dropping the model too
280
368
  // One flat, ordered cascade — never a per-stage nested search — so the
281
369
  // left group fully degrades (profile → effort → location) before usage
282
370
  // ever gives anything up, and usage fully degrades (gauges → secondary
@@ -285,26 +373,69 @@ export function renderShellHeaderBar(model: ShellHeaderModel, theme: ShellBarThe
285
373
  // Unlike the compact bar's usage meter (hidden with no data), this is a
286
374
  // standing, clickable affordance — even with nothing to report it still
287
375
  // reads "usage" plus the shortcut hint, unless disabled, width permitting.
288
- const usageStages: Array<UsageStage | undefined> = ["full", "text", "primary", undefined];
376
+ const usageStages: Array<UsageStage | undefined> = presentation?.visibility.usageCost === false ? [undefined] : ["full", "text", "primary", undefined];
289
377
  const attempts: Array<{ leftIndex: number; usageStage: UsageStage | undefined }> = [
290
- ...leftStages.slice(0, minimalLeft).map((_, leftIndex) => ({ leftIndex, usageStage: "full" as UsageStage })),
378
+ ...leftStages.slice(0, minimalLeft).map((_, leftIndex) => ({ leftIndex, usageStage: usageStages[0] })),
291
379
  ...usageStages.map((usageStage) => ({ leftIndex: minimalLeft, usageStage })),
292
380
  { leftIndex: leftStages.length - 1, usageStage: undefined },
293
381
  ];
294
382
  for (const { leftIndex, usageStage } of attempts) {
295
383
  const usageText = usageStage ? usageSegmentText(windows, theme, usageStage, usageHint) : undefined;
296
- const right = usageText ? joinSegments([ctxCost, usageText], theme) : ctxCost;
384
+ const right = presentation?.visibility.usageCost === false ? "" : usageText ? joinSegments([ctxCost, usageText], theme) : ctxCost;
297
385
  const left = joinSegments(leftStages[leftIndex]!, theme);
298
- if (visibleWidth(left) + RIGHT_PADDING + visibleWidth(right) > targetWidth) continue;
386
+ if (visibleWidth(left) + (right ? RIGHT_PADDING : 0) + visibleWidth(right) > targetWidth) continue;
299
387
  const text = left + " ".repeat(targetWidth - visibleWidth(left) - visibleWidth(right)) + right;
300
388
  if (!usageText) return { text };
301
389
  const usageStart = visibleWidth(left) + (targetWidth - visibleWidth(left) - visibleWidth(right)) + visibleWidth(ctxCost) + visibleWidth(` ${SHELL_BAR_SEPARATOR} `);
302
390
  return { text, usageSpan: { start: usageStart, end: usageStart + visibleWidth(usageText) } };
303
391
  }
304
- const brand = theme.fg(ROLE.BRAND, theme.bold(HEADER_BRAND));
392
+ const brand = presentation?.density === "minimal" ? "" : theme.fg(ROLE.BRAND, theme.bold(HEADER_BRAND));
305
393
  return { text: visibleWidth(brand) <= targetWidth ? brand : "" };
306
394
  }
307
395
 
396
+ // A single owner supplies real captured filenames, not ShellBarModel's totals.
397
+ // Undefined keeps every legacy surface alive for style/width/theme transitions.
398
+ export function renderShellBelowInputFloat(model: ShellBarModel, theme: ShellBarTheme, width: number, usageHint?: string, presentation?: HeaderPresentation, changes?: ChangesModel): ShellHeaderChrome | undefined {
399
+ if (presentation?.headerPlacement !== "below-input") return undefined;
400
+ const targetWidth = Math.max(0, Math.floor(width));
401
+ if (floatHeaderPaint(theme, targetWidth, () => "") === undefined) return undefined;
402
+ const chrome = renderShellHeaderChrome(buildShellHeaderModel(model), theme, targetWidth, usageHint, presentation);
403
+ const changesRow = changes?.files.length && presentation.visibility.changes !== false
404
+ ? floatHeaderRow(theme, targetWidth, (inner) => renderChangesWidget(changes, theme, inner)[0] ?? "")
405
+ : undefined;
406
+ const statuses = presentation.statusPlacement === "hidden" ? [] : model.statuses.map(sanitizeStatus).filter(Boolean);
407
+ const statusRow = statuses.length
408
+ ? floatHeaderRow(theme, targetWidth, () => joinSegments(statuses.map((status) => theme.fg(ROLE.STATUS, status)), theme))
409
+ : undefined;
410
+ return {
411
+ rows: [
412
+ chrome.rows[0]!,
413
+ chrome.rows[1]!,
414
+ ...(changesRow ? [changesRow] : []),
415
+ chrome.rows[2]!,
416
+ ...(statusRow ? [statusRow] : []),
417
+ chrome.rows[3]!,
418
+ ],
419
+ headerRow: changesRow ? 3 : 2,
420
+ usageSpan: chrome.usageSpan,
421
+ };
422
+ }
423
+
424
+ // The bottom bar when it is the only status row of a narrow fullscreen
425
+ // terminal (the header sits below the input and steps aside). It reuses the
426
+ // header row's own cascade, so context, cost and usage outlive the location
427
+ // on small screens, and keeps extension statuses on a second line instead of
428
+ // dropping them the way the header deliberately does.
429
+ export function renderShellBottomOnlyBar(model: ShellBarModel, theme: ShellBarTheme, width: number, usageHint?: string, presentation?: HeaderPresentation, changes?: ChangesModel): string[] {
430
+ const grouped = renderShellBelowInputFloat(model, theme, width, usageHint, presentation, changes);
431
+ if (grouped) return grouped.rows;
432
+ const headerModel = buildShellHeaderModel(model);
433
+ // Neon, missing backgrounds and sub-minimum widths keep the old row count.
434
+ const rows = [headerContent(headerModel, theme, width, usageHint, presentation).text];
435
+ const statuses = model.statuses.map(sanitizeStatus).filter((status) => status.length > 0).map((status) => theme.fg(ROLE.STATUS, status));
436
+ return statuses.length ? [...rows, truncateToWidth(joinSegments(statuses, theme), Math.max(0, Math.floor(width)), "…")] : rows;
437
+ }
438
+
308
439
  // The rule row painted directly under the header bar: one full-width horizontal
309
440
  // line in the same theme role as the editor frame (PROMPT_FRAME_ROLE in
310
441
  // extensions/gentle-shell.ts), so the status row and the prompt read as one
@@ -313,13 +444,37 @@ export function renderShellHeaderBar(model: ShellHeaderModel, theme: ShellBarThe
313
444
  // with it.
314
445
  const HEADER_RULE_CHAR = "─";
315
446
  const HEADER_RULE_ROLE = "border";
447
+ const HEADER_EDGE_CHAR = "▔";
448
+ const BG_RESET = "\x1b[49m";
449
+
450
+ // Paints the header content with floatRows and returns the row without its
451
+ // transparent margins plus the background opener. The painted row is
452
+ // `<open> <content> <reset>`; the opener has no spaces, so the first space ends
453
+ // it. Undefined when the float style does not apply.
454
+ function floatHeaderPaint(theme: ShellBarTheme, width: number, content: (width: number) => string): { inner: string; open: string } | undefined {
455
+ let floated = false;
456
+ const [row] = floatRows(CARD_TONE.INFO, theme, width, (inner) => {
457
+ floated = inner !== width;
458
+ return floated ? { body: [` ${fitHeaderContent(content(inner - 2), inner - 2)} `] } : {};
459
+ });
460
+ if (!floated || row === undefined) return undefined;
461
+ const inner = row.slice(1, -1);
462
+ return { inner, open: inner.slice(0, inner.indexOf(" ")) };
463
+ }
316
464
 
465
+ // In the float style the rule closes the hanging header tab with an upper
466
+ // one-eighth block line. Box-drawing lines sit mid-cell: a transparent `└──┘`
467
+ // leaves half an unpainted row under the tab, and a painted one overshoots it.
468
+ // `▔` hugs the top of its cell, so the line touches the tab's background
469
+ // exactly. The edge stays transparent beneath the painted bottom padding.
317
470
  export function renderShellHeaderRule(theme: ShellBarTheme, width: number): string {
318
- return theme.fg(HEADER_RULE_ROLE, HEADER_RULE_CHAR.repeat(Math.max(0, Math.floor(width))));
471
+ const targetWidth = Math.max(0, Math.floor(width));
472
+ if (floatHeaderPaint(theme, targetWidth, () => "") !== undefined) return theme.fg(HEADER_RULE_ROLE, HEADER_EDGE_CHAR.repeat(targetWidth));
473
+ return theme.fg(HEADER_RULE_ROLE, HEADER_RULE_CHAR.repeat(targetWidth));
319
474
  }
320
475
 
321
- export function renderShellBar(model: ShellBarModel, theme: ShellBarTheme, width: number): string[] {
322
- let segments = buildSegments(model, theme);
476
+ export function renderShellBar(model: ShellBarModel, theme: ShellBarTheme, width: number, presentation?: Presentation): string[] {
477
+ let segments = buildSegments(model, theme, presentation);
323
478
  const right = model.sessionName ? theme.fg(ROLE.SESSION, model.sessionName) : undefined;
324
479
 
325
480
  let left = joinSegments(segments, theme);
@@ -329,7 +484,7 @@ export function renderShellBar(model: ShellBarModel, theme: ShellBarTheme, width
329
484
  }
330
485
 
331
486
  if (visibleWidth(left) > width) {
332
- segments = buildSegments(compactModel(model), theme);
487
+ segments = buildSegments(compactModel(model), theme, presentation);
333
488
  left = joinSegments(segments, theme);
334
489
  }
335
490
  while (segments.length > 1 && visibleWidth(left) > width) {