@sirenodeck/sirenodeck 0.1.1-alpha.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 (485) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +69 -0
  3. package/bin/_wrapper-shared.js +42 -0
  4. package/bin/dev.js +95 -0
  5. package/bin/sirenodeck.js +42 -0
  6. package/config/default-config.yml +117 -0
  7. package/config/starter-config.yml +54 -0
  8. package/dist/main.mjs +64702 -0
  9. package/package.json +140 -0
  10. package/src/action/executor.ts +156 -0
  11. package/src/action/index.ts +7 -0
  12. package/src/addon/api-types.ts +15 -0
  13. package/src/addon/api.ts +461 -0
  14. package/src/addon/check-runner.ts +84 -0
  15. package/src/addon/index.ts +5 -0
  16. package/src/addon/loader.ts +441 -0
  17. package/src/addon/registry.ts +230 -0
  18. package/src/addon/spec.ts +69 -0
  19. package/src/api/addon.ts +33 -0
  20. package/src/api/asset-url.ts +24 -0
  21. package/src/api/index.ts +50 -0
  22. package/src/api/protocol-internal.ts +656 -0
  23. package/src/api/protocol.ts +2 -0
  24. package/src/api/react/index.ts +11 -0
  25. package/src/api/react/registry.ts +91 -0
  26. package/src/api/react/use-addon-channel.ts +26 -0
  27. package/src/api/react/use-button-state.tsx +23 -0
  28. package/src/api/react/use-deck.ts +24 -0
  29. package/src/assets/logo.png +0 -0
  30. package/src/assets/logo72x72.png +0 -0
  31. package/src/assets/logoFull.png +0 -0
  32. package/src/builtin-addons/brightness/README.md +51 -0
  33. package/src/builtin-addons/brightness/buttons/brightness/backend.ts +13 -0
  34. package/src/builtin-addons/brightness/buttons/brightness/config.ts +10 -0
  35. package/src/builtin-addons/brightness/buttons/brightness/frontend.tsx +50 -0
  36. package/src/builtin-addons/brightness/domain/macos.ts +46 -0
  37. package/src/builtin-addons/brightness/index.d.ts +5 -0
  38. package/src/builtin-addons/brightness/index.ts +20 -0
  39. package/src/builtin-addons/brightness/poller.ts +27 -0
  40. package/src/builtin-addons/brightness/providers/darwin.ts +107 -0
  41. package/src/builtin-addons/brightness/providers/index.ts +81 -0
  42. package/src/builtin-addons/brightness/providers/linux.ts +108 -0
  43. package/src/builtin-addons/brightness/providers/windows.ts +77 -0
  44. package/src/builtin-addons/brightness/sirenodeck.json +6 -0
  45. package/src/builtin-addons/coding-agents/assets/claude-code.svg +1 -0
  46. package/src/builtin-addons/coding-agents/assets/opencode-dark-square.svg +1 -0
  47. package/src/builtin-addons/coding-agents/buttons/agent/backend.ts +82 -0
  48. package/src/builtin-addons/coding-agents/buttons/agent/config.ts +13 -0
  49. package/src/builtin-addons/coding-agents/buttons/agent/frontend.tsx +130 -0
  50. package/src/builtin-addons/coding-agents/buttons/agent/pages.tsx +150 -0
  51. package/src/builtin-addons/coding-agents/buttons/summary/backend.ts +48 -0
  52. package/src/builtin-addons/coding-agents/buttons/summary/config.ts +11 -0
  53. package/src/builtin-addons/coding-agents/buttons/summary/frontend.tsx +254 -0
  54. package/src/builtin-addons/coding-agents/decks/agents.ts +69 -0
  55. package/src/builtin-addons/coding-agents/global/backend.ts +402 -0
  56. package/src/builtin-addons/coding-agents/global-entry.ts +7 -0
  57. package/src/builtin-addons/coding-agents/hook/script.ts +123 -0
  58. package/src/builtin-addons/coding-agents/index.ts +15 -0
  59. package/src/builtin-addons/coding-agents/manifest.ts +115 -0
  60. package/src/builtin-addons/coding-agents/providers/claude-code.ts +470 -0
  61. package/src/builtin-addons/coding-agents/providers/claude-instances.ts +184 -0
  62. package/src/builtin-addons/coding-agents/providers/opencode-instances.ts +108 -0
  63. package/src/builtin-addons/coding-agents/providers/opencode.ts +636 -0
  64. package/src/builtin-addons/coding-agents/providers/registry.ts +120 -0
  65. package/src/builtin-addons/coding-agents/providers/spawn.ts +112 -0
  66. package/src/builtin-addons/coding-agents/shared/claude-hook-events.ts +55 -0
  67. package/src/builtin-addons/coding-agents/shared/claude-status.ts +236 -0
  68. package/src/builtin-addons/coding-agents/shared/live-count.ts +39 -0
  69. package/src/builtin-addons/coding-agents/shared/notifier.ts +58 -0
  70. package/src/builtin-addons/coding-agents/shared/opencode-status.ts +82 -0
  71. package/src/builtin-addons/coding-agents/shared/snapshot.ts +66 -0
  72. package/src/builtin-addons/coding-agents/shared/state.ts +72 -0
  73. package/src/builtin-addons/coding-agents/sirenodeck.json +6 -0
  74. package/src/builtin-addons/coding-agents/types/types.ts +33 -0
  75. package/src/builtin-addons/core/README.md +89 -0
  76. package/src/builtin-addons/core/buttons/action/backend.ts +7 -0
  77. package/src/builtin-addons/core/buttons/action/config.ts +13 -0
  78. package/src/builtin-addons/core/buttons/action/frontend.tsx +16 -0
  79. package/src/builtin-addons/core/buttons/change-deck/backend.ts +18 -0
  80. package/src/builtin-addons/core/buttons/change-deck/config.ts +15 -0
  81. package/src/builtin-addons/core/buttons/change-deck/frontend.tsx +18 -0
  82. package/src/builtin-addons/core/buttons/media-sample/backend.ts +8 -0
  83. package/src/builtin-addons/core/buttons/media-sample/config.ts +7 -0
  84. package/src/builtin-addons/core/buttons/media-sample/frontend.tsx +2 -0
  85. package/src/builtin-addons/core/buttons/page-nav/backend.ts +28 -0
  86. package/src/builtin-addons/core/buttons/page-nav/config.ts +9 -0
  87. package/src/builtin-addons/core/buttons/page-nav/frontend.tsx +68 -0
  88. package/src/builtin-addons/core/buttons/toggle/backend.ts +119 -0
  89. package/src/builtin-addons/core/buttons/toggle/config.ts +57 -0
  90. package/src/builtin-addons/core/buttons/toggle/frontend.tsx +57 -0
  91. package/src/builtin-addons/core/buttons/toggle/global-service.ts +174 -0
  92. package/src/builtin-addons/core/index.ts +120 -0
  93. package/src/builtin-addons/core/sirenodeck.json +6 -0
  94. package/src/builtin-addons/date-time/README.md +73 -0
  95. package/src/builtin-addons/date-time/buttons/analog-clock/backend.ts +10 -0
  96. package/src/builtin-addons/date-time/buttons/analog-clock/config.ts +4 -0
  97. package/src/builtin-addons/date-time/buttons/analog-clock/frontend.tsx +88 -0
  98. package/src/builtin-addons/date-time/buttons/custom/backend.ts +10 -0
  99. package/src/builtin-addons/date-time/buttons/custom/config.ts +9 -0
  100. package/src/builtin-addons/date-time/buttons/custom/frontend.tsx +26 -0
  101. package/src/builtin-addons/date-time/buttons/date/backend.ts +10 -0
  102. package/src/builtin-addons/date-time/buttons/date/config.ts +10 -0
  103. package/src/builtin-addons/date-time/buttons/date/frontend.tsx +42 -0
  104. package/src/builtin-addons/date-time/buttons/time/backend.ts +10 -0
  105. package/src/builtin-addons/date-time/buttons/time/config.ts +9 -0
  106. package/src/builtin-addons/date-time/buttons/time/frontend.tsx +29 -0
  107. package/src/builtin-addons/date-time/index.d.ts +5 -0
  108. package/src/builtin-addons/date-time/index.ts +30 -0
  109. package/src/builtin-addons/date-time/poller.ts +11 -0
  110. package/src/builtin-addons/date-time/shared/format-date.ts +36 -0
  111. package/src/builtin-addons/date-time/shared/format.ts +105 -0
  112. package/src/builtin-addons/date-time/shared/use-now.ts +10 -0
  113. package/src/builtin-addons/date-time/sirenodeck.json +6 -0
  114. package/src/builtin-addons/emoji-selector/README.md +45 -0
  115. package/src/builtin-addons/emoji-selector/assets/launcher.svg +10 -0
  116. package/src/builtin-addons/emoji-selector/assets/smileys.svg +8 -0
  117. package/src/builtin-addons/emoji-selector/buttons/category/backend.ts +14 -0
  118. package/src/builtin-addons/emoji-selector/buttons/category/config.ts +8 -0
  119. package/src/builtin-addons/emoji-selector/buttons/category/frontend.tsx +19 -0
  120. package/src/builtin-addons/emoji-selector/buttons/emoji/backend.ts +7 -0
  121. package/src/builtin-addons/emoji-selector/buttons/emoji/config.ts +9 -0
  122. package/src/builtin-addons/emoji-selector/buttons/emoji/frontend.tsx +11 -0
  123. package/src/builtin-addons/emoji-selector/buttons/launcher/backend.ts +13 -0
  124. package/src/builtin-addons/emoji-selector/buttons/launcher/config.ts +6 -0
  125. package/src/builtin-addons/emoji-selector/buttons/launcher/frontend.tsx +31 -0
  126. package/src/builtin-addons/emoji-selector/data/categories.ts +1084 -0
  127. package/src/builtin-addons/emoji-selector/decks/index.ts +137 -0
  128. package/src/builtin-addons/emoji-selector/index.d.ts +5 -0
  129. package/src/builtin-addons/emoji-selector/index.ts +29 -0
  130. package/src/builtin-addons/emoji-selector/sirenodeck.json +6 -0
  131. package/src/builtin-addons/emoji-selector/support.ts +49 -0
  132. package/src/builtin-addons/index.ts +13 -0
  133. package/src/builtin-addons/internal-settings/README.md +26 -0
  134. package/src/builtin-addons/internal-settings/backend.ts +58 -0
  135. package/src/builtin-addons/internal-settings/buttons/app-info/backend.ts +7 -0
  136. package/src/builtin-addons/internal-settings/buttons/app-info/config.ts +13 -0
  137. package/src/builtin-addons/internal-settings/buttons/app-info/frontend.tsx +18 -0
  138. package/src/builtin-addons/internal-settings/buttons/brightness/common/frontend.tsx +50 -0
  139. package/src/builtin-addons/internal-settings/buttons/brightness-down/backend.ts +16 -0
  140. package/src/builtin-addons/internal-settings/buttons/brightness-down/config.ts +6 -0
  141. package/src/builtin-addons/internal-settings/buttons/brightness-down/frontend.tsx +8 -0
  142. package/src/builtin-addons/internal-settings/buttons/brightness-up/backend.ts +16 -0
  143. package/src/builtin-addons/internal-settings/buttons/brightness-up/config.ts +6 -0
  144. package/src/builtin-addons/internal-settings/buttons/brightness-up/frontend.tsx +8 -0
  145. package/src/builtin-addons/internal-settings/decks/settings.ts +25 -0
  146. package/src/builtin-addons/internal-settings/index.d.ts +4 -0
  147. package/src/builtin-addons/internal-settings/index.ts +53 -0
  148. package/src/builtin-addons/internal-settings/logo72x72.png +0 -0
  149. package/src/builtin-addons/internal-settings/sirenodeck.json +6 -0
  150. package/src/builtin-addons/media/README.md +45 -0
  151. package/src/builtin-addons/media/backend.ts +143 -0
  152. package/src/builtin-addons/media/buttons/media-player/backend.ts +19 -0
  153. package/src/builtin-addons/media/buttons/media-player/components/MediaStatus.tsx +25 -0
  154. package/src/builtin-addons/media/buttons/media-player/components/MediaSurface.tsx +52 -0
  155. package/src/builtin-addons/media/buttons/media-player/components/ProgressBar.tsx +21 -0
  156. package/src/builtin-addons/media/buttons/media-player/components/status-meta.ts +36 -0
  157. package/src/builtin-addons/media/buttons/media-player/config.ts +4 -0
  158. package/src/builtin-addons/media/buttons/media-player/frontend.tsx +29 -0
  159. package/src/builtin-addons/media/buttons/volume/common/frontend.tsx +52 -0
  160. package/src/builtin-addons/media/buttons/volume/mute/backend.ts +13 -0
  161. package/src/builtin-addons/media/buttons/volume/mute/config.ts +4 -0
  162. package/src/builtin-addons/media/buttons/volume/mute/frontend.tsx +26 -0
  163. package/src/builtin-addons/media/buttons/volume/volume-down/backend.ts +14 -0
  164. package/src/builtin-addons/media/buttons/volume/volume-down/config.ts +6 -0
  165. package/src/builtin-addons/media/buttons/volume/volume-down/frontend.tsx +10 -0
  166. package/src/builtin-addons/media/buttons/volume/volume-up/backend.ts +14 -0
  167. package/src/builtin-addons/media/buttons/volume/volume-up/config.ts +6 -0
  168. package/src/builtin-addons/media/buttons/volume/volume-up/frontend.tsx +8 -0
  169. package/src/builtin-addons/media/checks.ts +104 -0
  170. package/src/builtin-addons/media/executor.ts +36 -0
  171. package/src/builtin-addons/media/index.d.ts +5 -0
  172. package/src/builtin-addons/media/index.ts +53 -0
  173. package/src/builtin-addons/media/progress.ts +16 -0
  174. package/src/builtin-addons/media/providers/darwin.ts +391 -0
  175. package/src/builtin-addons/media/providers/index.ts +27 -0
  176. package/src/builtin-addons/media/providers/linux.ts +196 -0
  177. package/src/builtin-addons/media/providers/types.ts +48 -0
  178. package/src/builtin-addons/media/providers/windows.ts +121 -0
  179. package/src/builtin-addons/media/sirenodeck.json +6 -0
  180. package/src/builtin-addons/media/state.ts +19 -0
  181. package/src/builtin-addons/register-builtins.ts +65 -0
  182. package/src/builtin-addons/session/README.md +14 -0
  183. package/src/builtin-addons/session/buttons/session-info/backend.ts +8 -0
  184. package/src/builtin-addons/session/buttons/session-info/config.ts +4 -0
  185. package/src/builtin-addons/session/buttons/session-info/frontend.tsx +2 -0
  186. package/src/builtin-addons/session/buttons/time/backend.ts +7 -0
  187. package/src/builtin-addons/session/buttons/time/config.ts +6 -0
  188. package/src/builtin-addons/session/buttons/time/frontend.tsx +2 -0
  189. package/src/builtin-addons/session/index.d.ts +5 -0
  190. package/src/builtin-addons/session/index.ts +18 -0
  191. package/src/builtin-addons/session/sirenodeck.json +6 -0
  192. package/src/builtin-addons/system-status/README.md +169 -0
  193. package/src/builtin-addons/system-status/buttons/_shared.ts +131 -0
  194. package/src/builtin-addons/system-status/buttons/system-status/frontend.tsx +224 -0
  195. package/src/builtin-addons/system-status/buttons/system-status/schemas.ts +67 -0
  196. package/src/builtin-addons/system-status/domain/catalog.ts +53 -0
  197. package/src/builtin-addons/system-status/domain/chart-sampler.ts +58 -0
  198. package/src/builtin-addons/system-status/domain/display-metrics.ts +128 -0
  199. package/src/builtin-addons/system-status/domain/index.ts +10 -0
  200. package/src/builtin-addons/system-status/domain/live-metrics.ts +1021 -0
  201. package/src/builtin-addons/system-status/domain/metric-ids.ts +0 -0
  202. package/src/builtin-addons/system-status/index.ts +16 -0
  203. package/src/builtin-addons/system-status/manifest.ts +105 -0
  204. package/src/builtin-addons/system-status/shared/metrics-catalog.ts +316 -0
  205. package/src/builtin-addons/system-status/sirenodeck.json +6 -0
  206. package/src/builtin-addons/test-buildin/decks/action-buttons-test.ts +56 -0
  207. package/src/builtin-addons/test-buildin/index.d.ts +5 -0
  208. package/src/builtin-addons/test-buildin/index.ts +16 -0
  209. package/src/builtin-addons/value-display/README.md +59 -0
  210. package/src/builtin-addons/value-display/backend.ts +86 -0
  211. package/src/builtin-addons/value-display/buttons/value-display/backend.ts +17 -0
  212. package/src/builtin-addons/value-display/buttons/value-display/config.ts +36 -0
  213. package/src/builtin-addons/value-display/buttons/value-display/frontend.tsx +54 -0
  214. package/src/builtin-addons/value-display/index.d.ts +5 -0
  215. package/src/builtin-addons/value-display/index.ts +23 -0
  216. package/src/builtin-addons/value-display/sirenodeck.json +6 -0
  217. package/src/builtin-addons/weather/README.md +46 -0
  218. package/src/builtin-addons/weather/backend.ts +121 -0
  219. package/src/builtin-addons/weather/buttons/weather/backend.ts +27 -0
  220. package/src/builtin-addons/weather/buttons/weather/config.ts +29 -0
  221. package/src/builtin-addons/weather/buttons/weather/frontend.tsx +63 -0
  222. package/src/builtin-addons/weather/buttons/weather/icons.tsx +88 -0
  223. package/src/builtin-addons/weather/buttons/weather/pages.tsx +109 -0
  224. package/src/builtin-addons/weather/domain/codes.ts +35 -0
  225. package/src/builtin-addons/weather/domain/fetch.ts +47 -0
  226. package/src/builtin-addons/weather/index.d.ts +5 -0
  227. package/src/builtin-addons/weather/index.ts +23 -0
  228. package/src/builtin-addons/weather/poller.ts +70 -0
  229. package/src/builtin-addons/weather/provider/city-key.ts +9 -0
  230. package/src/builtin-addons/weather/provider/codes.ts +35 -0
  231. package/src/builtin-addons/weather/provider/fetch.ts +97 -0
  232. package/src/builtin-addons/weather/provider/geocode.ts +67 -0
  233. package/src/builtin-addons/weather/provider/types.ts +42 -0
  234. package/src/builtin-addons/weather/sirenodeck.json +6 -0
  235. package/src/cli/coding-agents-onboarding.ts +434 -0
  236. package/src/cli/commands/addon-decks.ts +441 -0
  237. package/src/cli/commands/addon-registry.ts +639 -0
  238. package/src/cli/commands/emulator-mode.ts +289 -0
  239. package/src/cli/commands/install.ts +128 -0
  240. package/src/cli/commands/logs.ts +74 -0
  241. package/src/cli/commands/network-bind.ts +111 -0
  242. package/src/cli/commands/pipeline/helpers.ts +75 -0
  243. package/src/cli/commands/pipeline/preflight.ts +20 -0
  244. package/src/cli/commands/port-identity.ts +152 -0
  245. package/src/cli/commands/reload.ts +37 -0
  246. package/src/cli/commands/restart.ts +139 -0
  247. package/src/cli/commands/run.ts +2270 -0
  248. package/src/cli/commands/service/install.ts +121 -0
  249. package/src/cli/commands/service/render-template.ts +158 -0
  250. package/src/cli/commands/service-manager.ts +184 -0
  251. package/src/cli/commands/spawn-daemon.ts +251 -0
  252. package/src/cli/commands/start.ts +1061 -0
  253. package/src/cli/commands/status.ts +110 -0
  254. package/src/cli/commands/stop.ts +121 -0
  255. package/src/cli/commands/subprocess-supervisor.ts +203 -0
  256. package/src/cli/commands/system-requirements.ts +699 -0
  257. package/src/cli/cwd.ts +28 -0
  258. package/src/cli/first-run.ts +149 -0
  259. package/src/cli/http-server.ts +244 -0
  260. package/src/cli/index.ts +355 -0
  261. package/src/cli/install-root.ts +26 -0
  262. package/src/cli/main.ts +194 -0
  263. package/src/cli/package-manager.ts +175 -0
  264. package/src/cli/probe-deps.ts +45 -0
  265. package/src/cli/prompt.ts +22 -0
  266. package/src/cli/startup-display.ts +452 -0
  267. package/src/config/builtin-icons.ts +57 -0
  268. package/src/config/config-diff.ts +54 -0
  269. package/src/config/discovery.ts +64 -0
  270. package/src/config/icon-resolver.ts +90 -0
  271. package/src/config/include-resolver.ts +202 -0
  272. package/src/config/index.ts +9 -0
  273. package/src/config/loader.ts +187 -0
  274. package/src/config/mutation.ts +514 -0
  275. package/src/config/reference-expander.ts +84 -0
  276. package/src/config/schemas.ts +223 -0
  277. package/src/config/validation.ts +254 -0
  278. package/src/core/asset-id.ts +21 -0
  279. package/src/core/gesture-state.ts +344 -0
  280. package/src/core/icon-asset-registry.ts +127 -0
  281. package/src/core/icon-source.ts +86 -0
  282. package/src/core/index.ts +30 -0
  283. package/src/core/mime.ts +16 -0
  284. package/src/core/pagination.ts +90 -0
  285. package/src/core/pub-sub.ts +126 -0
  286. package/src/core/settings.ts +153 -0
  287. package/src/core/store.ts +88 -0
  288. package/src/core/watcher.ts +73 -0
  289. package/src/deck/addon-handler-bridge.ts +516 -0
  290. package/src/deck/deck-config.ts +271 -0
  291. package/src/deck/editor-surfaces.ts +87 -0
  292. package/src/deck/host-context.ts +18 -0
  293. package/src/deck/index.ts +102 -0
  294. package/src/deck/macro-parse.ts +182 -0
  295. package/src/deck/methods.ts +348 -0
  296. package/src/deck/paginate-deck.ts +98 -0
  297. package/src/deck/position-buttons.ts +57 -0
  298. package/src/deck/runtime/index.ts +5 -0
  299. package/src/deck/runtime/runtime.ts +1060 -0
  300. package/src/deck/runtime-subscriptions.ts +45 -0
  301. package/src/deck/system-back-injection.ts +62 -0
  302. package/src/deck/system-buttons/registry.tsx +97 -0
  303. package/src/deck/system-buttons/types.ts +14 -0
  304. package/src/device/black-frame.ts +73 -0
  305. package/src/device/index.ts +13 -0
  306. package/src/device/linux-udev.ts +36 -0
  307. package/src/device/models.ts +89 -0
  308. package/src/device/reconnecting.ts +193 -0
  309. package/src/device/registry.ts +38 -0
  310. package/src/device/stream-deck.ts +155 -0
  311. package/src/index.ts +74 -0
  312. package/src/outputClient/emulator.ts +357 -0
  313. package/src/outputClient/index.ts +26 -0
  314. package/src/outputClient/open-browser.ts +26 -0
  315. package/src/outputClient/real.ts +393 -0
  316. package/src/outputClient/types.ts +83 -0
  317. package/src/render/browser-renderer.ts +240 -0
  318. package/src/render/buffer-hash.ts +20 -0
  319. package/src/render/editor-handler.ts +197 -0
  320. package/src/render/emulator-server.ts +128 -0
  321. package/src/render/icon-source-resolver.ts +69 -0
  322. package/src/render/index.ts +16 -0
  323. package/src/render/protocol.ts +61 -0
  324. package/src/render/push-raw-image.ts +100 -0
  325. package/src/render/screenshot-cadence.ts +87 -0
  326. package/src/render/state-publisher.ts +110 -0
  327. package/src/render/vite-server.ts +138 -0
  328. package/src/render/ws-bridge.ts +315 -0
  329. package/src/system/device-selection.ts +80 -0
  330. package/src/system/glob-match.ts +74 -0
  331. package/src/system/providers/active-app/darwin.ts +117 -0
  332. package/src/system/providers/active-app/linux.ts +245 -0
  333. package/src/system/providers/active-app/wayland-gnome.ts +303 -0
  334. package/src/system/providers/active-app/windows.ts +94 -0
  335. package/src/system/providers/active-app.ts +97 -0
  336. package/src/system/providers/clipboard/darwin.ts +65 -0
  337. package/src/system/providers/clipboard/linux.ts +163 -0
  338. package/src/system/providers/clipboard/windows.ts +94 -0
  339. package/src/system/providers/clipboard.ts +84 -0
  340. package/src/system/providers/error.ts +16 -0
  341. package/src/system/providers/key-macro/darwin.ts +221 -0
  342. package/src/system/providers/key-macro/linux.ts +651 -0
  343. package/src/system/providers/key-macro/parser.ts +215 -0
  344. package/src/system/providers/key-macro/windows.ts +476 -0
  345. package/src/system/providers/key-macro.ts +79 -0
  346. package/src/system/providers/notification/darwin.ts +36 -0
  347. package/src/system/providers/notification/linux.ts +115 -0
  348. package/src/system/providers/notification/windows.ts +72 -0
  349. package/src/system/providers/notification.ts +75 -0
  350. package/src/system/providers/session/darwin.ts +82 -0
  351. package/src/system/providers/session/linux.ts +175 -0
  352. package/src/system/providers/session/windows.ts +73 -0
  353. package/src/system/providers/session.ts +89 -0
  354. package/src/system/providers/shared.ts +65 -0
  355. package/src/system/requirements.ts +235 -0
  356. package/src/system/setup-wizard/config-seed.ts +30 -0
  357. package/src/system/setup-wizard/format.ts +275 -0
  358. package/src/system/setup-wizard/index.ts +45 -0
  359. package/src/system/setup-wizard/open-settings.ts +26 -0
  360. package/src/system/setup-wizard/plan.ts +230 -0
  361. package/src/system/setup-wizard/probe-cache.ts +34 -0
  362. package/src/system/setup-wizard/probe.ts +486 -0
  363. package/src/system/setup-wizard/runtime-features.ts +85 -0
  364. package/src/system/setup-wizard/sudo.ts +178 -0
  365. package/src/system/setup-wizard/types.ts +133 -0
  366. package/src/system/virtual-stream-deck.ts +67 -0
  367. package/src/themes/css.ts +210 -0
  368. package/src/themes/default/assets/IBMPlexSans-Italic-VariableFont_wdth,wght.ttf +0 -0
  369. package/src/themes/default/assets/IBMPlexSans-VariableFont_wdth,wght.ttf +0 -0
  370. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Bold.ttf +0 -0
  371. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-BoldItalic.ttf +0 -0
  372. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-ExtraLight.ttf +0 -0
  373. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-ExtraLightItalic.ttf +0 -0
  374. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Italic.ttf +0 -0
  375. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Light.ttf +0 -0
  376. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-LightItalic.ttf +0 -0
  377. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Medium.ttf +0 -0
  378. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-MediumItalic.ttf +0 -0
  379. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Regular.ttf +0 -0
  380. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-SemiBold.ttf +0 -0
  381. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-SemiBoldItalic.ttf +0 -0
  382. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-Thin.ttf +0 -0
  383. package/src/themes/default/assets/IBM_Plex_Mono/IBMPlexMono-ThinItalic.ttf +0 -0
  384. package/src/themes/default/assets/IBM_Plex_Mono/OFL.txt +93 -0
  385. package/src/themes/default/assets/OFL.txt +93 -0
  386. package/src/themes/default/assets/README.txt +106 -0
  387. package/src/themes/default/assets/static/IBMPlexSans-Bold.ttf +0 -0
  388. package/src/themes/default/assets/static/IBMPlexSans-BoldItalic.ttf +0 -0
  389. package/src/themes/default/assets/static/IBMPlexSans-ExtraLight.ttf +0 -0
  390. package/src/themes/default/assets/static/IBMPlexSans-ExtraLightItalic.ttf +0 -0
  391. package/src/themes/default/assets/static/IBMPlexSans-Italic.ttf +0 -0
  392. package/src/themes/default/assets/static/IBMPlexSans-Light.ttf +0 -0
  393. package/src/themes/default/assets/static/IBMPlexSans-LightItalic.ttf +0 -0
  394. package/src/themes/default/assets/static/IBMPlexSans-Medium.ttf +0 -0
  395. package/src/themes/default/assets/static/IBMPlexSans-MediumItalic.ttf +0 -0
  396. package/src/themes/default/assets/static/IBMPlexSans-Regular.ttf +0 -0
  397. package/src/themes/default/assets/static/IBMPlexSans-SemiBold.ttf +0 -0
  398. package/src/themes/default/assets/static/IBMPlexSans-SemiBoldItalic.ttf +0 -0
  399. package/src/themes/default/assets/static/IBMPlexSans-Thin.ttf +0 -0
  400. package/src/themes/default/assets/static/IBMPlexSans-ThinItalic.ttf +0 -0
  401. package/src/themes/default/assets/static/IBMPlexSans_Condensed-Bold.ttf +0 -0
  402. package/src/themes/default/assets/static/IBMPlexSans_Condensed-BoldItalic.ttf +0 -0
  403. package/src/themes/default/assets/static/IBMPlexSans_Condensed-ExtraLight.ttf +0 -0
  404. package/src/themes/default/assets/static/IBMPlexSans_Condensed-ExtraLightItalic.ttf +0 -0
  405. package/src/themes/default/assets/static/IBMPlexSans_Condensed-Italic.ttf +0 -0
  406. package/src/themes/default/assets/static/IBMPlexSans_Condensed-Light.ttf +0 -0
  407. package/src/themes/default/assets/static/IBMPlexSans_Condensed-LightItalic.ttf +0 -0
  408. package/src/themes/default/assets/static/IBMPlexSans_Condensed-Medium.ttf +0 -0
  409. package/src/themes/default/assets/static/IBMPlexSans_Condensed-MediumItalic.ttf +0 -0
  410. package/src/themes/default/assets/static/IBMPlexSans_Condensed-Regular.ttf +0 -0
  411. package/src/themes/default/assets/static/IBMPlexSans_Condensed-SemiBold.ttf +0 -0
  412. package/src/themes/default/assets/static/IBMPlexSans_Condensed-SemiBoldItalic.ttf +0 -0
  413. package/src/themes/default/assets/static/IBMPlexSans_Condensed-Thin.ttf +0 -0
  414. package/src/themes/default/assets/static/IBMPlexSans_Condensed-ThinItalic.ttf +0 -0
  415. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Bold.ttf +0 -0
  416. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-BoldItalic.ttf +0 -0
  417. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-ExtraLight.ttf +0 -0
  418. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-ExtraLightItalic.ttf +0 -0
  419. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Italic.ttf +0 -0
  420. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Light.ttf +0 -0
  421. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-LightItalic.ttf +0 -0
  422. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Medium.ttf +0 -0
  423. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-MediumItalic.ttf +0 -0
  424. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Regular.ttf +0 -0
  425. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-SemiBold.ttf +0 -0
  426. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-SemiBoldItalic.ttf +0 -0
  427. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-Thin.ttf +0 -0
  428. package/src/themes/default/assets/static/IBMPlexSans_SemiCondensed-ThinItalic.ttf +0 -0
  429. package/src/themes/default/components.css +81 -0
  430. package/src/themes/default/sirenodeck.json +170 -0
  431. package/src/themes/index.ts +14 -0
  432. package/src/themes/light/assets/IBMPlexSans-Italic-VariableFont_wdth,wght.ttf +0 -0
  433. package/src/themes/light/assets/IBMPlexSans-VariableFont_wdth,wght.ttf +0 -0
  434. package/src/themes/light/assets/IBM_Plex_Mono/IBMPlexMono-Bold.ttf +0 -0
  435. package/src/themes/light/assets/IBM_Plex_Mono/IBMPlexMono-Medium.ttf +0 -0
  436. package/src/themes/light/assets/IBM_Plex_Mono/IBMPlexMono-Regular.ttf +0 -0
  437. package/src/themes/light/assets/IBM_Plex_Mono/OFL.txt +93 -0
  438. package/src/themes/light/assets/OFL.txt +93 -0
  439. package/src/themes/light/assets/README.txt +106 -0
  440. package/src/themes/light/assets/static/IBMPlexSans-Light.ttf +0 -0
  441. package/src/themes/light/assets/static/IBMPlexSans-LightItalic.ttf +0 -0
  442. package/src/themes/light/assets/static/IBMPlexSans-Thin.ttf +0 -0
  443. package/src/themes/light/components.css +70 -0
  444. package/src/themes/light/sirenodeck.json +170 -0
  445. package/src/themes/loader.ts +343 -0
  446. package/src/themes/manifest.ts +164 -0
  447. package/src/themes/use-resolved-theme.tsx +32 -0
  448. package/src/ui/ButtonFrame.tsx +230 -0
  449. package/src/ui/contexts/AssetCacheContext.tsx +43 -0
  450. package/src/ui/index.ts +20 -0
  451. package/src/ui/primitives/Chip.tsx +53 -0
  452. package/src/ui/primitives/Icon.tsx +209 -0
  453. package/src/ui/primitives/Label.tsx +82 -0
  454. package/src/ui/primitives/ProgressBar.tsx +38 -0
  455. package/src/ui/primitives/TapIndicator.tsx +67 -0
  456. package/src/ui/primitives/Text.tsx +810 -0
  457. package/src/ui/primitives/index.ts +6 -0
  458. package/src/ui/surfaces/BarsSurface.tsx +211 -0
  459. package/src/ui/surfaces/IconLabelProgressSurface.tsx +133 -0
  460. package/src/ui/surfaces/IconLabelSurface.tsx +59 -0
  461. package/src/ui/surfaces/LabelValueListSurface.tsx +202 -0
  462. package/src/ui/surfaces/PaginatedSurface.tsx +164 -0
  463. package/src/ui/surfaces/SplitActionSurface.tsx +98 -0
  464. package/src/ui/surfaces/TemporaryErrorSurface.tsx +68 -0
  465. package/src/ui/surfaces/ValueChart.tsx +218 -0
  466. package/src/ui/surfaces/index.ts +32 -0
  467. package/src/ui/surfaces/paginated-schema.ts +22 -0
  468. package/src/ui/theme-presentation-builder.ts +184 -0
  469. package/src/ui/theme-presentation.tsx +146 -0
  470. package/src/ui/utils/cn.ts +12 -0
  471. package/src/util/cache-paths.ts +38 -0
  472. package/src/util/config-diff.ts +16 -0
  473. package/src/util/daemon-control.ts +85 -0
  474. package/src/util/daemon.ts +575 -0
  475. package/src/util/device-config.ts +56 -0
  476. package/src/util/errors.ts +13 -0
  477. package/src/util/log-reader.ts +114 -0
  478. package/src/util/log-tail.ts +103 -0
  479. package/src/util/logger.ts +417 -0
  480. package/src/util/process-title.ts +79 -0
  481. package/src/util/single-instance.ts +202 -0
  482. package/src/version.ts +10 -0
  483. package/src/vite/index.ts +6 -0
  484. package/src/vite/virtual-modules.ts +631 -0
  485. package/src/vite-env.d.ts +1 -0
