saeeol 1.3.0 → 1.4.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 (545) hide show
  1. package/AGENTS.md +72 -0
  2. package/BUN_SHELL_MIGRATION_PLAN.md +136 -0
  3. package/Dockerfile +18 -0
  4. package/assets/saeeol.ico +0 -0
  5. package/bin/saeeol.cjs +3 -1
  6. package/bunfig.toml +7 -0
  7. package/database.db +0 -0
  8. package/drizzle.config.ts +10 -0
  9. package/git +0 -0
  10. package/migration/20260127222353_familiar_lady_ursula/migration.sql +90 -0
  11. package/migration/20260127222353_familiar_lady_ursula/snapshot.json +796 -0
  12. package/migration/20260211171708_add_project_commands/migration.sql +1 -0
  13. package/migration/20260211171708_add_project_commands/snapshot.json +806 -0
  14. package/migration/20260213144116_wakeful_the_professor/migration.sql +11 -0
  15. package/migration/20260213144116_wakeful_the_professor/snapshot.json +897 -0
  16. package/migration/20260225215848_workspace/migration.sql +7 -0
  17. package/migration/20260225215848_workspace/snapshot.json +959 -0
  18. package/migration/20260227213759_add_session_workspace_id/migration.sql +2 -0
  19. package/migration/20260227213759_add_session_workspace_id/snapshot.json +983 -0
  20. package/migration/20260228203230_blue_harpoon/migration.sql +17 -0
  21. package/migration/20260228203230_blue_harpoon/snapshot.json +1102 -0
  22. package/migration/20260303231226_add_workspace_fields/migration.sql +5 -0
  23. package/migration/20260303231226_add_workspace_fields/snapshot.json +1013 -0
  24. package/migration/20260309230000_move_org_to_state/migration.sql +3 -0
  25. package/migration/20260309230000_move_org_to_state/snapshot.json +1156 -0
  26. package/migration/20260312043431_session_message_cursor/migration.sql +4 -0
  27. package/migration/20260312043431_session_message_cursor/snapshot.json +1168 -0
  28. package/migration/20260323234822_events/migration.sql +13 -0
  29. package/migration/20260323234822_events/snapshot.json +1271 -0
  30. package/migration/20260410174513_workspace-name/migration.sql +16 -0
  31. package/migration/20260410174513_workspace-name/snapshot.json +1271 -0
  32. package/migration/20260413175956_chief_energizer/migration.sql +13 -0
  33. package/migration/20260413175956_chief_energizer/snapshot.json +1399 -0
  34. package/migration/20260423070820_add_icon_url_override/migration.sql +2 -0
  35. package/migration/20260423070820_add_icon_url_override/snapshot.json +1409 -0
  36. package/migration/20260428004200_add_session_path/migration.sql +1 -0
  37. package/migration/20260428004200_add_session_path/snapshot.json +1419 -0
  38. package/npm/bin/saeeol +42 -0
  39. package/npm/package.json +39 -0
  40. package/npm/postinstall.js +162 -0
  41. package/package.json +201 -207
  42. package/parsers-config.ts +289 -0
  43. package/script/build.ts +393 -0
  44. package/script/check-migrations.ts +16 -0
  45. package/script/fix-node-pty.ts +34 -0
  46. package/script/generate.ts +23 -0
  47. package/script/postinstall.mjs +189 -0
  48. package/script/publish.ts +200 -0
  49. package/script/run-workspace-server +106 -0
  50. package/script/schema.ts +63 -0
  51. package/script/test-runner.ts +420 -0
  52. package/script/time.ts +6 -0
  53. package/script/trace-imports.ts +153 -0
  54. package/script/upgrade-opentui.ts +64 -0
  55. package/scripts/diff-sdk-types.sh +52 -0
  56. package/specs/effect/facades.md +221 -0
  57. package/specs/effect/http-api.md +401 -0
  58. package/specs/effect/instance-context.md +309 -0
  59. package/specs/effect/loose-ends.md +34 -0
  60. package/specs/effect/migration.md +299 -0
  61. package/specs/effect/routes.md +64 -0
  62. package/specs/effect/schema.md +399 -0
  63. package/specs/effect/server-package.md +668 -0
  64. package/specs/effect/tools.md +90 -0
  65. package/specs/tui-plugins.md +433 -0
  66. package/specs/v2/api.ts +67 -0
  67. package/specs/v2/keymappings.md +10 -0
  68. package/specs/v2/message-shape.md +136 -0
  69. package/src/acp/agent-message.ts +1 -1
  70. package/src/acp/agent-utils.ts +1 -1
  71. package/src/boxes/ansi.ts +17 -0
  72. package/src/boxes/atomic-write.ts +35 -0
  73. package/src/boxes/b64.ts +58 -0
  74. package/src/boxes/bash-security.ts +129 -0
  75. package/src/boxes/bom.ts +18 -0
  76. package/src/boxes/cancel.ts +16 -0
  77. package/src/boxes/chop.ts +12 -0
  78. package/src/boxes/clamp.ts +3 -0
  79. package/src/boxes/compact.ts +9 -0
  80. package/src/boxes/cost-tracker.ts +116 -0
  81. package/src/boxes/dataurl.ts +29 -0
  82. package/src/boxes/delay.ts +27 -0
  83. package/src/boxes/diff-apply.ts +53 -0
  84. package/src/boxes/disposable.ts +13 -0
  85. package/src/boxes/err.ts +34 -0
  86. package/src/boxes/human.ts +47 -0
  87. package/src/boxes/iife.ts +9 -0
  88. package/src/boxes/latch.ts +8 -0
  89. package/src/boxes/memory.ts +198 -0
  90. package/src/boxes/net.ts +16 -0
  91. package/src/boxes/plural.ts +4 -0
  92. package/src/boxes/puny.ts +21 -0
  93. package/src/boxes/retry.ts +49 -0
  94. package/src/boxes/rwlock.ts +41 -0
  95. package/src/boxes/schedule.ts +71 -0
  96. package/src/boxes/scope.ts +21 -0
  97. package/src/boxes/tokens.ts +9 -0
  98. package/src/boxes/ttl-cache.ts +63 -0
  99. package/src/boxes/typed-event.ts +51 -0
  100. package/src/boxes/uid.ts +50 -0
  101. package/src/boxes/wave6.test.ts +296 -0
  102. package/src/boxes/wildcard.ts +58 -0
  103. package/src/bus/global.ts +1 -1
  104. package/src/cli/cmd/github-run-api.ts +2 -2
  105. package/src/cli/cmd/run-events.ts +2 -2
  106. package/src/cli/cmd/tui/component/logo.tsx +1 -1
  107. package/src/cli/cmd/tui/component/prompt/use-prompt-memos.ts +2 -2
  108. package/src/cli/cmd/tui/context/app/editor-zed.ts +1 -1
  109. package/src/cli/cmd/tui/context/app/editor.ts +1 -1
  110. package/src/cli/cmd/tui/context/app/theme.tsx +1 -1
  111. package/src/cli/cmd/tui/preflight.ts +138 -0
  112. package/src/cli/cmd/tui/thread.ts +20 -0
  113. package/src/cli/cmd/tui/util/revert-diff.ts +1 -1
  114. package/src/overlay/cli/cmd/roll-call-call.ts +1 -1
  115. package/src/overlay/cost-tracker/format.ts +1 -1
  116. package/src/overlay/cost-tracker/index.ts +4 -4
  117. package/src/overlay/cost-tracker/state.ts +2 -2
  118. package/src/overlay/cost-tracker/types.ts +2 -2
  119. package/src/overlay/memory/age.ts +1 -1
  120. package/src/overlay/memory/index.ts +4 -4
  121. package/src/overlay/memory/paths.ts +2 -2
  122. package/src/overlay/memory/scan.ts +1 -1
  123. package/src/overlay/memory/types.ts +2 -2
  124. package/src/overlay/tool/bash-security.ts +3 -3
  125. package/src/overlay/util/url.ts +1 -1
  126. package/src/plugin/codex-auth.ts +1 -1
  127. package/src/provider/model-cache.ts +2 -2
  128. package/src/provider/provider-resolve.ts +3 -3
  129. package/src/provider/transform-message.ts +1 -1
  130. package/src/server/routes/game.ts +284 -0
  131. package/src/server/server.ts +2 -0
  132. package/src/session/core/compaction/compaction-helpers.ts +1 -1
  133. package/src/session/core/compaction/compaction.ts +1 -1
  134. package/src/session/core/session.ts +2 -0
  135. package/src/sessions/ingest-queue.ts +2 -2
  136. package/src/sessions/remote-ws.ts +1 -1
  137. package/src/tool/workflow/question.ts +1 -1
  138. package/src/util/abort.ts +1 -1
  139. package/src/util/bom.ts +2 -2
  140. package/src/util/color.ts +1 -1
  141. package/src/util/data-url.ts +1 -1
  142. package/src/util/defer.ts +1 -1
  143. package/src/util/error.ts +2 -2
  144. package/src/util/filesystem.ts +2 -2
  145. package/src/util/format.ts +1 -1
  146. package/src/util/iife.ts +1 -1
  147. package/src/util/local-context.ts +1 -1
  148. package/src/util/locale.ts +2 -2
  149. package/src/util/lock.ts +1 -1
  150. package/src/util/network.ts +1 -1
  151. package/src/util/signal.ts +1 -1
  152. package/src/util/token.ts +1 -1
  153. package/src/util/wildcard.ts +1 -1
  154. package/sst-env.d.ts +10 -0
  155. package/test/AGENTS.md +133 -0
  156. package/test/account/repo.test.ts +352 -0
  157. package/test/account/service.test.ts +456 -0
  158. package/test/acp/agent-interface.test.ts +51 -0
  159. package/test/acp/event-subscription.test.ts +725 -0
  160. package/test/agent/agent.test.ts +890 -0
  161. package/test/auth/auth.test.ts +86 -0
  162. package/test/bun/registry.test.ts +75 -0
  163. package/test/bus/bus-effect.test.ts +161 -0
  164. package/test/bus/bus-integration.test.ts +87 -0
  165. package/test/bus/bus.test.ts +219 -0
  166. package/test/cli/account.test.ts +26 -0
  167. package/test/cli/auto-mode.test.ts +75 -0
  168. package/test/cli/bin-saeeol.test.ts +8 -0
  169. package/test/cli/cmd/tui/prompt-part.test.ts +47 -0
  170. package/test/cli/cmd/tui/prompt-traits.test.ts +38 -0
  171. package/test/cli/cmd/tui/sync.test.tsx +159 -0
  172. package/test/cli/error.test.ts +18 -0
  173. package/test/cli/github-action.test.ts +198 -0
  174. package/test/cli/github-remote.test.ts +85 -0
  175. package/test/cli/import.test.ts +97 -0
  176. package/test/cli/install-artifact.test.ts +72 -0
  177. package/test/cli/plugin-auth-picker.test.ts +120 -0
  178. package/test/cli/pr.test.ts +59 -0
  179. package/test/cli/tui/editor-context-zed.test.ts +356 -0
  180. package/test/cli/tui/editor-context.test.tsx +228 -0
  181. package/test/cli/tui/keybind-plugin.test.ts +90 -0
  182. package/test/cli/tui/markdown.test.ts +161 -0
  183. package/test/cli/tui/plugin-add.test.ts +111 -0
  184. package/test/cli/tui/plugin-install.test.ts +87 -0
  185. package/test/cli/tui/plugin-lifecycle.test.ts +224 -0
  186. package/test/cli/tui/plugin-loader-entrypoint.test.ts +484 -0
  187. package/test/cli/tui/plugin-loader-pure.test.ts +71 -0
  188. package/test/cli/tui/plugin-loader.test.ts +816 -0
  189. package/test/cli/tui/plugin-toggle.test.ts +157 -0
  190. package/test/cli/tui/revert-diff.test.ts +35 -0
  191. package/test/cli/tui/slot-replace.test.tsx +47 -0
  192. package/test/cli/tui/theme-store.test.ts +54 -0
  193. package/test/cli/tui/thread.test.ts +28 -0
  194. package/test/cli/tui/transcript.test.ts +426 -0
  195. package/test/cli/tui/usage.test.ts +60 -0
  196. package/test/cli/tui/use-event.test.tsx +175 -0
  197. package/test/config/agent-color.test.ts +67 -0
  198. package/test/config/config.test.ts +2544 -0
  199. package/test/config/fixtures/empty-frontmatter.md +4 -0
  200. package/test/config/fixtures/frontmatter.md +28 -0
  201. package/test/config/fixtures/markdown-header.md +11 -0
  202. package/test/config/fixtures/no-frontmatter.md +1 -0
  203. package/test/config/fixtures/weird-model-id.md +13 -0
  204. package/test/config/lsp.test.ts +87 -0
  205. package/test/config/markdown.test.ts +228 -0
  206. package/test/config/plugin.test.ts +0 -0
  207. package/test/config/tui.test.ts +624 -0
  208. package/test/control-plane/adapters.test.ts +71 -0
  209. package/test/control-plane/workspace.test.ts +1526 -0
  210. package/test/effect/app-runtime-logger.test.ts +98 -0
  211. package/test/effect/config-service.test.ts +65 -0
  212. package/test/effect/instance-state.test.ts +394 -0
  213. package/test/effect/run-service.test.ts +89 -0
  214. package/test/effect/runner.test.ts +523 -0
  215. package/test/fake/provider.ts +82 -0
  216. package/test/file/fsmonitor.test.ts +68 -0
  217. package/test/file/ignore.test.ts +10 -0
  218. package/test/file/index.test.ts +954 -0
  219. package/test/file/path-traversal.test.ts +205 -0
  220. package/test/file/ripgrep.test.ts +226 -0
  221. package/test/file/watcher.test.ts +249 -0
  222. package/test/filesystem/filesystem.test.ts +319 -0
  223. package/test/fixture/db.ts +11 -0
  224. package/test/fixture/fixture.test.ts +26 -0
  225. package/test/fixture/fixture.ts +175 -0
  226. package/test/fixture/flock-worker.ts +72 -0
  227. package/test/fixture/log-init-worker.ts +62 -0
  228. package/test/fixture/lsp/fake-lsp-server.js +249 -0
  229. package/test/fixture/plug-worker.ts +93 -0
  230. package/test/fixture/plugin-meta-worker.ts +19 -0
  231. package/test/fixture/skills/agents-sdk/SKILL.md +152 -0
  232. package/test/fixture/skills/cloudflare/SKILL.md +211 -0
  233. package/test/fixture/skills/index.json +6 -0
  234. package/test/fixture/tui-plugin.ts +323 -0
  235. package/test/fixture/tui-runtime.ts +31 -0
  236. package/test/format/format.test.ts +272 -0
  237. package/test/git/git.test.ts +128 -0
  238. package/test/ide/ide.test.ts +82 -0
  239. package/test/installation/installation.test.ts +168 -0
  240. package/test/keybind.test.ts +421 -0
  241. package/test/lib/effect.ts +53 -0
  242. package/test/lib/filesystem.ts +10 -0
  243. package/test/lib/llm-server.ts +778 -0
  244. package/test/lib/websocket.ts +46 -0
  245. package/test/lsp/client.test.ts +482 -0
  246. package/test/lsp/index.test.ts +160 -0
  247. package/test/lsp/launch.test.ts +22 -0
  248. package/test/lsp/lifecycle.test.ts +184 -0
  249. package/test/ltm/ltm.test.ts +230 -0
  250. package/test/mcp/headers.test.ts +178 -0
  251. package/test/mcp/lifecycle.test.ts +787 -0
  252. package/test/mcp/oauth-auto-connect.test.ts +311 -0
  253. package/test/mcp/oauth-browser.test.ts +276 -0
  254. package/test/mcp/oauth-callback.test.ts +34 -0
  255. package/test/memory/abort-leak-webfetch.ts +49 -0
  256. package/test/memory/abort-leak.test.ts +128 -0
  257. package/test/patch/patch.test.ts +348 -0
  258. package/test/permission/arity.test.ts +33 -0
  259. package/test/permission/next.test.ts +1227 -0
  260. package/test/permission/next.toConfig.test.ts +110 -0
  261. package/test/permission-task.test.ts +326 -0
  262. package/test/plugin/auth-override.test.ts +79 -0
  263. package/test/plugin/cloudflare.test.ts +68 -0
  264. package/test/plugin/codex.test.ts +123 -0
  265. package/test/plugin/github-copilot-models.test.ts +261 -0
  266. package/test/plugin/install-concurrency.test.ts +140 -0
  267. package/test/plugin/install.test.ts +570 -0
  268. package/test/plugin/loader-shared.test.ts +1169 -0
  269. package/test/plugin/meta.test.ts +137 -0
  270. package/test/plugin/plugin-contract.test.ts +291 -0
  271. package/test/plugin/shared.test.ts +88 -0
  272. package/test/plugin/trigger.test.ts +102 -0
  273. package/test/plugin/workspace-adapter.test.ts +109 -0
  274. package/test/preload.ts +77 -0
  275. package/test/project/instance.test.ts +276 -0
  276. package/test/project/migrate-global.test.ts +152 -0
  277. package/test/project/project.test.ts +600 -0
  278. package/test/project/vcs.test.ts +286 -0
  279. package/test/project/worktree-remove.test.ts +126 -0
  280. package/test/project/worktree.test.ts +223 -0
  281. package/test/provider/amazon-bedrock.test.ts +462 -0
  282. package/test/provider/copilot/convert-to-copilot-messages.test.ts +523 -0
  283. package/test/provider/copilot/copilot-chat-model.test.ts +592 -0
  284. package/test/provider/gitlab-duo.test.ts +413 -0
  285. package/test/provider/local.test.ts +208 -0
  286. package/test/provider/models.test.ts +261 -0
  287. package/test/provider/provider-category.test.ts +190 -0
  288. package/test/provider/provider.test.ts +2758 -0
  289. package/test/provider/transform.test.ts +3681 -0
  290. package/test/pty/pty-output-isolation.test.ts +147 -0
  291. package/test/pty/pty-session.test.ts +102 -0
  292. package/test/pty/pty-shell.test.ts +104 -0
  293. package/test/question/question.test.ts +490 -0
  294. package/test/saeeol/agent-global-config-dirs.test.ts +24 -0
  295. package/test/saeeol/agent-manager-tool.test.ts +71 -0
  296. package/test/saeeol/agent-permission-overrides.test.ts +75 -0
  297. package/test/saeeol/agent-skill-permissions.test.ts +37 -0
  298. package/test/saeeol/ask-agent-permissions.test.ts +303 -0
  299. package/test/saeeol/bash-hierarchy.test.ts +64 -0
  300. package/test/saeeol/bash-permission-metadata.test.ts +66 -0
  301. package/test/saeeol/bash-security-extended.test.ts +243 -0
  302. package/test/saeeol/bedrock-claude-empty-content.test.ts +138 -0
  303. package/test/saeeol/boxes-integration.test.ts +415 -0
  304. package/test/saeeol/builtin-skills.test.ts +75 -0
  305. package/test/saeeol/cleanup.ts +28 -0
  306. package/test/saeeol/cli/dev-setup.test.ts +74 -0
  307. package/test/saeeol/cli/roll-call.test.ts +161 -0
  308. package/test/saeeol/cli-run-auto-helper.test.ts +58 -0
  309. package/test/saeeol/codex-auth-refresh.test.ts +124 -0
  310. package/test/saeeol/commit-message/generate.test.ts +188 -0
  311. package/test/saeeol/commit-message/git-context.test.ts +303 -0
  312. package/test/saeeol/commit-message-windows.test.ts +38 -0
  313. package/test/saeeol/compaction-payload-recovery.test.ts +406 -0
  314. package/test/saeeol/compaction-preservation-audit.test.ts +122 -0
  315. package/test/saeeol/compaction-skip-guard.test.ts +224 -0
  316. package/test/saeeol/compaction-smart-select.test.ts +100 -0
  317. package/test/saeeol/config/config.test.ts +166 -0
  318. package/test/saeeol/config/indexing-default-plugin.test.ts +82 -0
  319. package/test/saeeol/config/opentelemetry-default.test.ts +29 -0
  320. package/test/saeeol/config-gitignore.test.ts +70 -0
  321. package/test/saeeol/config-injector.test.ts +305 -0
  322. package/test/saeeol/config-resilience.test.ts +234 -0
  323. package/test/saeeol/config-validation.test.ts +183 -0
  324. package/test/saeeol/cost-propagation.test.ts +94 -0
  325. package/test/saeeol/cost-tracker-extended.test.ts +141 -0
  326. package/test/saeeol/cost-tracker.test.ts +64 -0
  327. package/test/saeeol/custom-provider-delete.test.ts +149 -0
  328. package/test/saeeol/diff-full.test.ts +226 -0
  329. package/test/saeeol/edit-permission-filediff.test.ts +223 -0
  330. package/test/saeeol/encoding.test.ts +364 -0
  331. package/test/saeeol/enhance-prompt.test.ts +61 -0
  332. package/test/saeeol/ensure-plan-dir.test.ts +32 -0
  333. package/test/saeeol/errors.test.ts +144 -0
  334. package/test/saeeol/external-directory-boundary.test.ts +96 -0
  335. package/test/saeeol/gateway-headers.test.ts +88 -0
  336. package/test/saeeol/help.test.ts +191 -0
  337. package/test/saeeol/ignore-migrator.test.ts +308 -0
  338. package/test/saeeol/indexing-auth.test.ts +45 -0
  339. package/test/saeeol/indexing-feature.test.ts +44 -0
  340. package/test/saeeol/indexing-label.test.ts +70 -0
  341. package/test/saeeol/indexing-startup.test.ts +381 -0
  342. package/test/saeeol/indexing-worktree.test.ts +73 -0
  343. package/test/saeeol/instruction.test.ts +136 -0
  344. package/test/saeeol/lancedb-runtime.test.ts +116 -0
  345. package/test/saeeol/loader-auth.test.ts +168 -0
  346. package/test/saeeol/local-model.test.ts +621 -0
  347. package/test/saeeol/logo.test.ts +31 -0
  348. package/test/saeeol/lsp-typescript-lightweight.test.ts +89 -0
  349. package/test/saeeol/mcp-branding.test.ts +33 -0
  350. package/test/saeeol/mcp-docker-rm.test.ts +32 -0
  351. package/test/saeeol/mcp-migrator.test.ts +736 -0
  352. package/test/saeeol/mcp-oauth-callback.test.ts +33 -0
  353. package/test/saeeol/memory-io.test.ts +198 -0
  354. package/test/saeeol/memory-paths.test.ts +87 -0
  355. package/test/saeeol/memory-security.test.ts +166 -0
  356. package/test/saeeol/model-cache-org.test.ts +164 -0
  357. package/test/saeeol/model-info-panel-utils.test.ts +52 -0
  358. package/test/saeeol/model-info-panel.types.test.ts +7 -0
  359. package/test/saeeol/models-401-fallback.test.ts +52 -0
  360. package/test/saeeol/modes-migrator.test.ts +320 -0
  361. package/test/saeeol/nvidia-headers.test.ts +74 -0
  362. package/test/saeeol/patch-jsonc.test.ts +73 -0
  363. package/test/saeeol/patch.test.ts +172 -0
  364. package/test/saeeol/paths.test.ts +265 -0
  365. package/test/saeeol/permission/config-paths.test.ts +174 -0
  366. package/test/saeeol/permission/env-read.test.ts +149 -0
  367. package/test/saeeol/permission/external-directory-allow.test.ts +327 -0
  368. package/test/saeeol/permission/next.always-rules.test.ts +882 -0
  369. package/test/saeeol/permission/next.reply-http.test.ts +205 -0
  370. package/test/saeeol/permission/next.reply-routing.test.ts +184 -0
  371. package/test/saeeol/plan-exit-detection.test.ts +494 -0
  372. package/test/saeeol/plan-followup.test.ts +1376 -0
  373. package/test/saeeol/project-config-update.test.ts +120 -0
  374. package/test/saeeol/project-id.test.ts +455 -0
  375. package/test/saeeol/provider-cost.test.ts +171 -0
  376. package/test/saeeol/provider-list-failed-state.test.ts +100 -0
  377. package/test/saeeol/question-dismiss-all.test.ts +174 -0
  378. package/test/saeeol/read-directory.test.ts +116 -0
  379. package/test/saeeol/rules-migrator.test.ts +257 -0
  380. package/test/saeeol/run-auto.test.ts +176 -0
  381. package/test/saeeol/run-network.test.ts +224 -0
  382. package/test/saeeol/semantic-search.test.ts +186 -0
  383. package/test/saeeol/server/permission-allow-everything.test.ts +125 -0
  384. package/test/saeeol/session/instruction-substitution.test.ts +72 -0
  385. package/test/saeeol/session/platform-attribution.test.ts +118 -0
  386. package/test/saeeol/session/session.test.ts +105 -0
  387. package/test/saeeol/session-compaction-cap.test.ts +399 -0
  388. package/test/saeeol/session-compaction-chunks.test.ts +501 -0
  389. package/test/saeeol/session-compaction-safety.test.ts +481 -0
  390. package/test/saeeol/session-fork-remap.test.ts +251 -0
  391. package/test/saeeol/session-import-service.test.ts +114 -0
  392. package/test/saeeol/session-list.test.ts +47 -0
  393. package/test/saeeol/session-message-metadata.test.ts +128 -0
  394. package/test/saeeol/session-overflow.test.ts +78 -0
  395. package/test/saeeol/session-processor-empty-tool-calls.test.ts +571 -0
  396. package/test/saeeol/session-processor-network-offline.test.ts +204 -0
  397. package/test/saeeol/session-processor-retry-limit.test.ts +238 -0
  398. package/test/saeeol/session-processor-review-telemetry.test.ts +82 -0
  399. package/test/saeeol/session-prompt-compaction-safety.test.ts +517 -0
  400. package/test/saeeol/session-prompt-queue.test.ts +815 -0
  401. package/test/saeeol/sessions/inflight-cache.test.ts +157 -0
  402. package/test/saeeol/sessions/ingest-queue.test.ts +402 -0
  403. package/test/saeeol/sessions/remote-protocol.test.ts +258 -0
  404. package/test/saeeol/sessions/remote-sender.test.ts +1036 -0
  405. package/test/saeeol/sessions/remote-ws.test.ts +367 -0
  406. package/test/saeeol/sessions/sessions-enable-remote.test.disable +181 -0
  407. package/test/saeeol/slot-prop-reactivity.test.ts +142 -0
  408. package/test/saeeol/snapshot-cache.test.ts +84 -0
  409. package/test/saeeol/snapshot-freeze-repro.test.ts +100 -0
  410. package/test/saeeol/snapshot-track-timeout.test.ts +519 -0
  411. package/test/saeeol/stats-subagent-cost.test.ts +123 -0
  412. package/test/saeeol/suggestion/auto-dismiss.test.ts +65 -0
  413. package/test/saeeol/suggestion/suggestion.test.ts +145 -0
  414. package/test/saeeol/suggestion/tool.test.ts +298 -0
  415. package/test/saeeol/summary-file-diff.test.ts +28 -0
  416. package/test/saeeol/system-prompt.test.ts +142 -0
  417. package/test/saeeol/task-nesting.test.ts +193 -0
  418. package/test/saeeol/telemetry/feedback.test.ts +8 -0
  419. package/test/saeeol/todo-view.test.ts +57 -0
  420. package/test/saeeol/tool-encoding.test.ts +455 -0
  421. package/test/saeeol/tool-registry-indexing-import-failure.test.ts +49 -0
  422. package/test/saeeol/tool-registry-indexing.test.ts +236 -0
  423. package/test/saeeol/tool-registry-semantic-import-failure.test.ts +55 -0
  424. package/test/saeeol/tool-task-model.test.ts +352 -0
  425. package/test/saeeol/transform-opus-4.7.test.ts +89 -0
  426. package/test/saeeol/tui-diff.test.ts +91 -0
  427. package/test/saeeol/tui-sync.test.ts +80 -0
  428. package/test/saeeol/util/url.test.ts +141 -0
  429. package/test/saeeol/workflows-migrator.test.ts +261 -0
  430. package/test/saeeol/worktree-diff-summary.test.ts +64 -0
  431. package/test/saeeol/worktree-diff.test.ts +223 -0
  432. package/test/saeeol/worktree-remove-lock.test.ts +82 -0
  433. package/test/server/AGENTS.md +15 -0
  434. package/test/server/contract.test.ts +357 -0
  435. package/test/server/experimental-session-list.test.ts +157 -0
  436. package/test/server/global-session-list.test.ts +155 -0
  437. package/test/server/httpapi-authorization.test.ts +103 -0
  438. package/test/server/httpapi-bridge.test.ts +440 -0
  439. package/test/server/httpapi-config.test.ts +67 -0
  440. package/test/server/httpapi-cors.test.ts +89 -0
  441. package/test/server/httpapi-event.test.ts +57 -0
  442. package/test/server/httpapi-experimental.test.ts +219 -0
  443. package/test/server/httpapi-file.test.ts +79 -0
  444. package/test/server/httpapi-instance-context.test.ts +237 -0
  445. package/test/server/httpapi-instance.legacy.test.ts +140 -0
  446. package/test/server/httpapi-instance.test.ts +83 -0
  447. package/test/server/httpapi-json-parity.test.ts +263 -0
  448. package/test/server/httpapi-mcp-oauth.test.ts +76 -0
  449. package/test/server/httpapi-mcp.test.ts +189 -0
  450. package/test/server/httpapi-provider.test.ts +153 -0
  451. package/test/server/httpapi-pty-websocket.test.ts +16 -0
  452. package/test/server/httpapi-pty.test.ts +175 -0
  453. package/test/server/httpapi-raw-route-auth.test.ts +89 -0
  454. package/test/server/httpapi-sdk.test.ts +681 -0
  455. package/test/server/httpapi-session.test.ts +464 -0
  456. package/test/server/httpapi-sync.test.ts +130 -0
  457. package/test/server/httpapi-tui.test.ts +121 -0
  458. package/test/server/httpapi-workspace-routing.test.ts +471 -0
  459. package/test/server/httpapi-workspace.test.ts +427 -0
  460. package/test/server/lib/conformance.ts +88 -0
  461. package/test/server/lib/stateful.ts +112 -0
  462. package/test/server/project-init-git.test.ts +113 -0
  463. package/test/server/proxy-util.test.ts +113 -0
  464. package/test/server/session-actions.test.ts +49 -0
  465. package/test/server/session-list.test.ts +238 -0
  466. package/test/server/session-messages.test.ts +167 -0
  467. package/test/server/session-select.test.ts +100 -0
  468. package/test/server/trace-attributes.test.ts +76 -0
  469. package/test/server/workspace-proxy.test.ts +165 -0
  470. package/test/server/workspace-routing.test.ts +85 -0
  471. package/test/session/compaction.test.ts +2420 -0
  472. package/test/session/instruction.test.ts +247 -0
  473. package/test/session/llm.test.ts +1273 -0
  474. package/test/session/message-v2.test.ts +1291 -0
  475. package/test/session/messages-pagination.test.ts +1173 -0
  476. package/test/session/network.test.ts +249 -0
  477. package/test/session/processor-effect.test.ts +847 -0
  478. package/test/session/prompt.test.ts +2131 -0
  479. package/test/session/retry.test.ts +340 -0
  480. package/test/session/revert-compact.test.ts +639 -0
  481. package/test/session/schema-decoding.test.ts +311 -0
  482. package/test/session/session-entry-stepper.test.ts +917 -0
  483. package/test/session/session-schema.test.ts +76 -0
  484. package/test/session/snapshot-tool-race.test.ts +257 -0
  485. package/test/session/structured-output-integration.test.ts +265 -0
  486. package/test/session/structured-output.test.ts +381 -0
  487. package/test/session/system.test.ts +73 -0
  488. package/test/share/share-next.test.ts +333 -0
  489. package/test/shell/shell.test.ts +99 -0
  490. package/test/skill/discovery.test.ts +116 -0
  491. package/test/skill/skill.test.ts +393 -0
  492. package/test/smoke/.tui-debug-output.txt +1 -0
  493. package/test/smoke/.tui-debug-plain.txt +1 -0
  494. package/test/smoke/.tui-walkthrough-report.txt +122 -0
  495. package/test/smoke/smoke-tui-pty.test.ts +123 -0
  496. package/test/smoke/smoke-tui.mjs +83 -0
  497. package/test/smoke/tui-walkthrough.test.ts +520 -0
  498. package/test/snapshot/snapshot.test.ts +1531 -0
  499. package/test/storage/db.test.ts +23 -0
  500. package/test/storage/json-migration.test.ts +832 -0
  501. package/test/storage/storage.test.ts +293 -0
  502. package/test/suggestion/suggestion.test.ts +1 -0
  503. package/test/sync/index.test.ts +256 -0
  504. package/test/tool/__snapshots__/parameters.test.ts.snap +500 -0
  505. package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
  506. package/test/tool/apply_patch.test.ts +614 -0
  507. package/test/tool/bash.test.ts +1225 -0
  508. package/test/tool/diagnostics-filter.test.ts +55 -0
  509. package/test/tool/edit.test.ts +754 -0
  510. package/test/tool/external-directory.test.ts +169 -0
  511. package/test/tool/fixtures/large-image.png +0 -0
  512. package/test/tool/fixtures/models-api.json +65179 -0
  513. package/test/tool/glob.test.ts +107 -0
  514. package/test/tool/grep.test.ts +114 -0
  515. package/test/tool/lsp.test.ts +187 -0
  516. package/test/tool/parameters.test.ts +243 -0
  517. package/test/tool/question.test.ts +129 -0
  518. package/test/tool/read.test.ts +500 -0
  519. package/test/tool/recall.test.ts +151 -0
  520. package/test/tool/registry.test.ts +203 -0
  521. package/test/tool/skill.test.ts +135 -0
  522. package/test/tool/suggest.test.ts +1 -0
  523. package/test/tool/task.test.ts +612 -0
  524. package/test/tool/tool-define.test.ts +99 -0
  525. package/test/tool/truncation.test.ts +260 -0
  526. package/test/tool/webfetch.test.ts +103 -0
  527. package/test/tool/write.test.ts +291 -0
  528. package/test/util/data-url.test.ts +14 -0
  529. package/test/util/effect-zod.test.ts +754 -0
  530. package/test/util/error.test.ts +38 -0
  531. package/test/util/filesystem.test.ts +656 -0
  532. package/test/util/format.test.ts +59 -0
  533. package/test/util/glob.test.ts +164 -0
  534. package/test/util/iife.test.ts +36 -0
  535. package/test/util/lazy.test.ts +50 -0
  536. package/test/util/lock.test.ts +72 -0
  537. package/test/util/log.test.ts +86 -0
  538. package/test/util/module.test.ts +59 -0
  539. package/test/util/process.test.ts +128 -0
  540. package/test/util/timeout.test.ts +21 -0
  541. package/test/util/which.test.ts +100 -0
  542. package/test/util/wildcard.test.ts +90 -0
  543. package/test/workspace/workspace-restore.test.ts +296 -0
  544. package/src/provider/models-snapshot.d.ts +0 -2
  545. package/src/provider/models-snapshot.js +0 -3
