gsd-pi 2.81.0 → 2.82.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 (290) hide show
  1. package/README.md +36 -24
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto/loop.js +111 -8
  4. package/dist/resources/extensions/gsd/auto/phases.js +190 -97
  5. package/dist/resources/extensions/gsd/auto/run-unit.js +66 -3
  6. package/dist/resources/extensions/gsd/auto/session.js +9 -0
  7. package/dist/resources/extensions/gsd/auto/verification-retry-policy.js +43 -0
  8. package/dist/resources/extensions/gsd/auto-dashboard.js +182 -178
  9. package/dist/resources/extensions/gsd/auto-dispatch.js +14 -11
  10. package/dist/resources/extensions/gsd/auto-post-unit.js +7 -1
  11. package/dist/resources/extensions/gsd/auto-recovery.js +6 -181
  12. package/dist/resources/extensions/gsd/auto-runtime-state.js +5 -0
  13. package/dist/resources/extensions/gsd/auto-start.js +20 -23
  14. package/dist/resources/extensions/gsd/auto-unit-closeout.js +33 -5
  15. package/dist/resources/extensions/gsd/auto-verification.js +12 -6
  16. package/dist/resources/extensions/gsd/auto-worktree.js +8 -0
  17. package/dist/resources/extensions/gsd/auto.js +265 -76
  18. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +13 -6
  19. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +8 -2
  20. package/dist/resources/extensions/gsd/bootstrap/register-shortcuts.js +4 -8
  21. package/dist/resources/extensions/gsd/commands/handlers/notifications-handler.js +4 -10
  22. package/dist/resources/extensions/gsd/commands/handlers/parallel.js +9 -0
  23. package/dist/resources/extensions/gsd/git-service.js +2 -1
  24. package/dist/resources/extensions/gsd/gsd-db.js +7 -23
  25. package/dist/resources/extensions/gsd/health-widget-core.js +1 -1
  26. package/dist/resources/extensions/gsd/health-widget.js +4 -10
  27. package/dist/resources/extensions/gsd/markdown-renderer.js +0 -95
  28. package/dist/resources/extensions/gsd/native-git-bridge.js +14 -14
  29. package/dist/resources/extensions/gsd/notification-overlay.js +35 -40
  30. package/dist/resources/extensions/gsd/parallel-merge.js +53 -30
  31. package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +25 -33
  32. package/dist/resources/extensions/gsd/prompts/complete-slice.md +14 -12
  33. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +20 -2
  34. package/dist/resources/extensions/gsd/prompts/discuss.md +20 -2
  35. package/dist/resources/extensions/gsd/recovery-classification.js +15 -1
  36. package/dist/resources/extensions/gsd/session-lock.js +40 -0
  37. package/dist/resources/extensions/gsd/state-reconciliation/drift/completion.js +131 -0
  38. package/dist/resources/extensions/gsd/state-reconciliation/drift/merge-state.js +247 -0
  39. package/dist/resources/extensions/gsd/state-reconciliation/drift/project-md.js +50 -0
  40. package/dist/resources/extensions/gsd/state-reconciliation/drift/roadmap.js +87 -0
  41. package/dist/resources/extensions/gsd/state-reconciliation/drift/sketch-flag.js +50 -0
  42. package/dist/resources/extensions/gsd/state-reconciliation/drift/stale-render.js +124 -0
  43. package/dist/resources/extensions/gsd/state-reconciliation/drift/stale-worker.js +32 -0
  44. package/dist/resources/extensions/gsd/state-reconciliation/errors.js +41 -0
  45. package/dist/resources/extensions/gsd/state-reconciliation/index.js +99 -0
  46. package/dist/resources/extensions/gsd/state-reconciliation/registry.js +24 -0
  47. package/dist/resources/extensions/gsd/state-reconciliation/spawn-gate.js +43 -0
  48. package/dist/resources/extensions/gsd/state-reconciliation/types.js +3 -0
  49. package/dist/resources/extensions/gsd/state-reconciliation.js +5 -26
  50. package/dist/resources/extensions/gsd/tui/render-kit.js +74 -0
  51. package/dist/resources/extensions/gsd/watch/header-renderer.js +92 -69
  52. package/dist/resources/extensions/gsd/watch/splash-palette.js +10 -0
  53. package/dist/resources/extensions/gsd/workflow-mcp.js +2 -2
  54. package/dist/resources/extensions/gsd/worktree-lifecycle.js +722 -316
  55. package/dist/resources/extensions/gsd/worktree-telemetry.js +3 -1
  56. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  57. package/dist/web/standalone/.next/BUILD_ID +1 -1
  58. package/dist/web/standalone/.next/app-path-routes-manifest.json +9 -9
  59. package/dist/web/standalone/.next/build-manifest.json +2 -2
  60. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  61. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  69. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/index.html +1 -1
  77. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  81. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  83. package/dist/web/standalone/.next/server/app-paths-manifest.json +9 -9
  84. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  85. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  86. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  87. package/dist/welcome-screen.d.ts +0 -7
  88. package/dist/welcome-screen.js +60 -69
  89. package/package.json +1 -1
  90. package/packages/daemon/package.json +2 -2
  91. package/packages/mcp-server/package.json +2 -2
  92. package/packages/native/package.json +1 -1
  93. package/packages/pi-agent-core/package.json +1 -1
  94. package/packages/pi-ai/package.json +1 -1
  95. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/assistant-message-design.test.d.ts +2 -0
  96. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/assistant-message-design.test.d.ts.map +1 -0
  97. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/assistant-message-design.test.js +47 -0
  98. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/assistant-message-design.test.js.map +1 -0
  99. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js +76 -9
  100. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js.map +1 -1
  101. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/user-message-design.test.d.ts +2 -0
  102. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/user-message-design.test.d.ts.map +1 -0
  103. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/user-message-design.test.js +40 -0
  104. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/user-message-design.test.js.map +1 -0
  105. package/packages/pi-coding-agent/dist/modes/interactive/components/adaptive-layout.d.ts +0 -1
  106. package/packages/pi-coding-agent/dist/modes/interactive/components/adaptive-layout.d.ts.map +1 -1
  107. package/packages/pi-coding-agent/dist/modes/interactive/components/adaptive-layout.js +30 -29
  108. package/packages/pi-coding-agent/dist/modes/interactive/components/adaptive-layout.js.map +1 -1
  109. package/packages/pi-coding-agent/dist/modes/interactive/components/adaptive-layout.test.js +10 -3
  110. package/packages/pi-coding-agent/dist/modes/interactive/components/adaptive-layout.test.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 +13 -13
  113. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
  114. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.d.ts +1 -3
  115. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  116. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.js +58 -3
  117. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.js.map +1 -1
  118. package/packages/pi-coding-agent/dist/modes/interactive/components/diff.d.ts +2 -2
  119. package/packages/pi-coding-agent/dist/modes/interactive/components/diff.d.ts.map +1 -1
  120. package/packages/pi-coding-agent/dist/modes/interactive/components/diff.js +12 -6
  121. package/packages/pi-coding-agent/dist/modes/interactive/components/diff.js.map +1 -1
  122. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
  123. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +14 -41
  124. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
  125. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +0 -1
  126. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  127. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +86 -82
  128. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  129. package/packages/pi-coding-agent/dist/modes/interactive/components/transcript-design.d.ts +35 -0
  130. package/packages/pi-coding-agent/dist/modes/interactive/components/transcript-design.d.ts.map +1 -0
  131. package/packages/pi-coding-agent/dist/modes/interactive/components/transcript-design.js +152 -0
  132. package/packages/pi-coding-agent/dist/modes/interactive/components/transcript-design.js.map +1 -0
  133. package/packages/pi-coding-agent/dist/modes/interactive/components/tui-style-kit.d.ts +16 -0
  134. package/packages/pi-coding-agent/dist/modes/interactive/components/tui-style-kit.d.ts.map +1 -0
  135. package/packages/pi-coding-agent/dist/modes/interactive/components/tui-style-kit.js +73 -0
  136. package/packages/pi-coding-agent/dist/modes/interactive/components/tui-style-kit.js.map +1 -0
  137. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts +1 -1
  138. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  139. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js +12 -8
  140. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js.map +1 -1
  141. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme-highlight.test.d.ts +2 -0
  142. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme-highlight.test.d.ts.map +1 -0
  143. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme-highlight.test.js +17 -0
  144. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme-highlight.test.js.map +1 -0
  145. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  146. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js +105 -1
  147. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js.map +1 -1
  148. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.d.ts.map +1 -1
  149. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js +27 -26
  150. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js.map +1 -1
  151. package/packages/pi-coding-agent/dist/modes/interactive/tui-mode.test.js +9 -6
  152. package/packages/pi-coding-agent/dist/modes/interactive/tui-mode.test.js.map +1 -1
  153. package/packages/pi-coding-agent/package.json +1 -1
  154. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/assistant-message-design.test.ts +56 -0
  155. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-execution.test.ts +113 -9
  156. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/user-message-design.test.ts +48 -0
  157. package/packages/pi-coding-agent/src/modes/interactive/components/adaptive-layout.test.ts +10 -3
  158. package/packages/pi-coding-agent/src/modes/interactive/components/adaptive-layout.ts +43 -42
  159. package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +14 -14
  160. package/packages/pi-coding-agent/src/modes/interactive/components/bash-execution.ts +64 -3
  161. package/packages/pi-coding-agent/src/modes/interactive/components/diff.ts +13 -7
  162. package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +15 -42
  163. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +84 -104
  164. package/packages/pi-coding-agent/src/modes/interactive/components/transcript-design.ts +196 -0
  165. package/packages/pi-coding-agent/src/modes/interactive/components/tui-style-kit.ts +94 -0
  166. package/packages/pi-coding-agent/src/modes/interactive/components/user-message.ts +14 -9
  167. package/packages/pi-coding-agent/src/modes/interactive/theme/theme-highlight.test.ts +23 -0
  168. package/packages/pi-coding-agent/src/modes/interactive/theme/theme.ts +106 -1
  169. package/packages/pi-coding-agent/src/modes/interactive/theme/themes.ts +27 -26
  170. package/packages/pi-coding-agent/src/modes/interactive/tui-mode.test.ts +9 -6
  171. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  172. package/packages/pi-tui/dist/__tests__/overlay-layout.test.js +14 -1
  173. package/packages/pi-tui/dist/__tests__/overlay-layout.test.js.map +1 -1
  174. package/packages/pi-tui/dist/overlay-layout.d.ts.map +1 -1
  175. package/packages/pi-tui/dist/overlay-layout.js +9 -6
  176. package/packages/pi-tui/dist/overlay-layout.js.map +1 -1
  177. package/packages/pi-tui/package.json +1 -1
  178. package/packages/pi-tui/src/__tests__/overlay-layout.test.ts +20 -1
  179. package/packages/pi-tui/src/overlay-layout.ts +10 -7
  180. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  181. package/packages/rpc-client/package.json +1 -1
  182. package/pkg/dist/modes/interactive/theme/theme-highlight.test.d.ts +2 -0
  183. package/pkg/dist/modes/interactive/theme/theme-highlight.test.d.ts.map +1 -0
  184. package/pkg/dist/modes/interactive/theme/theme-highlight.test.js +17 -0
  185. package/pkg/dist/modes/interactive/theme/theme-highlight.test.js.map +1 -0
  186. package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  187. package/pkg/dist/modes/interactive/theme/theme.js +105 -1
  188. package/pkg/dist/modes/interactive/theme/theme.js.map +1 -1
  189. package/pkg/dist/modes/interactive/theme/themes.d.ts.map +1 -1
  190. package/pkg/dist/modes/interactive/theme/themes.js +27 -26
  191. package/pkg/dist/modes/interactive/theme/themes.js.map +1 -1
  192. package/pkg/package.json +1 -1
  193. package/src/resources/extensions/gsd/auto/loop-deps.ts +9 -5
  194. package/src/resources/extensions/gsd/auto/loop.ts +113 -9
  195. package/src/resources/extensions/gsd/auto/phases.ts +144 -19
  196. package/src/resources/extensions/gsd/auto/run-unit.ts +69 -4
  197. package/src/resources/extensions/gsd/auto/session.ts +10 -0
  198. package/src/resources/extensions/gsd/auto/verification-retry-policy.ts +82 -0
  199. package/src/resources/extensions/gsd/auto-dashboard.ts +230 -183
  200. package/src/resources/extensions/gsd/auto-dispatch.ts +15 -1
  201. package/src/resources/extensions/gsd/auto-post-unit.ts +7 -1
  202. package/src/resources/extensions/gsd/auto-recovery.ts +7 -209
  203. package/src/resources/extensions/gsd/auto-runtime-state.ts +5 -0
  204. package/src/resources/extensions/gsd/auto-start.ts +22 -22
  205. package/src/resources/extensions/gsd/auto-unit-closeout.ts +51 -0
  206. package/src/resources/extensions/gsd/auto-verification.ts +12 -6
  207. package/src/resources/extensions/gsd/auto-worktree.ts +8 -0
  208. package/src/resources/extensions/gsd/auto.ts +295 -75
  209. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +21 -6
  210. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +6 -2
  211. package/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts +5 -8
  212. package/src/resources/extensions/gsd/commands/handlers/notifications-handler.ts +4 -10
  213. package/src/resources/extensions/gsd/commands/handlers/parallel.ts +12 -0
  214. package/src/resources/extensions/gsd/git-service.ts +2 -0
  215. package/src/resources/extensions/gsd/gsd-db.ts +7 -23
  216. package/src/resources/extensions/gsd/health-widget-core.ts +1 -1
  217. package/src/resources/extensions/gsd/health-widget.ts +6 -10
  218. package/src/resources/extensions/gsd/journal.ts +2 -0
  219. package/src/resources/extensions/gsd/markdown-renderer.ts +4 -95
  220. package/src/resources/extensions/gsd/native-git-bridge.ts +14 -13
  221. package/src/resources/extensions/gsd/notification-overlay.ts +50 -46
  222. package/src/resources/extensions/gsd/parallel-merge.ts +61 -34
  223. package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +33 -35
  224. package/src/resources/extensions/gsd/prompts/complete-slice.md +14 -12
  225. package/src/resources/extensions/gsd/prompts/discuss-headless.md +20 -2
  226. package/src/resources/extensions/gsd/prompts/discuss.md +20 -2
  227. package/src/resources/extensions/gsd/recovery-classification.ts +18 -1
  228. package/src/resources/extensions/gsd/session-lock.ts +41 -0
  229. package/src/resources/extensions/gsd/state-reconciliation/drift/completion.ts +172 -0
  230. package/src/resources/extensions/gsd/state-reconciliation/drift/merge-state.ts +337 -0
  231. package/src/resources/extensions/gsd/state-reconciliation/drift/project-md.ts +69 -0
  232. package/src/resources/extensions/gsd/state-reconciliation/drift/roadmap.ts +109 -0
  233. package/src/resources/extensions/gsd/state-reconciliation/drift/sketch-flag.ts +68 -0
  234. package/src/resources/extensions/gsd/state-reconciliation/drift/stale-render.ts +185 -0
  235. package/src/resources/extensions/gsd/state-reconciliation/drift/stale-worker.ts +46 -0
  236. package/src/resources/extensions/gsd/state-reconciliation/errors.ts +67 -0
  237. package/src/resources/extensions/gsd/state-reconciliation/index.ts +142 -0
  238. package/src/resources/extensions/gsd/state-reconciliation/registry.ts +27 -0
  239. package/src/resources/extensions/gsd/state-reconciliation/spawn-gate.ts +60 -0
  240. package/src/resources/extensions/gsd/state-reconciliation/types.ts +83 -0
  241. package/src/resources/extensions/gsd/state-reconciliation.ts +21 -53
  242. package/src/resources/extensions/gsd/tests/artifact-retry-cap.test.ts +1 -1
  243. package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +99 -0
  244. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +654 -176
  245. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +291 -4
  246. package/src/resources/extensions/gsd/tests/auto-runtime-state.test.ts +16 -1
  247. package/src/resources/extensions/gsd/tests/auto-start-orphan-bootstrap.test.ts +18 -0
  248. package/src/resources/extensions/gsd/tests/auto-unit-closeout.test.ts +68 -0
  249. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +28 -1
  250. package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +20 -2
  251. package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +44 -0
  252. package/src/resources/extensions/gsd/tests/header-renderer.test.ts +40 -0
  253. package/src/resources/extensions/gsd/tests/headless-milestone-parity.test.ts +10 -0
  254. package/src/resources/extensions/gsd/tests/health-widget.test.ts +14 -4
  255. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +26 -0
  256. package/src/resources/extensions/gsd/tests/integration/integration-proof.test.ts +1 -1
  257. package/src/resources/extensions/gsd/tests/integration/parallel-merge.test.ts +116 -24
  258. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +0 -1
  259. package/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +1 -1
  260. package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +46 -11
  261. package/src/resources/extensions/gsd/tests/notification-overlay.test.ts +78 -41
  262. package/src/resources/extensions/gsd/tests/notifications-handler.test.ts +44 -0
  263. package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +12 -217
  264. package/src/resources/extensions/gsd/tests/parallel-monitor-overlay.test.ts +38 -6
  265. package/src/resources/extensions/gsd/tests/post-exec-retry-bypass.test.ts +2 -2
  266. package/src/resources/extensions/gsd/tests/progressive-planning.test.ts +1 -1
  267. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +24 -1
  268. package/src/resources/extensions/gsd/tests/resume-dispatch-worktree.test.ts +7 -3
  269. package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +6 -3
  270. package/src/resources/extensions/gsd/tests/session-switch-abort-misclassification.test.ts +24 -0
  271. package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +65 -58
  272. package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +952 -0
  273. package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +4 -0
  274. package/src/resources/extensions/gsd/tests/tui-header-lifecycle.test.ts +121 -1
  275. package/src/resources/extensions/gsd/tests/tui-render-kit.test.ts +66 -0
  276. package/src/resources/extensions/gsd/tests/verification-retry-policy.test.ts +83 -0
  277. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +6 -0
  278. package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +158 -58
  279. package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +572 -118
  280. package/src/resources/extensions/gsd/tests/worktree-telemetry.test.ts +59 -2
  281. package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +18 -0
  282. package/src/resources/extensions/gsd/tui/render-kit.ts +109 -0
  283. package/src/resources/extensions/gsd/watch/header-renderer.ts +121 -79
  284. package/src/resources/extensions/gsd/watch/splash-palette.ts +11 -0
  285. package/src/resources/extensions/gsd/workflow-mcp.ts +2 -2
  286. package/src/resources/extensions/gsd/worktree-lifecycle.ts +1151 -524
  287. package/src/resources/extensions/gsd/worktree-telemetry.ts +7 -2
  288. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +0 -1544
  289. /package/dist/web/standalone/.next/static/{drLMkgfHQ8lzS229_HWYR → S44UQTFCUdA44dkjfYt6S}/_buildManifest.js +0 -0
  290. /package/dist/web/standalone/.next/static/{drLMkgfHQ8lzS229_HWYR → S44UQTFCUdA44dkjfYt6S}/_ssgManifest.js +0 -0