@@ -0,0 +1,120 @@
1
+ // ponytail: see packages/addons/app-shortcuts/src/index.ts for context.
2
+ import type { AgentProvider } from "../shared/state.js"
3
+ import { ClaudeCodeProvider } from "./claude-code.js"
4
+ import { OpenCodeProvider } from "./opencode.js"
5
+
6
+ export interface ProviderRegistryConfig {
7
+ readonly opencodeUrl: string
8
+ readonly claudeCodeProjectsDir?: string
9
+ readonly spawnOpencodeIfMissing: boolean
10
+ }
11
+
12
+ export interface LoadProvidersResult {
13
+ readonly providers: ReadonlyMap<string, AgentProvider>
14
+ readonly spawnedChild: { kill: () => Promise<void> } | null
15
+ }
16
+
17
+ export interface LoadProvidersDeps {
18
+ readonly ensureOpencodeServer?: typeof import("./spawn").ensureOpencodeServer
19
+ }
20
+
21
+ const DEFAULT_OPENCODE_URL = "http://127.0.0.1:4096"
22
+ const LEGACY_OPENCODE_URL = "http://127.0.0.1:4095"
23
+
24
+ export const loadProviders = async (
25
+ config: ProviderRegistryConfig,
26
+ signal: AbortSignal,
27
+ deps: LoadProvidersDeps = {},
28
+ ): Promise<LoadProvidersResult> => {
29
+ const providers = new Map<string, AgentProvider>()
30
+ let spawnedChild: { kill: () => Promise<void> } | null = null
31
+ const configuredUrl = config.opencodeUrl ?? DEFAULT_OPENCODE_URL
32
+ const reachableUrl = await resolveOpencodeUrl(configuredUrl, signal)
33
+ const opencodeUrl = reachableUrl ?? configuredUrl
34
+
35
+ try {
36
+ providers.set("opencode", new OpenCodeProvider({ baseUrl: opencodeUrl }))
37
+ } catch {
38
+ // ponytail: client construction is synchronous and should not throw;
39
+ // if SDK import is broken at runtime, we just skip opencode.
40
+ }
41
+
42
+ if (!providers.has("opencode") || reachableUrl === null) {
43
+ if (!(await opencodeInstalled())) {
44
+ // CLI not on $PATH — skip spawn entirely; requirement check
45
+ // surfaces "opencode not installed" distinctly.
46
+ providers.delete("opencode")
47
+ } else if (config.spawnOpencodeIfMissing && !signal.aborted) {
48
+ try {
49
+ const ensure =
50
+ deps.ensureOpencodeServer ??
51
+ (await import("./spawn")).ensureOpencodeServer
52
+ const spawned = await ensure(opencodeUrl, signal)
53
+ if (spawned) {
54
+ spawnedChild = spawned.child
55
+ providers.set(
56
+ "opencode",
57
+ new OpenCodeProvider({ baseUrl: spawned.baseUrl }),
58
+ )
59
+ }
60
+ } catch {
61
+ // spawn failed; opencode will remain absent from the registry
62
+ }
63
+ } else {
64
+ providers.delete("opencode")
65
+ }
66
+ }
67
+
68
+ try {
69
+ providers.set(
70
+ "claude-code",
71
+ new ClaudeCodeProvider({
72
+ ...(config.claudeCodeProjectsDir !== undefined
73
+ ? { projectsDir: config.claudeCodeProjectsDir }
74
+ : {}),
75
+ }),
76
+ )
77
+ } catch {
78
+ // claude-code has no native deps; skip if chokidar somehow fails
79
+ }
80
+
81
+ return { providers, spawnedChild }
82
+ }
83
+
84
+ export const resolveOpencodeUrl = async (
85
+ preferredUrl: string,
86
+ signal: AbortSignal,
87
+ ): Promise<string | null> => {
88
+ const candidates =
89
+ preferredUrl === DEFAULT_OPENCODE_URL
90
+ ? [preferredUrl, LEGACY_OPENCODE_URL]
91
+ : [preferredUrl]
92
+ for (const url of candidates) {
93
+ if (await probeOpencodeHealth(url, signal)) return url
94
+ }
95
+ return null
96
+ }
97
+
98
+ const probeOpencodeHealth = async (
99
+ baseUrl: string,
100
+ signal: AbortSignal,
101
+ ): Promise<boolean> => {
102
+ try {
103
+ const res = await fetch(`${baseUrl}/global/health`, { signal })
104
+ if (!res.ok) return false
105
+ const body = (await res.json()) as { healthy?: boolean }
106
+ return body.healthy === true
107
+ } catch {
108
+ return false
109
+ }
110
+ }
111
+
112
+ // ponytail: the opencode CLI must be on $PATH for `opencode serve` to be
113
+ // spawnable. Surfaced via manifest checks so the banner distinguishes
114
+ // "CLI not installed" from "installed but no server running".
115
+ export const opencodeInstalled = async (): Promise<boolean> => {
116
+ const { execFile } = await import("node:child_process")
117
+ return new Promise<boolean>((resolve) => {
118
+ execFile("which", ["opencode"], (err) => resolve(err === null))
119
+ })
120
+ }
@@ -0,0 +1,112 @@
1
+ import { execa, type ExecaError } from "execa"
2
+
3
+ export class NotReachableError extends Error {
4
+ override readonly name = "NotReachableError"
5
+ constructor(message: string) {
6
+ super(message)
7
+ }
8
+ }
9
+
10
+ export interface SpawnedServer {
11
+ readonly baseUrl: string
12
+ readonly child: { kill: () => Promise<void> }
13
+ }
14
+
15
+ interface ExecaChild {
16
+ kill: () => Promise<unknown>
17
+ }
18
+
19
+ interface SpawnDeps {
20
+ readonly fetchImpl?: typeof fetch
21
+ readonly execaImpl?: (cmd: string, args: ReadonlyArray<string>) => ExecaChild
22
+ readonly sleep?: (ms: number) => Promise<void>
23
+ }
24
+
25
+ const defaultSleep = (ms: number): Promise<void> =>
26
+ new Promise((resolve) => setTimeout(resolve, ms))
27
+
28
+ export const ensureOpencodeServer = async (
29
+ baseUrl: string,
30
+ signal: AbortSignal,
31
+ deps: SpawnDeps = {},
32
+ ): Promise<SpawnedServer | null> => {
33
+ const fetchImpl = deps.fetchImpl ?? fetch
34
+ const sleep = deps.sleep ?? defaultSleep
35
+
36
+ if (await isReachable(baseUrl, fetchImpl, signal)) return null
37
+
38
+ if (signal.aborted) throw new AbortError()
39
+
40
+ const port = parsePort(baseUrl)
41
+ const child: ExecaChild = deps.execaImpl
42
+ ? deps.execaImpl("opencode", ["serve", "--port", String(port)])
43
+ : (execa("opencode", [
44
+ "serve",
45
+ "--port",
46
+ String(port),
47
+ ]) as unknown as ExecaChild)
48
+
49
+ // ponytail: execa's returned promise rejects when the child exits
50
+ // non-zero (e.g. a broken opencode-ai postinstall exits 1 instantly).
51
+ // We never await `child` directly — reachability is polled via
52
+ // /global/health — so the floating rejection would surface as
53
+ // unhandledRejection and kill the daemon. Attach a sink; failures are
54
+ // reported by the probe loop below and the addon's checks entry.
55
+ void Promise.resolve(child).catch(() => undefined)
56
+
57
+ const deadline = Date.now() + 5000
58
+ while (Date.now() < deadline) {
59
+ if (signal.aborted) {
60
+ await child.kill().catch(() => undefined)
61
+ throw new AbortError()
62
+ }
63
+ if (await isReachable(baseUrl, fetchImpl, signal)) {
64
+ return {
65
+ baseUrl,
66
+ child: {
67
+ kill: async () => {
68
+ await child.kill().catch(() => undefined)
69
+ },
70
+ },
71
+ }
72
+ }
73
+ await sleep(200)
74
+ }
75
+ await child.kill().catch(() => undefined)
76
+ throw new NotReachableError(
77
+ `opencode serve did not become reachable at ${baseUrl} within 5 s`,
78
+ )
79
+ }
80
+
81
+ const isReachable = async (
82
+ baseUrl: string,
83
+ fetchImpl: typeof fetch,
84
+ signal: AbortSignal,
85
+ ): Promise<boolean> => {
86
+ try {
87
+ const res = await fetchImpl(`${baseUrl}/global/health`, { signal })
88
+ if (!res.ok) return false
89
+ const body = (await res.json()) as { healthy?: boolean }
90
+ return body.healthy === true
91
+ } catch (err) {
92
+ if ((err as ExecaError | undefined)?.shortMessage) return false
93
+ return false
94
+ }
95
+ }
96
+
97
+ const parsePort = (baseUrl: string): number => {
98
+ try {
99
+ const parsed = new URL(baseUrl)
100
+ const port = Number.parseInt(parsed.port, 10)
101
+ return Number.isFinite(port) && port > 0 ? port : 4096
102
+ } catch {
103
+ return 4096
104
+ }
105
+ }
106
+
107
+ export class AbortError extends Error {
108
+ override readonly name = "AbortError"
109
+ constructor() {
110
+ super("aborted")
111
+ }
112
+ }
@@ -0,0 +1,55 @@
1
+ import type { AgentStatus } from "./state.js"
2
+
3
+ /**
4
+ * Claude Code hook events this integration registers, and the status each
5
+ * implies. Every event below was verified present in the installed binary.
6
+ *
7
+ * Two deliberate omissions:
8
+ *
9
+ * - `PostToolUse` is not registered. `PreToolUse` already proves the session
10
+ * is running and `Stop` returns it to idle, so registering both would
11
+ * roughly double the hook invocations for no extra signal.
12
+ * - `Notification/idle_prompt` is not registered. It fires about a minute
13
+ * after `Stop` on a session that is already idle; mapping it to
14
+ * `waiting_for_human` would turn every idle session into an attention state
15
+ * and make the summary tile meaningless.
16
+ *
17
+ * `SubagentStart`/`SubagentStop` are likewise absent: subagents are aggregated
18
+ * into their parent session, never surfaced separately.
19
+ */
20
+ export interface ClaudeHookRegistration {
21
+ readonly event: string
22
+ /** Claude Code matcher, when the event supports one. */
23
+ readonly matcher?: string
24
+ /** `null` means "remove the lease" rather than set a status. */
25
+ readonly status: AgentStatus | null
26
+ }
27
+
28
+ export const CLAUDE_HOOK_REGISTRATIONS: ReadonlyArray<ClaudeHookRegistration> =
29
+ [
30
+ {
31
+ event: "SessionStart",
32
+ matcher: "startup|resume|clear|fork",
33
+ status: "idle",
34
+ },
35
+ { event: "UserPromptSubmit", status: "running" },
36
+ { event: "PreToolUse", matcher: "*", status: "running" },
37
+ { event: "PermissionRequest", status: "waiting_for_human" },
38
+ {
39
+ event: "Notification",
40
+ matcher: "permission_prompt",
41
+ status: "waiting_for_human",
42
+ },
43
+ { event: "PreCompact", status: "compacting" },
44
+ { event: "PostCompact", status: "running" },
45
+ { event: "Stop", status: "idle" },
46
+ { event: "StopFailure", status: "error" },
47
+ { event: "SessionEnd", status: null },
48
+ ]
49
+
50
+ export const statusForHookEvent = (
51
+ event: string,
52
+ ): AgentStatus | null | undefined => {
53
+ const hit = CLAUDE_HOOK_REGISTRATIONS.find((r) => r.event === event)
54
+ return hit === undefined ? undefined : hit.status
55
+ }
@@ -0,0 +1,236 @@
1
+ import type { AgentStatus } from "./state.js"
2
+
3
+ export interface ClaudeJsonlEntry {
4
+ readonly type: string
5
+ readonly subtype?: string
6
+ readonly message?: {
7
+ readonly role?: string
8
+ readonly content?: unknown
9
+ }
10
+ readonly toolUse?: unknown
11
+ readonly toolResult?: unknown
12
+ readonly permissionRequest?: unknown
13
+ readonly error?: { readonly message?: string }
14
+ readonly rateLimit?: unknown
15
+ readonly costUSD?: number
16
+ readonly cwd?: string
17
+ readonly timestamp?: string | number
18
+ }
19
+
20
+ // ponytail: real claude jsonl has none of the v1 phantom shapes. The
21
+ // signals that exist (verified against ~/.claude/projects) are:
22
+ // • message.content as an ARRAY of blocks — tool_use blocks carry id+name
23
+ // • user tool_result blocks resolve a pending tool_use
24
+ // • system entries with subtype "turn_duration" mark the turn finished
25
+ // • progress entries stream while a tool/step runs
26
+ // Stalled (needs-human) heuristics below still work on the edge where a
27
+ // permission prompt ends the turn as a pending tool_use.
28
+ const IDLE_AFTER_MS = 30 * 60 * 1000
29
+ // ponytail: a pending non-AskUserQuestion tool longer than this is stuck on
30
+ // a permission prompt or a slow tool → surface as "waiting".
31
+ const STALLED_MS = 90 * 1000
32
+
33
+ interface ToolUseBlock {
34
+ readonly type?: string
35
+ readonly id?: string
36
+ readonly name?: string
37
+ }
38
+
39
+ const textFromContent = (content: unknown, max = 60): string | undefined => {
40
+ if (typeof content === "string") {
41
+ return content.length > max ? `${content.slice(0, max - 1)}…` : content
42
+ }
43
+ if (Array.isArray(content)) {
44
+ const parts: string[] = []
45
+ for (const part of content) {
46
+ if (typeof part === "string") {
47
+ parts.push(part)
48
+ } else if (
49
+ part !== null &&
50
+ typeof part === "object" &&
51
+ "type" in part &&
52
+ (part as { type: unknown }).type === "text"
53
+ ) {
54
+ const text = (part as { text?: unknown }).text
55
+ if (typeof text === "string") parts.push(text)
56
+ }
57
+ }
58
+ const joined = parts.join(" ")
59
+ return joined.length > max ? `${joined.slice(0, max - 1)}…` : joined
60
+ }
61
+ return undefined
62
+ }
63
+
64
+ const lastTimestamp = (
65
+ entries: readonly ClaudeJsonlEntry[],
66
+ ): number | undefined => {
67
+ for (let i = entries.length - 1; i >= 0; i -= 1) {
68
+ const ts = entries[i]?.timestamp
69
+ if (typeof ts === "number") return ts
70
+ if (typeof ts === "string") {
71
+ const parsed = Date.parse(ts)
72
+ if (!Number.isNaN(parsed)) return parsed
73
+ }
74
+ }
75
+ return undefined
76
+ }
77
+
78
+ const firstTimestamp = (
79
+ entries: readonly ClaudeJsonlEntry[],
80
+ ): number | undefined => {
81
+ for (const e of entries) {
82
+ const ts = e?.timestamp
83
+ if (typeof ts === "number") return ts
84
+ if (typeof ts === "string") {
85
+ const parsed = Date.parse(ts)
86
+ if (!Number.isNaN(parsed)) return parsed
87
+ }
88
+ }
89
+ return undefined
90
+ }
91
+
92
+ const lastAssistantText = (
93
+ entries: readonly ClaudeJsonlEntry[],
94
+ ): string | undefined => {
95
+ for (let i = entries.length - 1; i >= 0; i -= 1) {
96
+ const e = entries[i]
97
+ if (e?.type !== "assistant") continue
98
+ const text = textFromContent(e.message?.content)
99
+ if (text !== undefined) return text
100
+ }
101
+ return undefined
102
+ }
103
+
104
+ interface PendingTool {
105
+ readonly id: string
106
+ readonly name: string
107
+ }
108
+
109
+ // ponytail: track tool_use blocks until their matching tool_result. A tool
110
+ // still pending at the end of the stream is what the agent is doing right now.
111
+ const pendingToolUses = (
112
+ entries: readonly ClaudeJsonlEntry[],
113
+ ): readonly PendingTool[] => {
114
+ const pending = new Map<string, string>()
115
+ for (const e of entries) {
116
+ if (e.type === "assistant") {
117
+ const content = e.message?.content
118
+ if (Array.isArray(content)) {
119
+ for (const block of content) {
120
+ if (
121
+ typeof block !== "object" ||
122
+ block === null ||
123
+ (block as ToolUseBlock).type !== "tool_use"
124
+ ) {
125
+ continue
126
+ }
127
+ const b = block as ToolUseBlock
128
+ if (typeof b.id === "string" && typeof b.name === "string") {
129
+ pending.set(b.id, b.name)
130
+ }
131
+ }
132
+ }
133
+ } else if (e.type === "user") {
134
+ const content = e.message?.content
135
+ if (Array.isArray(content)) {
136
+ for (const block of content) {
137
+ if (
138
+ typeof block !== "object" ||
139
+ block === null ||
140
+ (block as { type?: string }).type !== "tool_result"
141
+ ) {
142
+ continue
143
+ }
144
+ const id = (block as { tool_use_id?: unknown }).tool_use_id
145
+ if (typeof id === "string") pending.delete(id)
146
+ }
147
+ }
148
+ }
149
+ }
150
+ return [...pending.entries()].map(([id, name]) => ({ id, name }))
151
+ }
152
+
153
+ export interface ClaudeDeriveResult {
154
+ readonly status: AgentStatus
155
+ readonly preview?: string
156
+ readonly updatedAt: number
157
+ readonly cost?: number
158
+ readonly cwd?: string
159
+ readonly createdAt?: number
160
+ }
161
+
162
+ export const deriveClaudeStatus = (
163
+ entries: readonly ClaudeJsonlEntry[],
164
+ now: number = Date.now(),
165
+ // ponytail: some transcripts carry no timestamps at all (three exist on the
166
+ // reporting machine, one last touched a week ago). Falling back to `now` made
167
+ // them permanently "live" — always inside the recency window, always on the
168
+ // deck. The caller passes the file's mtime instead, which is the honest
169
+ // answer to "when did this session last do anything".
170
+ fallbackTimestamp?: number,
171
+ ): ClaudeDeriveResult | null => {
172
+ if (entries.length === 0) return null
173
+
174
+ const last = entries[entries.length - 1]!
175
+ const ts = lastTimestamp(entries) ?? fallbackTimestamp ?? now
176
+ let status: AgentStatus = "idle"
177
+ let preview: string | undefined
178
+
179
+ const pending = pendingToolUses(entries)
180
+ const pendingAsk = pending.find((t) => t.name === "AskUserQuestion")
181
+ if (pendingAsk !== undefined) {
182
+ status = "waiting_for_human"
183
+ preview = "asking for your input"
184
+ } else if (last.type === "error") {
185
+ status = "error"
186
+ preview =
187
+ (typeof last.error?.message === "string" && last.error.message) ||
188
+ "session error"
189
+ } else if (typeof last.rateLimit !== "undefined") {
190
+ status = "waiting"
191
+ } else if (last.type === "system" && last.subtype === "turn_duration") {
192
+ // ponytail: turn finished — agent await
193
+ status = "idle"
194
+ preview = lastAssistantText(entries) ?? preview
195
+ } else if (last.type === "progress") {
196
+ status = "running"
197
+ } else if (pending.length > 0) {
198
+ // A non-AskUserQuestion tool is still open: running now, "waiting" once
199
+ // it stalls (permission prompt / hung tool).
200
+ const stalled = now - ts > STALLED_MS
201
+ status = stalled ? "waiting" : "running"
202
+ const toolName = pending[pending.length - 1]?.name
203
+ preview = toolName !== undefined ? `running ${toolName}` : undefined
204
+ } else if (last.type === "user") {
205
+ // Agent hasn't replied yet — mid-turn.
206
+ status = "running"
207
+ } else if (last.type === "assistant") {
208
+ preview = textFromContent(last.message?.content)
209
+ status = "idle"
210
+ }
211
+
212
+ if (now - ts > IDLE_AFTER_MS) {
213
+ status = "idle"
214
+ }
215
+
216
+ // ponytail: costUSD is per-assistant-message; sum gives a rough session
217
+ // total. Not every version files cost on every message — absent values
218
+ // just don't accrue.
219
+ let cost = 0
220
+ for (const e of entries) {
221
+ if (typeof e.costUSD === "number" && Number.isFinite(e.costUSD)) {
222
+ cost += e.costUSD
223
+ }
224
+ }
225
+ const cwd = entries.find((e) => typeof e.cwd === "string")?.cwd
226
+ const createdAt = firstTimestamp(entries)
227
+
228
+ return {
229
+ status,
230
+ preview,
231
+ updatedAt: ts,
232
+ ...(cost > 0 ? { cost } : {}),
233
+ ...(cwd !== undefined && cwd.length > 0 ? { cwd } : {}),
234
+ ...(createdAt !== undefined ? { createdAt } : {}),
235
+ }
236
+ }
@@ -0,0 +1,39 @@
1
+ // ponytail: bridge between the global backend (which sees live agent counts)
2
+ // and createAgentsDecks (which materializes the deck once per rebuild). The
3
+ // backend records the count and page shape here; the deck factory reads them
4
+ // to decide how many pages to materialize.
5
+ // ponytail: the backend and the deck factory are loaded through two module
6
+ // graphs (register-builtins package import vs addon-loader file-path import),
7
+ // so plain module state duplicates. Keep the single store on globalThis so
8
+ // both graphs share one set of counters.
9
+ export const AGENTS_DECK_BASE = "coding-agents:agents"
10
+
11
+ interface LiveCountStore {
12
+ liveCount: number
13
+ pageCount: number
14
+ }
15
+
16
+ const GLOBAL_KEY = "__sirenoCodingAgentsLiveCount"
17
+
18
+ const store = (): LiveCountStore => {
19
+ const g = globalThis as unknown as Record<string, LiveCountStore | undefined>
20
+ g[GLOBAL_KEY] ??= { liveCount: 0, pageCount: 1 }
21
+ return g[GLOBAL_KEY] as LiveCountStore
22
+ }
23
+
24
+ export const setLiveCount = (n: number): void => {
25
+ store().liveCount = n
26
+ }
27
+
28
+ export const getLiveCount = (): number => store().liveCount
29
+
30
+ export const setPageCount = (n: number): void => {
31
+ store().pageCount = n
32
+ }
33
+
34
+ export const getPageCount = (): number => store().pageCount
35
+
36
+ // ponytail: the deck id the summary button should navigate to. Pagination
37
+ // names pages `base-pN`; a single page keeps the base id.
38
+ export const deckTarget = (): string =>
39
+ store().pageCount > 1 ? `${AGENTS_DECK_BASE}-p1` : AGENTS_DECK_BASE
@@ -0,0 +1,58 @@
1
+ import type { AgentStatus } from "./state.js"
2
+
3
+ interface NotifierHooks {
4
+ readonly notify: (args: {
5
+ title: string
6
+ body: string
7
+ sound?: boolean
8
+ }) => Promise<void> | void
9
+ readonly now: () => number
10
+ }
11
+
12
+ export class NotificationThrottle {
13
+ readonly #hooks: NotifierHooks
14
+ readonly #last: Map<string, AgentStatus> = new Map<string, AgentStatus>()
15
+
16
+ constructor(hooks: Partial<NotifierHooks> = {}) {
17
+ this.#hooks = {
18
+ now: hooks.now ?? (() => Date.now()),
19
+ notify:
20
+ hooks.notify ??
21
+ (() => {
22
+ // ponytail: no-op fallback so tests / null-provider mode don't crash.
23
+ }),
24
+ }
25
+ }
26
+
27
+ // ponytail: keys on (providerId, sessionId, status) so the same agent
28
+ // transitioning into the same status twice doesn't re-fire — but a
29
+ // different status for the same agent always notifies.
30
+ evaluate(input: {
31
+ providerId: string
32
+ sessionId: string
33
+ status: AgentStatus
34
+ title: string
35
+ body: string
36
+ sound?: boolean
37
+ attentionOnly?: boolean
38
+ }): boolean {
39
+ const { providerId, sessionId, status, title, body, sound } = input
40
+ if (input.attentionOnly !== false) {
41
+ if (status !== "waiting_for_human" && status !== "error") return false
42
+ }
43
+ const key = `${providerId}:${sessionId}`
44
+ const last = this.#last.get(key)
45
+ if (last === status) return false
46
+ this.#last.set(key, status)
47
+ void this.#hooks.notify({ title, body, sound })
48
+ return true
49
+ }
50
+
51
+ forget(providerId: string, sessionId: string): void {
52
+ this.#last.delete(`${providerId}:${sessionId}`)
53
+ }
54
+
55
+ reset(): void {
56
+ this.#last.clear()
57
+ }
58
+ }
@@ -0,0 +1,82 @@
1
+ import type { AgentStatus } from "./state.js"
2
+
3
+ export interface OpencodeEventStatusBusy {
4
+ readonly type: "busy"
5
+ }
6
+
7
+ export interface OpencodeEventStatusIdle {
8
+ readonly type: "idle"
9
+ }
10
+
11
+ export interface OpencodeEventStatusRetry {
12
+ readonly type: "retry"
13
+ readonly attempt: number
14
+ readonly message: string
15
+ readonly next: number
16
+ }
17
+
18
+ export type OpencodeSessionStatus =
19
+ | OpencodeEventStatusIdle
20
+ | OpencodeEventStatusBusy
21
+ | OpencodeEventStatusRetry
22
+
23
+ export type OpencodeEvent =
24
+ | {
25
+ readonly type: "session.status"
26
+ readonly properties: {
27
+ readonly sessionID: string
28
+ readonly status: OpencodeSessionStatus
29
+ }
30
+ }
31
+ | {
32
+ readonly type: "session.idle"
33
+ readonly properties: { readonly sessionID: string }
34
+ }
35
+ | {
36
+ readonly type: "session.compacted"
37
+ readonly properties: { readonly sessionID: string }
38
+ }
39
+ | {
40
+ readonly type: "session.error"
41
+ readonly properties: {
42
+ readonly sessionID: string
43
+ readonly message?: string
44
+ }
45
+ }
46
+ | {
47
+ readonly type: "permission.updated"
48
+ readonly properties: {
49
+ readonly sessionID: string
50
+ readonly id: string
51
+ readonly status: "pending" | "approved" | "denied"
52
+ }
53
+ }
54
+
55
+ export const opencodeEventToStatus = (
56
+ evt: OpencodeEvent,
57
+ ): { readonly status: AgentStatus; readonly sessionId: string } | null => {
58
+ if (evt.type === "session.status") {
59
+ const s = evt.properties.status
60
+ if (s.type === "busy")
61
+ return { status: "running", sessionId: evt.properties.sessionID }
62
+ if (s.type === "idle")
63
+ return { status: "idle", sessionId: evt.properties.sessionID }
64
+ if (s.type === "retry")
65
+ return { status: "waiting", sessionId: evt.properties.sessionID }
66
+ }
67
+ if (evt.type === "session.idle") {
68
+ return { status: "idle", sessionId: evt.properties.sessionID }
69
+ }
70
+ if (evt.type === "session.compacted") {
71
+ return { status: "compacting", sessionId: evt.properties.sessionID }
72
+ }
73
+ if (evt.type === "session.error") {
74
+ return { status: "error", sessionId: evt.properties.sessionID }
75
+ }
76
+ if (evt.type === "permission.updated") {
77
+ const status: AgentStatus =
78
+ evt.properties.status === "pending" ? "waiting_for_human" : "idle"
79
+ return { status, sessionId: evt.properties.sessionID }
80
+ }
81
+ return null
82
+ }