@@ -0,0 +1,816 @@
1
+ import { beforeAll, describe, expect, spyOn, test } from "bun:test"
2
+ import fs from "fs/promises"
3
+ import path from "path"
4
+ import { pathToFileURL } from "url"
5
+ import { tmpdir } from "../../fixture/fixture"
6
+ import { createTuiPluginApi } from "../../fixture/tui-plugin"
7
+ import { Global } from "@saeeol/core/global"
8
+ import { TuiConfig } from "../../../src/cli/cmd/tui/config/tui"
9
+ import { Filesystem } from "@/util/filesystem"
10
+
11
+ const { allThemes, addTheme } = await import("../../../src/cli/cmd/tui/context/theme")
12
+ const { TuiPluginRuntime } = await import("../../../src/cli/cmd/tui/plugin/runtime")
13
+
14
+ type Row = Record<string, unknown>
15
+
16
+ type Data = {
17
+ local: Row
18
+ global: Row
19
+ invalid: Row
20
+ preloaded: Row
21
+ fn_called: boolean
22
+ local_installed: string
23
+ global_installed: string
24
+ preloaded_installed: string
25
+ leaked_local_to_global: boolean
26
+ leaked_global_to_local: boolean
27
+ local_theme: string
28
+ global_theme: string
29
+ }
30
+
31
+ async function row(file: string): Promise<Row> {
32
+ return Filesystem.readJson<Row>(file)
33
+ }
34
+
35
+ async function load(): Promise<Data> {
36
+ const stamp = Date.now()
37
+ const globalConfigPath = path.join(Global.Path.config, "tui.json")
38
+ const backup = await Bun.file(globalConfigPath)
39
+ .text()
40
+ .catch(() => undefined)
41
+
42
+ await using tmp = await tmpdir({
43
+ init: async (dir) => {
44
+ const localPluginPath = path.join(dir, "local-plugin.ts")
45
+ const invalidPluginPath = path.join(dir, "invalid-plugin.ts")
46
+ const preloadedPluginPath = path.join(dir, "preloaded-plugin.ts")
47
+ const globalPluginPath = path.join(dir, "global-plugin.ts")
48
+ const localSpec = pathToFileURL(localPluginPath).href
49
+ const invalidSpec = pathToFileURL(invalidPluginPath).href
50
+ const preloadedSpec = pathToFileURL(preloadedPluginPath).href
51
+ const globalSpec = pathToFileURL(globalPluginPath).href
52
+ const localThemeFile = `local-theme-${stamp}.json`
53
+ const invalidThemeFile = `invalid-theme-${stamp}.json`
54
+ const globalThemeFile = `global-theme-${stamp}.json`
55
+ const preloadedThemeFile = `preloaded-theme-${stamp}.json`
56
+ const localThemeName = localThemeFile.replace(/\.json$/, "")
57
+ const invalidThemeName = invalidThemeFile.replace(/\.json$/, "")
58
+ const globalThemeName = globalThemeFile.replace(/\.json$/, "")
59
+ const preloadedThemeName = preloadedThemeFile.replace(/\.json$/, "")
60
+ const localThemePath = path.join(dir, localThemeFile)
61
+ const invalidThemePath = path.join(dir, invalidThemeFile)
62
+ const globalThemePath = path.join(dir, globalThemeFile)
63
+ const preloadedThemePath = path.join(dir, preloadedThemeFile)
64
+ const localDest = path.join(dir, ".saeeol", "themes", localThemeFile)
65
+ const globalDest = path.join(Global.Path.config, "themes", globalThemeFile)
66
+ const preloadedDest = path.join(dir, ".saeeol", "themes", preloadedThemeFile)
67
+ const fnMarker = path.join(dir, "function-called.txt")
68
+ const localMarker = path.join(dir, "local-called.json")
69
+ const invalidMarker = path.join(dir, "invalid-called.json")
70
+ const globalMarker = path.join(dir, "global-called.json")
71
+ const preloadedMarker = path.join(dir, "preloaded-called.json")
72
+ const localConfigPath = path.join(dir, "tui.json")
73
+
74
+ await Bun.write(localThemePath, JSON.stringify({ theme: { primary: "#101010" } }, null, 2))
75
+ await Bun.write(invalidThemePath, "{ invalid json }")
76
+ await Bun.write(globalThemePath, JSON.stringify({ theme: { primary: "#202020" } }, null, 2))
77
+ await Bun.write(preloadedThemePath, JSON.stringify({ theme: { primary: "#f0f0f0" } }, null, 2))
78
+ await Bun.write(preloadedDest, JSON.stringify({ theme: { primary: "#303030" } }, null, 2))
79
+
80
+ await Bun.write(
81
+ localPluginPath,
82
+ `export const ignored = async (_input, options) => {
83
+ if (!options?.fn_marker) return
84
+ await Bun.write(options.fn_marker, "called")
85
+ }
86
+
87
+ export default {
88
+ id: "demo.local",
89
+ tui: async (api, options) => {
90
+ if (!options?.marker) return
91
+ const cfg_theme = api.tuiConfig.theme
92
+ const cfg_diff = api.tuiConfig.diff_style
93
+ const cfg_speed = api.tuiConfig.scroll_speed
94
+ const cfg_accel = api.tuiConfig.scroll_acceleration?.enabled
95
+ const cfg_submit = api.tuiConfig.keybinds?.input_submit
96
+ const key = api.keybind.create(
97
+ { modal: "ctrl+shift+m", screen: "ctrl+shift+o", close: "escape" },
98
+ options.keybinds,
99
+ )
100
+ const kv_before = api.kv.get(options.kv_key, "missing")
101
+ api.kv.set(options.kv_key, "stored")
102
+ const kv_after = api.kv.get(options.kv_key, "missing")
103
+ const diff = api.state.session.diff(options.session_id)
104
+ const todo = api.state.session.todo(options.session_id)
105
+ const lsp = api.state.lsp()
106
+ const mcp = api.state.mcp()
107
+ const depth_before = api.ui.dialog.depth
108
+ const open_before = api.ui.dialog.open
109
+ const size_before = api.ui.dialog.size
110
+ api.ui.dialog.setSize("large")
111
+ const size_after = api.ui.dialog.size
112
+ api.ui.dialog.replace(() => null)
113
+ const depth_after = api.ui.dialog.depth
114
+ const open_after = api.ui.dialog.open
115
+ api.ui.dialog.clear()
116
+ const open_clear = api.ui.dialog.open
117
+ const before = api.theme.has(options.theme_name)
118
+ const set_missing = api.theme.set(options.theme_name)
119
+ await api.theme.install(options.theme_path)
120
+ const after = api.theme.has(options.theme_name)
121
+ const set_installed = api.theme.set(options.theme_name)
122
+ const first = await Bun.file(options.dest).text()
123
+ await Bun.write(options.source, JSON.stringify({ theme: { primary: "#fefefe" } }, null, 2))
124
+ await api.theme.install(options.theme_path)
125
+ const second = await Bun.file(options.dest).text()
126
+ await Bun.write(
127
+ options.marker,
128
+ JSON.stringify({
129
+ before,
130
+ set_missing,
131
+ after,
132
+ set_installed,
133
+ selected: api.theme.selected,
134
+ same: first === second,
135
+ key_modal: key.get("modal"),
136
+ key_close: key.get("close"),
137
+ key_unknown: key.get("ctrl+k"),
138
+ key_print: key.print("modal"),
139
+ kv_before,
140
+ kv_after,
141
+ kv_ready: api.kv.ready,
142
+ diff_count: diff.length,
143
+ diff_file: diff[0]?.file,
144
+ todo_count: todo.length,
145
+ todo_first: todo[0]?.content,
146
+ lsp_count: lsp.length,
147
+ mcp_count: mcp.length,
148
+ mcp_first: mcp[0]?.name,
149
+ depth_before,
150
+ open_before,
151
+ size_before,
152
+ size_after,
153
+ depth_after,
154
+ open_after,
155
+ open_clear,
156
+ cfg_theme,
157
+ cfg_diff,
158
+ cfg_speed,
159
+ cfg_accel,
160
+ cfg_submit,
161
+ }),
162
+ )
163
+ },
164
+ }
165
+ `,
166
+ )
167
+
168
+ await Bun.write(
169
+ invalidPluginPath,
170
+ `export default {
171
+ id: "demo.invalid",
172
+ tui: async (api, options) => {
173
+ if (!options?.marker) return
174
+ const before = api.theme.has(options.theme_name)
175
+ const set_missing = api.theme.set(options.theme_name)
176
+ await api.theme.install(options.theme_path)
177
+ const after = api.theme.has(options.theme_name)
178
+ const set_installed = api.theme.set(options.theme_name)
179
+ await Bun.write(
180
+ options.marker,
181
+ JSON.stringify({
182
+ before,
183
+ set_missing,
184
+ after,
185
+ set_installed,
186
+ }),
187
+ )
188
+ },
189
+ }
190
+ `,
191
+ )
192
+
193
+ await Bun.write(
194
+ preloadedPluginPath,
195
+ `export default {
196
+ id: "demo.preloaded",
197
+ tui: async (api, options) => {
198
+ if (!options?.marker) return
199
+ const before = api.theme.has(options.theme_name)
200
+ await api.theme.install(options.theme_path)
201
+ const after = api.theme.has(options.theme_name)
202
+ const text = await Bun.file(options.dest).text()
203
+ await Bun.write(
204
+ options.marker,
205
+ JSON.stringify({
206
+ before,
207
+ after,
208
+ text,
209
+ }),
210
+ )
211
+ },
212
+ }
213
+ `,
214
+ )
215
+
216
+ await Bun.write(
217
+ globalPluginPath,
218
+ `export default {
219
+ id: "demo.global",
220
+ tui: async (api, options) => {
221
+ if (!options?.marker) return
222
+ await api.theme.install(options.theme_path)
223
+ const has = api.theme.has(options.theme_name)
224
+ const set_installed = api.theme.set(options.theme_name)
225
+ await Bun.write(
226
+ options.marker,
227
+ JSON.stringify({
228
+ has,
229
+ set_installed,
230
+ selected: api.theme.selected,
231
+ }),
232
+ )
233
+ },
234
+ }
235
+ `,
236
+ )
237
+
238
+ await Bun.write(
239
+ globalConfigPath,
240
+ JSON.stringify(
241
+ {
242
+ plugin: [
243
+ [globalSpec, { marker: globalMarker, theme_path: `./${globalThemeFile}`, theme_name: globalThemeName }],
244
+ ],
245
+ },
246
+ null,
247
+ 2,
248
+ ),
249
+ )
250
+
251
+ await Bun.write(
252
+ localConfigPath,
253
+ JSON.stringify(
254
+ {
255
+ plugin: [
256
+ [
257
+ localSpec,
258
+ {
259
+ fn_marker: fnMarker,
260
+ marker: localMarker,
261
+ source: localThemePath,
262
+ dest: localDest,
263
+ theme_path: `./${localThemeFile}`,
264
+ theme_name: localThemeName,
265
+ kv_key: "plugin_state_key",
266
+ session_id: "ses_test",
267
+ keybinds: {
268
+ modal: "ctrl+alt+m",
269
+ close: "q",
270
+ },
271
+ },
272
+ ],
273
+ [
274
+ invalidSpec,
275
+ {
276
+ marker: invalidMarker,
277
+ theme_path: `./${invalidThemeFile}`,
278
+ theme_name: invalidThemeName,
279
+ },
280
+ ],
281
+ [
282
+ preloadedSpec,
283
+ {
284
+ marker: preloadedMarker,
285
+ dest: preloadedDest,
286
+ theme_path: `./${preloadedThemeFile}`,
287
+ theme_name: preloadedThemeName,
288
+ },
289
+ ],
290
+ ],
291
+ },
292
+ null,
293
+ 2,
294
+ ),
295
+ )
296
+
297
+ return {
298
+ localThemeFile,
299
+ invalidThemeFile,
300
+ globalThemeFile,
301
+ preloadedThemeFile,
302
+ localThemeName,
303
+ invalidThemeName,
304
+ globalThemeName,
305
+ preloadedThemeName,
306
+ localDest,
307
+ globalDest,
308
+ preloadedDest,
309
+ localPluginPath,
310
+ invalidPluginPath,
311
+ globalPluginPath,
312
+ preloadedPluginPath,
313
+ localSpec,
314
+ invalidSpec,
315
+ globalSpec,
316
+ preloadedSpec,
317
+ fnMarker,
318
+ localMarker,
319
+ invalidMarker,
320
+ globalMarker,
321
+ preloadedMarker,
322
+ }
323
+ },
324
+ })
325
+ const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
326
+ const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
327
+
328
+ try {
329
+ expect(addTheme(tmp.extra.preloadedThemeName, { theme: { primary: "#303030" } })).toBe(true)
330
+
331
+ const localOpts = {
332
+ fn_marker: tmp.extra.fnMarker,
333
+ marker: tmp.extra.localMarker,
334
+ source: path.join(tmp.path, tmp.extra.localThemeFile),
335
+ dest: tmp.extra.localDest,
336
+ theme_path: `./${tmp.extra.localThemeFile}`,
337
+ theme_name: tmp.extra.localThemeName,
338
+ kv_key: "plugin_state_key",
339
+ session_id: "ses_test",
340
+ keybinds: { modal: "ctrl+alt+m", close: "q" },
341
+ }
342
+ const invalidOpts = {
343
+ marker: tmp.extra.invalidMarker,
344
+ theme_path: `./${tmp.extra.invalidThemeFile}`,
345
+ theme_name: tmp.extra.invalidThemeName,
346
+ }
347
+ const preloadedOpts = {
348
+ marker: tmp.extra.preloadedMarker,
349
+ dest: tmp.extra.preloadedDest,
350
+ theme_path: `./${tmp.extra.preloadedThemeFile}`,
351
+ theme_name: tmp.extra.preloadedThemeName,
352
+ }
353
+ const globalOpts = {
354
+ marker: tmp.extra.globalMarker,
355
+ theme_path: `./${tmp.extra.globalThemeFile}`,
356
+ theme_name: tmp.extra.globalThemeName,
357
+ }
358
+
359
+ const config: TuiConfig.Info = {
360
+ plugin: [
361
+ [tmp.extra.localSpec, localOpts],
362
+ [tmp.extra.invalidSpec, invalidOpts],
363
+ [tmp.extra.preloadedSpec, preloadedOpts],
364
+ [tmp.extra.globalSpec, globalOpts],
365
+ ],
366
+ plugin_origins: [
367
+ { spec: [tmp.extra.localSpec, localOpts], scope: "local", source: path.join(tmp.path, "tui.json") },
368
+ { spec: [tmp.extra.invalidSpec, invalidOpts], scope: "local", source: path.join(tmp.path, "tui.json") },
369
+ { spec: [tmp.extra.preloadedSpec, preloadedOpts], scope: "local", source: path.join(tmp.path, "tui.json") },
370
+ {
371
+ spec: [tmp.extra.globalSpec, globalOpts],
372
+ scope: "global",
373
+ source: path.join(Global.Path.config, "tui.json"),
374
+ },
375
+ ],
376
+ }
377
+
378
+ await TuiPluginRuntime.init({
379
+ api: createTuiPluginApi({
380
+ tuiConfig: {
381
+ theme: "smoke",
382
+ diff_style: "stacked",
383
+ scroll_speed: 1.5,
384
+ scroll_acceleration: { enabled: true },
385
+ keybinds: {
386
+ input_submit: "ctrl+enter",
387
+ },
388
+ },
389
+ keybind: {
390
+ print: (key) => `print:${key}`,
391
+ },
392
+ state: {
393
+ session: {
394
+ diff(sessionID) {
395
+ if (sessionID !== "ses_test") return []
396
+ return [{ file: "src/app.ts", additions: 3, deletions: 1 }]
397
+ },
398
+ todo(sessionID) {
399
+ if (sessionID !== "ses_test") return []
400
+ return [{ content: "ship it", status: "pending" }]
401
+ },
402
+ },
403
+ lsp() {
404
+ return [{ id: "ts", root: "/tmp/project", status: "connected" }]
405
+ },
406
+ mcp() {
407
+ return [{ name: "github", status: "connected" }]
408
+ },
409
+ },
410
+ theme: {
411
+ has(name) {
412
+ return allThemes()[name] !== undefined
413
+ },
414
+ },
415
+ }),
416
+ config,
417
+ })
418
+ const local = await row(tmp.extra.localMarker)
419
+ const global = await row(tmp.extra.globalMarker)
420
+ const invalid = await row(tmp.extra.invalidMarker)
421
+ const preloaded = await row(tmp.extra.preloadedMarker)
422
+ const fn_called = await fs
423
+ .readFile(tmp.extra.fnMarker, "utf8")
424
+ .then(() => true)
425
+ .catch(() => false)
426
+ const local_installed = await fs.readFile(tmp.extra.localDest, "utf8")
427
+ const global_installed = await fs.readFile(tmp.extra.globalDest, "utf8")
428
+ const preloaded_installed = await fs.readFile(tmp.extra.preloadedDest, "utf8")
429
+ const leaked_local_to_global = await fs
430
+ .stat(path.join(Global.Path.config, "themes", tmp.extra.localThemeFile))
431
+ .then(() => true)
432
+ .catch(() => false)
433
+ const leaked_global_to_local = await fs
434
+ .stat(path.join(tmp.path, ".saeeol", "themes", tmp.extra.globalThemeFile))
435
+ .then(() => true)
436
+ .catch(() => false)
437
+
438
+ return {
439
+ local,
440
+ global,
441
+ invalid,
442
+ preloaded,
443
+ fn_called,
444
+ local_installed,
445
+ global_installed,
446
+ preloaded_installed,
447
+ leaked_local_to_global,
448
+ leaked_global_to_local,
449
+ local_theme: tmp.extra.localThemeName,
450
+ global_theme: tmp.extra.globalThemeName,
451
+ }
452
+ } finally {
453
+ await TuiPluginRuntime.dispose()
454
+ cwd.mockRestore()
455
+ wait.mockRestore()
456
+ if (backup === undefined) {
457
+ await fs.rm(globalConfigPath, { force: true })
458
+ } else {
459
+ await Bun.write(globalConfigPath, backup)
460
+ }
461
+ await fs.rm(tmp.extra.globalDest, { force: true }).catch(() => {})
462
+ }
463
+ }
464
+
465
+ test("continues loading when a plugin is missing config metadata", async () => {
466
+ await using tmp = await tmpdir({
467
+ init: async (dir) => {
468
+ const bad = path.join(dir, "missing-meta-plugin.ts")
469
+ const good = path.join(dir, "next-plugin.ts")
470
+ const bare = path.join(dir, "plain-plugin.ts")
471
+ const badSpec = pathToFileURL(bad).href
472
+ const goodSpec = pathToFileURL(good).href
473
+ const bareSpec = pathToFileURL(bare).href
474
+ const goodMarker = path.join(dir, "next-called.txt")
475
+ const bareMarker = path.join(dir, "plain-called.txt")
476
+
477
+ for (const [file, id] of [
478
+ [bad, "demo.missing-meta"],
479
+ [good, "demo.next"],
480
+ ] as const) {
481
+ await Bun.write(
482
+ file,
483
+ `export default {
484
+ id: "${id}",
485
+ tui: async (_api, options) => {
486
+ if (!options?.marker) return
487
+ await Bun.write(options.marker, "called")
488
+ },
489
+ }
490
+ `,
491
+ )
492
+ }
493
+
494
+ await Bun.write(
495
+ bare,
496
+ `export default {
497
+ id: "demo.plain",
498
+ tui: async (_api, options) => {
499
+ await Bun.write(${JSON.stringify(bareMarker)}, options === undefined ? "undefined" : "value")
500
+ },
501
+ }
502
+ `,
503
+ )
504
+
505
+ return { badSpec, goodSpec, bareSpec, goodMarker, bareMarker }
506
+ },
507
+ })
508
+
509
+ process.env.SAEEOL_PLUGIN_META_FILE = path.join(tmp.path, "plugin-meta.json")
510
+ const config: TuiConfig.Info = {
511
+ plugin: [
512
+ [tmp.extra.badSpec, { marker: path.join(tmp.path, "bad.txt") }],
513
+ [tmp.extra.goodSpec, { marker: tmp.extra.goodMarker }],
514
+ tmp.extra.bareSpec,
515
+ ],
516
+ plugin_origins: [
517
+ {
518
+ spec: [tmp.extra.goodSpec, { marker: tmp.extra.goodMarker }],
519
+ scope: "local",
520
+ source: path.join(tmp.path, "tui.json"),
521
+ },
522
+ {
523
+ spec: tmp.extra.bareSpec,
524
+ scope: "local",
525
+ source: path.join(tmp.path, "tui.json"),
526
+ },
527
+ ],
528
+ }
529
+ const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
530
+ const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
531
+
532
+ try {
533
+ await TuiPluginRuntime.init({ api: createTuiPluginApi(), config })
534
+ // bad plugin was skipped (no metadata entry)
535
+ await expect(fs.readFile(path.join(tmp.path, "bad.txt"), "utf8")).rejects.toThrow()
536
+ // good plugin loaded fine
537
+ await expect(fs.readFile(tmp.extra.goodMarker, "utf8")).resolves.toBe("called")
538
+ // bare string spec gets undefined options
539
+ await expect(fs.readFile(tmp.extra.bareMarker, "utf8")).resolves.toBe("undefined")
540
+ } finally {
541
+ await TuiPluginRuntime.dispose()
542
+ cwd.mockRestore()
543
+ wait.mockRestore()
544
+ delete process.env.SAEEOL_PLUGIN_META_FILE
545
+ }
546
+ })
547
+
548
+ test("initializes external tui plugins in config order", async () => {
549
+ const globalJson = path.join(Global.Path.config, "tui.json")
550
+ const globalJsonc = path.join(Global.Path.config, "tui.jsonc")
551
+ const backupJson = await Bun.file(globalJson)
552
+ .text()
553
+ .catch(() => undefined)
554
+ const backupJsonc = await Bun.file(globalJsonc)
555
+ .text()
556
+ .catch(() => undefined)
557
+
558
+ await fs.rm(globalJson, { force: true }).catch(() => {})
559
+ await fs.rm(globalJsonc, { force: true }).catch(() => {})
560
+
561
+ await using tmp = await tmpdir({
562
+ init: async (dir) => {
563
+ const a = path.join(dir, "order-a.ts")
564
+ const b = path.join(dir, "order-b.ts")
565
+ const aSpec = pathToFileURL(a).href
566
+ const bSpec = pathToFileURL(b).href
567
+ const marker = path.join(dir, "tui-order.txt")
568
+
569
+ await Bun.write(
570
+ a,
571
+ `import fs from "fs/promises"
572
+
573
+ export default {
574
+ id: "demo.tui.order.a",
575
+ tui: async () => {
576
+ await fs.appendFile(${JSON.stringify(marker)}, "a-start\\n")
577
+ await Bun.sleep(25)
578
+ await fs.appendFile(${JSON.stringify(marker)}, "a-end\\n")
579
+ },
580
+ }
581
+ `,
582
+ )
583
+ await Bun.write(
584
+ b,
585
+ `import fs from "fs/promises"
586
+
587
+ export default {
588
+ id: "demo.tui.order.b",
589
+ tui: async () => {
590
+ await fs.appendFile(${JSON.stringify(marker)}, "b\\n")
591
+ },
592
+ }
593
+ `,
594
+ )
595
+ await Bun.write(path.join(dir, "tui.json"), JSON.stringify({ plugin: [aSpec, bSpec] }, null, 2))
596
+
597
+ return { marker }
598
+ },
599
+ })
600
+
601
+ process.env.SAEEOL_PLUGIN_META_FILE = path.join(tmp.path, "plugin-meta.json")
602
+ const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
603
+
604
+ try {
605
+ const a = path.join(tmp.path, "order-a.ts")
606
+ const b = path.join(tmp.path, "order-b.ts")
607
+ const aSpec = pathToFileURL(a).href
608
+ const bSpec = pathToFileURL(b).href
609
+ const config: TuiConfig.Info = {
610
+ plugin: [aSpec, bSpec],
611
+ plugin_origins: [
612
+ { spec: aSpec, scope: "local", source: path.join(tmp.path, "tui.json") },
613
+ { spec: bSpec, scope: "local", source: path.join(tmp.path, "tui.json") },
614
+ ],
615
+ }
616
+ await TuiPluginRuntime.init({ api: createTuiPluginApi(), config })
617
+ const lines = (await fs.readFile(tmp.extra.marker, "utf8")).trim().split("\n")
618
+ expect(lines).toEqual(["a-start", "a-end", "b"])
619
+ } finally {
620
+ await TuiPluginRuntime.dispose()
621
+ cwd.mockRestore()
622
+ delete process.env.SAEEOL_PLUGIN_META_FILE
623
+
624
+ if (backupJson === undefined) {
625
+ await fs.rm(globalJson, { force: true }).catch(() => {})
626
+ } else {
627
+ await Bun.write(globalJson, backupJson)
628
+ }
629
+ if (backupJsonc === undefined) {
630
+ await fs.rm(globalJsonc, { force: true }).catch(() => {})
631
+ } else {
632
+ await Bun.write(globalJsonc, backupJsonc)
633
+ }
634
+ }
635
+ })
636
+
637
+ describe("tui.plugin.loader", () => {
638
+ let data: Data
639
+
640
+ beforeAll(async () => {
641
+ data = await load()
642
+ })
643
+
644
+ test("passes keybind, kv, state, and dialog APIs to v1 plugins", () => {
645
+ expect(data.local.key_modal).toBe("ctrl+alt+m")
646
+ expect(data.local.key_close).toBe("q")
647
+ expect(data.local.key_unknown).toBe("ctrl+k")
648
+ expect(data.local.key_print).toBe("print:ctrl+alt+m")
649
+ expect(data.local.kv_before).toBe("missing")
650
+ expect(data.local.kv_after).toBe("stored")
651
+ expect(data.local.kv_ready).toBe(true)
652
+ expect(data.local.diff_count).toBe(1)
653
+ expect(data.local.diff_file).toBe("src/app.ts")
654
+ expect(data.local.todo_count).toBe(1)
655
+ expect(data.local.todo_first).toBe("ship it")
656
+ expect(data.local.lsp_count).toBe(1)
657
+ expect(data.local.mcp_count).toBe(1)
658
+ expect(data.local.mcp_first).toBe("github")
659
+ expect(data.local.depth_before).toBe(0)
660
+ expect(data.local.open_before).toBe(false)
661
+ expect(data.local.size_before).toBe("medium")
662
+ expect(data.local.size_after).toBe("large")
663
+ expect(data.local.depth_after).toBe(1)
664
+ expect(data.local.open_after).toBe(true)
665
+ expect(data.local.open_clear).toBe(false)
666
+ expect(data.local.cfg_theme).toBe("smoke")
667
+ expect(data.local.cfg_diff).toBe("stacked")
668
+ expect(data.local.cfg_speed).toBe(1.5)
669
+ expect(data.local.cfg_accel).toBe(true)
670
+ expect(data.local.cfg_submit).toBe("ctrl+enter")
671
+ })
672
+
673
+ test("installs themes in the correct scope and remains resilient", () => {
674
+ expect(data.local.before).toBe(false)
675
+ expect(data.local.set_missing).toBe(false)
676
+ expect(data.local.after).toBe(true)
677
+ expect(data.local.set_installed).toBe(true)
678
+ expect(data.local.selected).toBe(data.local_theme)
679
+ expect(data.local.same).toBe(true)
680
+
681
+ expect(data.global.has).toBe(true)
682
+ expect(data.global.set_installed).toBe(true)
683
+ expect(data.global.selected).toBe(data.global_theme)
684
+
685
+ expect(data.invalid.before).toBe(false)
686
+ expect(data.invalid.set_missing).toBe(false)
687
+ expect(data.invalid.after).toBe(false)
688
+ expect(data.invalid.set_installed).toBe(false)
689
+
690
+ expect(data.preloaded.before).toBe(true)
691
+ expect(data.preloaded.after).toBe(true)
692
+ expect(data.preloaded.text).toContain("#303030")
693
+ expect(data.preloaded.text).not.toContain("#f0f0f0")
694
+
695
+ expect(data.fn_called).toBe(false)
696
+ expect(data.local_installed).toContain("#101010")
697
+ expect(data.local_installed).not.toContain("#fefefe")
698
+ expect(data.global_installed).toContain("#202020")
699
+ expect(data.preloaded_installed).toContain("#303030")
700
+ expect(data.preloaded_installed).not.toContain("#f0f0f0")
701
+ expect(data.leaked_local_to_global).toBe(false)
702
+ expect(data.leaked_global_to_local).toBe(false)
703
+ })
704
+ })
705
+
706
+ test("updates installed theme when plugin metadata changes", async () => {
707
+ await using tmp = await tmpdir<{
708
+ spec: string
709
+ pluginPath: string
710
+ themePath: string
711
+ dest: string
712
+ themeName: string
713
+ }>({
714
+ init: async (dir) => {
715
+ const pluginPath = path.join(dir, "theme-update-plugin.ts")
716
+ const spec = pathToFileURL(pluginPath).href
717
+ const themeFile = "theme-update.json"
718
+ const themePath = path.join(dir, themeFile)
719
+ const dest = path.join(dir, ".saeeol", "themes", themeFile)
720
+ const themeName = themeFile.replace(/\.json$/, "")
721
+ const configPath = path.join(dir, "tui.json")
722
+
723
+ await Bun.write(themePath, JSON.stringify({ theme: { primary: "#111111" } }, null, 2))
724
+ await Bun.write(
725
+ pluginPath,
726
+ `export default {
727
+ id: "demo.theme-update",
728
+ tui: async (api, options) => {
729
+ if (!options?.theme_path) return
730
+ await api.theme.install(options.theme_path)
731
+ },
732
+ }
733
+ `,
734
+ )
735
+ await Bun.write(
736
+ configPath,
737
+ JSON.stringify(
738
+ {
739
+ plugin: [[spec, { theme_path: `./${themeFile}` }]],
740
+ },
741
+ null,
742
+ 2,
743
+ ),
744
+ )
745
+
746
+ return {
747
+ spec,
748
+ pluginPath,
749
+ themePath,
750
+ dest,
751
+ themeName,
752
+ }
753
+ },
754
+ })
755
+
756
+ process.env.SAEEOL_PLUGIN_META_FILE = path.join(tmp.path, "plugin-meta.json")
757
+ const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
758
+ const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
759
+
760
+ const mkApi = () =>
761
+ createTuiPluginApi({
762
+ theme: {
763
+ has(name) {
764
+ return allThemes()[name] !== undefined
765
+ },
766
+ },
767
+ })
768
+
769
+ const mkConfig = (): TuiConfig.Info => ({
770
+ plugin: [[tmp.extra.spec, { theme_path: `./theme-update.json` }]],
771
+ plugin_origins: [
772
+ {
773
+ spec: [tmp.extra.spec, { theme_path: `./theme-update.json` }],
774
+ scope: "local",
775
+ source: path.join(tmp.path, "tui.json"),
776
+ },
777
+ ],
778
+ })
779
+
780
+ try {
781
+ await TuiPluginRuntime.init({ api: mkApi(), config: mkConfig() })
782
+ await TuiPluginRuntime.dispose()
783
+ await expect(fs.readFile(tmp.extra.dest, "utf8")).resolves.toContain("#111111")
784
+
785
+ await Bun.write(tmp.extra.themePath, JSON.stringify({ theme: { primary: "#222222" } }, null, 2))
786
+ await Bun.write(
787
+ tmp.extra.pluginPath,
788
+ `export default {
789
+ id: "demo.theme-update",
790
+ tui: async (api, options) => {
791
+ if (!options?.theme_path) return
792
+ await api.theme.install(options.theme_path)
793
+ },
794
+ }
795
+ // v2
796
+ `,
797
+ )
798
+ const stamp = new Date(Date.now() + 10_000)
799
+ await fs.utimes(tmp.extra.pluginPath, stamp, stamp)
800
+ await fs.utimes(tmp.extra.themePath, stamp, stamp)
801
+
802
+ await TuiPluginRuntime.init({ api: mkApi(), config: mkConfig() })
803
+ const text = await fs.readFile(tmp.extra.dest, "utf8")
804
+ expect(text).toContain("#222222")
805
+ expect(text).not.toContain("#111111")
806
+ const list = await Filesystem.readJson<Record<string, { themes?: Record<string, { dest: string }> }>>(
807
+ process.env.SAEEOL_PLUGIN_META_FILE!,
808
+ )
809
+ expect(list["demo.theme-update"]?.themes?.[tmp.extra.themeName]?.dest).toBe(tmp.extra.dest)
810
+ } finally {
811
+ await TuiPluginRuntime.dispose()
812
+ cwd.mockRestore()
813
+ wait.mockRestore()
814
+ delete process.env.SAEEOL_PLUGIN_META_FILE
815
+ }
816
+ })