gsd-pi 2.80.0-dev.96f4d89d5 → 2.80.0-dev.b2a374d5b

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 (239) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/GSD-WORKFLOW.md +2 -2
  3. package/dist/resources/extensions/gsd/auto/loop.js +32 -1
  4. package/dist/resources/extensions/gsd/auto/phases.js +37 -30
  5. package/dist/resources/extensions/gsd/auto-dispatch.js +10 -0
  6. package/dist/resources/extensions/gsd/auto-post-unit.js +10 -10
  7. package/dist/resources/extensions/gsd/auto-prompts.js +111 -1
  8. package/dist/resources/extensions/gsd/auto-recovery.js +154 -8
  9. package/dist/resources/extensions/gsd/auto-start.js +2 -3
  10. package/dist/resources/extensions/gsd/auto.js +16 -3
  11. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +32 -1
  12. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +129 -1
  13. package/dist/resources/extensions/gsd/clean-root-preflight.js +42 -4
  14. package/dist/resources/extensions/gsd/commands/dispatcher.js +5 -0
  15. package/dist/resources/extensions/gsd/commands-extract-learnings.js +17 -12
  16. package/dist/resources/extensions/gsd/crash-recovery.js +56 -10
  17. package/dist/resources/extensions/gsd/db-base-schema.js +14 -0
  18. package/dist/resources/extensions/gsd/db-migration-steps.js +16 -0
  19. package/dist/resources/extensions/gsd/detection.js +106 -0
  20. package/dist/resources/extensions/gsd/gsd-db.js +102 -2
  21. package/dist/resources/extensions/gsd/guided-flow.js +63 -12
  22. package/dist/resources/extensions/gsd/migrate/command.js +40 -1
  23. package/dist/resources/extensions/gsd/migration-auto-check.js +87 -0
  24. package/dist/resources/extensions/gsd/planning-path-scope.js +26 -0
  25. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +7 -8
  26. package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
  27. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +3 -1
  28. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  29. package/dist/resources/extensions/gsd/prompts/quick-task.md +1 -5
  30. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
  31. package/dist/resources/extensions/gsd/quick.js +34 -2
  32. package/dist/resources/extensions/gsd/safety/evidence-collector.js +10 -2
  33. package/dist/resources/extensions/gsd/tools/plan-slice.js +9 -0
  34. package/dist/resources/extensions/gsd/tools/plan-task.js +9 -0
  35. package/dist/resources/extensions/gsd/unit-runtime.js +11 -0
  36. package/dist/resources/extensions/gsd/worktree-manager.js +16 -14
  37. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  38. package/dist/web/standalone/.next/BUILD_ID +1 -1
  39. package/dist/web/standalone/.next/app-path-routes-manifest.json +13 -13
  40. package/dist/web/standalone/.next/build-manifest.json +2 -2
  41. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  42. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/index.html +1 -1
  59. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app-paths-manifest.json +13 -13
  66. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  67. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  68. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  69. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  70. package/package.json +4 -2
  71. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  72. package/packages/pi-coding-agent/dist/core/agent-session.js +3 -0
  73. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  74. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +76 -0
  75. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
  76. package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts +11 -0
  77. package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts.map +1 -1
  78. package/packages/pi-coding-agent/dist/core/compaction/compaction.js +9 -0
  79. package/packages/pi-coding-agent/dist/core/compaction/compaction.js.map +1 -1
  80. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts +2 -0
  81. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts.map +1 -0
  82. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js +103 -0
  83. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js.map +1 -0
  84. package/packages/pi-coding-agent/dist/core/db-snapshot.d.ts +15 -0
  85. package/packages/pi-coding-agent/dist/core/db-snapshot.d.ts.map +1 -0
  86. package/packages/pi-coding-agent/dist/core/db-snapshot.js +66 -0
  87. package/packages/pi-coding-agent/dist/core/db-snapshot.js.map +1 -0
  88. package/packages/pi-coding-agent/dist/core/db-snapshot.test.d.ts +2 -0
  89. package/packages/pi-coding-agent/dist/core/db-snapshot.test.d.ts.map +1 -0
  90. package/packages/pi-coding-agent/dist/core/db-snapshot.test.js +24 -0
  91. package/packages/pi-coding-agent/dist/core/db-snapshot.test.js.map +1 -0
  92. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +3 -0
  93. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  94. package/packages/pi-coding-agent/dist/core/extensions/runner.js +17 -1
  95. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  96. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +99 -0
  97. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
  98. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +7 -0
  99. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  100. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  101. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +20 -0
  102. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  103. package/packages/pi-coding-agent/dist/core/settings-manager.js +25 -0
  104. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  105. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/chat-frame-compaction-tone.test.js +6 -4
  106. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/chat-frame-compaction-tone.test.js.map +1 -1
  107. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js +50 -19
  108. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js.map +1 -1
  109. package/packages/pi-coding-agent/dist/modes/interactive/components/adaptive-layout.js +2 -2
  110. package/packages/pi-coding-agent/dist/modes/interactive/components/adaptive-layout.js.map +1 -1
  111. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  112. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
  113. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts.map +1 -1
  114. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js +2 -1
  115. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js.map +1 -1
  116. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-card-cleanup-and-success-runtime.test.js +7 -6
  117. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-card-cleanup-and-success-runtime.test.js.map +1 -1
  118. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +13 -0
  119. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  120. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +77 -13
  121. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  122. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  123. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +68 -2
  124. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  125. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js +53 -0
  126. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js.map +1 -1
  127. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  128. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +3 -0
  129. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  130. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.d.ts.map +1 -1
  131. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js +36 -27
  132. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js.map +1 -1
  133. package/packages/pi-coding-agent/dist/modes/interactive/tui-mode.test.js +21 -0
  134. package/packages/pi-coding-agent/dist/modes/interactive/tui-mode.test.js.map +1 -1
  135. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.d.ts +2 -0
  136. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.d.ts.map +1 -0
  137. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.js +10 -0
  138. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.js.map +1 -0
  139. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.d.ts.map +1 -1
  140. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.js +3 -2
  141. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.js.map +1 -1
  142. package/packages/pi-coding-agent/src/core/agent-session.ts +3 -0
  143. package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +89 -0
  144. package/packages/pi-coding-agent/src/core/compaction/compaction.ts +18 -0
  145. package/packages/pi-coding-agent/src/core/compaction-threshold.test.ts +121 -0
  146. package/packages/pi-coding-agent/src/core/db-snapshot.test.ts +32 -0
  147. package/packages/pi-coding-agent/src/core/db-snapshot.ts +66 -0
  148. package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +110 -0
  149. package/packages/pi-coding-agent/src/core/extensions/runner.ts +19 -1
  150. package/packages/pi-coding-agent/src/core/extensions/types.ts +7 -0
  151. package/packages/pi-coding-agent/src/core/settings-manager.ts +39 -1
  152. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/chat-frame-compaction-tone.test.ts +7 -5
  153. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-execution.test.ts +68 -19
  154. package/packages/pi-coding-agent/src/modes/interactive/components/adaptive-layout.ts +2 -2
  155. package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +1 -0
  156. package/packages/pi-coding-agent/src/modes/interactive/components/chat-frame.ts +2 -1
  157. package/packages/pi-coding-agent/src/modes/interactive/components/tool-card-cleanup-and-success-runtime.test.ts +10 -9
  158. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +88 -13
  159. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.test.ts +56 -0
  160. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +91 -3
  161. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +3 -0
  162. package/packages/pi-coding-agent/src/modes/interactive/theme/themes.ts +36 -27
  163. package/packages/pi-coding-agent/src/modes/interactive/tui-mode.test.ts +25 -0
  164. package/packages/pi-coding-agent/src/resources/extensions/memory/storage-safety-guard.test.ts +14 -0
  165. package/packages/pi-coding-agent/src/resources/extensions/memory/storage.ts +3 -2
  166. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  167. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  168. package/packages/pi-tui/dist/tui.js +17 -7
  169. package/packages/pi-tui/dist/tui.js.map +1 -1
  170. package/packages/pi-tui/src/tui.ts +19 -7
  171. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  172. package/pkg/dist/modes/interactive/theme/themes.d.ts.map +1 -1
  173. package/pkg/dist/modes/interactive/theme/themes.js +36 -27
  174. package/pkg/dist/modes/interactive/theme/themes.js.map +1 -1
  175. package/src/resources/GSD-WORKFLOW.md +2 -2
  176. package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
  177. package/src/resources/extensions/gsd/auto/loop.ts +50 -8
  178. package/src/resources/extensions/gsd/auto/phases.ts +42 -28
  179. package/src/resources/extensions/gsd/auto-dispatch.ts +17 -0
  180. package/src/resources/extensions/gsd/auto-post-unit.ts +10 -10
  181. package/src/resources/extensions/gsd/auto-prompts.ts +116 -1
  182. package/src/resources/extensions/gsd/auto-recovery.ts +153 -7
  183. package/src/resources/extensions/gsd/auto-start.ts +7 -6
  184. package/src/resources/extensions/gsd/auto.ts +23 -3
  185. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +39 -1
  186. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +135 -1
  187. package/src/resources/extensions/gsd/clean-root-preflight.ts +41 -3
  188. package/src/resources/extensions/gsd/commands/dispatcher.ts +6 -0
  189. package/src/resources/extensions/gsd/commands-extract-learnings.ts +17 -12
  190. package/src/resources/extensions/gsd/crash-recovery.ts +67 -10
  191. package/src/resources/extensions/gsd/db-base-schema.ts +15 -0
  192. package/src/resources/extensions/gsd/db-migration-steps.ts +17 -0
  193. package/src/resources/extensions/gsd/detection.ts +128 -0
  194. package/src/resources/extensions/gsd/gsd-db.ts +119 -1
  195. package/src/resources/extensions/gsd/guided-flow.ts +66 -12
  196. package/src/resources/extensions/gsd/migrate/command.ts +47 -1
  197. package/src/resources/extensions/gsd/migration-auto-check.ts +129 -0
  198. package/src/resources/extensions/gsd/planning-path-scope.ts +35 -0
  199. package/src/resources/extensions/gsd/prompts/complete-milestone.md +7 -8
  200. package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
  201. package/src/resources/extensions/gsd/prompts/plan-milestone.md +3 -1
  202. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  203. package/src/resources/extensions/gsd/prompts/quick-task.md +1 -5
  204. package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
  205. package/src/resources/extensions/gsd/quick.ts +37 -2
  206. package/src/resources/extensions/gsd/safety/evidence-collector.ts +11 -2
  207. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +1 -1
  208. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +170 -1
  209. package/src/resources/extensions/gsd/tests/clean-root-preflight.test.ts +88 -2
  210. package/src/resources/extensions/gsd/tests/commands-extract-learnings.test.ts +9 -0
  211. package/src/resources/extensions/gsd/tests/crash-handler-secondary.test.ts +55 -0
  212. package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +22 -0
  213. package/src/resources/extensions/gsd/tests/db-schema-metadata.test.ts +2 -2
  214. package/src/resources/extensions/gsd/tests/detection.test.ts +140 -0
  215. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +44 -0
  216. package/src/resources/extensions/gsd/tests/has-pending-deep-stage.test.ts +33 -1
  217. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +48 -0
  218. package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +127 -0
  219. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +50 -0
  220. package/src/resources/extensions/gsd/tests/plan-task.test.ts +21 -0
  221. package/src/resources/extensions/gsd/tests/prompt-path-audit.test.ts +40 -0
  222. package/src/resources/extensions/gsd/tests/quick-external-gsd.test.ts +40 -0
  223. package/src/resources/extensions/gsd/tests/right-sized-workflow-prompts.test.ts +192 -0
  224. package/src/resources/extensions/gsd/tests/safety-harness-false-positives.test.ts +29 -0
  225. package/src/resources/extensions/gsd/tests/smart-entry-complete.test.ts +38 -0
  226. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +63 -2
  227. package/src/resources/extensions/gsd/tests/status-db-open.test.ts +9 -0
  228. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +7 -0
  229. package/src/resources/extensions/gsd/tests/uok-plan-v2-wiring.test.ts +1 -1
  230. package/src/resources/extensions/gsd/tests/worktree-health-dispatch.test.ts +37 -6
  231. package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +7 -0
  232. package/src/resources/extensions/gsd/tests/worktree-nested-git-safety.test.ts +9 -2
  233. package/src/resources/extensions/gsd/tests/worktree-write-gate.test.ts +179 -0
  234. package/src/resources/extensions/gsd/tools/plan-slice.ts +13 -0
  235. package/src/resources/extensions/gsd/tools/plan-task.ts +10 -0
  236. package/src/resources/extensions/gsd/unit-runtime.ts +14 -0
  237. package/src/resources/extensions/gsd/worktree-manager.ts +15 -4
  238. /package/dist/web/standalone/.next/static/{y7d--cnc6Cibuva-Lzzw_ → FU0iL9dhdc2IBzEYBDOlY}/_buildManifest.js +0 -0
  239. /package/dist/web/standalone/.next/static/{y7d--cnc6Cibuva-Lzzw_ → FU0iL9dhdc2IBzEYBDOlY}/_ssgManifest.js +0 -0
