@otto-assistant/bridge 0.4.92

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 (483) hide show
  1. package/bin.js +2 -0
  2. package/dist/agent-model.e2e.test.js +755 -0
  3. package/dist/ai-tool-to-genai.js +233 -0
  4. package/dist/ai-tool-to-genai.test.js +267 -0
  5. package/dist/ai-tool.js +6 -0
  6. package/dist/anthropic-auth-plugin.js +728 -0
  7. package/dist/anthropic-auth-plugin.test.js +125 -0
  8. package/dist/anthropic-auth-state.js +231 -0
  9. package/dist/bin.js +90 -0
  10. package/dist/channel-management.js +227 -0
  11. package/dist/cli-parsing.test.js +137 -0
  12. package/dist/cli-send-thread.e2e.test.js +356 -0
  13. package/dist/cli.js +3276 -0
  14. package/dist/commands/abort.js +65 -0
  15. package/dist/commands/action-buttons.js +245 -0
  16. package/dist/commands/add-project.js +113 -0
  17. package/dist/commands/agent.js +335 -0
  18. package/dist/commands/ask-question.js +274 -0
  19. package/dist/commands/btw.js +116 -0
  20. package/dist/commands/compact.js +120 -0
  21. package/dist/commands/context-usage.js +140 -0
  22. package/dist/commands/create-new-project.js +130 -0
  23. package/dist/commands/diff.js +63 -0
  24. package/dist/commands/file-upload.js +275 -0
  25. package/dist/commands/fork.js +220 -0
  26. package/dist/commands/gemini-apikey.js +70 -0
  27. package/dist/commands/login.js +885 -0
  28. package/dist/commands/mcp.js +239 -0
  29. package/dist/commands/memory-snapshot.js +24 -0
  30. package/dist/commands/mention-mode.js +44 -0
  31. package/dist/commands/merge-worktree.js +159 -0
  32. package/dist/commands/model-variant.js +364 -0
  33. package/dist/commands/model.js +776 -0
  34. package/dist/commands/new-worktree.js +366 -0
  35. package/dist/commands/paginated-select.js +57 -0
  36. package/dist/commands/permissions.js +274 -0
  37. package/dist/commands/queue.js +206 -0
  38. package/dist/commands/remove-project.js +115 -0
  39. package/dist/commands/restart-opencode-server.js +127 -0
  40. package/dist/commands/resume.js +149 -0
  41. package/dist/commands/run-command.js +79 -0
  42. package/dist/commands/screenshare.js +303 -0
  43. package/dist/commands/screenshare.test.js +20 -0
  44. package/dist/commands/session-id.js +78 -0
  45. package/dist/commands/session.js +176 -0
  46. package/dist/commands/share.js +80 -0
  47. package/dist/commands/tasks.js +205 -0
  48. package/dist/commands/types.js +2 -0
  49. package/dist/commands/undo-redo.js +305 -0
  50. package/dist/commands/unset-model.js +138 -0
  51. package/dist/commands/upgrade.js +42 -0
  52. package/dist/commands/user-command.js +155 -0
  53. package/dist/commands/verbosity.js +125 -0
  54. package/dist/commands/worktree-settings.js +43 -0
  55. package/dist/commands/worktrees.js +410 -0
  56. package/dist/condense-memory.js +33 -0
  57. package/dist/config.js +94 -0
  58. package/dist/context-awareness-plugin.js +363 -0
  59. package/dist/context-awareness-plugin.test.js +124 -0
  60. package/dist/critique-utils.js +95 -0
  61. package/dist/database.js +1310 -0
  62. package/dist/db.js +251 -0
  63. package/dist/db.test.js +138 -0
  64. package/dist/debounce-timeout.js +28 -0
  65. package/dist/debounced-process-flush.js +77 -0
  66. package/dist/discord-bot.js +1008 -0
  67. package/dist/discord-command-registration.js +524 -0
  68. package/dist/discord-urls.js +81 -0
  69. package/dist/discord-utils.js +591 -0
  70. package/dist/discord-utils.test.js +134 -0
  71. package/dist/errors.js +157 -0
  72. package/dist/escape-backticks.test.js +429 -0
  73. package/dist/event-stream-real-capture.e2e.test.js +533 -0
  74. package/dist/eventsource-parser.test.js +327 -0
  75. package/dist/exec-async.js +26 -0
  76. package/dist/external-opencode-sync.js +480 -0
  77. package/dist/format-tables.js +302 -0
  78. package/dist/format-tables.test.js +308 -0
  79. package/dist/forum-sync/config.js +79 -0
  80. package/dist/forum-sync/discord-operations.js +154 -0
  81. package/dist/forum-sync/index.js +5 -0
  82. package/dist/forum-sync/markdown.js +113 -0
  83. package/dist/forum-sync/sync-to-discord.js +417 -0
  84. package/dist/forum-sync/sync-to-files.js +190 -0
  85. package/dist/forum-sync/types.js +53 -0
  86. package/dist/forum-sync/watchers.js +307 -0
  87. package/dist/gateway-proxy-reconnect.e2e.test.js +394 -0
  88. package/dist/gateway-proxy.e2e.test.js +483 -0
  89. package/dist/genai-worker-wrapper.js +111 -0
  90. package/dist/genai-worker.js +311 -0
  91. package/dist/genai.js +232 -0
  92. package/dist/generated/browser.js +17 -0
  93. package/dist/generated/client.js +37 -0
  94. package/dist/generated/commonInputTypes.js +10 -0
  95. package/dist/generated/enums.js +52 -0
  96. package/dist/generated/internal/class.js +49 -0
  97. package/dist/generated/internal/prismaNamespace.js +253 -0
  98. package/dist/generated/internal/prismaNamespaceBrowser.js +223 -0
  99. package/dist/generated/models/bot_api_keys.js +1 -0
  100. package/dist/generated/models/bot_tokens.js +1 -0
  101. package/dist/generated/models/channel_agents.js +1 -0
  102. package/dist/generated/models/channel_directories.js +1 -0
  103. package/dist/generated/models/channel_mention_mode.js +1 -0
  104. package/dist/generated/models/channel_models.js +1 -0
  105. package/dist/generated/models/channel_verbosity.js +1 -0
  106. package/dist/generated/models/channel_worktrees.js +1 -0
  107. package/dist/generated/models/forum_sync_configs.js +1 -0
  108. package/dist/generated/models/global_models.js +1 -0
  109. package/dist/generated/models/ipc_requests.js +1 -0
  110. package/dist/generated/models/part_messages.js +1 -0
  111. package/dist/generated/models/scheduled_tasks.js +1 -0
  112. package/dist/generated/models/session_agents.js +1 -0
  113. package/dist/generated/models/session_events.js +1 -0
  114. package/dist/generated/models/session_models.js +1 -0
  115. package/dist/generated/models/session_start_sources.js +1 -0
  116. package/dist/generated/models/thread_sessions.js +1 -0
  117. package/dist/generated/models/thread_worktrees.js +1 -0
  118. package/dist/generated/models.js +1 -0
  119. package/dist/heap-monitor.js +122 -0
  120. package/dist/hrana-server.js +263 -0
  121. package/dist/hrana-server.test.js +370 -0
  122. package/dist/html-actions.js +123 -0
  123. package/dist/html-actions.test.js +70 -0
  124. package/dist/html-components.js +117 -0
  125. package/dist/html-components.test.js +34 -0
  126. package/dist/image-optimizer-plugin.js +153 -0
  127. package/dist/image-utils.js +112 -0
  128. package/dist/interaction-handler.js +397 -0
  129. package/dist/ipc-polling.js +252 -0
  130. package/dist/ipc-tools-plugin.js +193 -0
  131. package/dist/kimaki-digital-twin.e2e.test.js +161 -0
  132. package/dist/kimaki-opencode-plugin-loading.e2e.test.js +87 -0
  133. package/dist/kimaki-opencode-plugin.js +17 -0
  134. package/dist/kimaki-opencode-plugin.test.js +98 -0
  135. package/dist/limit-heading-depth.js +25 -0
  136. package/dist/limit-heading-depth.test.js +105 -0
  137. package/dist/logger.js +165 -0
  138. package/dist/markdown.js +342 -0
  139. package/dist/markdown.test.js +257 -0
  140. package/dist/message-finish-field.e2e.test.js +165 -0
  141. package/dist/message-formatting.js +413 -0
  142. package/dist/message-formatting.test.js +73 -0
  143. package/dist/message-preprocessing.js +330 -0
  144. package/dist/onboarding-tutorial.js +172 -0
  145. package/dist/onboarding-welcome.js +37 -0
  146. package/dist/openai-realtime.js +224 -0
  147. package/dist/opencode-command-detection.js +65 -0
  148. package/dist/opencode-command-detection.test.js +240 -0
  149. package/dist/opencode-command.js +129 -0
  150. package/dist/opencode-command.test.js +48 -0
  151. package/dist/opencode-interrupt-plugin.js +361 -0
  152. package/dist/opencode-interrupt-plugin.test.js +458 -0
  153. package/dist/opencode.js +861 -0
  154. package/dist/otto/branding.js +22 -0
  155. package/dist/otto/index.js +21 -0
  156. package/dist/parse-permission-rules.test.js +117 -0
  157. package/dist/patch-text-parser.js +97 -0
  158. package/dist/plugin-logger.js +59 -0
  159. package/dist/privacy-sanitizer.js +105 -0
  160. package/dist/queue-advanced-abort.e2e.test.js +293 -0
  161. package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
  162. package/dist/queue-advanced-e2e-setup.js +786 -0
  163. package/dist/queue-advanced-footer.e2e.test.js +472 -0
  164. package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
  165. package/dist/queue-advanced-permissions-typing.e2e.test.js +180 -0
  166. package/dist/queue-advanced-question.e2e.test.js +261 -0
  167. package/dist/queue-advanced-typing-interrupt.e2e.test.js +114 -0
  168. package/dist/queue-advanced-typing.e2e.test.js +153 -0
  169. package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
  170. package/dist/queue-interrupt-drain.e2e.test.js +135 -0
  171. package/dist/queue-question-select-drain.e2e.test.js +120 -0
  172. package/dist/runtime-idle-sweeper.js +52 -0
  173. package/dist/runtime-lifecycle.e2e.test.js +508 -0
  174. package/dist/sentry.js +23 -0
  175. package/dist/session-handler/agent-utils.js +67 -0
  176. package/dist/session-handler/event-stream-state.js +420 -0
  177. package/dist/session-handler/event-stream-state.test.js +563 -0
  178. package/dist/session-handler/model-utils.js +124 -0
  179. package/dist/session-handler/opencode-session-event-log.js +94 -0
  180. package/dist/session-handler/thread-runtime-state.js +104 -0
  181. package/dist/session-handler/thread-session-runtime.js +3258 -0
  182. package/dist/session-handler.js +9 -0
  183. package/dist/session-search.js +100 -0
  184. package/dist/session-search.test.js +40 -0
  185. package/dist/session-title-rename.test.js +80 -0
  186. package/dist/startup-service.js +153 -0
  187. package/dist/startup-time.e2e.test.js +296 -0
  188. package/dist/store.js +17 -0
  189. package/dist/system-message.js +613 -0
  190. package/dist/system-message.test.js +602 -0
  191. package/dist/task-runner.js +295 -0
  192. package/dist/task-schedule.js +209 -0
  193. package/dist/task-schedule.test.js +71 -0
  194. package/dist/test-utils.js +299 -0
  195. package/dist/thinking-utils.js +35 -0
  196. package/dist/thread-message-queue.e2e.test.js +999 -0
  197. package/dist/tools.js +357 -0
  198. package/dist/undo-redo.e2e.test.js +161 -0
  199. package/dist/unnest-code-blocks.js +146 -0
  200. package/dist/unnest-code-blocks.test.js +673 -0
  201. package/dist/upgrade.js +114 -0
  202. package/dist/utils.js +144 -0
  203. package/dist/voice-attachment.js +34 -0
  204. package/dist/voice-handler.js +646 -0
  205. package/dist/voice-message.e2e.test.js +1021 -0
  206. package/dist/voice.js +447 -0
  207. package/dist/voice.test.js +235 -0
  208. package/dist/wait-session.js +94 -0
  209. package/dist/websockify.js +69 -0
  210. package/dist/worker-types.js +4 -0
  211. package/dist/worktree-lifecycle.e2e.test.js +308 -0
  212. package/dist/worktree-utils.js +3 -0
  213. package/dist/worktrees.js +929 -0
  214. package/dist/worktrees.test.js +189 -0
  215. package/dist/xml.js +92 -0
  216. package/dist/xml.test.js +32 -0
  217. package/package.json +98 -0
  218. package/schema.prisma +295 -0
  219. package/skills/batch/SKILL.md +87 -0
  220. package/skills/critique/SKILL.md +112 -0
  221. package/skills/egaki/SKILL.md +100 -0
  222. package/skills/errore/SKILL.md +647 -0
  223. package/skills/event-sourcing-state/SKILL.md +252 -0
  224. package/skills/gitchamber/SKILL.md +93 -0
  225. package/skills/goke/SKILL.md +644 -0
  226. package/skills/jitter/EDITOR.md +219 -0
  227. package/skills/jitter/EXPORT-INTERNALS.md +309 -0
  228. package/skills/jitter/SKILL.md +158 -0
  229. package/skills/jitter/jitter-clipboard.json +1042 -0
  230. package/skills/jitter/package.json +14 -0
  231. package/skills/jitter/tsconfig.json +15 -0
  232. package/skills/jitter/utils/actions.ts +212 -0
  233. package/skills/jitter/utils/export.ts +114 -0
  234. package/skills/jitter/utils/index.ts +141 -0
  235. package/skills/jitter/utils/snapshot.ts +154 -0
  236. package/skills/jitter/utils/traverse.ts +246 -0
  237. package/skills/jitter/utils/types.ts +279 -0
  238. package/skills/jitter/utils/wait.ts +133 -0
  239. package/skills/lintcn/SKILL.md +873 -0
  240. package/skills/new-skill/SKILL.md +211 -0
  241. package/skills/npm-package/SKILL.md +239 -0
  242. package/skills/playwriter/SKILL.md +35 -0
  243. package/skills/proxyman/SKILL.md +215 -0
  244. package/skills/security-review/SKILL.md +208 -0
  245. package/skills/simplify/SKILL.md +58 -0
  246. package/skills/spiceflow/SKILL.md +14 -0
  247. package/skills/termcast/SKILL.md +945 -0
  248. package/skills/tuistory/SKILL.md +250 -0
  249. package/skills/usecomputer/SKILL.md +264 -0
  250. package/skills/x-articles/SKILL.md +554 -0
  251. package/skills/zele/SKILL.md +112 -0
  252. package/skills/zustand-centralized-state/SKILL.md +1004 -0
  253. package/src/agent-model.e2e.test.ts +976 -0
  254. package/src/ai-tool-to-genai.test.ts +296 -0
  255. package/src/ai-tool-to-genai.ts +283 -0
  256. package/src/ai-tool.ts +39 -0
  257. package/src/anthropic-auth-plugin.test.ts +159 -0
  258. package/src/anthropic-auth-plugin.ts +861 -0
  259. package/src/anthropic-auth-state.ts +282 -0
  260. package/src/bin.ts +111 -0
  261. package/src/channel-management.ts +334 -0
  262. package/src/cli-parsing.test.ts +195 -0
  263. package/src/cli-send-thread.e2e.test.ts +464 -0
  264. package/src/cli.ts +4581 -0
  265. package/src/commands/abort.ts +89 -0
  266. package/src/commands/action-buttons.ts +364 -0
  267. package/src/commands/add-project.ts +149 -0
  268. package/src/commands/agent.ts +473 -0
  269. package/src/commands/ask-question.ts +390 -0
  270. package/src/commands/btw.ts +164 -0
  271. package/src/commands/compact.ts +157 -0
  272. package/src/commands/context-usage.ts +199 -0
  273. package/src/commands/create-new-project.ts +190 -0
  274. package/src/commands/diff.ts +91 -0
  275. package/src/commands/file-upload.ts +389 -0
  276. package/src/commands/fork.ts +321 -0
  277. package/src/commands/gemini-apikey.ts +104 -0
  278. package/src/commands/login.ts +1173 -0
  279. package/src/commands/mcp.ts +307 -0
  280. package/src/commands/memory-snapshot.ts +30 -0
  281. package/src/commands/mention-mode.ts +68 -0
  282. package/src/commands/merge-worktree.ts +223 -0
  283. package/src/commands/model-variant.ts +483 -0
  284. package/src/commands/model.ts +1053 -0
  285. package/src/commands/new-worktree.ts +510 -0
  286. package/src/commands/paginated-select.ts +81 -0
  287. package/src/commands/permissions.ts +397 -0
  288. package/src/commands/queue.ts +271 -0
  289. package/src/commands/remove-project.ts +155 -0
  290. package/src/commands/restart-opencode-server.ts +162 -0
  291. package/src/commands/resume.ts +230 -0
  292. package/src/commands/run-command.ts +123 -0
  293. package/src/commands/screenshare.test.ts +30 -0
  294. package/src/commands/screenshare.ts +366 -0
  295. package/src/commands/session-id.ts +109 -0
  296. package/src/commands/session.ts +227 -0
  297. package/src/commands/share.ts +106 -0
  298. package/src/commands/tasks.ts +293 -0
  299. package/src/commands/types.ts +25 -0
  300. package/src/commands/undo-redo.ts +386 -0
  301. package/src/commands/unset-model.ts +173 -0
  302. package/src/commands/upgrade.ts +52 -0
  303. package/src/commands/user-command.ts +198 -0
  304. package/src/commands/verbosity.ts +173 -0
  305. package/src/commands/worktree-settings.ts +70 -0
  306. package/src/commands/worktrees.ts +552 -0
  307. package/src/condense-memory.ts +36 -0
  308. package/src/config.ts +111 -0
  309. package/src/context-awareness-plugin.test.ts +142 -0
  310. package/src/context-awareness-plugin.ts +510 -0
  311. package/src/critique-utils.ts +139 -0
  312. package/src/database.ts +1876 -0
  313. package/src/db.test.ts +162 -0
  314. package/src/db.ts +286 -0
  315. package/src/debounce-timeout.ts +43 -0
  316. package/src/debounced-process-flush.ts +104 -0
  317. package/src/discord-bot.ts +1330 -0
  318. package/src/discord-command-registration.ts +693 -0
  319. package/src/discord-urls.ts +88 -0
  320. package/src/discord-utils.test.ts +153 -0
  321. package/src/discord-utils.ts +800 -0
  322. package/src/errors.ts +201 -0
  323. package/src/escape-backticks.test.ts +469 -0
  324. package/src/event-stream-real-capture.e2e.test.ts +692 -0
  325. package/src/eventsource-parser.test.ts +351 -0
  326. package/src/exec-async.ts +35 -0
  327. package/src/external-opencode-sync.ts +685 -0
  328. package/src/format-tables.test.ts +335 -0
  329. package/src/format-tables.ts +445 -0
  330. package/src/forum-sync/config.ts +92 -0
  331. package/src/forum-sync/discord-operations.ts +241 -0
  332. package/src/forum-sync/index.ts +9 -0
  333. package/src/forum-sync/markdown.ts +172 -0
  334. package/src/forum-sync/sync-to-discord.ts +595 -0
  335. package/src/forum-sync/sync-to-files.ts +294 -0
  336. package/src/forum-sync/types.ts +175 -0
  337. package/src/forum-sync/watchers.ts +454 -0
  338. package/src/gateway-proxy-reconnect.e2e.test.ts +523 -0
  339. package/src/gateway-proxy.e2e.test.ts +640 -0
  340. package/src/genai-worker-wrapper.ts +164 -0
  341. package/src/genai-worker.ts +386 -0
  342. package/src/genai.ts +321 -0
  343. package/src/generated/browser.ts +114 -0
  344. package/src/generated/client.ts +138 -0
  345. package/src/generated/commonInputTypes.ts +736 -0
  346. package/src/generated/enums.ts +88 -0
  347. package/src/generated/internal/class.ts +384 -0
  348. package/src/generated/internal/prismaNamespace.ts +2386 -0
  349. package/src/generated/internal/prismaNamespaceBrowser.ts +326 -0
  350. package/src/generated/models/bot_api_keys.ts +1288 -0
  351. package/src/generated/models/bot_tokens.ts +1656 -0
  352. package/src/generated/models/channel_agents.ts +1256 -0
  353. package/src/generated/models/channel_directories.ts +1859 -0
  354. package/src/generated/models/channel_mention_mode.ts +1300 -0
  355. package/src/generated/models/channel_models.ts +1288 -0
  356. package/src/generated/models/channel_verbosity.ts +1228 -0
  357. package/src/generated/models/channel_worktrees.ts +1300 -0
  358. package/src/generated/models/forum_sync_configs.ts +1452 -0
  359. package/src/generated/models/global_models.ts +1288 -0
  360. package/src/generated/models/ipc_requests.ts +1485 -0
  361. package/src/generated/models/part_messages.ts +1302 -0
  362. package/src/generated/models/scheduled_tasks.ts +2320 -0
  363. package/src/generated/models/session_agents.ts +1086 -0
  364. package/src/generated/models/session_events.ts +1439 -0
  365. package/src/generated/models/session_models.ts +1114 -0
  366. package/src/generated/models/session_start_sources.ts +1408 -0
  367. package/src/generated/models/thread_sessions.ts +1781 -0
  368. package/src/generated/models/thread_worktrees.ts +1356 -0
  369. package/src/generated/models.ts +30 -0
  370. package/src/heap-monitor.ts +152 -0
  371. package/src/hrana-server.test.ts +434 -0
  372. package/src/hrana-server.ts +314 -0
  373. package/src/html-actions.test.ts +87 -0
  374. package/src/html-actions.ts +174 -0
  375. package/src/html-components.test.ts +38 -0
  376. package/src/html-components.ts +181 -0
  377. package/src/image-optimizer-plugin.ts +194 -0
  378. package/src/image-utils.ts +149 -0
  379. package/src/interaction-handler.ts +576 -0
  380. package/src/ipc-polling.ts +326 -0
  381. package/src/ipc-tools-plugin.ts +236 -0
  382. package/src/kimaki-digital-twin.e2e.test.ts +199 -0
  383. package/src/kimaki-opencode-plugin-loading.e2e.test.ts +109 -0
  384. package/src/kimaki-opencode-plugin.test.ts +108 -0
  385. package/src/kimaki-opencode-plugin.ts +18 -0
  386. package/src/limit-heading-depth.test.ts +116 -0
  387. package/src/limit-heading-depth.ts +26 -0
  388. package/src/logger.ts +208 -0
  389. package/src/markdown.test.ts +308 -0
  390. package/src/markdown.ts +410 -0
  391. package/src/message-finish-field.e2e.test.ts +192 -0
  392. package/src/message-formatting.test.ts +81 -0
  393. package/src/message-formatting.ts +533 -0
  394. package/src/message-preprocessing.ts +455 -0
  395. package/src/onboarding-tutorial.ts +176 -0
  396. package/src/onboarding-welcome.ts +49 -0
  397. package/src/openai-realtime.ts +358 -0
  398. package/src/opencode-command-detection.test.ts +307 -0
  399. package/src/opencode-command-detection.ts +76 -0
  400. package/src/opencode-command.test.ts +70 -0
  401. package/src/opencode-command.ts +188 -0
  402. package/src/opencode-interrupt-plugin.test.ts +677 -0
  403. package/src/opencode-interrupt-plugin.ts +477 -0
  404. package/src/opencode.ts +1110 -0
  405. package/src/otto/branding.ts +23 -0
  406. package/src/otto/index.ts +22 -0
  407. package/src/parse-permission-rules.test.ts +127 -0
  408. package/src/patch-text-parser.ts +107 -0
  409. package/src/plugin-logger.ts +68 -0
  410. package/src/privacy-sanitizer.ts +142 -0
  411. package/src/queue-advanced-abort.e2e.test.ts +382 -0
  412. package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
  413. package/src/queue-advanced-e2e-setup.ts +873 -0
  414. package/src/queue-advanced-footer.e2e.test.ts +576 -0
  415. package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
  416. package/src/queue-advanced-permissions-typing.e2e.test.ts +245 -0
  417. package/src/queue-advanced-question.e2e.test.ts +316 -0
  418. package/src/queue-advanced-typing-interrupt.e2e.test.ts +146 -0
  419. package/src/queue-advanced-typing.e2e.test.ts +199 -0
  420. package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
  421. package/src/queue-interrupt-drain.e2e.test.ts +166 -0
  422. package/src/queue-question-select-drain.e2e.test.ts +152 -0
  423. package/src/runtime-idle-sweeper.ts +76 -0
  424. package/src/runtime-lifecycle.e2e.test.ts +641 -0
  425. package/src/schema.sql +173 -0
  426. package/src/sentry.ts +26 -0
  427. package/src/session-handler/agent-utils.ts +97 -0
  428. package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
  429. package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
  430. package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
  431. package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
  432. package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
  433. package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
  434. package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +21 -0
  435. package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
  436. package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
  437. package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
  438. package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
  439. package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
  440. package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
  441. package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
  442. package/src/session-handler/event-stream-state.test.ts +645 -0
  443. package/src/session-handler/event-stream-state.ts +608 -0
  444. package/src/session-handler/model-utils.ts +183 -0
  445. package/src/session-handler/opencode-session-event-log.ts +130 -0
  446. package/src/session-handler/thread-runtime-state.ts +212 -0
  447. package/src/session-handler/thread-session-runtime.ts +4281 -0
  448. package/src/session-handler.ts +15 -0
  449. package/src/session-search.test.ts +50 -0
  450. package/src/session-search.ts +148 -0
  451. package/src/session-title-rename.test.ts +112 -0
  452. package/src/startup-service.ts +200 -0
  453. package/src/startup-time.e2e.test.ts +373 -0
  454. package/src/store.ts +122 -0
  455. package/src/system-message.test.ts +612 -0
  456. package/src/system-message.ts +723 -0
  457. package/src/task-runner.ts +421 -0
  458. package/src/task-schedule.test.ts +84 -0
  459. package/src/task-schedule.ts +311 -0
  460. package/src/test-utils.ts +435 -0
  461. package/src/thinking-utils.ts +61 -0
  462. package/src/thread-message-queue.e2e.test.ts +1219 -0
  463. package/src/tools.ts +430 -0
  464. package/src/undici.d.ts +12 -0
  465. package/src/undo-redo.e2e.test.ts +209 -0
  466. package/src/unnest-code-blocks.test.ts +713 -0
  467. package/src/unnest-code-blocks.ts +185 -0
  468. package/src/upgrade.ts +127 -0
  469. package/src/utils.ts +212 -0
  470. package/src/voice-attachment.ts +51 -0
  471. package/src/voice-handler.ts +908 -0
  472. package/src/voice-message.e2e.test.ts +1255 -0
  473. package/src/voice.test.ts +281 -0
  474. package/src/voice.ts +627 -0
  475. package/src/wait-session.ts +147 -0
  476. package/src/websockify.ts +101 -0
  477. package/src/worker-types.ts +64 -0
  478. package/src/worktree-lifecycle.e2e.test.ts +391 -0
  479. package/src/worktree-utils.ts +4 -0
  480. package/src/worktrees.test.ts +223 -0
  481. package/src/worktrees.ts +1294 -0
  482. package/src/xml.test.ts +38 -0
  483. package/src/xml.ts +121 -0
