gsd-pi 2.64.0-dev.9c14bd0 → 2.64.0-dev.b3ee078

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 (223) hide show
  1. package/dist/headless.js +3 -1
  2. package/dist/resources/extensions/bg-shell/bg-shell-lifecycle.js +22 -7
  3. package/dist/resources/extensions/bg-shell/process-manager.js +6 -1
  4. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +24 -13
  5. package/dist/resources/extensions/gsd/bootstrap/notify-interceptor.js +28 -0
  6. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +8 -0
  7. package/dist/resources/extensions/gsd/bootstrap/register-shortcuts.js +16 -0
  8. package/dist/resources/extensions/gsd/bootstrap/system-context.js +20 -0
  9. package/dist/resources/extensions/gsd/commands/catalog.js +7 -1
  10. package/dist/resources/extensions/gsd/commands/handlers/core.js +1 -0
  11. package/dist/resources/extensions/gsd/commands/handlers/notifications-handler.js +104 -0
  12. package/dist/resources/extensions/gsd/commands/handlers/ops.js +5 -0
  13. package/dist/resources/extensions/gsd/notification-overlay.js +223 -0
  14. package/dist/resources/extensions/gsd/notification-store.js +273 -0
  15. package/dist/resources/extensions/gsd/notification-widget.js +56 -0
  16. package/dist/resources/extensions/gsd/workflow-logger.js +8 -0
  17. package/dist/web/standalone/.next/BUILD_ID +1 -1
  18. package/dist/web/standalone/.next/app-path-routes-manifest.json +20 -19
  19. package/dist/web/standalone/.next/build-manifest.json +2 -2
  20. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  21. package/dist/web/standalone/.next/routes-manifest.json +6 -0
  22. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  23. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  31. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/api/notifications/route.js +3 -0
  39. package/dist/web/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -0
  40. package/dist/web/standalone/.next/server/app/api/notifications/route_client-reference-manifest.js +1 -0
  41. package/dist/web/standalone/.next/server/app/index.html +1 -1
  42. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app-paths-manifest.json +20 -19
  49. package/dist/web/standalone/.next/server/functions-config-manifest.json +1 -0
  50. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  51. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  52. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  53. package/dist/web/standalone/.next/static/chunks/app/_global-error/page-8805a20e15762c3c.js +1 -0
  54. package/dist/web/standalone/.next/static/chunks/app/api/boot/route-8805a20e15762c3c.js +1 -0
  55. package/dist/web/standalone/.next/static/chunks/app/api/bridge-terminal/input/route-8805a20e15762c3c.js +1 -0
  56. package/dist/web/standalone/.next/static/chunks/app/api/bridge-terminal/resize/route-8805a20e15762c3c.js +1 -0
  57. package/dist/web/standalone/.next/static/chunks/app/api/bridge-terminal/stream/route-8805a20e15762c3c.js +1 -0
  58. package/dist/web/standalone/.next/static/chunks/app/api/browse-directories/route-8805a20e15762c3c.js +1 -0
  59. package/dist/web/standalone/.next/static/chunks/app/api/captures/route-8805a20e15762c3c.js +1 -0
  60. package/dist/web/standalone/.next/static/chunks/app/api/cleanup/route-8805a20e15762c3c.js +1 -0
  61. package/dist/web/standalone/.next/static/chunks/app/api/dev-mode/route-8805a20e15762c3c.js +1 -0
  62. package/dist/web/standalone/.next/static/chunks/app/api/doctor/route-8805a20e15762c3c.js +1 -0
  63. package/dist/web/standalone/.next/static/chunks/app/api/experimental/route-8805a20e15762c3c.js +1 -0
  64. package/dist/web/standalone/.next/static/chunks/app/api/export-data/route-8805a20e15762c3c.js +1 -0
  65. package/dist/web/standalone/.next/static/chunks/app/api/files/route-8805a20e15762c3c.js +1 -0
  66. package/dist/web/standalone/.next/static/chunks/app/api/forensics/route-8805a20e15762c3c.js +1 -0
  67. package/dist/web/standalone/.next/static/chunks/app/api/git/route-8805a20e15762c3c.js +1 -0
  68. package/dist/web/standalone/.next/static/chunks/app/api/history/route-8805a20e15762c3c.js +1 -0
  69. package/dist/web/standalone/.next/static/chunks/app/api/hooks/route-8805a20e15762c3c.js +1 -0
  70. package/dist/web/standalone/.next/static/chunks/app/api/inspect/route-8805a20e15762c3c.js +1 -0
  71. package/dist/web/standalone/.next/static/chunks/app/api/knowledge/route-8805a20e15762c3c.js +1 -0
  72. package/dist/web/standalone/.next/static/chunks/app/api/live-state/route-8805a20e15762c3c.js +1 -0
  73. package/dist/web/standalone/.next/static/chunks/app/api/notifications/route-8805a20e15762c3c.js +1 -0
  74. package/dist/web/standalone/.next/static/chunks/app/api/onboarding/route-8805a20e15762c3c.js +1 -0
  75. package/dist/web/standalone/.next/static/chunks/app/api/preferences/route-8805a20e15762c3c.js +1 -0
  76. package/dist/web/standalone/.next/static/chunks/app/api/projects/route-8805a20e15762c3c.js +1 -0
  77. package/dist/web/standalone/.next/static/chunks/app/api/recovery/route-8805a20e15762c3c.js +1 -0
  78. package/dist/web/standalone/.next/static/chunks/app/api/remote-questions/route-8805a20e15762c3c.js +1 -0
  79. package/dist/web/standalone/.next/static/chunks/app/api/session/browser/route-8805a20e15762c3c.js +1 -0
  80. package/dist/web/standalone/.next/static/chunks/app/api/session/command/route-8805a20e15762c3c.js +1 -0
  81. package/dist/web/standalone/.next/static/chunks/app/api/session/events/route-8805a20e15762c3c.js +1 -0
  82. package/dist/web/standalone/.next/static/chunks/app/api/session/manage/route-8805a20e15762c3c.js +1 -0
  83. package/dist/web/standalone/.next/static/chunks/app/api/settings-data/route-8805a20e15762c3c.js +1 -0
  84. package/dist/web/standalone/.next/static/chunks/app/api/shutdown/route-8805a20e15762c3c.js +1 -0
  85. package/dist/web/standalone/.next/static/chunks/app/api/skill-health/route-8805a20e15762c3c.js +1 -0
  86. package/dist/web/standalone/.next/static/chunks/app/api/steer/route-8805a20e15762c3c.js +1 -0
  87. package/dist/web/standalone/.next/static/chunks/app/api/switch-root/route-8805a20e15762c3c.js +1 -0
  88. package/dist/web/standalone/.next/static/chunks/app/api/terminal/input/route-8805a20e15762c3c.js +1 -0
  89. package/dist/web/standalone/.next/static/chunks/app/api/terminal/resize/route-8805a20e15762c3c.js +1 -0
  90. package/dist/web/standalone/.next/static/chunks/app/api/terminal/sessions/route-8805a20e15762c3c.js +1 -0
  91. package/dist/web/standalone/.next/static/chunks/app/api/terminal/stream/route-8805a20e15762c3c.js +1 -0
  92. package/dist/web/standalone/.next/static/chunks/app/api/terminal/upload/route-8805a20e15762c3c.js +1 -0
  93. package/dist/web/standalone/.next/static/chunks/app/api/undo/route-8805a20e15762c3c.js +1 -0
  94. package/dist/web/standalone/.next/static/chunks/app/api/update/route-8805a20e15762c3c.js +1 -0
  95. package/dist/web/standalone/.next/static/chunks/app/api/visualizer/route-8805a20e15762c3c.js +1 -0
  96. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/app-error-8805a20e15762c3c.js +1 -0
  97. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/forbidden-8805a20e15762c3c.js +1 -0
  98. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/not-found-8805a20e15762c3c.js +1 -0
  99. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/unauthorized-8805a20e15762c3c.js +1 -0
  100. package/dist/web/standalone/.next/static/l7tiSF0KtXOwxxYn0ZAyF/_buildManifest.js +1 -0
  101. package/package.json +1 -1
  102. package/packages/pi-agent-core/dist/agent-loop.js +26 -9
  103. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  104. package/packages/pi-agent-core/src/agent-loop.test.ts +100 -4
  105. package/packages/pi-agent-core/src/agent-loop.ts +43 -12
  106. package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.d.ts +2 -0
  107. package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.d.ts.map +1 -0
  108. package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js +38 -0
  109. package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js.map +1 -0
  110. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  111. package/packages/pi-coding-agent/dist/core/agent-session.js +11 -0
  112. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  113. package/packages/pi-coding-agent/dist/core/resource-loader-cache-reset.test.d.ts +2 -0
  114. package/packages/pi-coding-agent/dist/core/resource-loader-cache-reset.test.d.ts.map +1 -0
  115. package/packages/pi-coding-agent/dist/core/resource-loader-cache-reset.test.js +24 -0
  116. package/packages/pi-coding-agent/dist/core/resource-loader-cache-reset.test.js.map +1 -0
  117. package/packages/pi-coding-agent/dist/core/resource-loader.d.ts.map +1 -1
  118. package/packages/pi-coding-agent/dist/core/resource-loader.js +4 -1
  119. package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
  120. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -0
  121. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  122. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +8 -0
  123. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  124. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +6 -0
  125. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  126. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +36 -0
  127. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  128. package/packages/pi-coding-agent/src/core/agent-session-tool-refresh.test.ts +64 -0
  129. package/packages/pi-coding-agent/src/core/agent-session.ts +10 -0
  130. package/packages/pi-coding-agent/src/core/resource-loader-cache-reset.test.ts +42 -0
  131. package/packages/pi-coding-agent/src/core/resource-loader.ts +5 -1
  132. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +9 -0
  133. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +33 -0
  134. package/packages/pi-tui/dist/__tests__/overlay-layout.test.d.ts +2 -0
  135. package/packages/pi-tui/dist/__tests__/overlay-layout.test.d.ts.map +1 -0
  136. package/packages/pi-tui/dist/__tests__/overlay-layout.test.js +66 -0
  137. package/packages/pi-tui/dist/__tests__/overlay-layout.test.js.map +1 -0
  138. package/packages/pi-tui/dist/components/loader.d.ts +4 -2
  139. package/packages/pi-tui/dist/components/loader.d.ts.map +1 -1
  140. package/packages/pi-tui/dist/components/loader.js +27 -9
  141. package/packages/pi-tui/dist/components/loader.js.map +1 -1
  142. package/packages/pi-tui/dist/components/text.d.ts.map +1 -1
  143. package/packages/pi-tui/dist/components/text.js +2 -0
  144. package/packages/pi-tui/dist/components/text.js.map +1 -1
  145. package/packages/pi-tui/dist/overlay-layout.d.ts.map +1 -1
  146. package/packages/pi-tui/dist/overlay-layout.js +12 -1
  147. package/packages/pi-tui/dist/overlay-layout.js.map +1 -1
  148. package/packages/pi-tui/dist/tui.d.ts +4 -0
  149. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  150. package/packages/pi-tui/dist/tui.js +35 -0
  151. package/packages/pi-tui/dist/tui.js.map +1 -1
  152. package/packages/pi-tui/src/__tests__/overlay-layout.test.ts +82 -0
  153. package/packages/pi-tui/src/components/loader.ts +27 -10
  154. package/packages/pi-tui/src/components/text.ts +1 -0
  155. package/packages/pi-tui/src/overlay-layout.ts +14 -1
  156. package/packages/pi-tui/src/tui.ts +34 -0
  157. package/src/resources/extensions/bg-shell/bg-shell-lifecycle.ts +19 -7
  158. package/src/resources/extensions/bg-shell/process-manager.ts +8 -2
  159. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +25 -13
  160. package/src/resources/extensions/gsd/bootstrap/notify-interceptor.ts +34 -0
  161. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +8 -0
  162. package/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts +20 -0
  163. package/src/resources/extensions/gsd/bootstrap/system-context.ts +28 -0
  164. package/src/resources/extensions/gsd/commands/catalog.ts +7 -1
  165. package/src/resources/extensions/gsd/commands/handlers/core.ts +1 -0
  166. package/src/resources/extensions/gsd/commands/handlers/notifications-handler.ts +140 -0
  167. package/src/resources/extensions/gsd/commands/handlers/ops.ts +5 -0
  168. package/src/resources/extensions/gsd/notification-overlay.ts +266 -0
  169. package/src/resources/extensions/gsd/notification-store.ts +293 -0
  170. package/src/resources/extensions/gsd/notification-widget.ts +68 -0
  171. package/src/resources/extensions/gsd/tests/complete-slice-string-coercion.test.ts +36 -0
  172. package/src/resources/extensions/gsd/tests/discuss-tool-scope-leak.test.ts +76 -0
  173. package/src/resources/extensions/gsd/tests/notification-store.test.ts +282 -0
  174. package/src/resources/extensions/gsd/tests/unstructured-continue-context-injection.test.ts +163 -0
  175. package/src/resources/extensions/gsd/workflow-logger.ts +13 -0
  176. package/dist/web/standalone/.next/static/SoxM61WC_ia7R2gk4VMpJ/_buildManifest.js +0 -1
  177. package/dist/web/standalone/.next/static/chunks/app/_global-error/page-c4cc189e7b117ea2.js +0 -1
  178. package/dist/web/standalone/.next/static/chunks/app/api/boot/route-c4cc189e7b117ea2.js +0 -1
  179. package/dist/web/standalone/.next/static/chunks/app/api/bridge-terminal/input/route-c4cc189e7b117ea2.js +0 -1
  180. package/dist/web/standalone/.next/static/chunks/app/api/bridge-terminal/resize/route-c4cc189e7b117ea2.js +0 -1
  181. package/dist/web/standalone/.next/static/chunks/app/api/bridge-terminal/stream/route-c4cc189e7b117ea2.js +0 -1
  182. package/dist/web/standalone/.next/static/chunks/app/api/browse-directories/route-c4cc189e7b117ea2.js +0 -1
  183. package/dist/web/standalone/.next/static/chunks/app/api/captures/route-c4cc189e7b117ea2.js +0 -1
  184. package/dist/web/standalone/.next/static/chunks/app/api/cleanup/route-c4cc189e7b117ea2.js +0 -1
  185. package/dist/web/standalone/.next/static/chunks/app/api/dev-mode/route-c4cc189e7b117ea2.js +0 -1
  186. package/dist/web/standalone/.next/static/chunks/app/api/doctor/route-c4cc189e7b117ea2.js +0 -1
  187. package/dist/web/standalone/.next/static/chunks/app/api/experimental/route-c4cc189e7b117ea2.js +0 -1
  188. package/dist/web/standalone/.next/static/chunks/app/api/export-data/route-c4cc189e7b117ea2.js +0 -1
  189. package/dist/web/standalone/.next/static/chunks/app/api/files/route-c4cc189e7b117ea2.js +0 -1
  190. package/dist/web/standalone/.next/static/chunks/app/api/forensics/route-c4cc189e7b117ea2.js +0 -1
  191. package/dist/web/standalone/.next/static/chunks/app/api/git/route-c4cc189e7b117ea2.js +0 -1
  192. package/dist/web/standalone/.next/static/chunks/app/api/history/route-c4cc189e7b117ea2.js +0 -1
  193. package/dist/web/standalone/.next/static/chunks/app/api/hooks/route-c4cc189e7b117ea2.js +0 -1
  194. package/dist/web/standalone/.next/static/chunks/app/api/inspect/route-c4cc189e7b117ea2.js +0 -1
  195. package/dist/web/standalone/.next/static/chunks/app/api/knowledge/route-c4cc189e7b117ea2.js +0 -1
  196. package/dist/web/standalone/.next/static/chunks/app/api/live-state/route-c4cc189e7b117ea2.js +0 -1
  197. package/dist/web/standalone/.next/static/chunks/app/api/onboarding/route-c4cc189e7b117ea2.js +0 -1
  198. package/dist/web/standalone/.next/static/chunks/app/api/preferences/route-c4cc189e7b117ea2.js +0 -1
  199. package/dist/web/standalone/.next/static/chunks/app/api/projects/route-c4cc189e7b117ea2.js +0 -1
  200. package/dist/web/standalone/.next/static/chunks/app/api/recovery/route-c4cc189e7b117ea2.js +0 -1
  201. package/dist/web/standalone/.next/static/chunks/app/api/remote-questions/route-c4cc189e7b117ea2.js +0 -1
  202. package/dist/web/standalone/.next/static/chunks/app/api/session/browser/route-c4cc189e7b117ea2.js +0 -1
  203. package/dist/web/standalone/.next/static/chunks/app/api/session/command/route-c4cc189e7b117ea2.js +0 -1
  204. package/dist/web/standalone/.next/static/chunks/app/api/session/events/route-c4cc189e7b117ea2.js +0 -1
  205. package/dist/web/standalone/.next/static/chunks/app/api/session/manage/route-c4cc189e7b117ea2.js +0 -1
  206. package/dist/web/standalone/.next/static/chunks/app/api/settings-data/route-c4cc189e7b117ea2.js +0 -1
  207. package/dist/web/standalone/.next/static/chunks/app/api/shutdown/route-c4cc189e7b117ea2.js +0 -1
  208. package/dist/web/standalone/.next/static/chunks/app/api/skill-health/route-c4cc189e7b117ea2.js +0 -1
  209. package/dist/web/standalone/.next/static/chunks/app/api/steer/route-c4cc189e7b117ea2.js +0 -1
  210. package/dist/web/standalone/.next/static/chunks/app/api/switch-root/route-c4cc189e7b117ea2.js +0 -1
  211. package/dist/web/standalone/.next/static/chunks/app/api/terminal/input/route-c4cc189e7b117ea2.js +0 -1
  212. package/dist/web/standalone/.next/static/chunks/app/api/terminal/resize/route-c4cc189e7b117ea2.js +0 -1
  213. package/dist/web/standalone/.next/static/chunks/app/api/terminal/sessions/route-c4cc189e7b117ea2.js +0 -1
  214. package/dist/web/standalone/.next/static/chunks/app/api/terminal/stream/route-c4cc189e7b117ea2.js +0 -1
  215. package/dist/web/standalone/.next/static/chunks/app/api/terminal/upload/route-c4cc189e7b117ea2.js +0 -1
  216. package/dist/web/standalone/.next/static/chunks/app/api/undo/route-c4cc189e7b117ea2.js +0 -1
  217. package/dist/web/standalone/.next/static/chunks/app/api/update/route-c4cc189e7b117ea2.js +0 -1
  218. package/dist/web/standalone/.next/static/chunks/app/api/visualizer/route-c4cc189e7b117ea2.js +0 -1
  219. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/app-error-c4cc189e7b117ea2.js +0 -1
  220. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/forbidden-c4cc189e7b117ea2.js +0 -1
  221. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/not-found-c4cc189e7b117ea2.js +0 -1
  222. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/unauthorized-c4cc189e7b117ea2.js +0 -1
  223. /package/dist/web/standalone/.next/static/{SoxM61WC_ia7R2gk4VMpJ → l7tiSF0KtXOwxxYn0ZAyF}/_ssgManifest.js +0 -0
