@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,636 @@
1
+ // ponytail: talks to the `opencode serve` HTTP API directly — no SDK
2
+ // dependency. Endpoints on this fork (anomalyco): GET /session,
3
+ // GET /session/<id>/message (real per-session status lives here as message
4
+ // `parts[].state.status`), GET /event (SSE; /global/events is v1.x only).
5
+ // The old /session/status `attention` flag is still honored when present.
6
+ import type { Agent, AgentProvider, AgentStatus } from "../shared/state.js"
7
+ import {
8
+ opencodeEventToStatus,
9
+ type OpencodeEvent,
10
+ type OpencodeSessionStatus,
11
+ } from "../shared/opencode-status.js"
12
+ import {
13
+ readOpenCodeInstances,
14
+ type OpenCodeInstance,
15
+ } from "./opencode-instances.js"
16
+
17
+ // ponytail: "live" window — only sessions updated within this are surfaced.
18
+ const RECENT_WINDOW_MS = 6 * 60 * 60 * 1000
19
+
20
+ // ponytail: how many trailing messages to fetch per session for status. A
21
+ // mid-turn session's newest message streams with finish==null and empty parts,
22
+ // so the previous message(s) carry the live tool state — 6 is enough to see
23
+ // the open tool while the response streams.
24
+ const STATUS_MESSAGE_LIMIT = 6
25
+
26
+ interface OpencodeSessionLike {
27
+ readonly id: string
28
+ readonly title?: string
29
+ readonly directory?: string
30
+ readonly cost?: number
31
+ readonly contextTokens?: number
32
+ readonly contextPercent?: number
33
+ readonly agent?: string
34
+ readonly model?: { readonly id?: string; readonly providerID?: string }
35
+ readonly tokens?: TokenUsage
36
+ readonly time?: { readonly updated?: number; readonly created?: number }
37
+ }
38
+
39
+ type SessionStatusEntry = OpencodeSessionStatus & { attention?: boolean }
40
+
41
+ interface MessagePart {
42
+ readonly type?: string
43
+ readonly id?: string
44
+ readonly callID?: string
45
+ readonly tool_use_id?: string
46
+ readonly tool?: string
47
+ readonly state?: { readonly status?: string; readonly title?: string }
48
+ }
49
+
50
+ interface TokenUsage {
51
+ readonly input?: number
52
+ readonly output?: number
53
+ readonly reasoning?: number
54
+ readonly cache?: { readonly read?: number; readonly write?: number }
55
+ }
56
+
57
+ interface ProviderModelLike {
58
+ readonly limit?: { readonly context?: number }
59
+ }
60
+
61
+ interface ProviderLike {
62
+ readonly id?: string
63
+ readonly models?: Readonly<Record<string, ProviderModelLike>>
64
+ }
65
+
66
+ interface MessageLike {
67
+ readonly info?: {
68
+ readonly role?: string
69
+ readonly tokens?: TokenUsage
70
+ readonly cost?: number
71
+ readonly finish?: string | null
72
+ readonly modelID?: string
73
+ readonly providerID?: string
74
+ }
75
+ readonly finish?: string | null
76
+ readonly cost?: number
77
+ readonly parts?: readonly MessagePart[]
78
+ }
79
+
80
+ export interface OpencodeHttpApi {
81
+ listSessions(signal: AbortSignal): Promise<ReadonlyArray<OpencodeSessionLike>>
82
+ sessionStatus(
83
+ signal: AbortSignal,
84
+ ): Promise<Record<string, SessionStatusEntry>>
85
+ sessionMessages(
86
+ signal: AbortSignal,
87
+ id: string,
88
+ limit: number,
89
+ ): Promise<ReadonlyArray<MessageLike>>
90
+ providerModels?: (signal: AbortSignal) => Promise<readonly ProviderLike[]>
91
+ eventStream(
92
+ signal: AbortSignal,
93
+ ): Promise<AsyncIterable<OpencodeEvent | unknown>>
94
+ }
95
+
96
+ export const OPENCODE_LOGO =
97
+ "addon://coding-agents/assets/opencode-dark-square.svg"
98
+
99
+ // ponytail: every request gets a hard timeout — the opencode serve process
100
+ // can wedge (accepting connections, never answering) and an un-timed fetch
101
+ // would freeze the poll loop forever (observed live: daemon count froze for
102
+ // 25+ minutes with zero poll completions).
103
+ const DEFAULT_REQUEST_TIMEOUT_MS = 5_000
104
+
105
+ const requestSignal = (signal: AbortSignal, timeoutMs: number): AbortSignal =>
106
+ AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)])
107
+
108
+ // Bounds any api call (including injected test doubles) so a wedged server
109
+ // can never leave a poll pending forever. The default httpApi additionally
110
+ // aborts the underlying fetch via requestSignal.
111
+ const withTimeout = <T>(p: Promise<T>, timeoutMs: number): Promise<T> => {
112
+ let timer: ReturnType<typeof setTimeout> | undefined
113
+ return Promise.race([
114
+ p,
115
+ new Promise<never>((_, reject) => {
116
+ timer = setTimeout(
117
+ () => reject(new Error("opencode request timed out")),
118
+ timeoutMs,
119
+ )
120
+ }),
121
+ ]).finally(() => {
122
+ if (timer !== undefined) clearTimeout(timer)
123
+ })
124
+ }
125
+
126
+ const requestJson = async <T>(
127
+ baseUrl: string,
128
+ path: string,
129
+ signal: AbortSignal,
130
+ timeoutMs: number,
131
+ ): Promise<T> => {
132
+ const res = await fetch(`${baseUrl}${path}`, {
133
+ signal: requestSignal(signal, timeoutMs),
134
+ })
135
+ if (!res.ok) {
136
+ throw new Error(`opencode ${path} → HTTP ${res.status}`)
137
+ }
138
+ return (await res.json()) as T
139
+ }
140
+
141
+ const httpApi = (baseUrl: string, timeoutMs: number): OpencodeHttpApi => ({
142
+ listSessions: (signal) => requestJson(baseUrl, "/session", signal, timeoutMs),
143
+ sessionStatus: (signal) =>
144
+ requestJson<Record<string, SessionStatusEntry>>(
145
+ baseUrl,
146
+ "/session/status",
147
+ signal,
148
+ timeoutMs,
149
+ ),
150
+ sessionMessages: (signal, id, limit) =>
151
+ requestJson<ReadonlyArray<MessageLike>>(
152
+ baseUrl,
153
+ `/session/${encodeURIComponent(id)}/message?limit=${limit}`,
154
+ signal,
155
+ timeoutMs,
156
+ ),
157
+ providerModels: async (signal) => {
158
+ const response = await requestJson<{ all?: readonly ProviderLike[] }>(
159
+ baseUrl,
160
+ "/provider",
161
+ signal,
162
+ timeoutMs,
163
+ )
164
+ return response.all ?? []
165
+ },
166
+ eventStream: async (signal) => streamEvents(baseUrl, signal, timeoutMs),
167
+ })
168
+
169
+ // ponytail: minimal SSE reader over fetch's streaming body. opencode
170
+ // emits `data: <json>\n\n` frames; we only need the data payloads.
171
+ async function* streamEvents(
172
+ baseUrl: string,
173
+ signal: AbortSignal,
174
+ timeoutMs: number,
175
+ ): AsyncIterable<OpencodeEvent | unknown> {
176
+ // ponytail: the anomalyco fork serves the event stream at /event;
177
+ // v1.x uses /global/events. Either path can 200 with an HTML fallback, so
178
+ // validate the first chunk is a `data:` frame before committing.
179
+ for (const path of ["/event", "/global/events"] as const) {
180
+ try {
181
+ const attempt = await fetch(`${baseUrl}${path}`, {
182
+ signal: requestSignal(signal, timeoutMs),
183
+ headers: { Accept: "text/event-stream" },
184
+ })
185
+ if (!attempt.ok || attempt.body === null) continue
186
+ const reader = attempt.body.getReader()
187
+ const first = await reader.read()
188
+ reader.releaseLock()
189
+ if (first.done || !first.value || !startsDataFrame(first.value)) {
190
+ await attempt.body.cancel().catch(() => {})
191
+ continue
192
+ }
193
+ const res = attempt
194
+ void res
195
+ const decoder = new TextDecoder()
196
+ let buffer = new TextDecoder().decode(first.value as Uint8Array, {
197
+ stream: true,
198
+ })
199
+ const body = res.body as ReadableStream<Uint8Array>
200
+ for await (const chunk of body) {
201
+ buffer += decoder.decode(chunk as Uint8Array, { stream: true })
202
+ let idx: number
203
+ while ((idx = buffer.indexOf("\n\n")) !== -1) {
204
+ const frame = buffer.slice(0, idx)
205
+ buffer = buffer.slice(idx + 2)
206
+ for (const line of frame.split("\n")) {
207
+ if (!line.startsWith("data:")) continue
208
+ const payload = line.slice(5).trim()
209
+ if (payload.length === 0) continue
210
+ try {
211
+ yield JSON.parse(payload) as OpencodeEvent
212
+ } catch {
213
+ // non-JSON frame — ignore
214
+ }
215
+ }
216
+ }
217
+ }
218
+ return
219
+ } catch {
220
+ continue
221
+ }
222
+ }
223
+ }
224
+
225
+ const startsDataFrame = (chunk: Uint8Array): boolean =>
226
+ String.fromCharCode(...chunk.slice(0, 32)).includes("data:")
227
+
228
+ // ponytail: this fork's /session/status is empty even mid-turn, so live
229
+ // status comes from the trailing message parts. A `tool` part with
230
+ // state.status "pending" means the agent is waiting on the user (permission
231
+ // prompt or a `question`) → waiting_for_human; "running" → running. The
232
+ // newest message of an in-flight turn streams with finish==null and (often)
233
+ // empty parts — that's still "running". Otherwise the turn has finished.
234
+ const statusFromMessages = (
235
+ messages: ReadonlyArray<MessageLike>,
236
+ ): AgentStatus | null => {
237
+ const completedTools = new Set<string>()
238
+ for (const message of messages) {
239
+ for (const part of message.parts ?? []) {
240
+ if (part.type !== "tool_result") continue
241
+ const id = part.id ?? part.callID ?? part.tool_use_id
242
+ if (id !== undefined) completedTools.add(id)
243
+ }
244
+ }
245
+
246
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
247
+ const m = messages[i]
248
+ const parts = m?.parts ?? []
249
+ for (const p of parts) {
250
+ if (p?.type !== "tool") continue
251
+ const id = p.id ?? p.callID ?? p.tool_use_id
252
+ if (id !== undefined && completedTools.has(id)) continue
253
+ const st = p.state?.status
254
+ if (st === "pending") return "waiting_for_human"
255
+ if (st === "running") return "running"
256
+ }
257
+ }
258
+ const last = messages[messages.length - 1]
259
+ if (last === undefined) return null
260
+ // ponytail: an in-flight turn streams with finish==null and empty parts —
261
+ // that's still "running". A finished turn carries a real finish value.
262
+ if (last.finish === null || last.info?.finish === null) return "running"
263
+ return "idle"
264
+ }
265
+
266
+ const tokenTotal = (tokens: TokenUsage | undefined): number | undefined => {
267
+ if (tokens === undefined) return undefined
268
+ const values = [
269
+ tokens.input,
270
+ tokens.output,
271
+ tokens.reasoning,
272
+ tokens.cache?.read,
273
+ tokens.cache?.write,
274
+ ]
275
+ let total = 0
276
+ for (const value of values) {
277
+ if (typeof value === "number" && Number.isFinite(value)) total += value
278
+ }
279
+ return total > 0 ? total : undefined
280
+ }
281
+
282
+ const latestAssistantTokens = (
283
+ messages: ReadonlyArray<MessageLike>,
284
+ ): number | undefined => {
285
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
286
+ const info = messages[i]?.info
287
+ if (info?.role !== "assistant" || (info.tokens?.output ?? 0) <= 0) {
288
+ continue
289
+ }
290
+ return tokenTotal(info.tokens)
291
+ }
292
+ return undefined
293
+ }
294
+
295
+ const messageCost = (messages: ReadonlyArray<MessageLike>): number =>
296
+ messages.reduce((sum, message) => {
297
+ const cost = message.info?.cost ?? message.cost
298
+ return sum + (typeof cost === "number" && Number.isFinite(cost) ? cost : 0)
299
+ }, 0)
300
+
301
+ // ponytail: the /session list omits sessions owned by other opencode
302
+ // processes (verified live), but /session/<id>/message still answers. The
303
+ // newest assistant message carries modelID/providerID, enough to resolve a
304
+ // context limit and derive a percent for instance-only agents.
305
+ const latestMessageModel = (
306
+ messages: ReadonlyArray<MessageLike>,
307
+ ): { providerID: string; modelID: string } | null => {
308
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
309
+ const info = messages[i]?.info
310
+ const providerID = info?.providerID
311
+ const modelID = info?.modelID
312
+ if (
313
+ typeof providerID === "string" &&
314
+ providerID.length > 0 &&
315
+ typeof modelID === "string" &&
316
+ modelID.length > 0
317
+ ) {
318
+ return { providerID, modelID }
319
+ }
320
+ }
321
+ return null
322
+ }
323
+
324
+ // ponytail: the fork's /session/status is usually empty, so live status comes
325
+ // from the message parts; when a message-derived status is idle, fall back to
326
+ // whatever the status map reported (map status is authoritative when present).
327
+ const combineStatus = (
328
+ fromMap: AgentStatus,
329
+ fromMessages: AgentStatus | null,
330
+ ): AgentStatus => fromMessages ?? fromMap
331
+
332
+ export class OpenCodeProvider implements AgentProvider {
333
+ readonly id = "opencode" as const
334
+ readonly displayName = "OpenCode"
335
+ readonly logoPath = OPENCODE_LOGO
336
+
337
+ readonly #api: OpencodeHttpApi
338
+ readonly #apiFactory: (baseUrl: string, timeoutMs: number) => OpencodeHttpApi
339
+ readonly #recentWindowMs: number
340
+ readonly #requestTimeoutMs: number
341
+
342
+ constructor(opts: {
343
+ baseUrl: string
344
+ apiFactory?: (baseUrl: string, timeoutMs: number) => OpencodeHttpApi
345
+ recentWindowMs?: number
346
+ requestTimeoutMs?: number
347
+ }) {
348
+ this.#requestTimeoutMs = opts.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS
349
+ this.#apiFactory = opts.apiFactory ?? httpApi
350
+ this.#api = this.#apiFactory(opts.baseUrl, this.#requestTimeoutMs)
351
+ this.#recentWindowMs = opts.recentWindowMs ?? RECENT_WINDOW_MS
352
+ }
353
+
354
+ async fetchSnapshot(signal: AbortSignal): Promise<readonly Agent[]> {
355
+ if (signal.aborted) return []
356
+ const instances = await readOpenCodeInstances()
357
+ try {
358
+ const [sessions, statusMap, providers] = await Promise.all([
359
+ withTimeout(this.#api.listSessions(signal), this.#requestTimeoutMs),
360
+ withTimeout(this.#api.sessionStatus(signal), this.#requestTimeoutMs),
361
+ withTimeout(
362
+ this.#api.providerModels?.(signal).catch(() => []) ??
363
+ Promise.resolve([]),
364
+ this.#requestTimeoutMs,
365
+ ).catch(() => [] as readonly ProviderLike[]),
366
+ ])
367
+ const contextLimits = new Map<string, number>()
368
+ for (const provider of providers) {
369
+ for (const [modelId, model] of Object.entries(provider.models ?? {})) {
370
+ const context = model.limit?.context
371
+ if (provider.id !== undefined && typeof context === "number") {
372
+ contextLimits.set(`${provider.id}:${modelId}`, context)
373
+ }
374
+ }
375
+ }
376
+ const now = Date.now()
377
+ const recent = sessions
378
+ .filter((s) => (s.time?.updated ?? 0) >= now - this.#recentWindowMs)
379
+ .sort((a, b) => (b.time?.updated ?? 0) - (a.time?.updated ?? 0))
380
+ const agents = await this.#withMessages(
381
+ recent,
382
+ statusMap,
383
+ signal,
384
+ contextLimits,
385
+ )
386
+ const bySessionId = new Map(
387
+ instances.flatMap((instance) =>
388
+ instance.sessionId === undefined
389
+ ? []
390
+ : [[instance.sessionId, instance] as const],
391
+ ),
392
+ )
393
+ const represented = new Set<string>()
394
+ const merged = agents.map((session) => {
395
+ const instance = bySessionId.get(session.sessionId)
396
+ if (instance === undefined) return session
397
+ represented.add(instance.instanceId)
398
+ return {
399
+ ...session,
400
+ instanceId: instance.instanceId,
401
+ pid: instance.pid,
402
+ status: instance.status,
403
+ directory: instance.cwd,
404
+ updatedAt: instance.updatedAt,
405
+ }
406
+ })
407
+ return [
408
+ ...merged,
409
+ ...(await this.#instanceAgents(
410
+ instances.filter((instance) => !represented.has(instance.instanceId)),
411
+ contextLimits,
412
+ signal,
413
+ )),
414
+ ]
415
+ } catch (err) {
416
+ if (signal.aborted) return []
417
+ console.warn(
418
+ "[coding-agents] opencode snapshot failed:",
419
+ err instanceof Error ? err.message : String(err),
420
+ )
421
+ return instances.map(toInstanceAgent)
422
+ }
423
+ }
424
+
425
+ // ponytail: instances whose session is missing from the /session list get
426
+ // their metrics (tokens, cost, context %) derived from the message
427
+ // endpoint, which answers even when the list omits the session.
428
+ async #instanceAgents(
429
+ instances: ReadonlyArray<OpenCodeInstance>,
430
+ contextLimits: ReadonlyMap<string, number>,
431
+ signal: AbortSignal,
432
+ ): Promise<readonly Agent[]> {
433
+ const out: Agent[] = []
434
+ for (const instance of instances) {
435
+ if (instance.sessionId === undefined || signal.aborted) {
436
+ out.push(toInstanceAgent(instance))
437
+ continue
438
+ }
439
+ try {
440
+ const messages = await withTimeout(
441
+ this.#api.sessionMessages(
442
+ signal,
443
+ instance.sessionId,
444
+ STATUS_MESSAGE_LIMIT,
445
+ ),
446
+ this.#requestTimeoutMs,
447
+ )
448
+ const contextTokens = latestAssistantTokens(messages)
449
+ let cost: number | undefined
450
+ const derivedCost = messageCost(messages)
451
+ if (derivedCost > 0) cost = derivedCost
452
+ let contextPercent: number | undefined
453
+ const model = latestMessageModel(messages)
454
+ const limit =
455
+ model !== null
456
+ ? contextLimits.get(`${model.providerID}:${model.modelID}`)
457
+ : undefined
458
+ if (contextTokens !== undefined && limit !== undefined) {
459
+ contextPercent = Math.round((contextTokens / limit) * 100)
460
+ }
461
+ out.push({
462
+ ...toInstanceAgent(instance),
463
+ ...(cost !== undefined ? { cost } : {}),
464
+ ...(contextTokens !== undefined ? { contextTokens } : {}),
465
+ ...(contextPercent !== undefined ? { contextPercent } : {}),
466
+ })
467
+ } catch {
468
+ out.push(toInstanceAgent(instance))
469
+ }
470
+ }
471
+ return out
472
+ }
473
+
474
+ async #withMessages(
475
+ recent: ReadonlyArray<OpencodeSessionLike>,
476
+ statusMap: Record<string, SessionStatusEntry>,
477
+ signal: AbortSignal,
478
+ contextLimits: ReadonlyMap<string, number>,
479
+ ): Promise<readonly Agent[]> {
480
+ const out: Agent[] = []
481
+ for (const s of recent) {
482
+ const fromMap = toStatusFromMap(s, statusMap)
483
+ let status = fromMap
484
+ let contextTokens = tokenTotal(s.tokens)
485
+ let cost =
486
+ typeof s.cost === "number" && Number.isFinite(s.cost)
487
+ ? s.cost
488
+ : undefined
489
+ if (status !== "waiting_for_human") {
490
+ try {
491
+ const messages = await withTimeout(
492
+ this.#api.sessionMessages(signal, s.id, STATUS_MESSAGE_LIMIT),
493
+ this.#requestTimeoutMs,
494
+ )
495
+ status = combineStatus(fromMap, statusFromMessages(messages))
496
+ contextTokens = latestAssistantTokens(messages) ?? contextTokens
497
+ if (cost === undefined) {
498
+ const derivedCost = messageCost(messages)
499
+ if (derivedCost > 0) cost = derivedCost
500
+ }
501
+ } catch {
502
+ // keep derived status; message endpoint may 4xx for closed sessions
503
+ }
504
+ }
505
+ const contextLimit =
506
+ s.model?.providerID !== undefined && s.model.id !== undefined
507
+ ? contextLimits.get(`${s.model.providerID}:${s.model.id}`)
508
+ : undefined
509
+ const contextPercent =
510
+ contextTokens !== undefined && contextLimit !== undefined
511
+ ? Math.round((contextTokens / contextLimit) * 100)
512
+ : undefined
513
+ out.push(
514
+ toAgent(s, status, {
515
+ ...(s.directory !== undefined ? { directory: s.directory } : {}),
516
+ ...(cost !== undefined ? { cost } : {}),
517
+ ...(contextTokens !== undefined ? { contextTokens } : {}),
518
+ ...(contextPercent !== undefined ? { contextPercent } : {}),
519
+ }),
520
+ )
521
+ }
522
+ return out
523
+ }
524
+
525
+ subscribe(signal: AbortSignal, onChange: () => void): () => void {
526
+ let cancelled = false
527
+ const setCancelled = (): void => {
528
+ cancelled = true
529
+ }
530
+ signal.addEventListener("abort", setCancelled)
531
+ const noop = (): void => {}
532
+
533
+ const run = async () => {
534
+ outer: while (!cancelled && !signal.aborted) {
535
+ try {
536
+ const sub = await this.#api.eventStream(signal)
537
+ try {
538
+ for await (const raw of sub) {
539
+ if (cancelled || signal.aborted) break
540
+ const evt = raw as OpencodeEvent
541
+ if (!evt || typeof evt !== "object") continue
542
+ if (!("type" in evt)) continue
543
+ const mapped = opencodeEventToStatus(evt)
544
+ if (mapped) {
545
+ onChange()
546
+ }
547
+ }
548
+ } finally {
549
+ noop()
550
+ }
551
+ if (cancelled || signal.aborted) break outer
552
+ await new Promise<void>((resolve) => {
553
+ if (signal.aborted) {
554
+ resolve()
555
+ return
556
+ }
557
+ const t = setTimeout(resolve, 50)
558
+ signal.addEventListener("abort", () => {
559
+ clearTimeout(t)
560
+ resolve()
561
+ })
562
+ })
563
+ } catch {
564
+ await new Promise((resolve) => {
565
+ const t = setTimeout(resolve, 3000)
566
+ signal.addEventListener("abort", () => {
567
+ clearTimeout(t)
568
+ resolve(undefined)
569
+ })
570
+ })
571
+ }
572
+ }
573
+ }
574
+
575
+ void run()
576
+ return () => {
577
+ cancelled = true
578
+ }
579
+ }
580
+ }
581
+
582
+ const toStatusFromMap = (
583
+ s: OpencodeSessionLike,
584
+ statusMap: Record<string, SessionStatusEntry>,
585
+ ): AgentStatus => {
586
+ const status = statusMap[s.id]
587
+ let normalizedStatus: AgentStatus = "idle"
588
+ if (status?.type === "busy") normalizedStatus = "running"
589
+ else if (status?.type === "retry") normalizedStatus = "waiting"
590
+ // ponytail: opencode v1.1+ surfaces an `attention` boolean on
591
+ // /session/status entries when the assistant needs human input — trust
592
+ // it over our inference since it comes from the source itself.
593
+ if (status?.attention === true) normalizedStatus = "waiting_for_human"
594
+ return normalizedStatus
595
+ }
596
+
597
+ const toAgent = (
598
+ s: OpencodeSessionLike,
599
+ status: AgentStatus,
600
+ extra: {
601
+ directory?: string
602
+ cost?: number
603
+ contextTokens?: number
604
+ contextPercent?: number
605
+ },
606
+ ): Agent => ({
607
+ sessionId: s.id,
608
+ providerId: "opencode",
609
+ title: s.title && s.title.length > 0 ? s.title : "Untitled session",
610
+ status,
611
+ ...(extra.directory !== undefined ? { directory: extra.directory } : {}),
612
+ ...(extra.cost !== undefined && Number.isFinite(extra.cost)
613
+ ? { cost: extra.cost }
614
+ : {}),
615
+ ...(extra.contextTokens !== undefined && Number.isFinite(extra.contextTokens)
616
+ ? { contextTokens: extra.contextTokens }
617
+ : {}),
618
+ ...(extra.contextPercent !== undefined &&
619
+ Number.isFinite(extra.contextPercent)
620
+ ? { contextPercent: extra.contextPercent }
621
+ : {}),
622
+ ...(s.time?.created !== undefined ? { createdAt: s.time.created } : {}),
623
+ updatedAt: s.time?.updated ?? s.time?.created ?? 0,
624
+ })
625
+
626
+ const toInstanceAgent = (instance: OpenCodeInstance): Agent => ({
627
+ sessionId: instance.sessionId ?? `instance:${instance.instanceId}`,
628
+ instanceId: instance.instanceId,
629
+ pid: instance.pid,
630
+ providerId: "opencode",
631
+ title: "OpenCode",
632
+ status: instance.status,
633
+ directory: instance.cwd,
634
+ updatedAt: instance.updatedAt,
635
+ createdAt: instance.updatedAt,
636
+ })