@@ -0,0 +1,945 @@
1
+ ---
2
+ name: termcast
3
+ description: Build TUIs with a Raycast-like React API using termcast. Implements @raycast/api components (List, Detail, Form, Action) rendered to the terminal via opentui.
4
+ ---
5
+
6
+ # termcast — Build TUIs with a Raycast-like React API
7
+
8
+ termcast is a framework for building terminal user interfaces using React. It implements the Raycast extension API (`@raycast/api`) but renders to the terminal via opentui. If you know Raycast, you know termcast.
9
+
10
+ ```bash
11
+ bun install -g termcast
12
+ termcast new my-extension # scaffold
13
+ cd my-extension && termcast dev # hot-reload dev mode
14
+ ```
15
+
16
+ IMPORTANT: before starting every task ALWAYS read opentui docs:
17
+ ```bash
18
+ curl -s https://raw.githubusercontent.com/sst/opentui/refs/heads/main/packages/react/README.md
19
+ ```
20
+
21
+ ## Imports
22
+
23
+ For **new projects**, import from `termcast` and `@termcast/utils`:
24
+
25
+ ```tsx
26
+ import { List, Detail, Action, ActionPanel, showToast, Toast, Icon, Color } from 'termcast'
27
+ import { useCachedPromise, useCachedState } from '@termcast/utils'
28
+ ```
29
+
30
+ `@raycast/api` imports still work (for porting existing extensions) but `termcast` is preferred for new code.
31
+
32
+ ## Project Structure
33
+
34
+ ```
35
+ my-extension/
36
+ package.json # must have "commands" array
37
+ src/
38
+ index.tsx # default command entry point
39
+ other-command.tsx # additional commands
40
+ ```
41
+
42
+ **package.json** must declare commands:
43
+
44
+ ```json
45
+ {
46
+ "name": "my-extension",
47
+ "commands": [
48
+ {
49
+ "name": "index",
50
+ "title": "Browse Items",
51
+ "description": "Main command",
52
+ "mode": "view"
53
+ }
54
+ ],
55
+ "dependencies": {
56
+ "termcast": "latest",
57
+ "@termcast/utils": "latest"
58
+ }
59
+ }
60
+ ```
61
+
62
+ Each command file exports a default React component:
63
+
64
+ ```tsx
65
+ export default function Command() {
66
+ return <List>...</List>
67
+ }
68
+ ```
69
+
70
+ For standalone scripts (examples, prototyping), use `renderWithProviders`:
71
+
72
+ ```tsx
73
+ import { renderWithProviders } from 'termcast'
74
+
75
+ await renderWithProviders(<MyComponent />, {
76
+ extensionName: 'my-app', // required for LocalStorage/Cache to work
77
+ })
78
+ ```
79
+
80
+ ---
81
+
82
+ ## 1. List — The Core Component
83
+
84
+ The simplest termcast app is a searchable list:
85
+
86
+ ```tsx
87
+ import { List } from 'termcast'
88
+
89
+ export default function Command() {
90
+ return (
91
+ <List searchBarPlaceholder="Search items...">
92
+ <List.Item title="First Item" subtitle="A subtitle" />
93
+ <List.Item title="Second Item" accessories={[{ text: 'Badge' }]} />
94
+ <List.Item
95
+ title="Third Item"
96
+ accessories={[
97
+ { tag: { value: 'Important', color: Color.Red } },
98
+ { date: new Date() },
99
+ ]}
100
+ />
101
+ </List>
102
+ )
103
+ }
104
+ ```
105
+
106
+ Key props on `List`:
107
+ - `navigationTitle` — title in the top bar
108
+ - `searchBarPlaceholder` — placeholder text in search
109
+ - `isLoading` — shows a loading indicator
110
+ - `isShowingDetail` — enables the side detail panel
111
+ - `spacingMode` — `'default'` (single-line) or `'relaxed'` (two-line items)
112
+ - `onSelectionChange` — callback when selection moves
113
+ - `onSearchTextChange` — callback when search text changes
114
+ - `throttle` — throttle search change events
115
+
116
+ Key props on `List.Item`:
117
+ - `title`, `subtitle` — main text
118
+ - `icon` — emoji string or `{ source: Icon.Star, tintColor: Color.Orange }`
119
+ - `accessories` — array of `{ text?, tag?, date?, icon? }`
120
+ - `keywords` — extra search terms
121
+ - `id` — stable identifier for selection tracking
122
+ - `detail` — side panel content (when `isShowingDetail` is true)
123
+ - `actions` — ActionPanel for this item
124
+
125
+ ## 2. Actions
126
+
127
+ Actions are what users can do. The first action triggers on Enter. All actions show in the action panel (ctrl+k).
128
+
129
+ ```tsx
130
+ import { List, Action, ActionPanel, showToast, Toast, Icon } from 'termcast'
131
+
132
+ <List.Item
133
+ title="My Item"
134
+ actions={
135
+ <ActionPanel>
136
+ <Action
137
+ title="Open"
138
+ icon={Icon.Eye}
139
+ onAction={() => { /* primary action on Enter */ }}
140
+ />
141
+ <Action
142
+ title="Refresh"
143
+ icon={Icon.ArrowClockwise}
144
+ shortcut={{ modifiers: ['ctrl'], key: 'r' }}
145
+ onAction={() => { /* triggered by ctrl+r directly */ }}
146
+ />
147
+ <Action.CopyToClipboard title="Copy Name" content="My Item" />
148
+ </ActionPanel>
149
+ }
150
+ />
151
+ ```
152
+
153
+ ### Action sections
154
+
155
+ Group related actions:
156
+
157
+ ```tsx
158
+ <ActionPanel>
159
+ <ActionPanel.Section title="Primary">
160
+ <Action title="Open" onAction={() => {}} />
161
+ </ActionPanel.Section>
162
+ <ActionPanel.Section title="Copy">
163
+ <Action.CopyToClipboard title="Copy ID" content={item.id} />
164
+ <Action.CopyToClipboard title="Copy Title" content={item.title} />
165
+ </ActionPanel.Section>
166
+ </ActionPanel>
167
+ ```
168
+
169
+ ### Built-in action types
170
+
171
+ - `Action` — generic action with `onAction`
172
+ - `Action.Push` — push a new view onto the navigation stack
173
+ - `Action.CopyToClipboard` — copy text to clipboard
174
+ - `Action.SubmitForm` — submit a form (used inside Form)
175
+
176
+ ### Keyboard shortcuts
177
+
178
+ Shortcuts use `ctrl` or `alt` modifiers with letter keys. `cmd` (hyper) does **not** work in terminals — the parent terminal app intercepts it.
179
+
180
+ ```tsx
181
+ shortcut={{ modifiers: ['ctrl'], key: 'r' }} // ctrl+r
182
+ shortcut={{ modifiers: ['ctrl', 'shift'], key: 'r' }} // ctrl+shift+r
183
+ shortcut={{ modifiers: ['alt'], key: 'd' }} // alt+d
184
+ // Also available: Keyboard.Shortcut.Common.Refresh, etc.
185
+ ```
186
+
187
+ **Note**: `ctrl+digit` shortcuts don't work reliably. Always use letters.
188
+
189
+ ## 3. Navigation
190
+
191
+ Push and pop views onto a navigation stack. Esc goes back.
192
+
193
+ ```tsx
194
+ import { useNavigation, Detail, Action, ActionPanel } from 'termcast'
195
+
196
+ function ItemDetail({ item }: { item: Item }) {
197
+ const { pop } = useNavigation()
198
+ return (
199
+ <Detail
200
+ navigationTitle={item.title}
201
+ markdown={`# ${item.title}\n\n${item.description}`}
202
+ actions={
203
+ <ActionPanel>
204
+ <Action title="Go Back" onAction={() => { pop() }} />
205
+ </ActionPanel>
206
+ }
207
+ />
208
+ )
209
+ }
210
+
211
+ // In a list item:
212
+ function MyList() {
213
+ const { push } = useNavigation()
214
+ return (
215
+ <List>
216
+ <List.Item
217
+ title="Item A"
218
+ actions={
219
+ <ActionPanel>
220
+ <Action
221
+ title="View Detail"
222
+ onAction={() => { push(<ItemDetail item={itemA} />) }}
223
+ />
224
+ {/* Or use Action.Push for declarative navigation */}
225
+ <Action.Push
226
+ title="View Detail"
227
+ target={<ItemDetail item={itemA} />}
228
+ />
229
+ </ActionPanel>
230
+ }
231
+ />
232
+ </List>
233
+ )
234
+ }
235
+ ```
236
+
237
+ **Important**: props passed via `push()` are captured at push time and won't sync with parent state changes. If the child needs reactive parent state, use zustand or pass a zustand store via props.
238
+
239
+ ## 4. Detail View
240
+
241
+ Full-screen markdown view with optional metadata sidebar:
242
+
243
+ ```tsx
244
+ import { Detail, Color } from 'termcast'
245
+
246
+ <Detail
247
+ navigationTitle="Server Status"
248
+ markdown={`# Server Status\n\nAll systems operational.\n\n| Service | Status |\n|---------|--------|\n| API | Running |\n| DB | Running |`}
249
+ metadata={
250
+ <Detail.Metadata>
251
+ <Detail.Metadata.Label title="Status" text={{ value: "Active", color: Color.Green }} />
252
+ <Detail.Metadata.Label title="Uptime" text="14d 3h" />
253
+ <Detail.Metadata.Separator />
254
+ <Detail.Metadata.Link
255
+ title="Dashboard"
256
+ target="https://example.com"
257
+ text="example.com"
258
+ />
259
+ <Detail.Metadata.Separator />
260
+ <Detail.Metadata.TagList title="Tags">
261
+ <Detail.Metadata.TagList.Item text="production" color={Color.Green} />
262
+ <Detail.Metadata.TagList.Item text="critical" color={Color.Red} />
263
+ </Detail.Metadata.TagList>
264
+ </Detail.Metadata>
265
+ }
266
+ actions={
267
+ <ActionPanel>
268
+ <Action title="Refresh" onAction={() => {}} />
269
+ </ActionPanel>
270
+ }
271
+ />
272
+ ```
273
+
274
+ ### Metadata components
275
+
276
+ - `Label` — key-value row. `text` can be a string or `{ value, color }`
277
+ - `Separator` — horizontal divider
278
+ - `Link` — clickable link (OSC 8 hyperlinks in supported terminals)
279
+ - `TagList` — row of colored tags via `TagList.Item`
280
+
281
+ ## 5. List with Side Detail Panel
282
+
283
+ Show a detail panel alongside the list. The detail updates as the user navigates items:
284
+
285
+ ```tsx
286
+ <List isShowingDetail={true} navigationTitle="Pokemon List">
287
+ {pokemons.map((pokemon) => (
288
+ <List.Item
289
+ key={pokemon.id}
290
+ title={pokemon.name}
291
+ subtitle={`#${pokemon.id}`}
292
+ detail={
293
+ <List.Item.Detail
294
+ markdown={`# ${pokemon.name}\n\nTypes: ${pokemon.types.join(', ')}`}
295
+ metadata={
296
+ <List.Item.Detail.Metadata>
297
+ <List.Item.Detail.Metadata.Label title="Height" text={`${pokemon.height}m`} />
298
+ <List.Item.Detail.Metadata.Label title="Weight" text={`${pokemon.weight}kg`} />
299
+ <List.Item.Detail.Metadata.Separator />
300
+ <List.Item.Detail.Metadata.TagList title="Types">
301
+ {pokemon.types.map((t) => (
302
+ <List.Item.Detail.Metadata.TagList.Item key={t} text={t} />
303
+ ))}
304
+ </List.Item.Detail.Metadata.TagList>
305
+ </List.Item.Detail.Metadata>
306
+ }
307
+ />
308
+ }
309
+ actions={
310
+ <ActionPanel>
311
+ <Action title="Toggle Detail" onAction={() => { setShowingDetail(!showingDetail) }} />
312
+ </ActionPanel>
313
+ }
314
+ />
315
+ ))}
316
+ </List>
317
+ ```
318
+
319
+ ## 6. Sections and Dropdowns
320
+
321
+ ### Sections
322
+
323
+ Group items with headers:
324
+
325
+ ```tsx
326
+ <List>
327
+ <List.Section title="Fruits">
328
+ <List.Item title="Apple" />
329
+ <List.Item title="Banana" />
330
+ </List.Section>
331
+ <List.Section title="Vegetables">
332
+ <List.Item title="Carrot" />
333
+ </List.Section>
334
+ </List>
335
+ ```
336
+
337
+ Empty sections are automatically hidden.
338
+
339
+ ### Dropdown filter
340
+
341
+ Add a dropdown next to the search bar:
342
+
343
+ ```tsx
344
+ <List
345
+ searchBarAccessory={
346
+ <List.Dropdown tooltip="Category" onChange={setCategory}>
347
+ <List.Dropdown.Item title="All" value="all" />
348
+ <List.Dropdown.Section title="Types">
349
+ <List.Dropdown.Item title="Beer" value="beer" />
350
+ <List.Dropdown.Item title="Wine" value="wine" />
351
+ </List.Dropdown.Section>
352
+ </List.Dropdown>
353
+ }
354
+ >
355
+ {filteredItems.map((item) => (
356
+ <List.Item key={item.id} title={item.name} />
357
+ ))}
358
+ </List>
359
+ ```
360
+
361
+ ## 7. Forms
362
+
363
+ Collect user input. Navigate fields with Tab/arrows. Submit with ctrl+enter or via action panel.
364
+
365
+ ```tsx
366
+ import { Form, Action, ActionPanel, showToast, Toast } from 'termcast'
367
+
368
+ function CreateItem() {
369
+ return (
370
+ <Form
371
+ navigationTitle="New Item"
372
+ actions={
373
+ <ActionPanel>
374
+ <Action.SubmitForm
375
+ title="Create"
376
+ onSubmit={async (values) => {
377
+ await showToast({ style: Toast.Style.Success, title: 'Created!' })
378
+ }}
379
+ />
380
+ </ActionPanel>
381
+ }
382
+ >
383
+ <Form.TextField id="name" title="Name" placeholder="Item name" />
384
+ <Form.TextArea id="description" title="Description" placeholder="Describe..." />
385
+ <Form.Dropdown id="priority" title="Priority">
386
+ <Form.Dropdown.Item value="high" title="High" />
387
+ <Form.Dropdown.Item value="medium" title="Medium" />
388
+ <Form.Dropdown.Item value="low" title="Low" />
389
+ </Form.Dropdown>
390
+ <Form.Checkbox id="urgent" title="Flags" label="Mark as urgent" />
391
+ <Form.DatePicker id="dueDate" title="Due Date" type={Form.DatePicker.Type.Date} />
392
+ <Form.Separator />
393
+ <Form.Description title="Help" text="Tab to move between fields. ctrl+enter to submit." />
394
+ </Form>
395
+ )
396
+ }
397
+ ```
398
+
399
+ Form field types: `TextField`, `PasswordField`, `TextArea`, `Checkbox`, `Dropdown`, `DatePicker`, `TagPicker`, `FilePicker`, `Separator`, `Description`.
400
+
401
+ ## 8. Toasts
402
+
403
+ Show feedback to the user:
404
+
405
+ ```tsx
406
+ import { showToast, Toast, showFailureToast } from 'termcast'
407
+
408
+ // Success
409
+ await showToast({ style: Toast.Style.Success, title: 'Saved', message: 'Item updated' })
410
+
411
+ // Failure
412
+ await showToast({ style: Toast.Style.Failure, title: 'Error', message: 'Connection failed' })
413
+
414
+ // From a caught error (shows title + error message)
415
+ await showFailureToast(error, { title: 'Failed to fetch' })
416
+ ```
417
+
418
+ ---
419
+
420
+ ## Data Fetching
421
+
422
+ ### useCachedPromise
423
+
424
+ The primary hook for async data. Handles loading state, caching, revalidation, and pagination.
425
+
426
+ ```tsx
427
+ import { useCachedPromise } from '@termcast/utils'
428
+
429
+ function MyList() {
430
+ const { data, isLoading, revalidate } = useCachedPromise(
431
+ async (query: string) => {
432
+ const response = await fetch(`/api/search?q=${query}`)
433
+ return response.json()
434
+ },
435
+ [searchText], // re-fetches when these change
436
+ )
437
+
438
+ return (
439
+ <List isLoading={isLoading}>
440
+ {data?.map((item) => (
441
+ <List.Item key={item.id} title={item.name} />
442
+ ))}
443
+ </List>
444
+ )
445
+ }
446
+ ```
447
+
448
+ ### Pagination
449
+
450
+ For infinite scroll lists:
451
+
452
+ ```tsx
453
+ const { data, isLoading, pagination } = useCachedPromise(
454
+ (query: string) => {
455
+ return async ({ cursor }: { page: number; cursor?: string }) => {
456
+ const result = await fetchItems({ query, pageToken: cursor })
457
+ return {
458
+ data: result.items,
459
+ hasMore: !!result.nextPageToken,
460
+ cursor: result.nextPageToken,
461
+ }
462
+ }
463
+ },
464
+ [searchText],
465
+ { keepPreviousData: true },
466
+ )
467
+
468
+ return (
469
+ <List isLoading={isLoading} pagination={pagination}>
470
+ {data?.map((item) => <List.Item key={item.id} title={item.name} />)}
471
+ </List>
472
+ )
473
+ ```
474
+
475
+ ### useCachedState
476
+
477
+ Persistent UI state that survives across sessions (stored in SQLite):
478
+
479
+ ```tsx
480
+ import { useCachedState } from '@termcast/utils'
481
+
482
+ const [selectedAccount, setSelectedAccount] = useCachedState(
483
+ 'selectedAccount', // key
484
+ 'all', // default value
485
+ { cacheNamespace: 'my-extension' },
486
+ )
487
+
488
+ const [isShowingDetail, setIsShowingDetail] = useCachedState(
489
+ 'isShowingDetail',
490
+ true,
491
+ { cacheNamespace: 'my-extension' },
492
+ )
493
+ ```
494
+
495
+ ### Revalidation pattern
496
+
497
+ After mutations, call `revalidate()` to refresh the data:
498
+
499
+ ```tsx
500
+ const { data, revalidate } = useCachedPromise(fetchItems, [])
501
+
502
+ const handleDelete = async (id: string) => {
503
+ await deleteItem(id)
504
+ await showToast({ style: Toast.Style.Success, title: 'Deleted' })
505
+ revalidate() // refresh the list
506
+ }
507
+ ```
508
+
509
+ ---
510
+
511
+ ## Termcast-Exclusive Components
512
+
513
+ These components are unique to termcast — not available in Raycast. They can be placed inside `Detail.Metadata`, `List.Item.Detail.Metadata`, or used standalone in a Detail view.
514
+
515
+ ### Graph (line chart with braille rendering)
516
+
517
+ ```tsx
518
+ import { Graph, Color, Detail } from 'termcast'
519
+
520
+ <Detail
521
+ markdown="# Stock Price"
522
+ metadata={
523
+ <Graph height={15} xLabels={['Jan', 'Apr', 'Jul', 'Oct']} yTicks={6}>
524
+ <Graph.Line data={[150, 162, 175, 190, 201]} color={Color.Orange} title="AAPL" />
525
+ <Graph.Line data={[120, 135, 140, 155, 160]} color={Color.Blue} title="MSFT" />
526
+ </Graph>
527
+ }
528
+ />
529
+ ```
530
+
531
+ Variants: `'area'` (default), `'filled'`, `'striped'`. Set via the `variant` prop on Graph.
532
+
533
+ ### BarGraph (vertical stacked bars)
534
+
535
+ ```tsx
536
+ import { BarGraph } from 'termcast'
537
+
538
+ <BarGraph height={10} labels={['Mon', 'Tue', 'Wed', 'Thu', 'Fri']}>
539
+ <BarGraph.Series data={[40, 30, 25, 15, 50]} title="Direct" />
540
+ <BarGraph.Series data={[30, 35, 15, 20, 35]} title="Organic" />
541
+ <BarGraph.Series data={[20, 25, 10, 10, 25]} title="Referral" />
542
+ </BarGraph>
543
+ ```
544
+
545
+ ### BarChart (horizontal stacked bars)
546
+
547
+ ```tsx
548
+ import { BarChart } from 'termcast'
549
+
550
+ <BarChart
551
+ segments={[
552
+ { title: 'Used', value: 75 },
553
+ { title: 'Free', value: 25 },
554
+ ]}
555
+ />
556
+ ```
557
+
558
+ ### CalendarHeatmap
559
+
560
+ GitHub-style contribution grid:
561
+
562
+ ```tsx
563
+ import { CalendarHeatmap, Color } from 'termcast'
564
+ import type { CalendarHeatmapData } from 'termcast'
565
+
566
+ const data: CalendarHeatmapData[] = days.map((date) => ({
567
+ date: new Date(date),
568
+ value: Math.floor(Math.random() * 8),
569
+ }))
570
+
571
+ <CalendarHeatmap data={data} color={Color.Green} />
572
+ <CalendarHeatmap data={data} color={Color.Blue} emptyColor={Color.Purple} />
573
+ ```
574
+
575
+ ### Table
576
+
577
+ Borderless table with header background and alternating row stripes:
578
+
579
+ ```tsx
580
+ import { Table } from 'termcast'
581
+
582
+ <Table
583
+ headers={['Region', 'Latency', 'Status']}
584
+ rows={[
585
+ ['us-east-1', '**12ms**', 'OK'],
586
+ ['eu-west-1', '*45ms*', 'OK'],
587
+ ['ap-south-1', '`89ms`', 'Degraded'],
588
+ ]}
589
+ />
590
+ ```
591
+
592
+ Cells support inline markdown: `**bold**`, `*italic*`, `` `code` ``, `~~strikethrough~~`, `[links](url)`.
593
+
594
+ ### ProgressBar
595
+
596
+ Usage/progress display:
597
+
598
+ ```tsx
599
+ import { ProgressBar } from 'termcast'
600
+
601
+ <ProgressBar title="Current session" value={37} percentageSuffix="used" label="Resets 9pm" />
602
+ <ProgressBar title="Weekly quota" value={82} percentageSuffix="used" label="Resets Mar 1" />
603
+ ```
604
+
605
+ ### Row (side-by-side layout)
606
+
607
+ Place any components side by side:
608
+
609
+ ```tsx
610
+ import { Row, Graph, BarGraph, Table, Color } from 'termcast'
611
+
612
+ <Row>
613
+ <Graph height={10} xLabels={['Mon', 'Fri']}>
614
+ <Graph.Line data={cpuData} color={Color.Orange} title="CPU" />
615
+ </Graph>
616
+ <Graph height={10} xLabels={['Mon', 'Fri']}>
617
+ <Graph.Line data={memData} color={Color.Blue} title="Memory" />
618
+ </Graph>
619
+ </Row>
620
+
621
+ <Row>
622
+ <Table headers={['Region', 'Latency']} rows={[['us-east', '12ms']]} />
623
+ <Table headers={['Endpoint', 'RPS']} rows={[['/api/auth', '1200']]} />
624
+ </Row>
625
+ ```
626
+
627
+ ### Markdown (standalone block in metadata)
628
+
629
+ Render markdown anywhere inside metadata:
630
+
631
+ ```tsx
632
+ import { Markdown, CalendarHeatmap, Color, Detail } from 'termcast'
633
+
634
+ <Detail.Metadata>
635
+ <Markdown content="**Long history** — 5 years of daily data in purple." />
636
+ <CalendarHeatmap data={longData} color={Color.Purple} />
637
+ <Markdown content="**Recent** — last 150 days in red." />
638
+ <CalendarHeatmap data={recentData} color={Color.Red} />
639
+ </Detail.Metadata>
640
+ ```
641
+
642
+ ### Combining components in metadata
643
+
644
+ All termcast-exclusive components compose freely inside metadata:
645
+
646
+ ```tsx
647
+ <Detail
648
+ markdown="# Dashboard"
649
+ metadata={
650
+ <Detail.Metadata>
651
+ <Detail.Metadata.Label title="Status" text={{ value: "Active", color: Color.Green }} />
652
+ <Detail.Metadata.Separator />
653
+ <Graph height={12} xLabels={['6h', '12h', '18h', '24h']}>
654
+ <Graph.Line data={requestsPerHour} color={Color.Orange} title="RPS" />
655
+ </Graph>
656
+ <Row>
657
+ <BarGraph height={8} labels={['Mon', 'Tue', 'Wed']}>
658
+ <BarGraph.Series data={[100, 150, 120]} title="2xx" />
659
+ <BarGraph.Series data={[5, 8, 3]} title="5xx" />
660
+ </BarGraph>
661
+ <Table
662
+ headers={['Endpoint', 'p99']}
663
+ rows={[['/api/auth', '45ms'], ['/api/data', '120ms']]}
664
+ />
665
+ </Row>
666
+ <ProgressBar title="Rate limit" value={62} percentageSuffix="used" />
667
+ <CalendarHeatmap data={uptimeData} color={Color.Green} />
668
+ <Detail.Metadata.TagList title="Regions">
669
+ <Detail.Metadata.TagList.Item text="us-east" color={Color.Blue} />
670
+ <Detail.Metadata.TagList.Item text="eu-west" color={Color.Green} />
671
+ </Detail.Metadata.TagList>
672
+ </Detail.Metadata>
673
+ }
674
+ />
675
+ ```
676
+
677
+ ---
678
+
679
+ ## Real-World Patterns
680
+
681
+ These patterns are drawn from a production termcast extension (a Gmail TUI wrapping an existing CLI tool).
682
+
683
+ ### Gluing a CLI tool with a TUI
684
+
685
+ The pattern: import your existing business logic, wrap it with termcast components.
686
+
687
+ ```
688
+ ┌─────────────────────────────────────────────┐
689
+ │ mail-tui.tsx (termcast UI) │
690
+ │ - List, Detail, Form, ActionPanel │
691
+ │ - useCachedPromise for data fetching │
692
+ │ - useCachedState for persistent prefs │
693
+ ├─────────────────────────────────────────────┤
694
+ │ auth.ts / gmail-client.ts (business logic) │
695
+ │ - OAuth, API calls, data models │
696
+ │ - Pure TypeScript, no React dependencies │
697
+ └─────────────────────────────────────────────┘
698
+ ```
699
+
700
+ The TUI file only handles rendering. All API calls, auth, and data processing live in separate files that work independently of the UI.
701
+
702
+ ### Multi-account dropdown
703
+
704
+ ```tsx
705
+ function AccountDropdown({ accounts, value, onChange }: {
706
+ accounts: { email: string }[]
707
+ value: string
708
+ onChange: (value: string) => void
709
+ }) {
710
+ return (
711
+ <List.Dropdown tooltip="Account" value={value} onChange={onChange}>
712
+ <List.Dropdown.Item title="All Accounts" value="all" icon={Icon.Globe} />
713
+ <List.Dropdown.Section title="Accounts">
714
+ {accounts.map((a) => (
715
+ <List.Dropdown.Item key={a.email} title={a.email} value={a.email} />
716
+ ))}
717
+ </List.Dropdown.Section>
718
+ </List.Dropdown>
719
+ )
720
+ }
721
+
722
+ // Usage:
723
+ <List searchBarAccessory={
724
+ <AccountDropdown accounts={accounts} value={selected} onChange={setSelected} />
725
+ }>
726
+ ```
727
+
728
+ ### Date-based section grouping
729
+
730
+ ```tsx
731
+ function dateSection(dateStr: string): string {
732
+ const date = new Date(dateStr)
733
+ const now = new Date()
734
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
735
+ const yesterday = new Date(today.getTime() - 86400000)
736
+
737
+ if (date >= today) return 'Today'
738
+ if (date >= yesterday) return 'Yesterday'
739
+ return 'Older'
740
+ }
741
+
742
+ const sections = useMemo(() => {
743
+ const groups = new Map<string, Item[]>()
744
+ for (const item of items) {
745
+ const section = dateSection(item.date)
746
+ const list = groups.get(section) ?? []
747
+ list.push(item)
748
+ groups.set(section, list)
749
+ }
750
+ return [...groups.entries()].map(([name, items]) => ({ name, items }))
751
+ }, [items])
752
+
753
+ return (
754
+ <List>
755
+ {sections.map((section) => (
756
+ <List.Section key={section.name} title={section.name}>
757
+ {section.items.map((item) => (
758
+ <List.Item key={item.id} title={item.title} />
759
+ ))}
760
+ </List.Section>
761
+ ))}
762
+ </List>
763
+ )
764
+ ```
765
+
766
+ ### Mutations with loading state
767
+
768
+ ```tsx
769
+ const [activeMutations, setActiveMutations] = useState(0)
770
+ const isMutating = activeMutations > 0
771
+
772
+ const withMutation = async <T,>(fn: () => Promise<T>): Promise<T> => {
773
+ setActiveMutations((n) => n + 1)
774
+ try { return await fn() }
775
+ finally { setActiveMutations((n) => n - 1) }
776
+ }
777
+
778
+ // Usage in an action:
779
+ <Action
780
+ title="Archive"
781
+ onAction={() => withMutation(async () => {
782
+ await archiveItem(item.id)
783
+ await showToast({ style: Toast.Style.Success, title: 'Archived' })
784
+ revalidate()
785
+ })}
786
+ />
787
+
788
+ <List isLoading={isLoading || isMutating}>
789
+ ```
790
+
791
+ ### Compose forms via Action.Push
792
+
793
+ ```tsx
794
+ <ActionPanel.Section title="Reply & Forward">
795
+ <Action.Push
796
+ title="Reply"
797
+ icon={Icon.Reply}
798
+ shortcut={{ modifiers: ['ctrl'], key: 'r' }}
799
+ target={
800
+ <ComposeForm
801
+ mode={{ type: 'reply', threadId: thread.id }}
802
+ onSent={revalidate}
803
+ />
804
+ }
805
+ />
806
+ <Action.Push
807
+ title="Forward"
808
+ icon={Icon.Forward}
809
+ shortcut={{ modifiers: ['ctrl'], key: 'f' }}
810
+ target={
811
+ <ComposeForm
812
+ mode={{ type: 'forward', threadId: thread.id }}
813
+ onSent={revalidate}
814
+ />
815
+ }
816
+ />
817
+ </ActionPanel.Section>
818
+ ```
819
+
820
+ ---
821
+
822
+ ## Porting from Raycast
823
+
824
+ If you're converting an existing Raycast extension:
825
+
826
+ 1. **Change imports**: `@raycast/api` -> `termcast`, `@raycast/utils` -> `@termcast/utils`
827
+ 2. **Keyboard modifiers**: `cmd` doesn't work in terminals. Replace with `ctrl` or `alt`
828
+ 3. **Enter key**: named `return` in opentui key events
829
+ 4. **Images**: no pixel rendering in terminals. Emoji and text fallbacks are used
830
+ 5. **Everything else** works the same: List, Detail, Form, Action, Toast, Navigation, LocalStorage, Cache, Clipboard, OAuth
831
+
832
+ The compound component patterns are identical:
833
+ - `List.Item`, `List.Section`, `List.Dropdown`, `List.Dropdown.Item`
834
+ - `Detail.Metadata`, `Detail.Metadata.Label`, `Detail.Metadata.TagList`
835
+ - `Form.TextField`, `Form.Dropdown`, `Form.Dropdown.Item`
836
+ - `ActionPanel.Section`
837
+
838
+ ---
839
+
840
+ ## Gotchas
841
+
842
+ - **Use `logger.log`** instead of `console.log` — logs go to `app.log` in the extension directory
843
+ - **Never use `setTimeout`** for scheduling React state updates
844
+ - **Never pass functions** to `useEffect` dependencies — causes infinite loops
845
+ - **Minimize `useState`** — compute derived state inline when possible
846
+ - **Always use `.tsx` extension** for files with JSX
847
+ - **`useEffect` is discouraged** — colocate logic in event handlers when possible
848
+ - **Never use `as any`** — find proper types, import them, or use `@ts-expect-error` with explanation
849
+ - **Shortcuts**: use `ctrl`/`alt` + **letter** keys only (not digits)
850
+ - **`showFailureToast(error, { title })`** is the standard way to handle errors in actions
851
+ - **`revalidate()`** after every mutation to refresh data
852
+
853
+ ## Running and Testing Extensions
854
+
855
+ ### Running with `termcast dev`
856
+
857
+ The primary way to develop and try out an extension:
858
+
859
+ ```bash
860
+ cd my-extension
861
+ termcast dev
862
+ ```
863
+
864
+ This launches the TUI with hot-reload. File changes rebuild and refresh automatically. This is the fast iteration loop for development.
865
+
866
+ ### Interactive experimentation with tuistory CLI
867
+
868
+ tuistory is a CLI tool for driving terminal applications from the shell — like Playwright but for TUIs. Use it to launch your extension, interact with it, and take snapshots without manual intervention.
869
+
870
+ **Always run `tuistory --help` first** to see the latest commands and options.
871
+
872
+ ```bash
873
+ # Launch the extension in a managed terminal session
874
+ tuistory launch "termcast dev" -s my-ext --cols 120 --rows 36
875
+
876
+ # See current terminal state
877
+ tuistory -s my-ext snapshot --trim
878
+
879
+ # Interact
880
+ tuistory -s my-ext type "search query"
881
+ tuistory -s my-ext press enter
882
+ tuistory -s my-ext press ctrl k # open action panel
883
+ tuistory -s my-ext press tab # next form field
884
+ tuistory -s my-ext press esc # go back
885
+
886
+ # Take a screenshot as image
887
+ tuistory -s my-ext screenshot -o ./tmp/screenshot.jpg --pixel-ratio 2
888
+
889
+ # Observe after each action
890
+ tuistory -s my-ext snapshot --trim
891
+
892
+ # Cleanup
893
+ tuistory -s my-ext close
894
+ ```
895
+
896
+ ### Automated tests with vitest + tuistory JS API
897
+
898
+ tuistory provides a Playwright-style JS API for writing automated TUI tests. The workflow is **observe-act-observe**: take a snapshot, interact, take another snapshot.
899
+
900
+ ```ts
901
+ import { test, expect } from 'vitest'
902
+ import { launchTerminal } from 'tuistory'
903
+
904
+ test('extension shows items and navigates to detail', async () => {
905
+ const session = await launchTerminal({
906
+ command: 'termcast',
907
+ args: ['dev'],
908
+ cols: 120,
909
+ rows: 36,
910
+ cwd: '/path/to/my-extension',
911
+ })
912
+
913
+ // Wait for the list to render
914
+ await session.waitForText('Search', { timeout: 10000 })
915
+
916
+ // Observe initial state
917
+ const initial = await session.text({ trimEnd: true })
918
+ expect(initial).toMatchInlineSnapshot()
919
+
920
+ // Type a search query
921
+ await session.type('project')
922
+ const filtered = await session.text({ trimEnd: true })
923
+ expect(filtered).toMatchInlineSnapshot()
924
+
925
+ // Press Enter to trigger primary action
926
+ await session.press('enter')
927
+ await session.waitForText('Detail', { timeout: 5000 })
928
+ const detail = await session.text({ trimEnd: true })
929
+ expect(detail).toMatchInlineSnapshot()
930
+
931
+ // Go back
932
+ await session.press('esc')
933
+
934
+ session.close()
935
+ }, 30000)
936
+ ```
937
+
938
+ Run with:
939
+
940
+ ```bash
941
+ vitest --run -u # fill in snapshots
942
+ vitest --run # verify snapshots match
943
+ ```
944
+
945
+ Always leave `toMatchInlineSnapshot()` empty the first time, run with `-u` to fill them, then read back the test file to verify the captured output is correct.