@@ -0,0 +1,121 @@
1
+ // pi-coding-agent / Regression tests for compaction threshold percent (#5475)
2
+
3
+ import assert from "node:assert/strict";
4
+ import { describe, it } from "node:test";
5
+
6
+ import { shouldCompact, type CompactionSettings } from "./compaction/compaction.js";
7
+ import { SettingsManager } from "./settings-manager.js";
8
+
9
+ const REGISTRY_DEFAULTS: CompactionSettings = {
10
+ enabled: true,
11
+ reserveTokens: 16_384,
12
+ keepRecentTokens: 20_000,
13
+ };
14
+
15
+ describe("shouldCompact — thresholdPercent (#5475)", () => {
16
+ it("uses absolute reserveTokens when thresholdPercent is unset (legacy behavior)", () => {
17
+ // 200K window, 16384 reserve → fires at 183_617 tokens
18
+ assert.equal(shouldCompact(183_616, 200_000, REGISTRY_DEFAULTS), false);
19
+ assert.equal(shouldCompact(183_617, 200_000, REGISTRY_DEFAULTS), true);
20
+ });
21
+
22
+ it("uses thresholdPercent when set, ignoring reserveTokens", () => {
23
+ const settings: CompactionSettings = { ...REGISTRY_DEFAULTS, thresholdPercent: 0.7 };
24
+ // 200K * 0.7 = 140_000 → fires above that
25
+ assert.equal(shouldCompact(140_000, 200_000, settings), false);
26
+ assert.equal(shouldCompact(140_001, 200_000, settings), true);
27
+ // reserveTokens-based math would have said false at 183_616 — the percent override changes that
28
+ assert.equal(shouldCompact(150_000, 200_000, settings), true);
29
+ });
30
+
31
+ it("falls back to reserveTokens when thresholdPercent is out of range", () => {
32
+ // Defense in depth: reject 0, 1, negative, NaN, Infinity
33
+ for (const bad of [0, 1, -0.1, 1.5, Number.NaN, Number.POSITIVE_INFINITY]) {
34
+ const settings: CompactionSettings = { ...REGISTRY_DEFAULTS, thresholdPercent: bad };
35
+ assert.equal(
36
+ shouldCompact(183_616, 200_000, settings),
37
+ false,
38
+ `bad=${bad} should fall back to reserveTokens math`,
39
+ );
40
+ assert.equal(shouldCompact(183_617, 200_000, settings), true, `bad=${bad}`);
41
+ }
42
+ });
43
+
44
+ it("respects enabled=false regardless of thresholdPercent", () => {
45
+ const settings: CompactionSettings = {
46
+ ...REGISTRY_DEFAULTS,
47
+ enabled: false,
48
+ thresholdPercent: 0.5,
49
+ };
50
+ assert.equal(shouldCompact(199_999, 200_000, settings), false);
51
+ });
52
+
53
+ it("scales with contextWindow — same percent, different windows", () => {
54
+ const settings: CompactionSettings = { ...REGISTRY_DEFAULTS, thresholdPercent: 0.8 };
55
+ // 100K window: fires above 80_000
56
+ assert.equal(shouldCompact(80_000, 100_000, settings), false);
57
+ assert.equal(shouldCompact(80_001, 100_000, settings), true);
58
+ // 1M window: fires above 800_000
59
+ assert.equal(shouldCompact(800_000, 1_000_000, settings), false);
60
+ assert.equal(shouldCompact(800_001, 1_000_000, settings), true);
61
+ });
62
+ });
63
+
64
+ describe("SettingsManager — compaction threshold override (#5475)", () => {
65
+ it("getCompactionThresholdPercent returns undefined by default", () => {
66
+ const sm = SettingsManager.inMemory({});
67
+ assert.equal(sm.getCompactionThresholdPercent(), undefined);
68
+ assert.equal(sm.getCompactionSettings().thresholdPercent, undefined);
69
+ });
70
+
71
+ it("setCompactionThresholdOverride applies in-memory and is exposed via getCompactionSettings", () => {
72
+ const sm = SettingsManager.inMemory({});
73
+ sm.setCompactionThresholdOverride(0.7);
74
+ assert.equal(sm.getCompactionThresholdPercent(), 0.7);
75
+ assert.equal(sm.getCompactionSettings().thresholdPercent, 0.7);
76
+ });
77
+
78
+ it("setCompactionThresholdOverride(undefined) clears a prior override", () => {
79
+ const sm = SettingsManager.inMemory({});
80
+ sm.setCompactionThresholdOverride(0.7);
81
+ sm.setCompactionThresholdOverride(undefined);
82
+ assert.equal(sm.getCompactionThresholdPercent(), undefined);
83
+ assert.equal(sm.getCompactionSettings().thresholdPercent, undefined);
84
+ });
85
+
86
+ it("setCompactionThresholdOverride preserves other compaction fields (enabled, reserveTokens)", () => {
87
+ const sm = SettingsManager.inMemory({
88
+ compaction: { enabled: true, reserveTokens: 30_000, keepRecentTokens: 25_000 },
89
+ });
90
+ sm.setCompactionThresholdOverride(0.6);
91
+ const settings = sm.getCompactionSettings();
92
+ assert.equal(settings.enabled, true);
93
+ assert.equal(settings.reserveTokens, 30_000);
94
+ assert.equal(settings.keepRecentTokens, 25_000);
95
+ assert.equal(settings.thresholdPercent, 0.6);
96
+ });
97
+
98
+ it("setCompactionThresholdOverride works when no compaction config exists yet", () => {
99
+ const sm = SettingsManager.inMemory({});
100
+ sm.setCompactionThresholdOverride(0.85);
101
+ assert.equal(sm.getCompactionThresholdPercent(), 0.85);
102
+ // Other compaction fields fall back to their defaults
103
+ const settings = sm.getCompactionSettings();
104
+ assert.equal(settings.enabled, true);
105
+ assert.equal(typeof settings.reserveTokens, "number");
106
+ assert.equal(typeof settings.keepRecentTokens, "number");
107
+ });
108
+ });
109
+
110
+ describe("end-to-end — getCompactionSettings + shouldCompact (#5475)", () => {
111
+ it("70% threshold on a 200K window fires at the documented bug-report value (140_001 not 183_617)", () => {
112
+ const sm = SettingsManager.inMemory({});
113
+ sm.setCompactionThresholdOverride(0.7);
114
+ const settings = sm.getCompactionSettings();
115
+
116
+ assert.equal(shouldCompact(140_000, 200_000, settings), false);
117
+ assert.equal(shouldCompact(140_001, 200_000, settings), true);
118
+ // Pre-fix behavior would have required 183_617 — verify we no longer wait that long
119
+ assert.equal(shouldCompact(150_000, 200_000, settings), true);
120
+ });
121
+ });
@@ -0,0 +1,32 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it, afterEach } from "node:test";
3
+ import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+
7
+ import { atomicWriteDbSnapshotSync } from "./db-snapshot.js";
8
+
9
+ describe("atomicWriteDbSnapshotSync", () => {
10
+ let dir: string;
11
+
12
+ afterEach(() => {
13
+ if (dir) {
14
+ rmSync(dir, { recursive: true, force: true });
15
+ }
16
+ });
17
+
18
+ it("writes the full snapshot and leaves no temp file after success", () => {
19
+ dir = mkdtempSync(join(tmpdir(), "gsd-db-snapshot-test-"));
20
+ const dbPath = join(dir, "agent.db");
21
+ const snapshot = new Uint8Array([0x53, 0x51, 0x4c, 0x69, 0x74, 0x65]);
22
+
23
+ atomicWriteDbSnapshotSync(dbPath, snapshot);
24
+
25
+ assert.deepEqual(readFileSync(dbPath), Buffer.from(snapshot));
26
+ assert.equal(existsSync(`${dbPath}.tmp`), false);
27
+ assert.deepEqual(
28
+ readdirSync(dir).filter((entry) => entry.includes("agent.db") && entry.includes(".tmp")),
29
+ [],
30
+ );
31
+ });
32
+ });
@@ -0,0 +1,66 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { closeSync, fsyncSync, openSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { basename, dirname, join } from "node:path";
4
+
5
+ export type DbSnapshot = Buffer | Uint8Array;
6
+
7
+ function closeBestEffort(fd: number | null): void {
8
+ if (fd === null) return;
9
+ try {
10
+ closeSync(fd);
11
+ } catch {
12
+ // Preserve the original write/rename failure when cleaning up.
13
+ }
14
+ }
15
+
16
+ function fsyncDirectoryBestEffort(dirPath: string): void {
17
+ let fd: number | null = null;
18
+ try {
19
+ fd = openSync(dirPath, "r");
20
+ fsyncSync(fd);
21
+ } catch {
22
+ // Directory fsync is unsupported on some platforms/filesystems.
23
+ } finally {
24
+ closeBestEffort(fd);
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Persist a sql.js database export without exposing callers to a torn live file.
30
+ *
31
+ * The snapshot is written to a unique temp file in the same directory, flushed,
32
+ * then renamed over the target. A hard kill during the temp write leaves the
33
+ * previous target intact; after rename, readers see the complete new snapshot.
34
+ *
35
+ * The rename replaces the target inode: existing file mode/ownership is not
36
+ * preserved, and a symlink at dbPath is replaced rather than written through.
37
+ * Agent DB snapshots are owned runtime files, so this trade-off favors a
38
+ * private 0600 replacement over retaining prior target metadata.
39
+ */
40
+ export function atomicWriteDbSnapshotSync(dbPath: string, snapshot: DbSnapshot): void {
41
+ const dirPath = dirname(dbPath);
42
+ const tmpPath = join(dirPath, `.${basename(dbPath)}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`);
43
+ let fd: number | null = null;
44
+ let renamed = false;
45
+
46
+ try {
47
+ fd = openSync(tmpPath, "wx", 0o600);
48
+ writeFileSync(fd, Buffer.from(snapshot));
49
+ fsyncSync(fd);
50
+ closeSync(fd);
51
+ fd = null;
52
+
53
+ renameSync(tmpPath, dbPath);
54
+ renamed = true;
55
+ fsyncDirectoryBestEffort(dirPath);
56
+ } finally {
57
+ closeBestEffort(fd);
58
+ if (!renamed) {
59
+ try {
60
+ unlinkSync(tmpPath);
61
+ } catch {
62
+ // The temp file may not have been created yet.
63
+ }
64
+ }
65
+ }
66
+ }
@@ -102,6 +102,61 @@ describe("ExtensionRunner.emitToolCall", () => {
102
102
  assert.equal(errors[0].event, "tool_call");
103
103
  assert.equal(errors[0].extensionPath, "/test/throwing-ext");
104
104
  });
105
+
106
+ it("preserves shutdown in tool_call handler context", async (t) => {
107
+ const dir = mkdtempSync(join(tmpdir(), "runner-test-"));
108
+ t.after(() => {
109
+ rmSync(dir, { recursive: true, force: true });
110
+ });
111
+
112
+ const sessionManager = SessionManager.create(dir, dir);
113
+ const authStorage = AuthStorage.create();
114
+ const modelRegistry = new ModelRegistry(authStorage, join(dir, "models.json"));
115
+ const runtime = makeMinimalRuntime();
116
+ let shutdownCount = 0;
117
+ const handlers = new Map();
118
+ handlers.set("tool_call", [
119
+ async (_event: unknown, ctx: { shutdown: () => void }) => {
120
+ ctx.shutdown();
121
+ },
122
+ ]);
123
+ const extension = {
124
+ path: "/test/shutdown-on-tool-call",
125
+ handlers,
126
+ commands: new Map(),
127
+ shortcuts: new Map(),
128
+ tools: new Map(),
129
+ flags: new Map(),
130
+ diagnostics: [],
131
+ } as unknown as Extension;
132
+ const runner = new ExtensionRunner([extension], runtime, dir, sessionManager, modelRegistry);
133
+ runner.bindCore({} as any, {
134
+ getModel: () => undefined,
135
+ isIdle: () => true,
136
+ abort: () => {},
137
+ hasPendingMessages: () => false,
138
+ shutdown: () => {
139
+ shutdownCount += 1;
140
+ },
141
+ getContextUsage: () => undefined,
142
+ compact: () => {},
143
+ getSystemPrompt: () => "",
144
+ setCompactionThresholdOverride: () => {},
145
+ });
146
+
147
+ const errors: any[] = [];
148
+ runner.onError((err) => errors.push(err));
149
+
150
+ await runner.emitToolCall({
151
+ type: "tool_call",
152
+ toolCallId: "test-123",
153
+ toolName: "test_tool",
154
+ input: {},
155
+ } as ToolCallEvent);
156
+
157
+ assert.equal(shutdownCount, 1);
158
+ assert.equal(errors.length, 0);
159
+ });
105
160
  });
106
161
 
107
162
  describe("ExtensionRunner.createContext", () => {
@@ -127,6 +182,61 @@ describe("ExtensionRunner.createContext", () => {
127
182
  assert.equal(runner.createContext().cwd, realProjectDir);
128
183
  assert.equal(runner.createCommandContext().cwd, realProjectDir);
129
184
  });
185
+
186
+ it("does not let lifecycle event handlers close the TUI", async (t) => {
187
+ const dir = mkdtempSync(join(tmpdir(), "runner-test-"));
188
+ t.after(() => {
189
+ rmSync(dir, { recursive: true, force: true });
190
+ });
191
+
192
+ const sessionManager = SessionManager.create(dir, dir);
193
+ const authStorage = AuthStorage.create();
194
+ const modelRegistry = new ModelRegistry(authStorage, join(dir, "models.json"));
195
+ const runtime = makeMinimalRuntime();
196
+ let shutdownCount = 0;
197
+ const handlers = new Map();
198
+ handlers.set("agent_end", [
199
+ async (_event: unknown, ctx: { shutdown: () => void }) => {
200
+ ctx.shutdown();
201
+ },
202
+ ]);
203
+ const extension = {
204
+ path: "/test/shutdown-on-agent-end",
205
+ handlers,
206
+ commands: new Map(),
207
+ shortcuts: new Map(),
208
+ tools: new Map(),
209
+ flags: new Map(),
210
+ diagnostics: [],
211
+ } as unknown as Extension;
212
+ const runner = new ExtensionRunner([extension], runtime, dir, sessionManager, modelRegistry);
213
+ runner.bindCore({} as any, {
214
+ getModel: () => undefined,
215
+ isIdle: () => true,
216
+ abort: () => {},
217
+ hasPendingMessages: () => false,
218
+ shutdown: () => {
219
+ shutdownCount += 1;
220
+ },
221
+ getContextUsage: () => undefined,
222
+ compact: () => {},
223
+ getSystemPrompt: () => "",
224
+ setCompactionThresholdOverride: () => {},
225
+ });
226
+
227
+ const errors: any[] = [];
228
+ runner.onError((err) => errors.push(err));
229
+
230
+ await runner.emit({ type: "agent_end", messages: [] } as any);
231
+
232
+ assert.equal(shutdownCount, 0);
233
+ assert.equal(errors.length, 1);
234
+ assert.equal(errors[0].event, "agent_end");
235
+ assert.match(errors[0].error, /cannot request TUI shutdown/);
236
+
237
+ runner.createCommandContext().shutdown();
238
+ assert.equal(shutdownCount, 1);
239
+ });
130
240
  });
131
241
 
132
242
  describe("ExtensionRunner protected commands", () => {
@@ -235,6 +235,7 @@ export class ExtensionRunner {
235
235
  private getContextUsageFn: () => ContextUsage | undefined = () => undefined;
236
236
  private compactFn: (options?: CompactOptions) => void = () => {};
237
237
  private getSystemPromptFn: () => string = () => "";
238
+ private setCompactionThresholdOverrideFn: (percent: number | undefined) => void = () => {};
238
239
  private newSessionHandler: NewSessionHandler = async () => {
239
240
  throw new Error("Command context not yet bound: newSession is unavailable during early lifecycle");
240
241
  };
@@ -428,6 +429,7 @@ export class ExtensionRunner {
428
429
  this.getContextUsageFn = contextActions.getContextUsage;
429
430
  this.compactFn = contextActions.compact;
430
431
  this.getSystemPromptFn = contextActions.getSystemPrompt;
432
+ this.setCompactionThresholdOverrideFn = contextActions.setCompactionThresholdOverride;
431
433
 
432
434
  // Flush provider registrations queued during extension loading
433
435
  for (const { name, config } of this.runtime.pendingProviderRegistrations) {
@@ -714,9 +716,23 @@ export class ExtensionRunner {
714
716
  getContextUsage: () => this.getContextUsageFn(),
715
717
  compact: (options) => this.compactFn(options),
716
718
  getSystemPrompt: () => this.getSystemPromptFn(),
719
+ setCompactionThresholdOverride: (percent) => this.setCompactionThresholdOverrideFn(percent),
717
720
  };
718
721
  }
719
722
 
723
+ private createEventContext(eventType: string): ExtensionContext {
724
+ return {
725
+ ...this.createContext(),
726
+ shutdown: () => {
727
+ throw new Error(`Extension event '${eventType}' cannot request TUI shutdown`);
728
+ },
729
+ };
730
+ }
731
+
732
+ private isShutdownGuardedEvent(eventType: string): boolean {
733
+ return eventType === "agent_end" || eventType === "stop" || eventType === "session_end";
734
+ }
735
+
720
736
  createCommandContext(): ExtensionCommandContext {
721
737
  return {
722
738
  ...this.createContext(),
@@ -757,7 +773,9 @@ export class ExtensionRunner {
757
773
  getEvent: () => unknown,
758
774
  processResult: (handlerResult: unknown, extensionPath: string) => { done: boolean },
759
775
  ): Promise<void> {
760
- const ctx = this.createContext();
776
+ const ctx = this.isShutdownGuardedEvent(eventType)
777
+ ? this.createEventContext(eventType)
778
+ : this.createContext();
761
779
 
762
780
  for (const ext of this.extensions) {
763
781
  const handlers = ext.handlers.get(eventType);
@@ -289,6 +289,12 @@ export interface ExtensionContext {
289
289
  compact(options?: CompactOptions): void;
290
290
  /** Get the current effective system prompt. */
291
291
  getSystemPrompt(): string;
292
+ /**
293
+ * Set or clear an in-memory compaction threshold-percent override (0 < value < 1).
294
+ * Pass `undefined` to clear. The override is not persisted; host integrations
295
+ * are expected to re-apply on each session_start.
296
+ */
297
+ setCompactionThresholdOverride(percent: number | undefined): void;
292
298
  }
293
299
 
294
300
  /**
@@ -1741,6 +1747,7 @@ export interface ExtensionContextActions {
1741
1747
  getContextUsage: () => ContextUsage | undefined;
1742
1748
  compact: (options?: CompactOptions) => void;
1743
1749
  getSystemPrompt: () => string;
1750
+ setCompactionThresholdOverride: (percent: number | undefined) => void;
1744
1751
  }
1745
1752
 
1746
1753
  /**
@@ -15,6 +15,13 @@ export interface CompactionSettings {
15
15
  enabled?: boolean; // default: true
16
16
  reserveTokens?: number; // default: 16384
17
17
  keepRecentTokens?: number; // default: 20000
18
+ /**
19
+ * Optional percent-of-context-window trigger (0 < value < 1). When set,
20
+ * compaction fires at `contextWindow * thresholdPercent` and overrides
21
+ * `reserveTokens`. Typically set as a runtime override by host integrations
22
+ * (see `setCompactionThresholdOverride`) and not persisted by users directly.
23
+ */
24
+ thresholdPercent?: number;
18
25
  }
19
26
 
20
27
  export interface BranchSummarySettings {
@@ -812,11 +819,42 @@ export class SettingsManager {
812
819
  return this.settings.compaction?.keepRecentTokens ?? COMPACTION_KEEP_RECENT_TOKENS;
813
820
  }
814
821
 
815
- getCompactionSettings(): { enabled: boolean; reserveTokens: number; keepRecentTokens: number } {
822
+ getCompactionThresholdPercent(): number | undefined {
823
+ return this.settings.compaction?.thresholdPercent;
824
+ }
825
+
826
+ /**
827
+ * Set or clear an in-memory compaction threshold-percent override.
828
+ *
829
+ * Applied to `this.settings` only; never persisted to disk. Pass `undefined`
830
+ * to clear a previously set override (necessary for idempotent re-sync from
831
+ * host integrations whose preference may have been removed).
832
+ *
833
+ * Direct mutation is used instead of `applyOverrides()` because deep-merge
834
+ * semantics skip `undefined` values, which would prevent clearing.
835
+ */
836
+ setCompactionThresholdOverride(percent: number | undefined): void {
837
+ if (!this.settings.compaction) {
838
+ this.settings.compaction = {};
839
+ }
840
+ if (percent === undefined) {
841
+ delete this.settings.compaction.thresholdPercent;
842
+ } else {
843
+ this.settings.compaction.thresholdPercent = percent;
844
+ }
845
+ }
846
+
847
+ getCompactionSettings(): {
848
+ enabled: boolean;
849
+ reserveTokens: number;
850
+ keepRecentTokens: number;
851
+ thresholdPercent?: number;
852
+ } {
816
853
  return {
817
854
  enabled: this.getCompactionEnabled(),
818
855
  reserveTokens: this.getCompactionReserveTokens(),
819
856
  keepRecentTokens: this.getCompactionKeepRecentTokens(),
857
+ thresholdPercent: this.getCompactionThresholdPercent(),
820
858
  };
821
859
  }
822
860
 
@@ -1,3 +1,4 @@
1
+ // GSD2 TUI Tests - Chat frame card visual contract coverage.
1
2
  import { test, describe } from "node:test";
2
3
  import assert from "node:assert/strict";
3
4
  import stripAnsi from "strip-ansi";
@@ -8,12 +9,12 @@ initTheme("dark", false);
8
9
 
9
10
  // Regression tests for the "compaction" tone added to renderChatFrame.
10
11
  // The compaction notice shares the same visual frame as user / assistant
11
- // messages (top rule, `• label` header, `│ ` body prefix) but uses the
12
+ // messages (top rule, label header, `│ ` body prefix) but uses the
12
13
  // purple `customMessageLabel` color key so it is visually distinct from
13
14
  // conversation turns.
14
15
 
15
16
  describe("renderChatFrame — compaction tone", () => {
16
- test("produces a top rule, `• compaction` header row, and a │ body margin", () => {
17
+ test("produces a top rule, compaction header row, and a │ body margin", () => {
17
18
  const lines = renderChatFrame(
18
19
  ["Compacted from 1,224,262 tokens (ctrl+o to expand)"],
19
20
  60,
@@ -33,11 +34,12 @@ describe("renderChatFrame — compaction tone", () => {
33
34
  // Top rule is a solid horizontal bar
34
35
  assert.match(plain[0], /^─+$/, "first line should be the solid top rule");
35
36
 
36
- // Header row contains `• compaction`
37
+ // Header row contains `compaction`
37
38
  assert.ok(
38
- plain[1].includes("compaction"),
39
- `expected header to contain "compaction", got ${JSON.stringify(plain[1])}`,
39
+ plain[1].includes("compaction"),
40
+ `expected header to contain "compaction", got ${JSON.stringify(plain[1])}`,
40
41
  );
42
+ assert.ok(!plain[1].includes("•"), `header should not render a bullet prefix, got ${JSON.stringify(plain[1])}`);
41
43
 
42
44
  // Body line(s) start with `│ `
43
45
  assert.ok(
@@ -1,7 +1,8 @@
1
+ // GSD-2 Interactive Tool Execution Rendering Tests
1
2
  import { describe, test } from "node:test";
2
3
  import assert from "node:assert/strict";
3
4
  import stripAnsi from "strip-ansi";
4
- import { ToolExecutionComponent } from "../tool-execution.js";
5
+ import { ToolExecutionComponent, ToolPhaseSummaryComponent, type ToolExecutionPhase } from "../tool-execution.js";
5
6
  import { initTheme } from "../../theme/theme.js";
6
7
 
7
8
  initTheme("dark", false);
@@ -50,24 +51,26 @@ function renderToolCollapsed(
50
51
  }
51
52
 
52
53
  describe("ToolExecutionComponent", () => {
53
- test("renders framed header with Running status while tool is partial", () => {
54
+ test("renders framed header with running status while tool is partial", () => {
54
55
  const rendered = renderToolCollapsed("mcp__demo__do_thing", { ok: true });
55
56
 
56
- assert.match(rendered, /Tool demo\u00b7do_thing/);
57
- assert.match(rendered, /Running/);
58
- assert.match(rendered, /Running · \d+(ms|s)/);
57
+ assert.match(rendered, /demo\u00b7do_thing/);
58
+ assert.doesNotMatch(rendered, /Tool demo\u00b7do_thing/);
59
+ assert.match(rendered, /running/);
60
+ assert.match(rendered, /running · \d+(ms|s)/);
59
61
  });
60
62
 
61
- test("renders framed header with Error status for failed tool result", () => {
63
+ test("renders framed header with failed status for failed tool result", () => {
62
64
  const rendered = renderTool(
63
65
  "mcp__demo__do_thing",
64
66
  { ok: true },
65
67
  { content: [{ type: "text", text: "boom" }], isError: true },
66
68
  );
67
69
 
68
- assert.match(rendered, /Tool demo\u00b7do_thing/);
69
- assert.match(rendered, /Error/);
70
- assert.match(rendered, /Error · \d+(ms|s)/);
70
+ assert.match(rendered, /demo\u00b7do_thing/);
71
+ assert.doesNotMatch(rendered, /Tool demo\u00b7do_thing/);
72
+ assert.match(rendered, /failed/);
73
+ assert.match(rendered, /failed · \d+(ms|s)/);
71
74
  assert.match(rendered, /boom/);
72
75
  });
73
76
 
@@ -78,11 +81,55 @@ describe("ToolExecutionComponent", () => {
78
81
  { content: [], isError: false },
79
82
  );
80
83
 
81
- assert.match(rendered, /Done · \d+(ms|s)/);
82
- assert.match(rendered, /Completed/);
84
+ assert.match(rendered, /success · \d+(ms|s)/);
85
+ assert.match(rendered, /demo\u00b7noop/);
86
+ assert.doesNotMatch(rendered, /Completed/);
83
87
  assert.doesNotMatch(rendered, /ok=true/);
84
88
  });
85
89
 
90
+ test("exposes phase metadata for successful low-signal tool rows", () => {
91
+ const component = new ToolExecutionComponent(
92
+ "gsd_requirement_update",
93
+ { id: "R001" },
94
+ {},
95
+ { label: "Update Requirement" } as any,
96
+ { requestRender() {} } as any,
97
+ );
98
+ component.updateResult({ content: [], isError: false });
99
+
100
+ assert.deepEqual(component.getRollupPhase()?.label, "Requirement writes");
101
+ });
102
+
103
+ test("exposes phase metadata for collapsed output-bearing generic tools", () => {
104
+ const component = new ToolExecutionComponent(
105
+ "mcp__demo__do_thing",
106
+ { ok: true },
107
+ {},
108
+ undefined,
109
+ { requestRender() {} } as any,
110
+ );
111
+ component.updateResult({ content: [{ type: "text", text: "important output" }], isError: false });
112
+
113
+ assert.deepEqual(component.getRollupPhase()?.label, "Other tool actions");
114
+ });
115
+
116
+ test("renders phase-based summaries for rolled-up tool executions", () => {
117
+ const phases: ToolExecutionPhase[] = [
118
+ { label: "Setup / shell", count: 6, durationMs: 12 },
119
+ { label: "Context reads", count: 4, durationMs: 6 },
120
+ { label: "Requirement writes", count: 4, durationMs: 4 },
121
+ { label: "Memory lookups", count: 4, durationMs: 4 },
122
+ { label: "Finalization", count: 1, durationMs: 1 },
123
+ ];
124
+ const rendered = stripAnsi(new ToolPhaseSummaryComponent(phases).render(120).join("\n"));
125
+
126
+ assert.match(rendered, /Setup \/ shell 6 actions\s+success · 12ms/);
127
+ assert.match(rendered, /Context reads 4 actions\s+success · 6ms/);
128
+ assert.match(rendered, /Requirement writes 4 actions\s+success · 4ms/);
129
+ assert.match(rendered, /Memory lookups 4 actions\s+success · 4ms/);
130
+ assert.match(rendered, /Finalization 1 action\s+success · 1ms/);
131
+ });
132
+
86
133
  test("passes failed result status to custom result renderers", () => {
87
134
  const rendered = renderTool(
88
135
  "gsd_requirement_save",
@@ -99,7 +146,7 @@ describe("ToolExecutionComponent", () => {
99
146
  },
100
147
  );
101
148
 
102
- assert.match(rendered, /Error/);
149
+ assert.match(rendered, /failed/);
103
150
  assert.match(rendered, /custom saw error/);
104
151
  assert.doesNotMatch(rendered, /custom saw success/);
105
152
  });
@@ -162,14 +209,16 @@ describe("ToolExecutionComponent", () => {
162
209
  { label: "Complete Slice" },
163
210
  );
164
211
 
165
- assert.match(rendered, /Tool Complete Slice/);
212
+ assert.match(rendered, /Complete Slice/);
213
+ assert.doesNotMatch(rendered, /Tool Complete Slice/);
166
214
  assert.doesNotMatch(rendered, /gsd_slice_complete/);
167
215
  });
168
216
 
169
217
  test("frame header strips gsd_ prefix and title-cases when no label is registered", () => {
170
218
  const rendered = renderToolCollapsed("gsd_requirement_update", { id: "R005" });
171
219
 
172
- assert.match(rendered, /Tool Requirement Update/);
220
+ assert.match(rendered, /Requirement Update/);
221
+ assert.doesNotMatch(rendered, /Tool Requirement Update/);
173
222
  assert.doesNotMatch(rendered, /gsd_requirement_update/);
174
223
  });
175
224
 
@@ -198,7 +247,7 @@ describe("ToolExecutionComponent", () => {
198
247
  assert.doesNotMatch(rendered, /…/);
199
248
  });
200
249
 
201
- test("generic fallback truncates long output when collapsed", () => {
250
+ test("generic fallback collapses successful output rows until expanded", () => {
202
251
  const longOutput = Array.from({ length: 25 }, (_, i) => `line ${i + 1}`).join("\n");
203
252
  const rendered = renderToolCollapsed(
204
253
  "mcp__demo__do_thing",
@@ -206,10 +255,10 @@ describe("ToolExecutionComponent", () => {
206
255
  { content: [{ type: "text", text: longOutput }], isError: false },
207
256
  );
208
257
 
209
- assert.match(rendered, /line 1\b/);
210
- assert.match(rendered, /line 10\b/);
211
- assert.doesNotMatch(rendered, /line 20\b/);
212
- assert.match(rendered, /\(15 more lines/);
258
+ assert.match(rendered, /demo\u00b7do_thing/);
259
+ assert.match(rendered, /success · \d+(ms|s)/);
260
+ assert.doesNotMatch(rendered, /line 1\b/);
261
+ assert.doesNotMatch(rendered, /\(15 more lines/);
213
262
  });
214
263
 
215
264
  test("generic fallback falls back to truncated JSON for complex args", () => {
@@ -126,8 +126,8 @@ export class AdaptiveLayoutComponent implements Component {
126
126
 
127
127
  private frame(lines: string[], width: number, title: string, rightTitle: string, accent: ThemeColor): string[] {
128
128
  return style()
129
- .border("rounded")
130
- .density("comfortable")
129
+ .border("rule")
130
+ .density("compact")
131
131
  .toneColor((text) => theme.fg("surfaceMuted", text))
132
132
  .borderColor((text) => theme.fg("surfaceBorder", text))
133
133
  .title(theme.fg("surfaceTitle", title))
@@ -1,3 +1,4 @@
1
+ // GSD2 TUI - Assistant message card renderer for interactive terminal sessions.
1
2
  import type { AssistantMessage } from "@gsd/pi-ai";
2
3
  import { Container, Markdown, type MarkdownTheme, Spacer, Text } from "@gsd/pi-tui";
3
4
  import { getMarkdownTheme, theme } from "../theme/theme.js";