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,6 +1,7 @@
1
1
  import assert from "node:assert/strict";
2
- import test from "node:test";
2
+ import test, { after, before, type TestContext } from "node:test";
3
3
  import { visibleWidth } from "@earendil-works/pi-tui";
4
+ import { DEFAULT_VISUAL_SETTINGS } from "../lib/visual-customization-policy.ts";
4
5
  import {
5
6
  buildShellHeaderModel,
6
7
  formatCost,
@@ -8,13 +9,20 @@ import {
8
9
  gaugeTone,
9
10
  renderGauge,
10
11
  renderShellBar,
12
+ renderShellBottomOnlyBar,
13
+ renderShellBelowInputFloat,
11
14
  renderShellHeaderBar,
15
+ renderShellHeaderChrome,
12
16
  renderShellHeaderRule,
13
17
  renderShellSidebarBar,
14
18
  shellEnabled,
19
+ shellHeaderUsageHit,
15
20
  type ShellBarModel,
16
21
  type ShellBarTheme,
17
22
  } from "../lib/shell-bar.ts";
23
+ import { REVIEW_SCOPE_UNAVAILABLE } from "../lib/review-sidebar-state.ts";
24
+ import { stripAnsi } from "../lib/terminal-theme.ts";
25
+ import { CARD_STYLE, cardStyle, setCardStyle, type CardStyle } from "../lib/shell-card.ts";
18
26
 
19
27
  // The Gentle Shell bar replaces pi's three-line footer with one line of
20
28
  // segments. Rendering is pure so it can be verified without a TUI.
@@ -37,6 +45,42 @@ const plainTheme: ShellBarTheme = {
37
45
  },
38
46
  };
39
47
 
