repoburg 1.3.188 → 1.3.196

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 (265) hide show
  1. package/agent-pane/.prettierignore +5 -0
  2. package/agent-pane/.prettierrc +8 -0
  3. package/agent-pane/backend/coverage/base.css +224 -0
  4. package/agent-pane/backend/coverage/block-navigation.js +87 -0
  5. package/agent-pane/backend/coverage/clover.xml +887 -0
  6. package/agent-pane/backend/coverage/coverage-final.json +10 -0
  7. package/agent-pane/backend/coverage/favicon.png +0 -0
  8. package/agent-pane/backend/coverage/index.html +176 -0
  9. package/agent-pane/backend/coverage/prettify.css +1 -0
  10. package/agent-pane/backend/coverage/prettify.js +2 -0
  11. package/agent-pane/backend/coverage/sort-arrow-sprite.png +0 -0
  12. package/agent-pane/backend/coverage/sorter.js +210 -0
  13. package/agent-pane/backend/coverage/src/codex/codex-provider.ts.html +1402 -0
  14. package/agent-pane/backend/coverage/src/codex/index.html +131 -0
  15. package/agent-pane/backend/coverage/src/codex/item-mapper.ts.html +331 -0
  16. package/agent-pane/backend/coverage/src/db/index.html +131 -0
  17. package/agent-pane/backend/coverage/src/db/index.ts.html +436 -0
  18. package/agent-pane/backend/coverage/src/db/schema.ts.html +175 -0
  19. package/agent-pane/backend/coverage/src/index.html +116 -0
  20. package/agent-pane/backend/coverage/src/index.ts.html +262 -0
  21. package/agent-pane/backend/coverage/src/routes/index.html +146 -0
  22. package/agent-pane/backend/coverage/src/routes/sessions.ts.html +991 -0
  23. package/agent-pane/backend/coverage/src/routes/turns.ts.html +256 -0
  24. package/agent-pane/backend/coverage/src/routes/workspaces.ts.html +406 -0
  25. package/agent-pane/backend/coverage/src/ws/index.html +116 -0
  26. package/agent-pane/backend/coverage/src/ws/index.ts.html +238 -0
  27. package/agent-pane/backend/data.db +0 -0
  28. package/agent-pane/backend/data.db-shm +0 -0
  29. package/agent-pane/backend/data.db-wal +0 -0
  30. package/agent-pane/backend/dist/index.js +70869 -0
  31. package/agent-pane/backend/package-lock.json +4529 -1013
  32. package/agent-pane/backend/package.json +10 -6
  33. package/agent-pane/backend/src/claude/claude-message-mapper.test.ts +163 -0
  34. package/agent-pane/backend/src/claude/claude-message-mapper.ts +154 -0
  35. package/agent-pane/backend/src/claude/claude-provider.test.ts +65 -0
  36. package/agent-pane/backend/src/claude/claude-provider.ts +482 -0
  37. package/agent-pane/backend/src/claude/claude-types.ts +33 -0
  38. package/agent-pane/backend/src/codex/app-server-client.test.ts +183 -0
  39. package/agent-pane/backend/src/codex/app-server-client.ts +225 -0
  40. package/agent-pane/backend/src/codex/app-server-types.ts +259 -0
  41. package/agent-pane/backend/src/codex/codex-provider-runtime-config.test.ts +56 -0
  42. package/agent-pane/backend/src/codex/codex-provider.test.ts +60 -0
  43. package/agent-pane/backend/src/codex/codex-provider.ts +521 -265
  44. package/agent-pane/backend/src/codex/item-mapper.test.ts +221 -0
  45. package/agent-pane/backend/src/codex/item-mapper.ts +35 -8
  46. package/agent-pane/backend/src/codex/shared-app-server.ts +25 -0
  47. package/agent-pane/backend/src/db/index.test.ts +125 -0
  48. package/agent-pane/backend/src/db/index.ts +39 -8
  49. package/agent-pane/backend/src/db/schema.ts +28 -74
  50. package/agent-pane/backend/src/index.ts +29 -2
  51. package/agent-pane/backend/src/integration/__fixtures__/cancel-turn.json +29 -0
  52. package/agent-pane/backend/src/integration/__fixtures__/delta-streaming.json +40 -0
  53. package/agent-pane/backend/src/integration/__fixtures__/failed-turn.json +16 -0
  54. package/agent-pane/backend/src/integration/__fixtures__/mcp-tool-call.json +60 -0
  55. package/agent-pane/backend/src/integration/__fixtures__/missing-item-started.json +34 -0
  56. package/agent-pane/backend/src/integration/__fixtures__/multi-item-turn.json +85 -0
  57. package/agent-pane/backend/src/integration/__fixtures__/multi-turn.json +48 -0
  58. package/agent-pane/backend/src/integration/__fixtures__/simple-turn.json +35 -0
  59. package/agent-pane/backend/src/integration/delta-streaming.test.ts +130 -0
  60. package/agent-pane/backend/src/integration/helpers/app-server-mock.ts +272 -0
  61. package/agent-pane/backend/src/integration/helpers/test-app.ts +108 -0
  62. package/agent-pane/backend/src/integration/helpers/ws-capture.ts +64 -0
  63. package/agent-pane/backend/src/integration/item-lifecycle.test.ts +133 -0
  64. package/agent-pane/backend/src/integration/mcp-tool-call.test.ts +102 -0
  65. package/agent-pane/backend/src/integration/missing-item-started.test.ts +91 -0
  66. package/agent-pane/backend/src/integration/multi-turn.test.ts +121 -0
  67. package/agent-pane/backend/src/integration/session-operations.test.ts +168 -0
  68. package/agent-pane/backend/src/integration/title-generation.test.ts +109 -0
  69. package/agent-pane/backend/src/integration/turn-cancel.test.ts +111 -0
  70. package/agent-pane/backend/src/integration/turn-failure.test.ts +97 -0
  71. package/agent-pane/backend/src/integration/turn-lifecycle.test.ts +141 -0
  72. package/agent-pane/backend/src/providers/index.ts +14 -0
  73. package/agent-pane/backend/src/providers/registry.test.ts +92 -0
  74. package/agent-pane/backend/src/providers/registry.ts +43 -0
  75. package/agent-pane/backend/src/providers/runtime-provider.ts +25 -0
  76. package/agent-pane/backend/src/routes/files.ts +31 -0
  77. package/agent-pane/backend/src/routes/mcp.ts +33 -0
  78. package/agent-pane/backend/src/routes/models.test.ts +92 -0
  79. package/agent-pane/backend/src/routes/models.ts +16 -0
  80. package/agent-pane/backend/src/routes/plugins.ts +40 -0
  81. package/agent-pane/backend/src/routes/providers.test.ts +69 -0
  82. package/agent-pane/backend/src/routes/providers.ts +57 -0
  83. package/agent-pane/backend/src/routes/sessions.test.ts +424 -0
  84. package/agent-pane/backend/src/routes/sessions.ts +295 -42
  85. package/agent-pane/backend/src/routes/skills.test.ts +114 -0
  86. package/agent-pane/backend/src/routes/skills.ts +70 -0
  87. package/agent-pane/backend/src/routes/turns.test.ts +152 -0
  88. package/agent-pane/backend/src/routes/turns.ts +36 -16
  89. package/agent-pane/backend/src/routes/workspaces.test.ts +197 -0
  90. package/agent-pane/backend/src/routes/workspaces.ts +73 -55
  91. package/agent-pane/backend/src/ws/index.test.ts +107 -0
  92. package/agent-pane/backend/src/ws/index.ts +19 -11
  93. package/agent-pane/backend/tsconfig.json +4 -2
  94. package/agent-pane/backend/vitest.config.ts +20 -0
  95. package/agent-pane/eslint.config.js +33 -0
  96. package/agent-pane/frontend/coverage/base.css +224 -0
  97. package/agent-pane/frontend/coverage/block-navigation.js +87 -0
  98. package/agent-pane/frontend/coverage/clover.xml +2962 -0
  99. package/agent-pane/frontend/coverage/coverage-final.json +37 -0
  100. package/agent-pane/frontend/coverage/favicon.png +0 -0
  101. package/agent-pane/frontend/coverage/index.html +251 -0
  102. package/agent-pane/frontend/coverage/prettify.css +1 -0
  103. package/agent-pane/frontend/coverage/prettify.js +2 -0
  104. package/agent-pane/frontend/coverage/sort-arrow-sprite.png +0 -0
  105. package/agent-pane/frontend/coverage/sorter.js +210 -0
  106. package/agent-pane/frontend/coverage/src/App.tsx.html +217 -0
  107. package/agent-pane/frontend/coverage/src/AppInitializer.tsx.html +223 -0
  108. package/agent-pane/frontend/coverage/src/api/client.ts.html +187 -0
  109. package/agent-pane/frontend/coverage/src/api/index.html +161 -0
  110. package/agent-pane/frontend/coverage/src/api/sessions.ts.html +316 -0
  111. package/agent-pane/frontend/coverage/src/api/websocket.ts.html +544 -0
  112. package/agent-pane/frontend/coverage/src/api/workspaces.ts.html +199 -0
  113. package/agent-pane/frontend/coverage/src/components/CollapsibleToolCalls.tsx.html +226 -0
  114. package/agent-pane/frontend/coverage/src/components/CommandInputBar.tsx.html +382 -0
  115. package/agent-pane/frontend/coverage/src/components/CommandPalette.tsx.html +715 -0
  116. package/agent-pane/frontend/coverage/src/components/IdeWorkspace.tsx.html +160 -0
  117. package/agent-pane/frontend/coverage/src/components/ModelSelector.tsx.html +478 -0
  118. package/agent-pane/frontend/coverage/src/components/NewSessionDialog.tsx.html +472 -0
  119. package/agent-pane/frontend/coverage/src/components/RenderArea.tsx.html +574 -0
  120. package/agent-pane/frontend/coverage/src/components/RightPanel.tsx.html +205 -0
  121. package/agent-pane/frontend/coverage/src/components/RuntimeIcon.tsx.html +133 -0
  122. package/agent-pane/frontend/coverage/src/components/SessionTabs.tsx.html +217 -0
  123. package/agent-pane/frontend/coverage/src/components/StatusBar.tsx.html +271 -0
  124. package/agent-pane/frontend/coverage/src/components/StatusDot.tsx.html +109 -0
  125. package/agent-pane/frontend/coverage/src/components/Toast.tsx.html +313 -0
  126. package/agent-pane/frontend/coverage/src/components/TurnContent.tsx.html +1216 -0
  127. package/agent-pane/frontend/coverage/src/components/WorkspaceSidebar.tsx.html +1138 -0
  128. package/agent-pane/frontend/coverage/src/components/index.html +326 -0
  129. package/agent-pane/frontend/coverage/src/components/panels/ChangesPanel.tsx.html +247 -0
  130. package/agent-pane/frontend/coverage/src/components/panels/LogsPanel.tsx.html +160 -0
  131. package/agent-pane/frontend/coverage/src/components/panels/MetricsPanel.tsx.html +427 -0
  132. package/agent-pane/frontend/coverage/src/components/panels/TurnsPanel.tsx.html +397 -0
  133. package/agent-pane/frontend/coverage/src/components/panels/index.html +161 -0
  134. package/agent-pane/frontend/coverage/src/constants/index.html +116 -0
  135. package/agent-pane/frontend/coverage/src/constants/models.ts.html +406 -0
  136. package/agent-pane/frontend/coverage/src/hooks/index.html +146 -0
  137. package/agent-pane/frontend/coverage/src/hooks/useEscapeKey.ts.html +124 -0
  138. package/agent-pane/frontend/coverage/src/hooks/useOutsideClick.ts.html +142 -0
  139. package/agent-pane/frontend/coverage/src/hooks/useSessionStatus.ts.html +187 -0
  140. package/agent-pane/frontend/coverage/src/index.html +146 -0
  141. package/agent-pane/frontend/coverage/src/main.tsx.html +115 -0
  142. package/agent-pane/frontend/coverage/src/store/index.html +116 -0
  143. package/agent-pane/frontend/coverage/src/store/store.ts.html +2404 -0
  144. package/agent-pane/frontend/coverage/src/test/component-helpers.ts.html +190 -0
  145. package/agent-pane/frontend/coverage/src/test/fixtures.ts.html +367 -0
  146. package/agent-pane/frontend/coverage/src/test/index.html +131 -0
  147. package/agent-pane/frontend/coverage/src/types/index.html +116 -0
  148. package/agent-pane/frontend/coverage/src/types/index.ts.html +466 -0
  149. package/agent-pane/frontend/coverage/src/utils/changes.ts.html +202 -0
  150. package/agent-pane/frontend/coverage/src/utils/duration.ts.html +127 -0
  151. package/agent-pane/frontend/coverage/src/utils/index.html +131 -0
  152. package/agent-pane/frontend/dist/assets/index-Bg0PF2aV.css +1 -0
  153. package/agent-pane/frontend/dist/assets/index-BlybeX5e.js +281 -0
  154. package/agent-pane/frontend/dist/index.html +19 -0
  155. package/agent-pane/frontend/package-lock.json +2434 -213
  156. package/agent-pane/frontend/package.json +10 -3
  157. package/agent-pane/frontend/src/App.test.tsx +74 -0
  158. package/agent-pane/frontend/src/App.tsx +1 -1
  159. package/agent-pane/frontend/src/AppInitializer.test.tsx +142 -0
  160. package/agent-pane/frontend/src/AppInitializer.tsx +1 -1
  161. package/agent-pane/frontend/src/api/client.test.ts +126 -0
  162. package/agent-pane/frontend/src/api/client.ts +22 -29
  163. package/agent-pane/frontend/src/api/models.ts +7 -0
  164. package/agent-pane/frontend/src/api/providers.ts +12 -0
  165. package/agent-pane/frontend/src/api/sessions.test.ts +241 -0
  166. package/agent-pane/frontend/src/api/sessions.ts +43 -1
  167. package/agent-pane/frontend/src/api/skills.ts +12 -0
  168. package/agent-pane/frontend/src/api/websocket.test.ts +288 -0
  169. package/agent-pane/frontend/src/api/websocket.ts +27 -2
  170. package/agent-pane/frontend/src/api/workspaces.test.ts +112 -0
  171. package/agent-pane/frontend/src/api/workspaces.ts +17 -3
  172. package/agent-pane/frontend/src/components/CollapsibleToolCalls.test.tsx +49 -0
  173. package/agent-pane/frontend/src/components/CollapsibleToolCalls.tsx +2 -5
  174. package/agent-pane/frontend/src/components/CommandInputBar.test.tsx +133 -0
  175. package/agent-pane/frontend/src/components/CommandInputBar.tsx +296 -17
  176. package/agent-pane/frontend/src/components/CommandPalette.test.tsx +230 -0
  177. package/agent-pane/frontend/src/components/CommandPalette.tsx +26 -23
  178. package/agent-pane/frontend/src/components/IdeWorkspace.tsx +1 -1
  179. package/agent-pane/frontend/src/components/ModelSelector.test.tsx +129 -0
  180. package/agent-pane/frontend/src/components/ModelSelector.tsx +191 -0
  181. package/agent-pane/frontend/src/components/NewSessionDialog.test.tsx +95 -0
  182. package/agent-pane/frontend/src/components/NewSessionDialog.tsx +58 -13
  183. package/agent-pane/frontend/src/components/RenderArea.test.tsx +160 -0
  184. package/agent-pane/frontend/src/components/RenderArea.tsx +18 -25
  185. package/agent-pane/frontend/src/components/RightPanel.test.tsx +43 -0
  186. package/agent-pane/frontend/src/components/RightPanel.tsx +1 -1
  187. package/agent-pane/frontend/src/components/RuntimeIcon.test.tsx +32 -0
  188. package/agent-pane/frontend/src/components/RuntimeIcon.tsx +1 -1
  189. package/agent-pane/frontend/src/components/SessionTabs.test.tsx +72 -0
  190. package/agent-pane/frontend/src/components/SessionTabs.tsx +12 -12
  191. package/agent-pane/frontend/src/components/StatusBar.test.tsx +56 -0
  192. package/agent-pane/frontend/src/components/StatusBar.tsx +25 -31
  193. package/agent-pane/frontend/src/components/StatusDot.test.tsx +25 -0
  194. package/agent-pane/frontend/src/components/StatusDot.tsx +2 -7
  195. package/agent-pane/frontend/src/components/Toast.test.tsx +79 -0
  196. package/agent-pane/frontend/src/components/Toast.tsx +2 -6
  197. package/agent-pane/frontend/src/components/TurnContent.test.tsx +287 -0
  198. package/agent-pane/frontend/src/components/TurnContent.tsx +107 -24
  199. package/agent-pane/frontend/src/components/WorkspaceSidebar.test.tsx +297 -0
  200. package/agent-pane/frontend/src/components/WorkspaceSidebar.tsx +33 -32
  201. package/agent-pane/frontend/src/components/input/OptionSubPopup.tsx +32 -0
  202. package/agent-pane/frontend/src/components/input/SkillMentionPopup.test.tsx +63 -0
  203. package/agent-pane/frontend/src/components/input/SkillMentionPopup.tsx +78 -0
  204. package/agent-pane/frontend/src/components/input/SlashPopup.test.tsx +96 -0
  205. package/agent-pane/frontend/src/components/input/SlashPopup.tsx +63 -0
  206. package/agent-pane/frontend/src/components/panels/ChangesPanel.test.tsx +61 -0
  207. package/agent-pane/frontend/src/components/panels/ChangesPanel.tsx +12 -39
  208. package/agent-pane/frontend/src/components/panels/LogsPanel.test.tsx +58 -0
  209. package/agent-pane/frontend/src/components/panels/LogsPanel.tsx +1 -1
  210. package/agent-pane/frontend/src/components/panels/MetricsPanel.test.tsx +71 -0
  211. package/agent-pane/frontend/src/components/panels/MetricsPanel.tsx +63 -31
  212. package/agent-pane/frontend/src/components/panels/TurnsPanel.test.tsx +83 -0
  213. package/agent-pane/frontend/src/components/panels/TurnsPanel.tsx +66 -7
  214. package/agent-pane/frontend/src/constants/models.test.ts +97 -0
  215. package/agent-pane/frontend/src/constants/models.ts +129 -0
  216. package/agent-pane/frontend/src/hooks/useEscapeKey.test.ts +42 -0
  217. package/agent-pane/frontend/src/hooks/useEscapeKey.ts +13 -0
  218. package/agent-pane/frontend/src/hooks/useOutsideClick.test.ts +92 -0
  219. package/agent-pane/frontend/src/hooks/useOutsideClick.ts +19 -0
  220. package/agent-pane/frontend/src/hooks/useProvider.ts +44 -0
  221. package/agent-pane/frontend/src/hooks/useSessionStatus.test.ts +114 -0
  222. package/agent-pane/frontend/src/hooks/useSessionStatus.ts +34 -0
  223. package/agent-pane/frontend/src/main.test.tsx +37 -0
  224. package/agent-pane/frontend/src/main.tsx +1 -1
  225. package/agent-pane/frontend/src/store/sessionSlice.ts +417 -0
  226. package/agent-pane/frontend/src/store/store.test.ts +1284 -0
  227. package/agent-pane/frontend/src/store/store.ts +16 -617
  228. package/agent-pane/frontend/src/store/uiSlice.ts +83 -0
  229. package/agent-pane/frontend/src/store/workspaceSlice.ts +77 -0
  230. package/agent-pane/frontend/src/store/wsSlice.ts +292 -0
  231. package/agent-pane/frontend/src/styles/app.css +113 -7
  232. package/agent-pane/frontend/src/test/component-helpers.ts +36 -0
  233. package/agent-pane/frontend/src/test/fixtures.ts +96 -0
  234. package/agent-pane/frontend/src/test-setup.ts +5 -0
  235. package/agent-pane/frontend/src/types/index.test.ts +128 -0
  236. package/agent-pane/frontend/src/types/index.ts +80 -104
  237. package/agent-pane/frontend/src/utils/changes.test.ts +81 -0
  238. package/agent-pane/frontend/src/utils/changes.ts +39 -0
  239. package/agent-pane/frontend/src/utils/duration.test.ts +71 -0
  240. package/agent-pane/frontend/src/utils/duration.ts +14 -0
  241. package/agent-pane/frontend/tsconfig.json +4 -1
  242. package/agent-pane/frontend/vite.config.ts +6 -0
  243. package/agent-pane/frontend/vitest.config.ts +29 -0
  244. package/agent-pane/package-lock.json +1527 -0
  245. package/agent-pane/package.json +19 -0
  246. package/agent-pane/shared/provider-types.ts +53 -0
  247. package/agent-pane/shared/types.ts +107 -0
  248. package/agentpane-branch-session.png +0 -0
  249. package/agentpane-expanded-all.png +0 -0
  250. package/agentpane-fixed-rendering.png +0 -0
  251. package/agentpane-focus-check.png +0 -0
  252. package/agentpane-initial.png +0 -0
  253. package/agentpane-mcp-detail.png +0 -0
  254. package/agentpane-no-focus-ring.png +0 -0
  255. package/agentpane-soft-focus.png +0 -0
  256. package/backend/dist/src/llm-provider/ollama-llm.provider.js +1 -0
  257. package/backend/dist/src/llm-provider/ollama-llm.provider.js.map +1 -1
  258. package/backend/dist/tsconfig.build.tsbuildinfo +1 -1
  259. package/data.db +0 -0
  260. package/empty-state.png +0 -0
  261. package/frontend-home.png +0 -0
  262. package/model-selector-open.png +0 -0
  263. package/package.json +1 -1
  264. package/slash-popup-working.png +0 -0
  265. package/agent-pane/frontend/src/components/DiffViewer.tsx +0 -27