@@ -2,13 +2,16 @@ import type { TUI } from "../tui.js";
2
2
  import { Text } from "./text.js";
3
3
 
4
4
  /**
5
- * Loader component that updates every 80ms with spinning animation
5
+ * Loader component that updates every 80ms with spinning animation.
6
+ * Frame rotation is isolated from message text to avoid invalidating
7
+ * Text's render cache (wrapTextWithAnsi, visibleWidth) on every tick.
6
8
  */
7
9
  export class Loader extends Text {
8
10
  private frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
9
11
  private currentFrame = 0;
10
12
  private intervalId: NodeJS.Timeout | null = null;
11
13
  private ui: TUI | null = null;
14
+ private _lastMessage: string = "";
12
15
 
13
16
  constructor(
14
17
  ui: TUI,
@@ -22,18 +25,38 @@ export class Loader extends Text {
22
25
  }
23
26
 
24
27
  render(width: number): string[] {
25
- return ["", ...super.render(width)];
28
+ // Only update Text content when message actually changes —
29
+ // frame rotation is prepended below without touching the cache
30
+ if (this.message !== this._lastMessage) {
31
+ this.setText(this.messageColorFn(this.message));
32
+ this._lastMessage = this.message;
33
+ }
34
+ const messageLines = super.render(width);
35
+ // Shallow copy so we don't mutate cachedLines from Text
36
+ const result = ["", ...messageLines];
37
+ // Prepend spinner frame to first content line
38
+ if (result.length > 1) {
39
+ const frame = this.frames[this.currentFrame];
40
+ result[1] = this.spinnerColorFn(frame) + " " + result[1];
41
+ }
42
+ return result;
26
43
  }
27
44
 
28
45
  start() {
29
46
  if (this.intervalId) {
30
47
  clearInterval(this.intervalId);
31
48
  }
32
- this.updateDisplay();
49
+ this.currentFrame = 0;
33
50
  this.intervalId = setInterval(() => {
34
51
  this.currentFrame = (this.currentFrame + 1) % this.frames.length;
35
- this.updateDisplay();
52
+ if (this.ui) {
53
+ this.ui.requestRender();
54
+ }
36
55
  }, 80);
56
+ // Trigger initial render
57
+ if (this.ui) {
58
+ this.ui.requestRender();
59
+ }
37
60
  }
38
61
 
39
62
  stop() {
@@ -50,12 +73,6 @@ export class Loader extends Text {
50
73
 
51
74
  setMessage(message: string) {
52
75
  this.message = message;
53
- this.updateDisplay();
54
- }
55
-
56
- private updateDisplay() {
57
- const frame = this.frames[this.currentFrame];
58
- this.setText(`${this.spinnerColorFn(frame)} ${this.messageColorFn(this.message)}`);
59
76
  if (this.ui) {
60
77
  this.ui.requestRender();
61
78
  }
@@ -23,6 +23,7 @@ export class Text implements Component {
23
23
  }
24
24
 
25
25
  setText(text: string): void {
26
+ if (this.text === text) return;
26
27
  this.text = text;
27
28
  this.cachedText = undefined;
28
29
  this.cachedWidth = undefined;
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { OverlayAnchor, OverlayOptions, SizeValue } from "./tui.js";
9
- import { extractSegments, sliceByColumn, sliceWithWidth, truncateToWidth, visibleWidth } from "./utils.js";
9
+ import { applyBackgroundToLine, extractSegments, sliceByColumn, sliceWithWidth, truncateToWidth, visibleWidth } from "./utils.js";
10
10
  import { isImageLine } from "./terminal-image.js";
11
11
  import { CURSOR_MARKER } from "./tui.js";
12
12
 
@@ -324,6 +324,19 @@ export function compositeOverlays(
324
324
 
325
325
  const viewportStart = Math.max(0, workingHeight - termHeight);
326
326
 
327
+ // Apply backdrop dimming if any visible overlay requests it.
328
+ // Uses dim + dark gray background (256-color 233) so the overlay pops visually.
329
+ const hasBackdrop = visibleEntries.some((e) => e.options?.backdrop);
330
+ if (hasBackdrop) {
331
+ const dimFn = (text: string) =>
332
+ `\x1b[2m\x1b[38;5;242m\x1b[48;5;233m${text}\x1b[49m\x1b[39m\x1b[22m`;
333
+ for (let i = viewportStart; i < result.length; i++) {
334
+ if (!isImageLine(result[i]) && result[i].length > 0) {
335
+ result[i] = applyBackgroundToLine(result[i], termWidth, dimFn);
336
+ }
337
+ }
338
+ }
339
+
327
340
  // Composite each overlay
328
341
  for (const { overlayLines, row, col, w } of rendered) {
329
342
  for (let i = 0; i < overlayLines.length; i++) {
@@ -141,6 +141,8 @@ export interface OverlayOptions {
141
141
  visible?: (termWidth: number, termHeight: number) => boolean;
142
142
  /** If true, don't capture keyboard focus when shown */
143
143
  nonCapturing?: boolean;
144
+ /** If true, dim the background behind the overlay */
145
+ backdrop?: boolean;
144
146
  }
145
147
 
146
148
  /**
@@ -166,20 +168,33 @@ export interface OverlayHandle {
166
168
  */
167
169
  export class Container implements Component {
168
170
  children: Component[] = [];
171
+ private _prevRender: string[] | null = null;
169
172
 
170
173
  addChild(component: Component): void {
171
174
  this.children.push(component);
175
+ this._prevRender = null;
172
176
  }
173
177
 
174
178
  removeChild(component: Component): void {
175
179
  const index = this.children.indexOf(component);
176
180
  if (index !== -1) {
181
+ const child = this.children[index];
177
182
  this.children.splice(index, 1);
183
+ if ('dispose' in child && typeof (child as any).dispose === 'function') {
184
+ (child as any).dispose();
185
+ }
186
+ this._prevRender = null;
178
187
  }
179
188
  }
180
189
 
181
190
  clear(): void {
191
+ for (const child of this.children) {
192
+ if ('dispose' in child && typeof (child as any).dispose === 'function') {
193
+ (child as any).dispose();
194
+ }
195
+ }
182
196
  this.children = [];
197
+ this._prevRender = null;
183
198
  }
184
199
 
185
200
  invalidate(): void {
@@ -194,6 +209,17 @@ export class Container implements Component {
194
209
  const rendered = child.render(width);
195
210
  for (let i = 0; i < rendered.length; i++) lines.push(rendered[i]);
196
211
  }
212
+ // Return stable reference if output unchanged — allows doRender()
213
+ // to skip ALL post-processing (isImageLine, applyLineResets, diffs)
214
+ const prev = this._prevRender;
215
+ if (prev && prev.length === lines.length) {
216
+ let same = true;
217
+ for (let i = 0; i < lines.length; i++) {
218
+ if (lines[i] !== prev[i]) { same = false; break; }
219
+ }
220
+ if (same) return prev;
221
+ }
222
+ this._prevRender = lines;
197
223
  return lines;
198
224
  }
199
225
  }
@@ -222,6 +248,7 @@ export class TUI extends Container {
222
248
  private previousViewportTop = 0; // Track previous viewport top for resize-aware cursor moves
223
249
  private fullRedrawCount = 0;
224
250
  private stopped = false;
251
+ private _lastRenderedComponents: string[] | null = null;
225
252
 
226
253
  // Overlay stack for modal components rendered on top of base content
227
254
  private focusOrderCounter = 0;
@@ -599,6 +626,13 @@ export class TUI extends Container {
599
626
  // Render all components to get new lines
600
627
  let newLines = this.render(width);
601
628
 
629
+ // Skip ALL post-processing if component output is unchanged.
630
+ // Container.render() returns the same array reference when stable.
631
+ if (newLines === this._lastRenderedComponents && this.overlayStack.length === 0) {
632
+ return;
633
+ }
634
+ this._lastRenderedComponents = newLines;
635
+
602
636
  // Composite overlays into the rendered lines (before differential compare)
603
637
  if (this.overlayStack.length > 0) {
604
638
  newLines = compositeOverlays(newLines, this.overlayStack, width, height, this.maxLinesRendered);
@@ -16,6 +16,7 @@ import {
16
16
  import {
17
17
  processes,
18
18
  pendingAlerts,
19
+ pushAlert,
19
20
  cleanupAll,
20
21
  cleanupSessionProcesses,
21
22
  persistManifest,
@@ -37,19 +38,30 @@ export function registerBgShellLifecycle(pi: ExtensionAPI, state: BgShellSharedS
37
38
  }
38
39
  }
39
40
 
40
- // Clean up on session shutdown
41
- pi.on("session_shutdown", async () => {
42
- cleanupAll();
43
- });
44
-
45
41
  // Register signal handlers to clean up bg processes on unexpected exit (fixes #428)
46
42
  const signalCleanup = () => {
47
43
  cleanupAll();
44
+ // Also kill bash-tool spawned children that bg-shell doesn't track
45
+ try {
46
+ const { listDescendants } = require("@gsd/native") as typeof import("@gsd/native");
47
+ const descendants = listDescendants(process.pid);
48
+ for (const childPid of descendants) {
49
+ try { process.kill(childPid, "SIGKILL"); } catch {}
50
+ }
51
+ } catch {}
48
52
  };
49
53
  process.on("SIGTERM", signalCleanup);
50
54
  process.on("SIGINT", signalCleanup);
51
55
  process.on("beforeExit", signalCleanup);
52
56
 
57
+ // Clean up on session shutdown — remove signal handlers to prevent accumulation
58
+ pi.on("session_shutdown", async () => {
59
+ process.off("SIGTERM", signalCleanup);
60
+ process.off("SIGINT", signalCleanup);
61
+ process.off("beforeExit", signalCleanup);
62
+ cleanupAll();
63
+ });
64
+
53
65
  // ── Compaction Awareness: Survive Context Resets ───────────────
54
66
 
55
67
  /** Build a compact state summary of all alive processes for context re-injection */
@@ -65,7 +77,7 @@ export function registerBgShellLifecycle(pi: ExtensionAPI, state: BgShellSharedS
65
77
  return ` - id:${p.id} "${p.label}" [${p.processType}] status:${p.status} uptime:${formatUptime(Date.now() - p.startedAt)}${portInfo}${urlInfo}${errInfo}${groupInfo}`;
66
78
  }).join("\n");
67
79
 
68
- pendingAlerts.push(
80
+ pushAlert(null,
69
81
  `${reason} ${alive.length} background process(es) are still running:\n${processSummaries}\nUse bg_shell digest/output/kill with these IDs.`
70
82
  );
71
83
  }
@@ -150,7 +162,7 @@ export function registerBgShellLifecycle(pi: ExtensionAPI, state: BgShellSharedS
150
162
  ` - ${s.id}: ${s.label} (pid ${s.pid}, type: ${s.processType}${s.group ? `, group: ${s.group}` : ""})`
151
163
  ).join("\n");
152
164
 
153
- pendingAlerts.push(
165
+ pushAlert(null,
154
166
  `${surviving.length} background process(es) from previous session still running:\n${summary}\n Note: These processes are outside bg_shell's control. Kill them manually if needed.`
155
167
  );
156
168
  }
@@ -33,6 +33,8 @@ export const processes = new Map<string, BgProcess>();
33
33
  /** Pending alerts to inject into the next agent context */
34
34
  export let pendingAlerts: string[] = [];
35
35
 
36
+ const MAX_PENDING_ALERTS = 50;
37
+
36
38
  /** Replace the pendingAlerts array (used by the extension entry point) */
37
39
  export function setPendingAlerts(alerts: string[]): void {
38
40
  pendingAlerts = alerts;
@@ -58,8 +60,12 @@ export function addEvent(bg: BgProcess, event: Omit<ProcessEvent, "timestamp">):
58
60
  }
59
61
  }
60
62
 
61
- export function pushAlert(bg: BgProcess, message: string): void {
62
- pendingAlerts.push(`[bg:${bg.id} ${bg.label}] ${message}`);
63
+ export function pushAlert(bg: BgProcess | null, message: string): void {
64
+ const prefix = bg ? `[bg:${bg.id} ${bg.label}] ` : "";
65
+ pendingAlerts.push(`${prefix}${message}`);
66
+ if (pendingAlerts.length > MAX_PENDING_ALERTS) {
67
+ pendingAlerts.splice(0, pendingAlerts.length - MAX_PENDING_ALERTS);
68
+ }
63
69
  }
64
70
 
65
71
  export function getInfo(p: BgProcess): BgProcessInfo {
@@ -804,27 +804,39 @@ export function registerDbTools(pi: ExtensionAPI): void {
804
804
  return m ? [m[1].trim(), m[2].trim()] : [s.trim(), ""];
805
805
  };
806
806
  const coerced = { ...params };
807
- coerced.filesModified = (params.filesModified ?? []).map((f: any) => {
807
+ // Coerce simple string-array fields: LLMs sometimes pass a plain string
808
+ // instead of a single-element array (#3585).
809
+ const wrapArray = (v: any): any[] =>
810
+ v == null ? [] : Array.isArray(v) ? v : [v];
811
+ coerced.provides = wrapArray(params.provides);
812
+ coerced.keyFiles = wrapArray(params.keyFiles);
813
+ coerced.keyDecisions = wrapArray(params.keyDecisions);
814
+ coerced.patternsEstablished = wrapArray(params.patternsEstablished);
815
+ coerced.observabilitySurfaces = wrapArray(params.observabilitySurfaces);
816
+ coerced.requirementsSurfaced = wrapArray(params.requirementsSurfaced);
817
+ coerced.drillDownPaths = wrapArray(params.drillDownPaths);
818
+ coerced.affects = wrapArray(params.affects);
819
+ coerced.filesModified = wrapArray(params.filesModified).map((f: any) => {
808
820
  if (typeof f !== "string") return f;
809
821
  const [path, description] = splitPair(f);
810
822
  return { path, description };
811
823
  });
812
- coerced.requires = (params.requires ?? []).map((r: any) => {
824
+ coerced.requires = wrapArray(params.requires).map((r: any) => {
813
825
  if (typeof r !== "string") return r;
814
826
  const [slice, provides] = splitPair(r);
815
827
  return { slice, provides };
816
828
  });
817
- coerced.requirementsAdvanced = (params.requirementsAdvanced ?? []).map((r: any) => {
829
+ coerced.requirementsAdvanced = wrapArray(params.requirementsAdvanced).map((r: any) => {
818
830
  if (typeof r !== "string") return r;
819
831
  const [id, how] = splitPair(r);
820
832
  return { id, how };
821
833
  });
822
- coerced.requirementsValidated = (params.requirementsValidated ?? []).map((r: any) => {
834
+ coerced.requirementsValidated = wrapArray(params.requirementsValidated).map((r: any) => {
823
835
  if (typeof r !== "string") return r;
824
836
  const [id, proof] = splitPair(r);
825
837
  return { id, proof };
826
838
  });
827
- coerced.requirementsInvalidated = (params.requirementsInvalidated ?? []).map((r: any) => {
839
+ coerced.requirementsInvalidated = wrapArray(params.requirementsInvalidated).map((r: any) => {
828
840
  if (typeof r !== "string") return r;
829
841
  const [id, what] = splitPair(r);
830
842
  return { id, what };
@@ -884,14 +896,14 @@ export function registerDbTools(pi: ExtensionAPI): void {
884
896
  deviations: Type.Optional(Type.String({ description: "Deviations from the slice plan, or 'None.'" })),
885
897
  knownLimitations: Type.Optional(Type.String({ description: "Known limitations or gaps, or 'None.'" })),
886
898
  followUps: Type.Optional(Type.String({ description: "Follow-up work discovered during execution, or 'None.'" })),
887
- keyFiles: Type.Optional(Type.Array(Type.String(), { description: "Key files created or modified" })),
888
- keyDecisions: Type.Optional(Type.Array(Type.String(), { description: "Key decisions made during this slice" })),
889
- patternsEstablished: Type.Optional(Type.Array(Type.String(), { description: "Patterns established by this slice" })),
890
- observabilitySurfaces: Type.Optional(Type.Array(Type.String(), { description: "Observability surfaces added" })),
891
- provides: Type.Optional(Type.Array(Type.String(), { description: "What this slice provides to downstream slices" })),
892
- requirementsSurfaced: Type.Optional(Type.Array(Type.String(), { description: "New requirements surfaced" })),
893
- drillDownPaths: Type.Optional(Type.Array(Type.String(), { description: "Paths to task summaries for drill-down" })),
894
- affects: Type.Optional(Type.Array(Type.String(), { description: "Downstream slices affected" })),
899
+ keyFiles: Type.Optional(Type.Union([Type.Array(Type.String()), Type.String()], { description: "Key files created or modified" })),
900
+ keyDecisions: Type.Optional(Type.Union([Type.Array(Type.String()), Type.String()], { description: "Key decisions made during this slice" })),
901
+ patternsEstablished: Type.Optional(Type.Union([Type.Array(Type.String()), Type.String()], { description: "Patterns established by this slice" })),
902
+ observabilitySurfaces: Type.Optional(Type.Union([Type.Array(Type.String()), Type.String()], { description: "Observability surfaces added" })),
903
+ provides: Type.Optional(Type.Union([Type.Array(Type.String()), Type.String()], { description: "What this slice provides to downstream slices" })),
904
+ requirementsSurfaced: Type.Optional(Type.Union([Type.Array(Type.String()), Type.String()], { description: "New requirements surfaced" })),
905
+ drillDownPaths: Type.Optional(Type.Union([Type.Array(Type.String()), Type.String()], { description: "Paths to task summaries for drill-down" })),
906
+ affects: Type.Optional(Type.Union([Type.Array(Type.String()), Type.String()], { description: "Downstream slices affected" })),
895
907
  requirementsAdvanced: Type.Optional(Type.Array(
896
908
  Type.Union([
897
909
  Type.Object({
@@ -0,0 +1,34 @@
1
+ // GSD Extension — Notify Interceptor
2
+ // Wraps ctx.ui.notify() in-place to persist every notification through the
3
+ // notification store. Uses a WeakSet to prevent double-wrapping and handle
4
+ // UI context replacement on /reload gracefully.
5
+
6
+ import type { ExtensionContext } from "@gsd/pi-coding-agent";
7
+
8
+ import { appendNotification, type NotifySeverity } from "../notification-store.js";
9
+
10
+ // Track which ui context objects have been wrapped to prevent double-install.
11
+ // WeakSet allows GC to collect replaced uiContext instances after /reload.
12
+ const _wrappedContexts = new WeakSet<object>();
13
+
14
+ /**
15
+ * Install the notify interceptor on a context's UI object.
16
+ * Mutates ctx.ui.notify in place — the original is called after persistence.
17
+ * Safe to call multiple times; no-ops if already installed on the same ui object.
18
+ */
19
+ export function installNotifyInterceptor(ctx: ExtensionContext): void {
20
+ if (_wrappedContexts.has(ctx.ui)) return;
21
+
22
+ const originalNotify = ctx.ui.notify.bind(ctx.ui);
23
+
24
+ (ctx.ui as any).notify = (message: string, type?: "info" | "warning" | "error" | "success"): void => {
25
+ try {
26
+ appendNotification(message, (type ?? "info") as NotifySeverity, "notify");
27
+ } catch {
28
+ // Non-fatal — never let persistence break the UI
29
+ }
30
+ originalNotify(message, type);
31
+ };
32
+
33
+ _wrappedContexts.add(ctx.ui);
34
+ }
@@ -21,6 +21,9 @@ import { resetAskUserQuestionsCache } from "../../ask-user-questions.js";
21
21
  import { recordToolCall as safetyRecordToolCall, recordToolResult as safetyRecordToolResult } from "../safety/evidence-collector.js";
22
22
  import { classifyCommand } from "../safety/destructive-guard.js";
23
23
  import { logWarning as safetyLogWarning } from "../workflow-logger.js";
24
+ import { installNotifyInterceptor } from "./notify-interceptor.js";
25
+ import { initNotificationStore } from "../notification-store.js";
26
+ import { initNotificationWidget } from "../notification-widget.js";
24
27
 
25
28
  // Skip the welcome screen on the very first session_start — cli.ts already
26
29
  // printed it before the TUI launched. Only re-print on /clear (subsequent sessions).
@@ -33,6 +36,9 @@ async function syncServiceTierStatus(ctx: ExtensionContext): Promise<void> {
33
36
 
34
37
  export function registerHooks(pi: ExtensionAPI): void {
35
38
  pi.on("session_start", async (_event, ctx) => {
39
+ initNotificationStore(process.cwd());
40
+ installNotifyInterceptor(ctx);
41
+ initNotificationWidget(ctx);
36
42
  resetWriteGateState();
37
43
  resetToolCallLoopGuard();
38
44
  resetAskUserQuestionsCache();
@@ -70,6 +76,8 @@ export function registerHooks(pi: ExtensionAPI): void {
70
76
  });
71
77
 
72
78
  pi.on("session_switch", async (_event, ctx) => {
79
+ initNotificationStore(process.cwd());
80
+ installNotifyInterceptor(ctx);
73
81
  resetWriteGateState();
74
82
  resetToolCallLoopGuard();
75
83
  resetAskUserQuestionsCache();
@@ -5,6 +5,7 @@ import type { ExtensionAPI } from "@gsd/pi-coding-agent";
5
5
  import { Key } from "@gsd/pi-tui";
6
6
 
7
7
  import { GSDDashboardOverlay } from "../dashboard-overlay.js";
8
+ import { GSDNotificationOverlay } from "../notification-overlay.js";
8
9
  import { ParallelMonitorOverlay } from "../parallel-monitor-overlay.js";
9
10
  import { shortcutDesc } from "../../shared/mod.js";
10
11
 
@@ -31,6 +32,25 @@ export function registerShortcuts(pi: ExtensionAPI): void {
31
32
  },
32
33
  });
33
34
 
35
+ pi.registerShortcut(Key.ctrlAlt("n"), {
36
+ description: shortcutDesc("Open notification history", "/gsd notifications"),
37
+ handler: async (ctx) => {
38
+ await ctx.ui.custom<void>(
39
+ (tui, theme, _kb, done) => new GSDNotificationOverlay(tui, theme, () => done()),
40
+ {
41
+ overlay: true,
42
+ overlayOptions: {
43
+ width: "80%",
44
+ minWidth: 60,
45
+ maxHeight: "88%",
46
+ anchor: "center",
47
+ backdrop: true,
48
+ },
49
+ },
50
+ );
51
+ },
52
+ });
53
+
34
54
  pi.registerShortcut(Key.ctrlAlt("p"), {
35
55
  description: shortcutDesc("Open parallel worker monitor", "/gsd parallel watch"),
36
56
  handler: async (ctx) => {
@@ -264,6 +264,13 @@ function buildWorktreeContextBlock(): string {
264
264
  return "";
265
265
  }
266
266
 
267
+ /**
268
+ * Low-entropy resume intent patterns — short phrases a user types to
269
+ * continue work after a pause, rate limit, or context reset (#3615).
270
+ * Tested against the trimmed, lowercased prompt with trailing punctuation stripped.
271
+ */
272
+ const RESUME_INTENT_PATTERNS = /^(continue|resume|ok|go|go ahead|proceed|keep going|carry on|next|yes|yeah|yep|sure|do it|let's go|pick up where you left off)$/;
273
+
267
274
  async function buildGuidedExecuteContextInjection(prompt: string, basePath: string): Promise<string | null> {
268
275
  const executeMatch = prompt.match(/Execute the next task:\s+(T\d+)\s+\("([^"]+)"\)\s+in slice\s+(S\d+)\s+of milestone\s+(M\d+(?:-[a-z0-9]{6})?)/i);
269
276
  if (executeMatch) {
@@ -280,6 +287,27 @@ async function buildGuidedExecuteContextInjection(prompt: string, basePath: stri
280
287
  }
281
288
  }
282
289
 
290
+ // Fallback: low-entropy resume prompt (e.g., "continue", "ok", "go ahead")
291
+ // during an active executing task — inject task context so the agent
292
+ // doesn't rebuild from scratch (#3615).
293
+ // Intent-gated: only fire for short, resume-like prompts to avoid hijacking
294
+ // control/help/diagnostic prompts with unrelated execution context.
295
+ // Phase-gated: only fire during "executing" to avoid misrouting during
296
+ // replanning, gate evaluation, or other non-execution phases.
297
+ const trimmed = prompt.trim().toLowerCase().replace(/[.!?,]+$/g, "");
298
+ if (RESUME_INTENT_PATTERNS.test(trimmed)) {
299
+ const state = await deriveState(basePath);
300
+ if (state.phase === "executing" && state.activeTask && state.activeMilestone && state.activeSlice) {
301
+ return buildTaskExecutionContextInjection(
302
+ basePath,
303
+ state.activeMilestone.id,
304
+ state.activeSlice.id,
305
+ state.activeTask.id,
306
+ state.activeTask.title,
307
+ );
308
+ }
309
+ }
310
+
283
311
  return null;
284
312
  }
285
313
 
@@ -15,7 +15,7 @@ export interface GsdCommandDefinition {
15
15
  type CompletionMap = Record<string, readonly GsdCommandDefinition[]>;
16
16
 
17
17
  export const GSD_COMMAND_DESCRIPTION =
18
- "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|widget|visualize|queue|quick|discuss|capture|triage|dispatch|history|undo|undo-task|reset-slice|rate|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|logs|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|cmux|park|unpark|init|setup|inspect|extensions|update|fast|mcp|rethink|codebase";
18
+ "GSD — Get Shit Done: /gsd help|start|templates|next|auto|stop|pause|status|widget|visualize|queue|quick|discuss|capture|triage|dispatch|history|undo|undo-task|reset-slice|rate|skip|export|cleanup|mode|prefs|config|keys|hooks|run-hook|skill-health|doctor|logs|forensics|changelog|migrate|remote|steer|knowledge|new-milestone|parallel|cmux|park|unpark|init|setup|inspect|extensions|update|fast|mcp|rethink|codebase|notifications";
19
19
 
20
20
  export const TOP_LEVEL_SUBCOMMANDS: readonly GsdCommandDefinition[] = [
21
21
  { cmd: "help", desc: "Categorized command reference with descriptions" },
@@ -48,6 +48,7 @@ export const TOP_LEVEL_SUBCOMMANDS: readonly GsdCommandDefinition[] = [
48
48
  { cmd: "hooks", desc: "Show configured post-unit and pre-dispatch hooks" },
49
49
  { cmd: "run-hook", desc: "Manually trigger a specific hook" },
50
50
  { cmd: "skill-health", desc: "Skill lifecycle dashboard" },
51
+ { cmd: "notifications", desc: "View, filter, and clear persistent notification history" },
51
52
  { cmd: "doctor", desc: "Runtime health checks with auto-fix" },
52
53
  { cmd: "logs", desc: "Browse activity logs, debug logs, and metrics" },
53
54
  { cmd: "forensics", desc: "Examine execution logs" },
@@ -110,6 +111,11 @@ const NESTED_COMPLETIONS: CompletionMap = {
110
111
  { cmd: "keys", desc: "Manage API keys" },
111
112
  { cmd: "prefs", desc: "Configure global preferences" },
112
113
  ],
114
+ notifications: [
115
+ { cmd: "clear", desc: "Clear all notifications" },
116
+ { cmd: "tail", desc: "Show last N notifications (default: 20)" },
117
+ { cmd: "filter", desc: "Filter by severity (error|warning|info|success)" },
118
+ ],
113
119
  logs: [
114
120
  { cmd: "debug", desc: "List or view debug log files" },
115
121
  { cmd: "tail", desc: "Show last N activity log summaries" },
@@ -29,6 +29,7 @@ export function showHelp(ctx: ExtensionCommandContext): void {
29
29
  " /gsd queue Show queued/dispatched units and execution order",
30
30
  " /gsd history View execution history [--cost] [--phase] [--model] [N]",
31
31
  " /gsd changelog Show categorized release notes [version]",
32
+ " /gsd notifications View persistent notification history [clear|tail|filter] (Ctrl+Alt+N)",
32
33
  "",
33
34
  "COURSE CORRECTION",
34
35
  " /gsd steer <desc> Apply user override to active work",
@@ -0,0 +1,140 @@
1
+ // GSD Extension — /gsd notifications Command Handler
2
+ // View, filter, and clear the persistent notification history.
3
+
4
+ import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
5
+
6
+ import {
7
+ readNotifications,
8
+ clearNotifications,
9
+ getUnreadCount,
10
+ suppressPersistence,
11
+ unsuppressPersistence,
12
+ type NotifySeverity,
13
+ } from "../../notification-store.js";
14
+ import { GSDNotificationOverlay } from "../../notification-overlay.js";
15
+
16
+ function severityIcon(severity: NotifySeverity): string {
17
+ switch (severity) {
18
+ case "error": return "✗";
19
+ case "warning": return "⚠";
20
+ case "success": return "✓";
21
+ case "info":
22
+ default: return "●";
23
+ }
24
+ }
25
+
26
+ function formatTimestamp(ts: string): string {
27
+ try {
28
+ const d = new Date(ts);
29
+ return d.toLocaleString("en-US", { hour12: false, month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
30
+ } catch {
31
+ return ts.slice(0, 19);
32
+ }
33
+ }
34
+
35
+ export async function handleNotificationsCommand(
36
+ args: string,
37
+ ctx: ExtensionCommandContext,
38
+ pi: ExtensionAPI,
39
+ ): Promise<boolean> {
40
+ // /gsd notifications clear
41
+ if (args === "clear") {
42
+ clearNotifications();
43
+ // Suppress persistence so the confirmation toast doesn't re-populate the store
44
+ suppressPersistence();
45
+ try {
46
+ ctx.ui.notify("All notifications cleared.", "success");
47
+ } finally {
48
+ unsuppressPersistence();
49
+ }
50
+ return true;
51
+ }
52
+
53
+ // /gsd notifications tail [N]
54
+ if (args === "tail" || args.startsWith("tail ")) {
55
+ const countStr = args.replace(/^tail\s*/, "").trim();
56
+ const count = countStr ? parseInt(countStr, 10) : 20;
57
+ const n = isNaN(count) || count < 1 ? 20 : Math.min(count, 100);
58
+ const entries = readNotifications().slice(0, n);
59
+
60
+ if (entries.length === 0) {
61
+ ctx.ui.notify("No notifications.", "info");
62
+ return true;
63
+ }
64
+
65
+ const lines = entries.map((e) =>
66
+ `${severityIcon(e.severity)} [${formatTimestamp(e.ts)}] ${e.message}`,
67
+ );
68
+ ctx.ui.notify(`Last ${entries.length} notification(s):\n${lines.join("\n")}`, "info");
69
+ return true;
70
+ }
71
+
72
+ // /gsd notifications filter <severity>
73
+ if (args.startsWith("filter ")) {
74
+ const severity = args.replace(/^filter\s+/, "").trim().toLowerCase();
75
+ if (!["error", "warning", "info", "success"].includes(severity)) {
76
+ ctx.ui.notify("Usage: /gsd notifications filter <error|warning|info|success>", "warning");
77
+ return true;
78
+ }
79
+ const entries = readNotifications().filter((e) => e.severity === severity);
80
+
81
+ if (entries.length === 0) {
82
+ ctx.ui.notify(`No ${severity} notifications.`, "info");
83
+ return true;
84
+ }
85
+
86
+ const lines = entries.slice(0, 20).map((e) =>
87
+ `${severityIcon(e.severity)} [${formatTimestamp(e.ts)}] ${e.message}`,
88
+ );
89
+ const suffix = entries.length > 20 ? `\n... and ${entries.length - 20} more` : "";
90
+ ctx.ui.notify(`${severity} notifications (${entries.length}):\n${lines.join("\n")}${suffix}`, "info");
91
+ return true;
92
+ }
93
+
94
+ // /gsd notifications (no args) — open overlay in TUI, or print summary
95
+ if (args === "" || args === "status") {
96
+ // Try overlay first (TUI mode)
97
+ if (ctx.hasUI) {
98
+ try {
99
+ await ctx.ui.custom<void>(
100
+ (tui, theme, _kb, done) => new GSDNotificationOverlay(tui, theme, () => done()),
101
+ {
102
+ overlay: true,
103
+ overlayOptions: {
104
+ width: "80%",
105
+ minWidth: 60,
106
+ maxHeight: "88%",
107
+ anchor: "center",
108
+ backdrop: true,
109
+ },
110
+ },
111
+ );
112
+ return true;
113
+ } catch {
114
+ // Fall through to text output if overlay fails
115
+ }
116
+ }
117
+
118
+ // Text fallback (RPC/headless mode)
119
+ const unread = getUnreadCount();
120
+ const entries = readNotifications().slice(0, 10);
121
+ if (entries.length === 0) {
122
+ ctx.ui.notify("No notifications.", "info");
123
+ return true;
124
+ }
125
+
126
+ const lines = entries.map((e) =>
127
+ `${severityIcon(e.severity)} [${formatTimestamp(e.ts)}] ${e.message}`,
128
+ );
129
+ const header = unread > 0 ? `${unread} unread — ` : "";
130
+ ctx.ui.notify(`${header}Recent notifications:\n${lines.join("\n")}`, "info");
131
+ return true;
132
+ }
133
+
134
+ // Unknown subcommand
135
+ ctx.ui.notify(
136
+ "Usage: /gsd notifications [clear|tail [N]|filter <severity>]",
137
+ "warning",
138
+ );
139
+ return true;
140
+ }