@@ -1,8 +1,12 @@
1
+ // Project/App: GSD-2
2
+ // File Purpose: Interactive terminal footer renderer for workspace, model, usage, context, and extension status.
3
+
1
4
  import { type Component, truncateToWidth, visibleWidth } from "@gsd/pi-tui";
2
5
  import type { AgentSession } from "../../../core/agent-session.js";
3
6
  import type { ReadonlyFooterDataProvider } from "../../../core/footer-data-provider.js";
4
7
  import { theme } from "../theme/theme.js";
5
8
  import { providerAuthBadge, providerDisplayName } from "./model-selector.js";
9
+ import { renderFooterStrip } from "./transcript-design.js";
6
10
 
7
11
  /**
8
12
  * Sanitize text for display in a single-line status.
@@ -155,8 +159,8 @@ export class FooterComponent implements Component {
155
159
  ? Math.max(0, Math.min(BAR_WIDTH, Math.round((contextPercentValue / 100) * BAR_WIDTH)))
156
160
  : 0;
157
161
  const bar =
158
- theme.fg(barColor, "".repeat(filled)) +
159
- theme.fg("dim", "".repeat(Math.max(0, BAR_WIDTH - filled)));
162
+ theme.fg(barColor, "".repeat(filled)) +
163
+ theme.fg("dim", "".repeat(Math.max(0, BAR_WIDTH - filled)));
160
164
  const pctText = contextPercent === "?" ? "?" : `${contextPercent}%`;
161
165
  const suffix = `/${formatTokens(contextWindow)}${autoIndicator}`;
162
166
  const colorizedPct =
@@ -218,34 +222,9 @@ export class FooterComponent implements Component {
218
222
  }
219
223
  }
220
224
 
221
- const rightSideWidth = visibleWidth(rightSide);
222
- const totalNeeded = statsLeftWidth + minPadding + rightSideWidth;
223
-
224
- let statsLine: string;
225
- if (totalNeeded <= width) {
226
- // Both fit - add padding to right-align model
227
- const padding = " ".repeat(width - statsLeftWidth - rightSideWidth);
228
- statsLine = statsLeft + padding + rightSide;
229
- } else {
230
- // Need to truncate right side
231
- const availableForRight = width - statsLeftWidth - minPadding;
232
- if (availableForRight > 0) {
233
- const truncatedRight = truncateToWidth(rightSide, availableForRight, "");
234
- const truncatedRightWidth = visibleWidth(truncatedRight);
235
- const padding = " ".repeat(Math.max(0, width - statsLeftWidth - truncatedRightWidth));
236
- statsLine = statsLeft + padding + truncatedRight;
237
- } else {
238
- // Not enough space for right side at all
239
- statsLine = statsLeft;
240
- }
241
- }
242
-
243
- // Apply dim to each part separately. statsLeft may contain color codes (for context %)
244
- // that end with a reset, which would clear an outer dim wrapper. So we dim the parts
245
- // before and after the colored section independently.
225
+ // Apply dim to the stats group before handing it to the shared footer strip.
226
+ // statsLeft may contain color codes for context %, so keep coloring local to the group.
246
227
  const dimStatsLeft = theme.fg("dim", statsLeft);
247
- const remainder = statsLine.slice(statsLeft.length); // padding + rightSide
248
- const dimRemainder = theme.fg("dim", remainder);
249
228
 
250
229
  // Extension statuses right-aligned on the pwd line (sorted by key).
251
230
  // Keeps the footer compact by avoiding a dedicated row when the content
@@ -260,18 +239,12 @@ export class FooterComponent implements Component {
260
239
  .join(" ")
261
240
  : "";
262
241
 
263
- const pwdWidth = visibleWidth(pwd);
264
- const extWidth = visibleWidth(extStatusText);
265
- let pwdLine: string;
266
- if (extStatusText && pwdWidth + 2 + extWidth <= width) {
267
- const padding = " ".repeat(width - pwdWidth - extWidth);
268
- pwdLine = theme.fg("dim", pwd + padding + extStatusText);
269
- } else {
270
- pwdLine = truncateToWidth(theme.fg("dim", pwd), width, theme.fg("dim", "..."));
271
- }
272
-
273
- const lines = [pwdLine, dimStatsLeft + dimRemainder];
274
-
275
- return lines;
242
+ const leftSegments = [
243
+ theme.fg("accent", "● GSD"),
244
+ theme.fg("dim", pwd),
245
+ dimStatsLeft,
246
+ ];
247
+ const footerRight = [rightSide, extStatusText].filter(Boolean).join(" ");
248
+ return renderFooterStrip(leftSegments, footerRight, width);
276
249
  }
277
250
  }
@@ -1,4 +1,5 @@
1
- // GSD-2 Interactive Tool Execution Rendering
1
+ // Project/App: GSD-2
2
+ // File Purpose: Interactive terminal tool execution renderer for commands, tool calls, diffs, images, and summaries.
2
3
  import {
3
4
  Box,
4
5
  Container,
@@ -24,6 +25,7 @@ import { getLanguageFromPath, highlightCode, theme } from "../theme/theme.js";
24
25
  import { shortenPath } from "../utils/shorten-path.js";
25
26
  import { renderDiff } from "./diff.js";
26
27
  import { keyHint } from "./keybinding-hints.js";
28
+ import { renderCommandCard, renderToolLineCard, renderTranscriptCard, type StatusTone } from "./transcript-design.js";
27
29
  import { truncateToVisualLines } from "./visual-truncate.js";
28
30
 
29
31
  // Preview line limit for bash when not expanded
@@ -83,73 +85,6 @@ function prettifyToolName(name: string, label?: string): string {
83
85
  .join(" ");
84
86
  }
85
87
 
86
- type ToolFrameTone = "pending" | "success" | "error";
87
-
88
- function trimOuterBlankLines(lines: string[]): string[] {
89
- let start = 0;
90
- let end = lines.length;
91
- while (start < end && lines[start].trim().length === 0) start++;
92
- while (end > start && lines[end - 1].trim().length === 0) end--;
93
- return lines.slice(start, end);
94
- }
95
-
96
- function renderToolFrame(
97
- contentLines: string[],
98
- width: number,
99
- opts: {
100
- label: string;
101
- status: string;
102
- tone: ToolFrameTone;
103
- },
104
- ): string[] {
105
- const outerWidth = Math.max(20, width);
106
- const contentWidth = Math.max(1, outerWidth - 2); // "│ " + content
107
-
108
- const borderColor = opts.tone === "error" ? "toolError" : opts.tone === "pending" ? "toolRunning" : "toolSuccess";
109
- const topColor = borderColor;
110
- const labelColor = opts.tone === "error" ? "toolError" : opts.tone === "pending" ? "toolRunning" : "toolSuccess";
111
- const statusColor = opts.tone === "error" ? "toolError" : opts.tone === "pending" ? "toolRunning" : "toolSuccess";
112
- const leftStyled = theme.fg(labelColor, theme.bold(opts.label));
113
- const rightStyled = theme.fg(statusColor, opts.status);
114
- const titleWidth = Math.max(1, outerWidth - 4);
115
- const gap = Math.max(1, titleWidth - visibleWidth(leftStyled) - visibleWidth(rightStyled));
116
- const headerRow = `${leftStyled}${" ".repeat(gap)}${rightStyled}`;
117
- const headerPad = Math.max(0, titleWidth - visibleWidth(headerRow));
118
-
119
- const sourceLines = trimOuterBlankLines(contentLines);
120
- const bodyLines = (sourceLines.length > 0 ? sourceLines : [""]).map((line) => {
121
- const clipped = truncateToWidth(line, contentWidth, "");
122
- return clipped;
123
- });
124
-
125
- return style()
126
- .border("rounded")
127
- .borderColor((line) => theme.fg(line.startsWith("─") ? topColor : borderColor, line))
128
- .title(headerRow + " ".repeat(headerPad))
129
- .render(bodyLines, outerWidth);
130
- }
131
-
132
- function renderCollapsedToolRow(
133
- label: string,
134
- status: string,
135
- width: number,
136
- tone: Extract<ToolFrameTone, "success">,
137
- ): string[] {
138
- const outerWidth = Math.max(20, width);
139
- const contentWidth = Math.max(1, outerWidth - 2);
140
- const statusColor = tone === "success" ? "toolSuccess" : "toolMuted";
141
- const labelStyled = theme.fg(statusColor, label);
142
- const statusStyled = theme.fg(statusColor, status);
143
- const labelWidth = Math.max(1, contentWidth - visibleWidth(statusStyled) - 1);
144
- const clippedLabel = truncateToWidth(labelStyled, labelWidth, "");
145
- const gap = Math.max(1, contentWidth - visibleWidth(clippedLabel) - visibleWidth(statusStyled));
146
- const line = `${clippedLabel}${" ".repeat(gap)}${statusStyled}`;
147
- return style()
148
- .border("minimal")
149
- .borderColor((text) => theme.fg(statusColor, text))
150
- .render([line], outerWidth);
151
- }
152
-
153
88
  const COMPACT_ARG_VALUE_LIMIT = 60;
154
89
  const GENERIC_OUTPUT_PREVIEW_LINES = 10;
155
90
  const GENERIC_ARGS_JSON_PREVIEW_LINES = 10;
@@ -213,6 +148,40 @@ function formatToolTarget(target: ToolTargetMetadata): string | undefined {
213
148
  return appendLineOrRange(displayPath, target);
214
149
  }
215
150
 
151
+ function directDetailsTarget(details: unknown, action: string): ToolTargetMetadata | undefined {
152
+ if (!details || typeof details !== "object") return undefined;
153
+ const record = details as Record<string, unknown>;
154
+ const rawPath = record.resolvedPath ?? record.inputPath ?? record.file_path ?? record.path;
155
+ if (typeof rawPath !== "string" || rawPath.trim().length === 0) return undefined;
156
+ const target: ToolTargetMetadata = {
157
+ kind: "file",
158
+ action,
159
+ resolvedPath: typeof record.resolvedPath === "string" ? record.resolvedPath : rawPath,
160
+ inputPath: typeof record.inputPath === "string" ? record.inputPath : rawPath,
161
+ };
162
+ if (typeof record.line === "number") {
163
+ target.line = record.line;
164
+ }
165
+ const range = record.range;
166
+ if (range && typeof range === "object") {
167
+ const rangeRecord = range as Record<string, unknown>;
168
+ target.range = {
169
+ start: typeof rangeRecord.start === "number" ? rangeRecord.start : undefined,
170
+ end: typeof rangeRecord.end === "number" ? rangeRecord.end : undefined,
171
+ };
172
+ }
173
+ return target;
174
+ }
175
+
176
+ function firstStringArg(args: Record<string, unknown>, keys: string[]): string | null {
177
+ for (const key of keys) {
178
+ const value = str(args[key]);
179
+ if (value === null) continue;
180
+ if (value) return value;
181
+ }
182
+ return "";
183
+ }
184
+
216
185
  function formatArgsPathTarget(path: string | null, args: Record<string, unknown>): string | undefined {
217
186
  if (!path) return undefined;
218
187
  const start = typeof args.offset === "number" ? args.offset : undefined;
@@ -692,22 +661,47 @@ export class ToolExecutionComponent extends Container {
692
661
  }
693
662
  const frameWidth = Math.max(20, width);
694
663
  const contentWidth = Math.max(1, frameWidth - 4);
695
- const frameTone: ToolFrameTone =
664
+ const frameTone: "pending" | "success" | "error" =
696
665
  this.result?.isError ? "error" : this.isPartial || !this.result ? "pending" : "success";
697
666
  const elapsed = formatElapsed((this.endedAt ?? Date.now()) - this.startedAt);
698
- const frameStatus = `${this.isPartial || !this.result ? "running" : this.result.isError ? "failed" : "success"} · ${elapsed}`;
667
+ const statusWord = this.isPartial || !this.result ? "running" : this.result.isError ? "failed" : "success";
668
+ const frameStatus = `${statusWord} · ${elapsed}`;
699
669
  const parsed = parseMcpToolName(this.toolName);
700
670
  const frameLabel = parsed
701
671
  ? `${parsed.server}·${parsed.tool}`
702
672
  : prettifyToolName(this.toolName, this.toolDefinition?.label) || "unknown";
703
- if (this.shouldRenderCompactSuccess()) {
704
- return ["", ...renderCollapsedToolRow(this.getCompactSummary(frameLabel), frameStatus, frameWidth, "success")];
673
+ const recommendedTone: StatusTone =
674
+ frameTone === "pending" ? "running" : frameTone === "error" ? "error" : "success";
675
+
676
+ if (this.normalizedToolName === "bash" && !this.expanded && !this.result?.isError) {
677
+ const command = str(this.args?.command);
678
+ return [
679
+ "",
680
+ ...renderCommandCard(command && command.length > 0 ? formatCommandPreview(command) : frameLabel, frameWidth, {
681
+ status: frameStatus,
682
+ tone: recommendedTone,
683
+ }),
684
+ ];
685
+ }
686
+ const hasImages = this.result?.content?.some((block) => block.type === "image") ?? false;
687
+ if (!this.expanded && !this.result?.isError && !hasImages) {
688
+ const compactTarget = this.getCompactTarget();
689
+ return [
690
+ "",
691
+ ...renderToolLineCard(frameLabel, compactTarget, frameWidth, {
692
+ status: frameStatus,
693
+ tone: recommendedTone,
694
+ hidden: !this.isPartial && !!this.result,
695
+ }),
696
+ ];
705
697
  }
706
698
  const lines = super.render(contentWidth);
707
- const framed = renderToolFrame(lines, frameWidth, {
708
- label: frameLabel,
709
- status: frameStatus,
710
- tone: frameTone,
699
+ const framed = renderTranscriptCard(lines, frameWidth, {
700
+ title: frameLabel,
701
+ right: frameStatus,
702
+ tone: recommendedTone,
703
+ footerLeft: this.expanded ? "output expanded" : undefined,
704
+ footerRight: this.expanded ? "ctrl+o collapse" : undefined,
711
705
  });
712
706
  return framed.length > 0 ? ["", ...framed] : framed;
713
707
  }
@@ -748,15 +742,6 @@ export class ToolExecutionComponent extends Container {
748
742
  return "Other tool actions";
749
743
  }
750
744
 
751
- private getCompactSummary(frameLabel: string): string {
752
- if (this.normalizedToolName === "bash") {
753
- const command = str(this.args?.command);
754
- return command ? `bash ${formatCommandPreview(command)}` : frameLabel;
755
- }
756
- const target = this.getCompactTarget();
757
- return target ? `${this.getCompactAction()} ${target}` : frameLabel;
758
- }
759
-
760
745
  private getCompactAction(): string {
761
746
  const target = this.getTargetMetadata();
762
747
  if (target?.action) return target.action === "list" ? "ls" : target.action;
@@ -765,7 +750,8 @@ export class ToolExecutionComponent extends Container {
765
750
 
766
751
  private getTargetMetadata(): ToolTargetMetadata | undefined {
767
752
  const target = this.result?.details?.target;
768
- return target && typeof target === "object" ? target : undefined;
753
+ if (target && typeof target === "object") return target;
754
+ return directDetailsTarget(this.result?.details, this.normalizedToolName);
769
755
  }
770
756
 
771
757
  private getCompactTarget(): string | undefined {
@@ -773,7 +759,7 @@ export class ToolExecutionComponent extends Container {
773
759
  const metadataTarget = metadata ? formatToolTarget(metadata) : undefined;
774
760
  if (metadataTarget) return metadataTarget;
775
761
 
776
- const path = str(this.args?.file_path ?? this.args?.path);
762
+ const path = firstStringArg(this.args ?? {}, ["file_path", "path", "notebook_path"]);
777
763
  if (path === null) return undefined;
778
764
  if (this.normalizedToolName === "read" || this.normalizedToolName === "hashline_read") {
779
765
  return formatArgsPathTarget(path, this.args);
@@ -782,16 +768,18 @@ export class ToolExecutionComponent extends Container {
782
768
  return path ? shortenPath(path) : undefined;
783
769
  }
784
770
  if (this.normalizedToolName === "ls") {
785
- return shortenPath(path || ".");
771
+ return path ? shortenPath(path) : undefined;
786
772
  }
787
773
  if (this.normalizedToolName === "find") {
788
774
  const pattern = str(this.args?.pattern);
789
- return pattern ? `${pattern} in ${shortenPath(path || ".")}` : shortenPath(path || ".");
775
+ if (pattern) return path ? `${pattern} in ${shortenPath(path)}` : pattern;
776
+ return path ? shortenPath(path) : undefined;
790
777
  }
791
778
  if (this.normalizedToolName === "grep") {
792
779
  const pattern = str(this.args?.pattern);
793
780
  const glob = str(this.args?.glob);
794
- const label = pattern ? `${pattern} in ${shortenPath(path || ".")}` : shortenPath(path || ".");
781
+ const label = pattern ? (path ? `${pattern} in ${shortenPath(path)}` : pattern) : path ? shortenPath(path) : undefined;
782
+ if (!label) return glob || undefined;
795
783
  return glob ? `${label} (${glob})` : label;
796
784
  }
797
785
  return undefined;
@@ -1410,23 +1398,14 @@ export class ToolExecutionComponent extends Container {
1410
1398
  }
1411
1399
  } else {
1412
1400
  // Generic tool / MCP tool without a registered renderer.
1413
- // MCP tool names from Claude Code arrive as `mcp__<server>__<tool>`;
1414
- // render the server prefix in muted style so the tool name reads
1415
- // cleanly. GSD-registered MCP tools have already had their prefix
1416
- // stripped upstream in partial-builder.ts and won't reach this branch.
1417
- const parsed = parseMcpToolName(this.toolName);
1418
- const displayName = parsed
1419
- ? parsed.tool
1420
- : prettifyToolName(this.toolName, this.toolDefinition?.label);
1421
- const serverPrefix = parsed ? theme.fg("muted", `${parsed.server}\u00b7`) : "";
1422
- text = serverPrefix + theme.fg("toolTitle", theme.bold(displayName));
1423
-
1401
+ // The frame header already contains the tool identity, so the body
1402
+ // should show only arguments and output.
1424
1403
  const argsText = formatCompactArgs(this.args, this.expanded);
1425
1404
  if (argsText) {
1426
1405
  if (argsText.includes("\n")) {
1427
- text += `\n\n${theme.fg("toolOutput", argsText)}`;
1406
+ text = theme.fg("toolOutput", argsText);
1428
1407
  } else {
1429
- text += " " + theme.fg("toolOutput", argsText);
1408
+ text = theme.fg("toolOutput", argsText);
1430
1409
  }
1431
1410
  }
1432
1411
 
@@ -1437,7 +1416,8 @@ export class ToolExecutionComponent extends Container {
1437
1416
  const maxLines = this.expanded ? lines.length : GENERIC_OUTPUT_PREVIEW_LINES;
1438
1417
  const displayLines = lines.slice(0, maxLines);
1439
1418
  const remaining = lines.length - maxLines;
1440
- text += `\n\n${displayLines.map((line: string) => theme.fg("toolOutput", line)).join("\n")}`;
1419
+ const outputText = displayLines.map((line: string) => theme.fg("toolOutput", line)).join("\n");
1420
+ text += `${text ? "\n\n" : ""}${outputText}`;
1441
1421
  if (remaining > 0) {
1442
1422
  text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("expandTools", "to expand")})`;
1443
1423
  }
@@ -0,0 +1,196 @@
1
+ // Project/App: GSD-2
2
+ // File Purpose: Shared recommended transcript rendering primitives for assistant, tool, command, footer, and auto-mode TUI surfaces.
3
+
4
+ import { truncateToWidth, visibleWidth } from "@gsd/pi-tui";
5
+ import { theme, type ThemeBg, type ThemeColor } from "../theme/theme.js";
6
+ import { alignRight, padRight, roundedPanel } from "./tui-style-kit.js";
7
+
8
+ export type StatusTone = "running" | "success" | "error" | "warning" | "muted";
9
+
10
+ export function chatMessageWidth(width: number): number {
11
+ return Math.max(24, Math.min(width, Math.floor(width * 0.72)));
12
+ }
13
+
14
+ function trimOuterBlankLines(lines: string[]): string[] {
15
+ let start = 0;
16
+ let end = lines.length;
17
+ while (start < end && lines[start].trim().length === 0) start++;
18
+ while (end > start && lines[end - 1].trim().length === 0) end--;
19
+ return lines.slice(start, end);
20
+ }
21
+
22
+ function toneColor(tone: StatusTone): ThemeColor {
23
+ switch (tone) {
24
+ case "running": return "toolRunning";
25
+ case "success": return "border";
26
+ case "error": return "toolError";
27
+ case "warning": return "warning";
28
+ case "muted":
29
+ default: return "toolMuted";
30
+ }
31
+ }
32
+
33
+ export function rightAlign(left: string, right: string, width: number): string {
34
+ return alignRight(left, right, width);
35
+ }
36
+
37
+ export function renderAssistantRail(
38
+ lines: string[],
39
+ width: number,
40
+ opts: { label: string; meta?: string; railColor?: ThemeColor } = { label: "GSD" },
41
+ ): string[] {
42
+ const outerWidth = Math.max(20, width);
43
+ const indent = " ";
44
+ const rail = theme.fg(opts.railColor ?? "borderAccent", "┃");
45
+ const blockWidth = Math.max(1, outerWidth - visibleWidth(indent));
46
+ const contentWidth = Math.max(1, blockWidth - 2);
47
+ const header = opts.meta
48
+ ? `${theme.fg("borderAccent", theme.bold(opts.label))} ${theme.fg("dim", opts.meta)}`
49
+ : theme.fg("borderAccent", theme.bold(opts.label));
50
+ const source = lines.length > 0 ? lines : [""];
51
+ const row = (line: string) => `${indent}${theme.bg("customMessageBg", padRight(`${rail} ${line}`, blockWidth))}`;
52
+ return [
53
+ row(""),
54
+ row(truncateToWidth(header, contentWidth, "")),
55
+ ...source.map((line) => row(truncateToWidth(line, contentWidth, ""))),
56
+ row(""),
57
+ ];
58
+ }
59
+
60
+ export function renderUserRail(
61
+ lines: string[],
62
+ width: number,
63
+ opts: { label: string; meta?: string },
64
+ ): string[] {
65
+ const outerWidth = Math.max(20, width);
66
+ const maxMessageWidth = chatMessageWidth(outerWidth);
67
+ const rawBodyLines = trimOuterBlankLines(lines).length > 0 ? trimOuterBlankLines(lines) : [""];
68
+ const rail = theme.fg("border", "┃");
69
+ const contentWidth = Math.max(1, Math.min(maxMessageWidth, outerWidth - 2));
70
+ const header = opts.meta
71
+ ? `${theme.fg("border", theme.bold(opts.label))} ${theme.fg("dim", opts.meta)}`
72
+ : theme.fg("border", theme.bold(opts.label));
73
+ const bodyLines = rawBodyLines.map((line) =>
74
+ theme.fg("userMessageText", truncateToWidth(line.trimEnd(), contentWidth, ""))
75
+ );
76
+ const row = (line: string) => theme.bg("userMessageBg", padRight(`${rail} ${line}`, outerWidth));
77
+ return [
78
+ row(""),
79
+ row(truncateToWidth(header, contentWidth, "")),
80
+ ...bodyLines.map((line) => row(truncateToWidth(line, contentWidth, ""))),
81
+ row(""),
82
+ ];
83
+ }
84
+
85
+ function cardRow(line: string, contentWidth: number, paddingX: number, border: (text: string) => string): string {
86
+ const padded = " ".repeat(paddingX) + padRight(line, contentWidth) + " ".repeat(paddingX);
87
+ return `${border("│")}${padded}${border("│")}`;
88
+ }
89
+
90
+ export function renderTranscriptCard(
91
+ lines: string[],
92
+ width: number,
93
+ opts: {
94
+ title: string;
95
+ right?: string;
96
+ tone: StatusTone;
97
+ footerLeft?: string;
98
+ footerRight?: string;
99
+ },
100
+ ): string[] {
101
+ const outerWidth = Math.max(20, width);
102
+ const innerWidth = Math.max(1, outerWidth - 2);
103
+ const paddingX = 2;
104
+ const contentWidth = Math.max(1, innerWidth - paddingX * 2);
105
+ const borderColor = toneColor(opts.tone);
106
+ const border = (text: string) => theme.fg(borderColor, text);
107
+ const title = theme.fg("borderAccent", opts.title);
108
+ const right = opts.right ? theme.fg(toneColor(opts.tone), opts.right) : "";
109
+ const header = rightAlign(title, right, contentWidth);
110
+ const body = lines.map((line) => padRight(line, contentWidth));
111
+ const footer =
112
+ opts.footerLeft || opts.footerRight
113
+ ? [rightAlign(theme.fg("dim", opts.footerLeft ?? ""), theme.fg("dim", opts.footerRight ?? ""), contentWidth)]
114
+ : [];
115
+ return [
116
+ border("╭" + "─".repeat(outerWidth - 2) + "╮"),
117
+ cardRow(header, contentWidth, paddingX, border),
118
+ ...body.map((line) => cardRow(line, contentWidth, paddingX, border)),
119
+ ...footer.map((line) => cardRow(line, contentWidth, paddingX, border)),
120
+ border("╰" + "─".repeat(outerWidth - 2) + "╯"),
121
+ ];
122
+ }
123
+
124
+ export function renderToolLineCard(
125
+ title: string,
126
+ target: string | undefined,
127
+ width: number,
128
+ opts: { status: string; tone: StatusTone; hidden?: boolean; titlePrefix?: string; bg?: ThemeBg },
129
+ ): string[] {
130
+ const outerWidth = Math.max(20, width);
131
+ const innerWidth = Math.max(1, outerWidth - 2);
132
+ const paddingX = 2;
133
+ const contentWidth = Math.max(1, innerWidth - paddingX * 2);
134
+ const borderColor = toneColor(opts.tone);
135
+ const border = (text: string) => theme.fg(borderColor, text);
136
+ const titleText = `${opts.titlePrefix ?? ""}${theme.fg("borderAccent", title)}`;
137
+ const targetText = target ? ` ${theme.fg("text", target)}` : "";
138
+ const left = `${titleText}${targetText}`;
139
+ const statusText = opts.hidden
140
+ ? `${opts.status} · output hidden · ctrl+o expand`
141
+ : opts.status;
142
+ const right = theme.fg(opts.tone === "success" ? "success" : borderColor, statusText);
143
+ const line = rightAlign(left, right, contentWidth);
144
+ const row = cardRow(line, contentWidth, paddingX, border);
145
+ return [
146
+ border("╭" + "─".repeat(outerWidth - 2) + "╮"),
147
+ opts.bg ? theme.bg(opts.bg, row) : row,
148
+ border("╰" + "─".repeat(outerWidth - 2) + "╯"),
149
+ ];
150
+ }
151
+
152
+ export function renderCommandCard(
153
+ command: string,
154
+ width: number,
155
+ opts: { status: string; tone: StatusTone; progress?: string },
156
+ ): string[] {
157
+ const outerWidth = Math.max(20, width);
158
+ const innerWidth = Math.max(1, outerWidth - 2);
159
+ const paddingX = 2;
160
+ const contentWidth = Math.max(1, innerWidth - paddingX * 2);
161
+ const borderColor = toneColor(opts.tone);
162
+ const left = `${theme.fg("accent", "$")} ${theme.fg("text", command)}`;
163
+ const statusText = opts.progress
164
+ ? `${opts.progress} ${opts.status}`
165
+ : `${opts.status} · output hidden · ctrl+o expand`;
166
+ const right = theme.fg(opts.tone === "success" ? "success" : borderColor, statusText);
167
+ const line = rightAlign(left, right, contentWidth);
168
+ const border = (text: string) => theme.fg(borderColor, text);
169
+ return [
170
+ border("╭" + "─".repeat(outerWidth - 2) + "╮"),
171
+ cardRow(line, contentWidth, paddingX, border),
172
+ border("╰" + "─".repeat(outerWidth - 2) + "╯"),
173
+ ];
174
+ }
175
+
176
+ export function renderProgressBar(done: number, total: number, width: number, tone: StatusTone = "success"): string {
177
+ const clampedWidth = Math.max(0, width);
178
+ const pct = total > 0 ? Math.max(0, Math.min(1, done / total)) : 0;
179
+ const filled = Math.round(pct * clampedWidth);
180
+ return (
181
+ theme.fg(toneColor(tone), "█".repeat(filled)) +
182
+ theme.fg("dim", "░".repeat(clampedWidth - filled))
183
+ );
184
+ }
185
+
186
+ export function renderFooterStrip(leftSegments: string[], right: string, width: number): string[] {
187
+ const outerWidth = Math.max(20, width);
188
+ const innerWidth = Math.max(1, outerWidth - 2);
189
+ const sep = theme.fg("dim", " │ ");
190
+ const rightStyled = theme.fg("dim", right);
191
+ const rightWidth = visibleWidth(rightStyled);
192
+ const leftBudget = right ? Math.max(1, innerWidth - rightWidth - 3) : innerWidth;
193
+ const left = truncateToWidth(leftSegments.filter(Boolean).join(sep), leftBudget, "");
194
+ const content = rightAlign(left, rightStyled, innerWidth);
195
+ return roundedPanel([content], outerWidth);
196
+ }
@@ -0,0 +1,94 @@
1
+ // Project/App: GSD-2
2
+ // File Purpose: Lip Gloss-inspired terminal layout primitives for GSD interactive TUI surfaces.
3
+
4
+ import { truncateToWidth, visibleWidth } from "@gsd/pi-tui";
5
+ import { theme, type ThemeColor } from "../theme/theme.js";
6
+
7
+ export type TuiTone = "default" | "accent" | "success" | "warning" | "error" | "muted";
8
+
9
+ export type TuiBreakpoint = "compact" | "regular" | "wide";
10
+
11
+ export function breakpoint(width: number): TuiBreakpoint {
12
+ if (width < 72) return "compact";
13
+ if (width < 112) return "regular";
14
+ return "wide";
15
+ }
16
+
17
+ export function padRight(text: string, width: number): string {
18
+ const clipped = truncateToWidth(text, Math.max(0, width), "");
19
+ return clipped + " ".repeat(Math.max(0, width - visibleWidth(clipped)));
20
+ }
21
+
22
+ export function alignRight(left: string, right: string, width: number): string {
23
+ if (width <= 0) return "";
24
+ if (!right) return truncateToWidth(left, width, "");
25
+ const gap = Math.max(1, width - visibleWidth(left) - visibleWidth(right));
26
+ return truncateToWidth(left + " ".repeat(gap) + right, width, "");
27
+ }
28
+
29
+ export function toneColor(tone: TuiTone): ThemeColor {
30
+ switch (tone) {
31
+ case "accent": return "borderAccent";
32
+ case "success": return "success";
33
+ case "warning": return "warning";
34
+ case "error": return "error";
35
+ case "muted": return "borderMuted";
36
+ case "default":
37
+ default: return "border";
38
+ }
39
+ }
40
+
41
+ export function badge(text: string, tone: TuiTone = "default"): string {
42
+ return theme.fg(toneColor(tone), text);
43
+ }
44
+
45
+ export function keyValue(label: string, value: string, valueColor: ThemeColor = "text", labelWidth = 10): string {
46
+ return `${theme.fg("dim", label.padEnd(labelWidth))}${theme.fg(valueColor, value)}`;
47
+ }
48
+
49
+ export function roundedPanel(
50
+ lines: string[],
51
+ width: number,
52
+ opts: {
53
+ tone?: TuiTone;
54
+ title?: string;
55
+ rightTitle?: string;
56
+ paddingX?: number;
57
+ } = {},
58
+ ): string[] {
59
+ const outerWidth = Math.max(1, width);
60
+ const paddingX = Math.max(0, opts.paddingX ?? 0);
61
+ const borderColor = toneColor(opts.tone ?? "default");
62
+ const border = (text: string) => theme.fg(borderColor, text);
63
+ const title = opts.title ? theme.fg("borderAccent", opts.title) : "";
64
+ const rightTitle = opts.rightTitle ? theme.fg("dim", opts.rightTitle) : "";
65
+ const body = lines.length > 0 ? lines : [""];
66
+
67
+ if (outerWidth < 3) {
68
+ return body.map((line) => truncateToWidth(line, outerWidth, ""));
69
+ }
70
+
71
+ const innerWidth = Math.max(1, outerWidth - 2);
72
+ const contentWidth = Math.max(1, innerWidth - paddingX * 2);
73
+
74
+ const renderedBody = body.map((line) => {
75
+ const padded = " ".repeat(paddingX) + padRight(line, contentWidth) + " ".repeat(paddingX);
76
+ return `${border("│")}${padRight(padded, innerWidth)}${border("│")}`;
77
+ });
78
+
79
+ if (!title && !rightTitle) {
80
+ return [
81
+ border("╭" + "─".repeat(outerWidth - 2) + "╮"),
82
+ ...renderedBody,
83
+ border("╰" + "─".repeat(outerWidth - 2) + "╯"),
84
+ ];
85
+ }
86
+
87
+ const header = alignRight(title, rightTitle, innerWidth);
88
+ return [
89
+ border("╭" + "─".repeat(outerWidth - 2) + "╮"),
90
+ `${border("│")}${padRight(header, innerWidth)}${border("│")}`,
91
+ ...renderedBody,
92
+ border("╰" + "─".repeat(outerWidth - 2) + "╯"),
93
+ ];
94
+ }