@@ -0,0 +1,69 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import Fastify, { type FastifyInstance } from 'fastify';
3
+ import { registerProviderRoutes } from './providers.ts';
4
+
5
+ describe('Provider routes', () => {
6
+ let app: FastifyInstance;
7
+
8
+ beforeEach(async () => {
9
+ app = Fastify();
10
+ await registerProviderRoutes(app);
11
+ });
12
+
13
+ describe('GET /api/providers', () => {
14
+ it('returns all provider definitions', async () => {
15
+ const res = await app.inject({ method: 'GET', url: '/api/providers' });
16
+ expect(res.statusCode).toBe(200);
17
+ const providers = res.json();
18
+ expect(providers).toHaveLength(2);
19
+ expect(providers[0].id).toBe('codex');
20
+ expect(providers[0].available).toBe(true);
21
+ expect(providers[1].id).toBe('claude');
22
+ expect(providers[1].available).toBe(true);
23
+ });
24
+
25
+ it('codex provider has all capabilities enabled', async () => {
26
+ const res = await app.inject({ method: 'GET', url: '/api/providers' });
27
+ const codex = res.json().find((p: { id: string }) => p.id === 'codex');
28
+ expect(codex.capabilities.dynamicModels).toBe(true);
29
+ expect(codex.capabilities.skills).toBe(true);
30
+ expect(codex.capabilities.plugins).toBe(true);
31
+ expect(codex.capabilities.mcpServers).toBe(true);
32
+ expect(codex.capabilities.imageAttachments).toBe(true);
33
+ expect(codex.capabilities.sandbox).toBe(true);
34
+ expect(codex.capabilities.webSearch).toBe(true);
35
+ expect(codex.capabilities.networkAccess).toBe(true);
36
+ expect(codex.capabilities.fileReferences).toBe(true);
37
+ });
38
+
39
+ it('claude provider has correct capabilities', async () => {
40
+ const res = await app.inject({ method: 'GET', url: '/api/providers' });
41
+ const claude = res.json().find((p: { id: string }) => p.id === 'claude');
42
+ expect(claude.available).toBe(true);
43
+ expect(claude.capabilities.dynamicModels).toBe(true);
44
+ expect(claude.capabilities.skills).toBe(true);
45
+ expect(claude.capabilities.plugins).toBe(true);
46
+ expect(claude.capabilities.mcpServers).toBe(true);
47
+ expect(claude.capabilities.imageAttachments).toBe(true);
48
+ expect(claude.capabilities.sandbox).toBe(false);
49
+ expect(claude.capabilities.webSearch).toBe(false);
50
+ expect(claude.capabilities.networkAccess).toBe(false);
51
+ expect(claude.capabilities.fileReferences).toBe(true);
52
+ });
53
+ });
54
+
55
+ describe('GET /api/providers/:id', () => {
56
+ it('returns codex provider by id', async () => {
57
+ const res = await app.inject({ method: 'GET', url: '/api/providers/codex' });
58
+ expect(res.statusCode).toBe(200);
59
+ expect(res.json().id).toBe('codex');
60
+ expect(res.json().label).toBe('Codex');
61
+ });
62
+
63
+ it('returns 404 for unknown provider', async () => {
64
+ const res = await app.inject({ method: 'GET', url: '/api/providers/unknown' });
65
+ expect(res.statusCode).toBe(404);
66
+ expect(res.json()).toEqual({ error: 'Provider not found' });
67
+ });
68
+ });
69
+ });
@@ -0,0 +1,57 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { ProviderDefinition } from '@agentpane/shared';
3
+
4
+ /** Codex provider definition — all capabilities enabled. */
5
+ const CODEX_PROVIDER: ProviderDefinition = {
6
+ id: 'codex',
7
+ label: 'Codex',
8
+ available: true,
9
+ capabilities: {
10
+ dynamicModels: true,
11
+ skills: true,
12
+ plugins: true,
13
+ mcpServers: true,
14
+ imageAttachments: true,
15
+ sandbox: true,
16
+ webSearch: true,
17
+ networkAccess: true,
18
+ fileReferences: true,
19
+ },
20
+ };
21
+
22
+ /** Claude provider definition — available via @anthropic-ai/claude-agent-sdk. */
23
+ const CLAUDE_PROVIDER: ProviderDefinition = {
24
+ id: 'claude',
25
+ label: 'Claude',
26
+ available: true,
27
+ capabilities: {
28
+ dynamicModels: true,
29
+ skills: true,
30
+ plugins: true,
31
+ mcpServers: true,
32
+ imageAttachments: true,
33
+ sandbox: false,
34
+ webSearch: false,
35
+ networkAccess: false,
36
+ fileReferences: true,
37
+ },
38
+ };
39
+
40
+ const PROVIDERS: ProviderDefinition[] = [CODEX_PROVIDER, CLAUDE_PROVIDER];
41
+
42
+ /** Register provider routes. */
43
+ export async function registerProviderRoutes(fastify: FastifyInstance): Promise<void> {
44
+ // GET /api/providers — list all provider definitions with capabilities
45
+ fastify.get('/api/providers', async () => {
46
+ return PROVIDERS;
47
+ });
48
+
49
+ // GET /api/providers/:id — get a single provider definition
50
+ fastify.get<{ Params: { id: string } }>('/api/providers/:id', async (request, reply) => {
51
+ const provider = PROVIDERS.find((p) => p.id === request.params.id);
52
+ if (!provider) {
53
+ return reply.code(404).send({ error: 'Provider not found' });
54
+ }
55
+ return provider;
56
+ });
57
+ }
@@ -0,0 +1,424 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import Fastify, { type FastifyInstance } from 'fastify';
3
+ import { initDb, getDb } from '../db/index.ts';
4
+ import { registerSessionRoutes } from './sessions.ts';
5
+ // Mock registry to avoid spawning real provider processes
6
+ const mocks = vi.hoisted(() => ({
7
+ clearThread: vi.fn(),
8
+ }));
9
+
10
+ vi.mock('../providers/registry.ts', () => ({
11
+ getProvider: vi.fn(() => ({
12
+ clearThread: mocks.clearThread,
13
+ startTurn: vi.fn(),
14
+ interruptTurn: vi.fn(),
15
+ clearAllThreads: vi.fn(),
16
+ getModels: vi.fn(),
17
+ shutdown: vi.fn(),
18
+ })),
19
+ hasProvider: vi.fn(() => true),
20
+ }));
21
+
22
+ const NOW = '2025-01-01T00:00:00Z';
23
+
24
+ /** Insert a session row with sensible defaults. */
25
+ function insertSession(db: ReturnType<typeof getDb>, id: string, overrides: Record<string, unknown> = {}): void {
26
+ db.prepare(
27
+ `INSERT INTO sessions (id, title, runtime_type, model, model_reasoning_effort, cwd, external_thread_id, status, is_running, lifecycle, has_unreviewed, created_at, updated_at, runtime_config)
28
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
29
+ ).run(
30
+ id,
31
+ (overrides['title'] as string | null) ?? null,
32
+ (overrides['runtime_type'] as string) ?? 'codex',
33
+ (overrides['model'] as string | null) ?? null,
34
+ (overrides['model_reasoning_effort'] as string | null) ?? null,
35
+ (overrides['cwd'] as string | null) ?? null,
36
+ (overrides['external_thread_id'] as string | null) ?? null,
37
+ (overrides['status'] as string) ?? 'active',
38
+ (overrides['is_running'] as number) ?? 0,
39
+ (overrides['lifecycle'] as string) ?? 'working',
40
+ (overrides['has_unreviewed'] as number) ?? 0,
41
+ (overrides['created_at'] as string) ?? NOW,
42
+ (overrides['updated_at'] as string) ?? NOW,
43
+ (overrides['runtime_config'] as string | null) ?? null,
44
+ );
45
+ }
46
+
47
+ /** Insert a turn row. */
48
+ function insertTurn(db: ReturnType<typeof getDb>, id: string, sessionId: string, createdAt = NOW): void {
49
+ db.prepare(
50
+ `INSERT INTO turns (id, session_id, user_prompt, agent_response, thoughts, token_usage, external_turn_id, status, error, created_at, updated_at)
51
+ VALUES (?, ?, ?, NULL, NULL, NULL, NULL, 'pending', NULL, ?, ?)`,
52
+ ).run(id, sessionId, 'hello', createdAt, createdAt);
53
+ }
54
+
55
+ /** Insert an item row. */
56
+ function insertItem(db: ReturnType<typeof getDb>, id: string, turnId: string, type = 'agent_message'): void {
57
+ db.prepare(
58
+ `INSERT INTO items (id, turn_id, external_item_id, type, data, status, created_at, updated_at)
59
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
60
+ ).run(id, turnId, `ext-${id}`, type, JSON.stringify({ text: 'test' }), 'completed', NOW, NOW);
61
+ }
62
+
63
+ describe('Session routes', () => {
64
+ let app: FastifyInstance;
65
+
66
+ beforeEach(async () => {
67
+ vi.clearAllMocks();
68
+ initDb(':memory:');
69
+ app = Fastify();
70
+ await registerSessionRoutes(app);
71
+ });
72
+
73
+ describe('GET /api/sessions', () => {
74
+ it('returns empty array when no sessions', async () => {
75
+ const res = await app.inject({ method: 'GET', url: '/api/sessions' });
76
+ expect(res.statusCode).toBe(200);
77
+ expect(res.json()).toEqual([]);
78
+ });
79
+
80
+ it('returns sessions ordered by created_at DESC', async () => {
81
+ const db = getDb();
82
+ insertSession(db, 's1', { created_at: '2025-01-01T00:00:00Z' });
83
+ insertSession(db, 's2', { created_at: '2025-01-02T00:00:00Z' });
84
+ const res = await app.inject({ method: 'GET', url: '/api/sessions' });
85
+ const sessions = res.json();
86
+ expect(sessions).toHaveLength(2);
87
+ expect(sessions[0].id).toBe('s2');
88
+ expect(sessions[1].id).toBe('s1');
89
+ });
90
+ });
91
+
92
+ describe('GET /api/sessions/:id', () => {
93
+ it('returns 404 for missing session', async () => {
94
+ const res = await app.inject({ method: 'GET', url: '/api/sessions/nonexistent' });
95
+ expect(res.statusCode).toBe(404);
96
+ expect(res.json()).toEqual({ error: 'Session not found' });
97
+ });
98
+
99
+ it('returns session with turns and items', async () => {
100
+ const db = getDb();
101
+ insertSession(db, 's1');
102
+ insertTurn(db, 't1', 's1');
103
+ insertItem(db, 'i1', 't1', 'agent_message');
104
+
105
+ const res = await app.inject({ method: 'GET', url: '/api/sessions/s1' });
106
+ expect(res.statusCode).toBe(200);
107
+ const detail = res.json();
108
+ expect(detail.id).toBe('s1');
109
+ expect(detail.turns).toHaveLength(1);
110
+ expect(detail.turns[0].id).toBe('t1');
111
+ expect(detail.turns[0].items).toHaveLength(1);
112
+ expect(detail.turns[0].items[0].id).toBe('i1');
113
+ expect(detail.turns[0].items[0].data).toEqual({ text: 'test' });
114
+ });
115
+ });
116
+
117
+ describe('POST /api/sessions', () => {
118
+ it('creates a session with defaults', async () => {
119
+ const res = await app.inject({ method: 'POST', url: '/api/sessions', payload: {} });
120
+ expect(res.statusCode).toBe(200);
121
+ const session = res.json();
122
+ expect(session.id).toBeDefined();
123
+ expect(session.runtime_type).toBe('codex');
124
+ expect(session.status).toBe('active');
125
+ expect(session.is_running).toBe(0);
126
+ expect(session.title).toBeNull();
127
+ expect(session.model).toBeNull();
128
+ });
129
+
130
+ it('creates a session with provided values', async () => {
131
+ const res = await app.inject({
132
+ method: 'POST',
133
+ url: '/api/sessions',
134
+ payload: {
135
+ title: 'Test',
136
+ runtime_type: 'claude',
137
+ model: 'gpt-4',
138
+ model_reasoning_effort: 'high',
139
+ cwd: '/home',
140
+ },
141
+ });
142
+ expect(res.statusCode).toBe(200);
143
+ const session = res.json();
144
+ expect(session.title).toBe('Test');
145
+ expect(session.runtime_type).toBe('claude');
146
+ expect(session.model).toBe('gpt-4');
147
+ expect(session.model_reasoning_effort).toBe('high');
148
+ expect(session.cwd).toBe('/home');
149
+ });
150
+
151
+ it('creates a session with runtime_config', async () => {
152
+ const config = JSON.stringify({ sandboxMode: 'read-only' });
153
+ const res = await app.inject({
154
+ method: 'POST',
155
+ url: '/api/sessions',
156
+ payload: { runtime_config: config },
157
+ });
158
+ expect(res.statusCode).toBe(200);
159
+ expect(res.json().runtime_config).toBe(config);
160
+ });
161
+ });
162
+
163
+ describe('PATCH /api/sessions/:id — runtime_config', () => {
164
+ it('updates runtime_config', async () => {
165
+ const db = getDb();
166
+ insertSession(db, 's1');
167
+ const config = JSON.stringify({ sandboxMode: 'read-only', webSearchMode: 'live' });
168
+ const res = await app.inject({
169
+ method: 'PATCH',
170
+ url: '/api/sessions/s1',
171
+ payload: { runtime_config: config },
172
+ });
173
+ expect(res.statusCode).toBe(200);
174
+ expect(res.json().runtime_config).toBe(config);
175
+ });
176
+
177
+ it('clears runtime_config with null', async () => {
178
+ const db = getDb();
179
+ insertSession(db, 's1', { runtime_config: JSON.stringify({ sandboxMode: 'read-only' }) });
180
+ const res = await app.inject({
181
+ method: 'PATCH',
182
+ url: '/api/sessions/s1',
183
+ payload: { runtime_config: null },
184
+ });
185
+ expect(res.statusCode).toBe(200);
186
+ expect(res.json().runtime_config).toBeNull();
187
+ });
188
+ });
189
+
190
+ describe('DELETE /api/sessions/:id', () => {
191
+ it('returns 404 for missing session', async () => {
192
+ const res = await app.inject({ method: 'DELETE', url: '/api/sessions/nonexistent' });
193
+ expect(res.statusCode).toBe(404);
194
+ });
195
+
196
+ it('deletes session and cascades to turns and items', async () => {
197
+ const db = getDb();
198
+ insertSession(db, 's1');
199
+ insertTurn(db, 't1', 's1');
200
+ insertItem(db, 'i1', 't1');
201
+
202
+ const res = await app.inject({ method: 'DELETE', url: '/api/sessions/s1' });
203
+ expect(res.statusCode).toBe(200);
204
+ expect(res.json()).toEqual({ success: true });
205
+
206
+ expect(db.prepare('SELECT * FROM sessions WHERE id = ?').get('s1')).toBeUndefined();
207
+ expect(db.prepare('SELECT * FROM turns WHERE session_id = ?').get('s1')).toBeUndefined();
208
+ expect(db.prepare('SELECT * FROM items WHERE turn_id = ?').get('t1')).toBeUndefined();
209
+ });
210
+ });
211
+
212
+ describe('PATCH /api/sessions/:id', () => {
213
+ it('returns 404 for missing session', async () => {
214
+ const res = await app.inject({
215
+ method: 'PATCH',
216
+ url: '/api/sessions/nonexistent',
217
+ payload: { title: 'New' },
218
+ });
219
+ expect(res.statusCode).toBe(404);
220
+ });
221
+
222
+ it('updates title', async () => {
223
+ const db = getDb();
224
+ insertSession(db, 's1');
225
+ const res = await app.inject({ method: 'PATCH', url: '/api/sessions/s1', payload: { title: 'Updated' } });
226
+ expect(res.statusCode).toBe(200);
227
+ expect(res.json().title).toBe('Updated');
228
+ });
229
+
230
+ it('updates lifecycle', async () => {
231
+ const db = getDb();
232
+ insertSession(db, 's1');
233
+ const res = await app.inject({ method: 'PATCH', url: '/api/sessions/s1', payload: { lifecycle: 'finished' } });
234
+ expect(res.statusCode).toBe(200);
235
+ expect(res.json().lifecycle).toBe('finished');
236
+ });
237
+
238
+ it('updates has_unreviewed', async () => {
239
+ const db = getDb();
240
+ insertSession(db, 's1');
241
+ const res = await app.inject({ method: 'PATCH', url: '/api/sessions/s1', payload: { has_unreviewed: 1 } });
242
+ expect(res.statusCode).toBe(200);
243
+ expect(res.json().has_unreviewed).toBe(1);
244
+ });
245
+
246
+ it('clears thread when model changes and external_thread_id exists', async () => {
247
+ const db = getDb();
248
+ insertSession(db, 's1', { model: 'gpt-4', external_thread_id: 'thread-123' });
249
+ const res = await app.inject({ method: 'PATCH', url: '/api/sessions/s1', payload: { model: 'claude-3' } });
250
+ expect(res.statusCode).toBe(200);
251
+ expect(mocks.clearThread).toHaveBeenCalledWith('s1');
252
+ const session = db.prepare('SELECT external_thread_id FROM sessions WHERE id = ?').get('s1') as {
253
+ external_thread_id: string | null;
254
+ };
255
+ expect(session.external_thread_id).toBeNull();
256
+ });
257
+
258
+ it('does not clear thread when model is unchanged', async () => {
259
+ const db = getDb();
260
+ insertSession(db, 's1', { model: 'gpt-4', external_thread_id: 'thread-123' });
261
+ const res = await app.inject({ method: 'PATCH', url: '/api/sessions/s1', payload: { model: 'gpt-4' } });
262
+ expect(res.statusCode).toBe(200);
263
+ expect(mocks.clearThread).not.toHaveBeenCalled();
264
+ });
265
+
266
+ it('switches runtime_type and clears thread', async () => {
267
+ const db = getDb();
268
+ insertSession(db, 's1', { runtime_type: 'codex', model: 'gpt-5.6-sol', external_thread_id: 'thread-123' });
269
+ const res = await app.inject({
270
+ method: 'PATCH',
271
+ url: '/api/sessions/s1',
272
+ payload: { runtime_type: 'claude' },
273
+ });
274
+ expect(res.statusCode).toBe(200);
275
+ const session = res.json();
276
+ expect(session.runtime_type).toBe('claude');
277
+ expect(session.model).toBeNull();
278
+ expect(session.model_reasoning_effort).toBeNull();
279
+ expect(session.external_thread_id).toBeNull();
280
+ expect(mocks.clearThread).toHaveBeenCalledWith('s1');
281
+ });
282
+
283
+ it('switches runtime_type with explicit model', async () => {
284
+ const db = getDb();
285
+ insertSession(db, 's1', { runtime_type: 'codex', model: 'gpt-5.6-sol' });
286
+ const res = await app.inject({
287
+ method: 'PATCH',
288
+ url: '/api/sessions/s1',
289
+ payload: { runtime_type: 'claude', model: 'claude-sonnet-5', model_reasoning_effort: 'high' },
290
+ });
291
+ expect(res.statusCode).toBe(200);
292
+ const session = res.json();
293
+ expect(session.runtime_type).toBe('claude');
294
+ expect(session.model).toBe('claude-sonnet-5');
295
+ expect(session.model_reasoning_effort).toBe('high');
296
+ });
297
+
298
+ it('returns 409 when switching provider while running', async () => {
299
+ const db = getDb();
300
+ insertSession(db, 's1', { runtime_type: 'codex', is_running: 1 });
301
+ const res = await app.inject({
302
+ method: 'PATCH',
303
+ url: '/api/sessions/s1',
304
+ payload: { runtime_type: 'claude' },
305
+ });
306
+ expect(res.statusCode).toBe(409);
307
+ expect(res.json().error).toContain('Cannot switch provider');
308
+ });
309
+ });
310
+
311
+ describe('POST /api/sessions/:id/rewind', () => {
312
+ it('returns 400 when turn_id is missing', async () => {
313
+ const res = await app.inject({ method: 'POST', url: '/api/sessions/s1/rewind', payload: {} });
314
+ expect(res.statusCode).toBe(400);
315
+ });
316
+
317
+ it('returns 404 for missing session', async () => {
318
+ const res = await app.inject({
319
+ method: 'POST',
320
+ url: '/api/sessions/nonexistent/rewind',
321
+ payload: { turn_id: 't1' },
322
+ });
323
+ expect(res.statusCode).toBe(404);
324
+ });
325
+
326
+ it('returns 409 when session is running', async () => {
327
+ const db = getDb();
328
+ insertSession(db, 's1', { is_running: 1 });
329
+ const res = await app.inject({ method: 'POST', url: '/api/sessions/s1/rewind', payload: { turn_id: 't1' } });
330
+ expect(res.statusCode).toBe(409);
331
+ });
332
+
333
+ it('returns 404 when turn is not found', async () => {
334
+ const db = getDb();
335
+ insertSession(db, 's1');
336
+ const res = await app.inject({
337
+ method: 'POST',
338
+ url: '/api/sessions/s1/rewind',
339
+ payload: { turn_id: 'nonexistent' },
340
+ });
341
+ expect(res.statusCode).toBe(404);
342
+ expect(res.json().error).toBe('Turn not found');
343
+ });
344
+
345
+ it('deletes turns after target and nulls external_thread_id', async () => {
346
+ const db = getDb();
347
+ insertSession(db, 's1', { external_thread_id: 'thread-123' });
348
+ insertTurn(db, 't1', 's1', '2025-01-01T00:00:00Z');
349
+ insertTurn(db, 't2', 's1', '2025-01-01T01:00:00Z');
350
+ insertTurn(db, 't3', 's1', '2025-01-01T02:00:00Z');
351
+
352
+ const res = await app.inject({ method: 'POST', url: '/api/sessions/s1/rewind', payload: { turn_id: 't1' } });
353
+ expect(res.statusCode).toBe(200);
354
+
355
+ const turns = db.prepare('SELECT id FROM turns WHERE session_id = ? ORDER BY created_at').all('s1') as {
356
+ id: string;
357
+ }[];
358
+ expect(turns).toHaveLength(1);
359
+ expect(turns[0].id).toBe('t1');
360
+
361
+ const session = db.prepare('SELECT external_thread_id FROM sessions WHERE id = ?').get('s1') as {
362
+ external_thread_id: string | null;
363
+ };
364
+ expect(session.external_thread_id).toBeNull();
365
+ expect(mocks.clearThread).toHaveBeenCalledWith('s1');
366
+ });
367
+ });
368
+
369
+ describe('POST /api/sessions/:id/branch', () => {
370
+ it('returns 400 when turn_id is missing', async () => {
371
+ const res = await app.inject({ method: 'POST', url: '/api/sessions/s1/branch', payload: {} });
372
+ expect(res.statusCode).toBe(400);
373
+ });
374
+
375
+ it('returns 404 for missing session', async () => {
376
+ const res = await app.inject({
377
+ method: 'POST',
378
+ url: '/api/sessions/nonexistent/branch',
379
+ payload: { turn_id: 't1' },
380
+ });
381
+ expect(res.statusCode).toBe(404);
382
+ });
383
+
384
+ it('returns 404 when turn is not found', async () => {
385
+ const db = getDb();
386
+ insertSession(db, 's1');
387
+ const res = await app.inject({
388
+ method: 'POST',
389
+ url: '/api/sessions/s1/branch',
390
+ payload: { turn_id: 'nonexistent' },
391
+ });
392
+ expect(res.statusCode).toBe(404);
393
+ expect(res.json().error).toBe('Turn not found');
394
+ });
395
+
396
+ it('creates new session with copied turns up to target', async () => {
397
+ const db = getDb();
398
+ insertSession(db, 's1', { title: 'Original', model: 'gpt-4' });
399
+ insertTurn(db, 't1', 's1', '2025-01-01T00:00:00Z');
400
+ insertTurn(db, 't2', 's1', '2025-01-01T01:00:00Z');
401
+ insertItem(db, 'i1', 't1');
402
+
403
+ const res = await app.inject({ method: 'POST', url: '/api/sessions/s1/branch', payload: { turn_id: 't1' } });
404
+ expect(res.statusCode).toBe(200);
405
+ const newSession = res.json();
406
+ expect(newSession.id).not.toBe('s1');
407
+ expect(newSession.title).toBe('Branch of Original');
408
+ expect(newSession.model).toBe('gpt-4');
409
+ expect(newSession.external_thread_id).toBeNull();
410
+
411
+ // Only t1 copied (not t2)
412
+ const newTurns = db
413
+ .prepare('SELECT * FROM turns WHERE session_id = ? ORDER BY created_at')
414
+ .all(newSession.id) as { id: string; user_prompt: string }[];
415
+ expect(newTurns).toHaveLength(1);
416
+ expect(newTurns[0].user_prompt).toBe('hello');
417
+
418
+ // Item copied
419
+ const newItems = db.prepare('SELECT * FROM items WHERE turn_id = ?').all(newTurns[0].id) as { type: string }[];
420
+ expect(newItems).toHaveLength(1);
421
+ expect(newItems[0].type).toBe('agent_message');
422
+ });
423
+ });
424
+ });