48
+ // The card style defaults to float; these assertions pin the outlined (neon)
49
+ // panels unless a test switches the style itself.
50
+ const initialCardStyle = cardStyle();
51
+ before(() => setCardStyle(CARD_STYLE.NEON));
52
+ after(() => setCardStyle(initialCardStyle));
53
+
54
+ function useCardStyle(t: TestContext, style: CardStyle): void {
55
+ const found = cardStyle();
56
+ t.after(() => setCardStyle(found));
57
+ setCardStyle(style);
58
+ }
59
+
60
+ const BG_OPEN = "\x1b[48;5;22m";
61
+ const BG_CLOSE = "\x1b[49m";
62
+
63
+ /** The same theme with a background, so the float style applies (without one panels keep the frame). */
64
+ function withBackground<T extends object>(theme: T): T & { bg(color: string, text: string): string } {
65
+ return { ...theme, bg: (_color: string, text: string) => `${BG_OPEN}${text}${BG_CLOSE}` };
66
+ }
67
+
68
+ /** Float panel rows: a painted panel inside transparent one-column margins, between padding rows that keep the accent bar. */
69
+ function assertFloatRows(lines: readonly string[], width: number): void {
70
+ for (const line of lines) {
71
+ assert.equal(visibleWidth(line), width, `"${stripAnsi(line)}" is not ${width} wide`);
72
+ assert.ok(line.startsWith(` ${BG_OPEN}`) && line.endsWith(`${BG_CLOSE} `), `painted inside the margins: ${JSON.stringify(line)}`);
73
+ }
74
+ const padding = ` ▎${" ".repeat(width - 3)} `;
75
+ assert.equal(stripAnsi(lines[0]!), padding, "a padding row with the accent bar sits above the header");
76
+ assert.equal(stripAnsi(lines.at(-1)!), padding, "a padding row with the accent bar replaces the bottom rule");
77
+ }
78
+
79
+ /** The text of a body row, without the neon side rails or the float accent bar. */
80
+ function bodyText(row: string): string {
81
+ return stripAnsi(row).replace(/^ ?[│▎] /u, "").replace(/ ?│? ?$/u, "").trimEnd();
82
+ }
83
+
40
84
  function model(overrides: Partial<ShellBarModel> = {}): ShellBarModel {
41
85
  return {
42
86
  cwd: "~/work/gentle-pi",
@@ -55,6 +99,53 @@ function model(overrides: Partial<ShellBarModel> = {}): ShellBarModel {
55
99
  };
56
100
  }
57
101
 
102
+ test("visual visibility hides only selected optional status segments", () => {
103
+ const settings = { ...DEFAULT_VISUAL_SETTINGS, visibility: { ...DEFAULT_VISUAL_SETTINGS.visibility, changes: false, modelDetails: false, usageCost: false } };
104
+ const data = model({ changes: { files: 2, added: 1, deleted: 1 } });
105
+ assert.doesNotMatch(renderShellBar(data, plainTheme, 160, settings).join(""), /gpt-5\.5|ctx|\$9\.49/);
106
+ assert.doesNotMatch(renderShellSidebarBar(data, plainTheme, 50, settings).join(""), /Changes|2 files/);
107
+ assert.doesNotMatch(renderShellHeaderBar(buildShellHeaderModel(data), plainTheme, 160, undefined, settings).text, /gpt-5\.5|ctx|\$9\.49|usage/);
108
+ });
109
+
110
+ test("Status title stays plain without an active review", () => {
111
+ const lines = renderShellSidebarBar(model(), plainTheme, 60);
112
+ assert.match(lines[0], /^╭─ ✿ Status ─+╮$/);
113
+ assert.doesNotMatch(lines.slice(1).join("\n"), /🌹 RDD/);
114
+ });
115
+
116
+ test("Status title stays plain above the review lifecycle block", () => {
117
+ const lines = renderShellSidebarBar(model({ review: { state: "reviewing", scope: "first.ts +2 files" } }), plainTheme, 60);
118
+ assert.match(lines[0], /^╭─ ✿ Status ─+╮$/);
119
+ assert.match(lines.slice(1).join("\n"), /🌹 RDD[\s\S]*Reviewers running…[\s\S]*first\.ts \+2 files/);
120
+ });
121
+
122
+ test("review lifecycle block omits the scope line when the candidate scope is unknown", () => {
123
+ const known = renderShellSidebarBar(model({ review: { state: "checking", scope: "first.ts" } }), plainTheme, 60);
124
+ const unknown = renderShellSidebarBar(model({ review: { state: "checking", scope: REVIEW_SCOPE_UNAVAILABLE } }), plainTheme, 60);
125
+ const text = unknown.join("\n");
126
+ assert.match(text, /🌹 RDD[\s\S]*Updating…/);
127
+ assert.doesNotMatch(text, /Candidate scope unavailable/);
128
+ assert.equal(unknown.length, known.length - 1);
129
+ });
130
+
131
+ test("rdd visibility hides only the review lifecycle block", () => {
132
+ const data = model({ review: { state: "reviewing", scope: "first.ts +2" }, changes: { files: 2, added: 1, deleted: 1 } });
133
+ assert.match(renderShellSidebarBar(data, plainTheme, 60, DEFAULT_VISUAL_SETTINGS).join("\n"), /🌹 RDD[\s\S]*Reviewers running…/);
134
+ const hidden = renderShellSidebarBar(data, plainTheme, 60, { ...DEFAULT_VISUAL_SETTINGS, visibility: { ...DEFAULT_VISUAL_SETTINGS.visibility, rdd: false } }).join("\n");
135
+ assert.doesNotMatch(hidden, /🌹 RDD|Reviewers running|first\.ts \+2 files/);
136
+ assert.match(hidden, /Changes[\s\S]*2 files/);
137
+ });
138
+
139
+ test("Status and review lifecycle block respect terminal width", () => {
140
+ for (const width of [8, 12, 16, 20, 32, 60]) {
141
+ for (const review of [undefined, { state: "reviewing" as const, scope: "first.ts +2 files" }, { state: "approved" as const, scope: REVIEW_SCOPE_UNAVAILABLE }]) {
142
+ const lines = renderShellSidebarBar(model({ review }), plainTheme, width);
143
+ for (const line of lines) assert.ok(visibleWidth(line) <= width, `${width}: ${line}`);
144
+ if (width >= 20) assert.match(lines[0], /^╭─ ✿ Status ─+╮$/);
145
+ }
146
+ }
147
+ });
148
+
58
149
  test("renderGauge fills cells proportionally to the percentage", () => {
59
150
  assert.equal(renderGauge(45, 8), "▰▰▰▰▱▱▱▱");
60
151
  assert.equal(renderGauge(0, 8), "▱▱▱▱▱▱▱▱");
@@ -435,3 +526,293 @@ test("renderShellHeaderRule paints one full-width line in the editor frame color
435
526
  assert.equal(renderShellHeaderRule(plainTheme, 0), "");
436
527
  assert.equal(renderShellHeaderRule(plainTheme, -3), "", "negative widths clamp to an empty rule");
437
528
  });
529
+
530
+ test("the sidebar Status card in the float style is a float panel two rows taller than neon, with a separator row after the header", (t) => {
531
+ const theme = withBackground(plainTheme);
532
+ const neon = renderShellSidebarBar(model({ review: { state: "reviewing", scope: "first.ts +2 files" } }), theme, 60);
533
+ useCardStyle(t, CARD_STYLE.FLOAT);
534
+ const float = renderShellSidebarBar(model({ review: { state: "reviewing", scope: "first.ts +2 files" } }), theme, 60);
535
+ assert.equal(float.length, neon.length + 2);
536
+ assert.equal(stripAnsi(float[1]!), ` ▎ ✿ Status${" ".repeat(49)}`);
537
+ assert.equal(stripAnsi(float[2]!), ` ▎${" ".repeat(57)} `, "a blank separator row follows the header");
538
+ assertFloatRows(float, 60);
539
+ assert.deepEqual(float.slice(3, -1).map(bodyText), neon.slice(1, -1).map(bodyText));
540
+ });
541
+
542
+ // The float top bar has full-width painted padding above and below its
543
+ // content, followed by a transparent bottom edge. Neon keeps two rows.
544
+
545
+ // The left inset before the header content.
546
+ const FLOAT_HEADER_OFFSET = 2;
547
+ // Both insets around the content.
548
+ const FLOAT_HEADER_CHROME = 4;
549
+
550
+ test("renderShellHeaderBar in the float style is one full-width background row", (t) => {
551
+ const theme = withBackground(plainTheme);
552
+ const header = buildShellHeaderModel(model({ usage: USAGE_TWO_WINDOWS }));
553
+ // Without a background the header keeps the neon cascade, even in the float style.
554
+ const neonInner = renderShellHeaderBar(header, plainTheme, 140 - FLOAT_HEADER_CHROME, "alt+u");
555
+ useCardStyle(t, CARD_STYLE.FLOAT);
556
+ const float = renderShellHeaderBar(header, theme, 140, "alt+u");
557
+ assert.equal(float.text.split("\n").length, 1, "the header stays one row tall");
558
+ assert.equal(visibleWidth(float.text), 140);
559
+ // The background reaches both edges: no side rules and no unpainted cell.
560
+ assert.ok(float.text.startsWith(`${BG_OPEN} `) && float.text.endsWith(`${BG_OPEN} ${BG_CLOSE}`), `painted edge to edge: ${JSON.stringify(float.text)}`);
561
+ assert.equal(stripAnsi(float.text), ` ${neonInner.text} `);
562
+ });
563
+
564
+ test("renderShellHeaderBar draws no float side rules", (t) => {
565
+ const header = buildShellHeaderModel(model({ usage: USAGE_TWO_WINDOWS }));
566
+ useCardStyle(t, CARD_STYLE.FLOAT);
567
+ const { text } = renderShellHeaderBar(header, withBackground(taggedTheme), 140, "alt+u");
568
+ assert.ok(!text.includes("│"), `no side rules: ${JSON.stringify(text.slice(0, 40))}`);
569
+ });
570
+
571
+ test("renderShellHeaderBar shifts the float usage span past its two-column inset", (t) => {
572
+ const theme = withBackground(plainTheme);
573
+ const header = buildShellHeaderModel(model({ usage: USAGE_TWO_WINDOWS }));
574
+ useCardStyle(t, CARD_STYLE.FLOAT);
575
+ for (const width of [140, 100, 90]) {
576
+ const { text, usageSpan } = renderShellHeaderBar(header, theme, width, "alt+u");
577
+ const neonInner = renderShellHeaderBar(header, plainTheme, width - FLOAT_HEADER_CHROME, "alt+u");
578
+ assert.deepEqual(usageSpan, neonInner.usageSpan && { start: neonInner.usageSpan.start + FLOAT_HEADER_OFFSET, end: neonInner.usageSpan.end + FLOAT_HEADER_OFFSET });
579
+ if (!usageSpan) continue;
580
+ const plain = stripAnsi(text);
581
+ assert.match(plain.slice(usageSpan.start, usageSpan.end), /^usage/);
582
+ assert.equal(usageSpan.end, visibleWidth(plain) - FLOAT_HEADER_OFFSET, "the usage segment ends before the right inset");
583
+ }
584
+ });
585
+
586
+ test("renderShellHeaderRule in the float style closes the tab with a top-hugging edge line", (t) => {
587
+ const theme = withBackground(taggedTheme);
588
+ useCardStyle(t, CARD_STYLE.FLOAT);
589
+ // `▔` sits at the top of its cell, touching the tab background with no gap and no painted overshoot below it.
590
+ assert.equal(renderShellHeaderRule(theme, 40), `<border>${"▔".repeat(40)}</border>`);
591
+ assert.equal(renderShellHeaderRule(theme, 0), renderShellHeaderRule(taggedTheme, 0), "a zero width keeps the neon rule");
592
+ });
593
+
594
+ test("the float header keeps the neon output below the float minimum width or without a background", (t) => {
595
+ const header = buildShellHeaderModel(model({ usage: USAGE_TWO_WINDOWS }));
596
+ const neonNarrow = renderShellHeaderBar(header, withBackground(plainTheme), 9, "alt+u");
597
+ const neonWide = renderShellHeaderBar(header, plainTheme, 140, "alt+u");
598
+ const neonRule = renderShellHeaderRule(taggedTheme, 9);
599
+ useCardStyle(t, CARD_STYLE.FLOAT);
600
+ assert.deepEqual(renderShellHeaderBar(header, withBackground(plainTheme), 9, "alt+u"), neonNarrow, "width < 10 falls back to neon");
601
+ assert.deepEqual(renderShellHeaderBar(header, plainTheme, 140, "alt+u"), neonWide, "a theme without bg falls back to neon");
602
+ assert.equal(renderShellHeaderRule(withBackground(taggedTheme), 9), neonRule);
603
+ assert.equal(renderShellHeaderRule(taggedTheme, 40), "<border>" + "─".repeat(40) + "</border>");
604
+ });
605
+
606
+ test("neon header bar and rule are byte-identical with a background-capable theme", () => {
607
+ const header = buildShellHeaderModel(model({ usage: USAGE_TWO_WINDOWS }));
608
+ for (const width of [140, 100, 60, 12]) {
609
+ assert.deepEqual(renderShellHeaderBar(header, withBackground(taggedTheme), width, "alt+u"), renderShellHeaderBar(header, taggedTheme, width, "alt+u"));
610
+ assert.equal(renderShellHeaderRule(withBackground(taggedTheme), width), "<border>" + "─".repeat(width) + "</border>");
611
+ }
612
+ });
613
+
614
+ test("float header chrome shares padded geometry with usage hit-testing", (t) => {
615
+ useCardStyle(t, CARD_STYLE.FLOAT);
616
+ const header = buildShellHeaderModel(model({ usage: USAGE_TWO_WINDOWS }));
617
+ const theme = withBackground(plainTheme);
618
+ for (const width of [10, 40, 90, 100, 140, 140.9]) {
619
+ const chrome = renderShellHeaderChrome(header, theme, width, "alt+u");
620
+ const target = Math.floor(width);
621
+ assert.equal(chrome.headerRow, 1);
622
+ assert.equal(chrome.rows.length, 4, "padding, content, padding, transparent edge");
623
+ assert.equal(chrome.rows[0], `${BG_OPEN}${" ".repeat(target)}${BG_CLOSE}`);
624
+ assert.equal(chrome.rows[2], chrome.rows[0]);
625
+ assert.equal(chrome.rows[1], renderShellHeaderBar(header, theme, width, "alt+u").text);
626
+ assert.equal(chrome.rows[3], "▔".repeat(target));
627
+ for (const row of chrome.rows) {
628
+ assert.equal(visibleWidth(row), target);
629
+ assert.doesNotMatch(stripAnsi(row), /[│▎╭╮╰╯└┘]/u, "no side rules");
630
+ }
631
+ const span = chrome.usageSpan;
632
+ if (!span) {
633
+ assert.equal(shellHeaderUsageHit(chrome, 0, 1), false, "hidden usage never clicks");
634
+ continue;
635
+ }
636
+ assert.match(stripAnsi(chrome.rows[chrome.headerRow]!).slice(span.start, span.end), /^usage/);
637
+ for (const y of [-1, 0, 2, 3, 4]) assert.equal(shellHeaderUsageHit(chrome, span.start, y), false, `decorative row ${y}`);
638
+ assert.equal(shellHeaderUsageHit(chrome, span.start - 1, 1), false);
639
+ assert.equal(shellHeaderUsageHit(chrome, span.start, 1), true);
640
+ assert.equal(shellHeaderUsageHit(chrome, span.end - 1, 1), true);
641
+ assert.equal(shellHeaderUsageHit(chrome, span.end, 1), false, "exclusive end");
642
+ }
643
+ });
644
+
645
+ test("float header paints every available cell even after ANSI resets", (t) => {
646
+ useCardStyle(t, CARD_STYLE.FLOAT);
647
+ const theme = withBackground({
648
+ fg: (_color: string, text: string) => `\x1b[31m${text}\x1b[0m`,
649
+ bold: (text: string) => `\x1b[1m${text}\x1b[m`,
650
+ });
651
+ const header = buildShellHeaderModel(model({ cwd: "directory " + "x".repeat(180) }));
652
+ for (const width of [10, 40, 90, 140]) {
653
+ const { rows } = renderShellHeaderChrome(header, theme, width, "alt+u");
654
+ for (const [index, row] of rows.entries()) {
655
+ let painted = false;
656
+ let columns = 0;
657
+ for (const token of row.split(/(\x1b\[[\d;]*m)/u)) {
658
+ if (token.startsWith("\x1b")) {
659
+ if (token === BG_OPEN) painted = true;
660
+ if (/^\x1b\[(?:0|49)?m$/u.test(token)) painted = false;
661
+ } else if (token) {
662
+ assert.equal(painted, index !== 3, `row ${index} at width ${width}: ${JSON.stringify(token)}`);
663
+ columns += visibleWidth(token);
664
+ }
665
+ }
666
+ assert.equal(columns, width, "background reaches both terminal edges");
667
+ }
668
+ }
669
+ });
670
+
671
+ test("float header usage clicks move to row one and reject row zero", (t) => {
672
+ useCardStyle(t, CARD_STYLE.FLOAT);
673
+ const chrome = renderShellHeaderChrome(buildShellHeaderModel(model()), withBackground(plainTheme), 140, "alt+u");
674
+ assert.ok(chrome.usageSpan);
675
+ assert.equal(shellHeaderUsageHit(chrome, chrome.usageSpan.start, 1), true, "content row is clickable");
676
+ assert.equal(shellHeaderUsageHit(chrome, chrome.usageSpan.start, 0), false, "top padding is decorative");
677
+ });
678
+
679
+ test("header chrome preserves neon bytes and row-zero interactions for every fallback", (t) => {
680
+ const header = buildShellHeaderModel(model());
681
+ const theme = withBackground(plainTheme);
682
+ for (const width of [-3, 0, 9, 10, 90, 140]) {
683
+ setCardStyle(CARD_STYLE.NEON);
684
+ const neon = renderShellHeaderChrome(header, theme, width, "alt+u");
685
+ assert.deepEqual(neon.rows, [renderShellHeaderBar(header, plainTheme, width, "alt+u").text, renderShellHeaderRule(plainTheme, width)]);
686
+ assert.equal(neon.headerRow, 0);
687
+ if (neon.usageSpan) {
688
+ assert.equal(shellHeaderUsageHit(neon, neon.usageSpan.start, 0), true);
689
+ assert.equal(shellHeaderUsageHit(neon, neon.usageSpan.start, 1), false);
690
+ }
691
+ useCardStyle(t, CARD_STYLE.FLOAT);
692
+ for (const fallback of [
693
+ plainTheme,
694
+ { ...plainTheme, bg: (_color: string, text: string) => text },
695
+ { ...plainTheme, bg: () => { throw new Error("missing theme token"); } },
696
+ ]) assert.deepEqual(renderShellHeaderChrome(header, fallback, width, "alt+u"), neon, "missing background falls back");
697
+ if (width < 10) assert.deepEqual(renderShellHeaderChrome(header, theme, width, "alt+u"), neon, "narrow fallback");
698
+ }
699
+ });
700
+
701
+ test("below-input float chrome mirrors only the edge and moves usage clicks to row two", (t) => {
702
+ useCardStyle(t, CARD_STYLE.FLOAT);
703
+ const header = buildShellHeaderModel(model({ usage: USAGE_TWO_WINDOWS }));
704
+ const theme = withBackground(plainTheme);
705
+ const presentation = { ...DEFAULT_VISUAL_SETTINGS, headerPlacement: "below-input" as const };
706
+ for (const width of [10, 40, 90, 100, 140, 140.9]) {
707
+ const above = renderShellHeaderChrome(header, theme, width, "alt+u");
708
+ const below = renderShellHeaderChrome(header, theme, width, "alt+u", presentation);
709
+ assert.deepEqual(below.rows, ["▁".repeat(Math.floor(width)), ...above.rows.slice(0, 3)]);
710
+ assert.equal(below.headerRow, 2);
711
+ assert.deepEqual(below.usageSpan, above.usageSpan);
712
+ if (!below.usageSpan) {
713
+ assert.equal(shellHeaderUsageHit(below, 0, 2), false);
714
+ continue;
715
+ }
716
+ const { start, end } = below.usageSpan;
717
+ for (const y of [-1, 0, 1, 3, 4]) assert.equal(shellHeaderUsageHit(below, start, y), false);
718
+ assert.equal(shellHeaderUsageHit(below, start, 2), true);
719
+ assert.equal(shellHeaderUsageHit(below, end - 1, 2), true);
720
+ assert.equal(shellHeaderUsageHit(below, start - 1, 2), false);
721
+ assert.equal(shellHeaderUsageHit(below, end, 2), false);
722
+ }
723
+ });
724
+
725
+ test("below-input placement preserves neon and float fallback bytes", (t) => {
726
+ const header = buildShellHeaderModel(model());
727
+ const presentation = { ...DEFAULT_VISUAL_SETTINGS, headerPlacement: "below-input" as const };
728
+ useCardStyle(t, CARD_STYLE.NEON);
729
+ for (const width of [0, 9, 10, 100, 140]) {
730
+ const theme = withBackground(plainTheme);
731
+ const neon = renderShellHeaderChrome(header, theme, width, "alt+u");
732
+ const data = model({ statuses: ["mcp ok"] });
733
+ const bottom = renderShellBottomOnlyBar(data, theme, width, "alt+u");
734
+ assert.deepEqual(renderShellHeaderChrome(header, theme, width, "alt+u", presentation), neon);
735
+ assert.deepEqual(renderShellBottomOnlyBar(data, theme, width, "alt+u", presentation), bottom);
736
+ setCardStyle(CARD_STYLE.FLOAT);
737
+ assert.deepEqual(renderShellHeaderChrome(header, plainTheme, width, "alt+u", presentation), neon);
738
+ assert.deepEqual(renderShellBottomOnlyBar(data, plainTheme, width, "alt+u", presentation), bottom);
739
+ if (width < 10) {
740
+ assert.deepEqual(renderShellHeaderChrome(header, theme, width, "alt+u", presentation), neon);
741
+ assert.deepEqual(renderShellBottomOnlyBar(data, theme, width, "alt+u", presentation), bottom);
742
+ }
743
+ setCardStyle(CARD_STYLE.NEON);
744
+ }
745
+ });
746
+
747
+ test("below-input bottom-only float bar retains mirrored padding and extension statuses", (t) => {
748
+ useCardStyle(t, CARD_STYLE.FLOAT);
749
+ const theme = withBackground(plainTheme);
750
+ const presentation = { ...DEFAULT_VISUAL_SETTINGS, headerPlacement: "below-input" as const };
751
+ for (const width of [10, 60, 80, 100]) {
752
+ for (const statuses of [[], ["mcp ok", "notice\nready"]]) {
753
+ const data = model({ usage: USAGE_TWO_WINDOWS, statuses });
754
+ const rows = renderShellBottomOnlyBar(data, theme, width, "alt+u", presentation);
755
+ const chrome = renderShellHeaderChrome(buildShellHeaderModel(data), theme, width, "alt+u", presentation);
756
+ assert.deepEqual(rows.slice(0, 3), chrome.rows.slice(0, 3));
757
+ assert.equal(rows.length, statuses.length ? 5 : 4);
758
+ assert.equal(rows.at(-1), chrome.rows.at(-1), "padding closes the entire group");
759
+ if (statuses.length) {
760
+ assert.match(stripAnsi(rows[3]!), width === 10 ? /^ mcp o…/ : /^ mcp ok/);
761
+ assert.ok(rows[3]!.startsWith(BG_OPEN), "statuses share the full-width background");
762
+ }
763
+ assert.ok(rows.every((row) => visibleWidth(row) <= width));
764
+ }
765
+ }
766
+ });
767
+
768
+ test("unified below-input float includes optional Changes and sanitized statuses inside one painted group", (t) => {
769
+ useCardStyle(t, CARD_STYLE.FLOAT);
770
+ const theme = withBackground({ ...plainTheme, fg: (_role: string, text: string) => `\x1b[31m${text}\x1b[0m` });
771
+ const presentation = { ...DEFAULT_VISUAL_SETTINGS, headerPlacement: "below-input" as const };
772
+ const changes = { files: [{ path: "lib/live.ts", added: 3, deleted: 1, status: "modified" as const }], added: 3, deleted: 1 };
773
+ for (const width of [10, 40, 139, 140, 240]) {
774
+ for (const captured of [undefined, { files: [], added: 0, deleted: 0 }, changes]) {
775
+ for (const statuses of [[], ["\x1b[31mMCP\x1b[0m\nready\t now", "notice"]]) {
776
+ const chrome = renderShellBelowInputFloat(model({ statuses }), theme, width, "alt+u", presentation, captured)!;
777
+ const hasChanges = Boolean(captured?.files.length);
778
+ assert.equal(chrome.headerRow, hasChanges ? 3 : 2);
779
+ assert.equal(chrome.rows.length, 4 + Number(hasChanges) + Number(statuses.length > 0));
780
+ assert.equal(stripAnsi(chrome.rows[0]!), "▁".repeat(width));
781
+ for (const row of chrome.rows.slice(1)) {
782
+ let painted = false;
783
+ for (const token of row.split(/(\x1b\[[\d;]*m)/u)) {
784
+ if (token === BG_OPEN) painted = true;
785
+ else if (/^\x1b\[(?:0|49)?m$/u.test(token)) painted = false;
786
+ else if (token && !token.startsWith("\x1b")) assert.equal(painted, true, JSON.stringify(token));
787
+ }
788
+ assert.equal(visibleWidth(row), width);
789
+ }
790
+ if (width === 240 && hasChanges) assert.match(stripAnsi(chrome.rows[2]!), /^ .*lib\/live.ts/);
791
+ if (width >= 40 && statuses.length) assert.match(stripAnsi(chrome.rows.at(-2)!), /^ MCP ready now ⟡ notice/);
792
+ if (chrome.usageSpan) {
793
+ assert.equal(shellHeaderUsageHit(chrome, chrome.usageSpan.start, chrome.headerRow), true);
794
+ for (let y = 0; y < chrome.rows.length; y++) if (y !== chrome.headerRow) assert.equal(shellHeaderUsageHit(chrome, chrome.usageSpan.start, y), false);
795
+ }
796
+ }
797
+ }
798
+ }
799
+ const hidden = renderShellBelowInputFloat(model({ statuses: ["secret status"] }), theme, 140, "alt+u", { ...presentation, statusPlacement: "hidden", visibility: { ...presentation.visibility, changes: false } }, changes)!;
800
+ assert.equal(hidden.rows.length, 4);
801
+ assert.equal(hidden.headerRow, 2);
802
+ assert.doesNotMatch(stripAnsi(hidden.rows.join("\n")), /secret status|live.ts/);
803
+ for (const width of [9, 10]) {
804
+ assert.equal(renderShellBelowInputFloat(model(), plainTheme, width, "alt+u", presentation, changes), undefined);
805
+ if (width === 9) assert.equal(renderShellBelowInputFloat(model(), theme, width, "alt+u", presentation, changes), undefined);
806
+ }
807
+ assert.equal(renderShellBelowInputFloat(model(), theme, 140, "alt+u", DEFAULT_VISUAL_SETTINGS, changes), undefined);
808
+ setCardStyle(CARD_STYLE.NEON);
809
+ assert.equal(renderShellBelowInputFloat(model(), theme, 140, "alt+u", presentation, changes), undefined);
810
+ });
811
+
812
+ test("the narrow-layout bottom-only bar stays unchanged in the float style", (t) => {
813
+ const theme = withBackground(taggedTheme);
814
+ const data = model({ usage: USAGE_TWO_WINDOWS, statuses: ["mcp ok"] });
815
+ const neon = renderShellBottomOnlyBar(data, theme, 100, "alt+u");
816
+ useCardStyle(t, CARD_STYLE.FLOAT);
817
+ assert.deepEqual(renderShellBottomOnlyBar(data, theme, 100, "alt+u"), neon);
818
+ });