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
@@ -1,10 +1,11 @@
1
1
  import type { FastifyInstance } from 'fastify';
2
2
  import { v4 as uuidv4 } from 'uuid';
3
3
  import { getDb } from '../db/index.ts';
4
+ import { getProvider } from '../providers/registry.ts';
4
5
  import type { Session, SessionDetail, Turn, Item } from '../db/schema.ts';
5
6
 
6
- /** Raw DB row for turns — token_usage is a JSON string, not parsed. */
7
- interface TurnRow extends Omit<Turn, 'token_usage'> {
7
+ /** Raw DB row for turns — token_usage is a JSON string, not parsed; no items field. */
8
+ interface TurnRow extends Omit<Turn, 'token_usage' | 'items'> {
8
9
  token_usage: string | null;
9
10
  }
10
11
 
@@ -30,10 +31,14 @@ export async function registerSessionRoutes(fastify: FastifyInstance): Promise<v
30
31
  return reply.code(404).send({ error: 'Session not found' });
31
32
  }
32
33
 
33
- const turnRows = db.prepare('SELECT * FROM turns WHERE session_id = ? ORDER BY created_at ASC').all(id) as TurnRow[];
34
- const itemRows = db.prepare(
35
- 'SELECT * FROM items WHERE turn_id IN (SELECT id FROM turns WHERE session_id = ?) ORDER BY created_at ASC',
36
- ).all(id) as ItemRow[];
34
+ const turnRows = db
35
+ .prepare('SELECT * FROM turns WHERE session_id = ? ORDER BY created_at ASC')
36
+ .all(id) as TurnRow[];
37
+ const itemRows = db
38
+ .prepare(
39
+ 'SELECT * FROM items WHERE turn_id IN (SELECT id FROM turns WHERE session_id = ?) ORDER BY created_at ASC',
40
+ )
41
+ .all(id) as ItemRow[];
37
42
 
38
43
  // Parse item data from JSON string and group by turn_id
39
44
  const itemsByTurn = new Map<string, Item[]>();
@@ -60,22 +65,44 @@ export async function registerSessionRoutes(fastify: FastifyInstance): Promise<v
60
65
  });
61
66
 
62
67
  // POST /api/sessions — create session
63
- fastify.post<{ Body: { title?: string; runtime_type?: string; model?: string; cwd?: string } }>(
64
- '/api/sessions',
65
- async (request) => {
66
- const { title = null, runtime_type = 'codex', model = null, cwd = null } = request.body || {};
67
- const id = uuidv4();
68
- const now = new Date().toISOString();
68
+ fastify.post<{
69
+ Body: { title?: string; runtime_type?: string; model?: string; model_reasoning_effort?: string; cwd?: string; runtime_config?: string };
70
+ }>('/api/sessions', {
71
+ schema: {
72
+ body: {
73
+ type: 'object',
74
+ properties: {
75
+ title: { type: 'string' },
76
+ runtime_type: { type: 'string' },
77
+ model: { type: ['string', 'null'] },
78
+ model_reasoning_effort: { type: ['string', 'null'] },
79
+ cwd: { type: ['string', 'null'] },
80
+ runtime_config: { type: ['string', 'null'] },
81
+ },
82
+ },
83
+ },
84
+ }, async (request) => {
85
+ const {
86
+ title = null,
87
+ runtime_type = 'codex',
88
+ model = null,
89
+ model_reasoning_effort = null,
90
+ cwd = null,
91
+ runtime_config = null,
92
+ } = request.body || {};
93
+ const id = uuidv4();
94
+ const now = new Date().toISOString();
69
95
 
70
- getDb().prepare(
71
- `INSERT INTO sessions (id, title, runtime_type, model, cwd, external_thread_id, status, is_running, created_at, updated_at)
72
- VALUES (?, ?, ?, ?, ?, NULL, 'active', 0, ?, ?)`,
73
- ).run(id, title, runtime_type, model, cwd, now, now);
96
+ getDb()
97
+ .prepare(
98
+ `INSERT INTO sessions (id, title, runtime_type, model, model_reasoning_effort, cwd, external_thread_id, status, is_running, created_at, updated_at, runtime_config)
99
+ VALUES (?, ?, ?, ?, ?, ?, NULL, 'active', 0, ?, ?, ?)`,
100
+ )
101
+ .run(id, title, runtime_type, model, model_reasoning_effort, cwd, now, now, runtime_config);
74
102
 
75
- const session = getDb().prepare('SELECT * FROM sessions WHERE id = ?').get(id) as Session;
76
- return session;
77
- },
78
- );
103
+ const session = getDb().prepare('SELECT * FROM sessions WHERE id = ?').get(id) as Session;
104
+ return session;
105
+ });
79
106
 
80
107
  // DELETE /api/sessions/:id — delete session (cascade)
81
108
  fastify.delete<{ Params: { id: string } }>('/api/sessions/:id', async (request, reply) => {
@@ -95,40 +122,266 @@ export async function registerSessionRoutes(fastify: FastifyInstance): Promise<v
95
122
  return { success: true };
96
123
  });
97
124
 
98
- // PATCH /api/sessions/:id — update session (title, lifecycle, has_unreviewed)
99
- fastify.patch<{ Params: { id: string }; Body: { title?: string; lifecycle?: string; has_unreviewed?: number } }>(
100
- '/api/sessions/:id',
125
+ // PATCH /api/sessions/:id — update session (title, lifecycle, has_unreviewed, model, model_reasoning_effort, runtime_config)
126
+ fastify.patch<{
127
+ Params: { id: string };
128
+ Body: {
129
+ title?: string;
130
+ lifecycle?: string;
131
+ has_unreviewed?: number;
132
+ model?: string | null;
133
+ model_reasoning_effort?: string | null;
134
+ runtime_config?: string | null;
135
+ runtime_type?: string;
136
+ };
137
+ }>('/api/sessions/:id', {
138
+ schema: {
139
+ body: {
140
+ type: 'object',
141
+ properties: {
142
+ title: { type: 'string' },
143
+ lifecycle: { type: 'string' },
144
+ has_unreviewed: { type: 'number' },
145
+ model: { type: ['string', 'null'] },
146
+ model_reasoning_effort: { type: ['string', 'null'] },
147
+ runtime_config: { type: ['string', 'null'] },
148
+ runtime_type: { type: 'string' },
149
+ },
150
+ },
151
+ },
152
+ }, async (request, reply) => {
153
+ const { id } = request.params;
154
+ const { title, lifecycle, has_unreviewed, model, model_reasoning_effort, runtime_config, runtime_type } = request.body || {};
155
+ const db = getDb();
156
+
157
+ const existing = db.prepare('SELECT * FROM sessions WHERE id = ?').get(id) as Session | undefined;
158
+ if (!existing) {
159
+ return reply.code(404).send({ error: 'Session not found' });
160
+ }
161
+
162
+ // Block provider switch while a turn is running
163
+ if (runtime_type !== undefined && runtime_type !== existing.runtime_type && existing.is_running) {
164
+ return reply.code(409).send({ error: 'Cannot switch provider while a turn is running' });
165
+ }
166
+
167
+ const now = new Date().toISOString();
168
+ const updates: string[] = ['updated_at = ?'];
169
+ const params: (string | number | null)[] = [now];
170
+
171
+ let modelChanged = false;
172
+
173
+ if (title !== undefined) {
174
+ updates.push('title = ?');
175
+ params.push(title);
176
+ }
177
+ if (lifecycle !== undefined) {
178
+ updates.push('lifecycle = ?');
179
+ params.push(lifecycle);
180
+ }
181
+ if (has_unreviewed !== undefined) {
182
+ updates.push('has_unreviewed = ?');
183
+ params.push(has_unreviewed);
184
+ }
185
+
186
+ // Provider switch — clear model/reasoning if not explicitly provided
187
+ if (runtime_type !== undefined && runtime_type !== existing.runtime_type) {
188
+ modelChanged = true; // old model is invalid for new provider
189
+ updates.push('runtime_type = ?');
190
+ params.push(runtime_type);
191
+
192
+ // Clear model if not explicitly set in this request
193
+ if (model === undefined) {
194
+ updates.push('model = ?');
195
+ params.push(null);
196
+ }
197
+ // Clear reasoning if not explicitly set
198
+ if (model_reasoning_effort === undefined) {
199
+ updates.push('model_reasoning_effort = ?');
200
+ params.push(null);
201
+ }
202
+ }
203
+
204
+ if (model !== undefined) {
205
+ updates.push('model = ?');
206
+ params.push(model);
207
+ if (model !== existing.model) modelChanged = true;
208
+ }
209
+ if (model_reasoning_effort !== undefined) {
210
+ updates.push('model_reasoning_effort = ?');
211
+ params.push(model_reasoning_effort);
212
+ if (model_reasoning_effort !== existing.model_reasoning_effort) modelChanged = true;
213
+ }
214
+ if (runtime_config !== undefined) {
215
+ updates.push('runtime_config = ?');
216
+ params.push(runtime_config);
217
+ }
218
+
219
+ db.prepare(`UPDATE sessions SET ${updates.join(', ')} WHERE id = ?`).run(...params, id);
220
+
221
+ // If provider or model changed, clear the cached thread and clean up old provider
222
+ if (modelChanged && existing.external_thread_id) {
223
+ db.prepare('UPDATE sessions SET external_thread_id = NULL, updated_at = ? WHERE id = ?').run(now, id);
224
+ // Use old provider to clear its in-memory thread state
225
+ getProvider(existing.runtime_type).clearThread(id);
226
+ }
227
+
228
+ const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(id) as Session;
229
+ return session;
230
+ });
231
+
232
+ // POST /api/sessions/:id/rewind — delete turns after target turn, reset Codex thread
233
+ fastify.post<{ Params: { id: string }; Body: { turn_id: string } }>(
234
+ '/api/sessions/:id/rewind',
235
+ {
236
+ schema: {
237
+ body: {
238
+ type: 'object',
239
+ required: ['turn_id'],
240
+ properties: {
241
+ turn_id: { type: 'string' },
242
+ },
243
+ },
244
+ },
245
+ },
101
246
  async (request, reply) => {
102
247
  const { id } = request.params;
103
- const { title, lifecycle, has_unreviewed } = request.body || {};
104
- const db = getDb();
248
+ const { turn_id } = request.body || {};
249
+ if (!turn_id) {
250
+ return reply.code(400).send({ error: 'turn_id is required' });
251
+ }
105
252
 
106
- const existing = db.prepare('SELECT id FROM sessions WHERE id = ?').get(id);
107
- if (!existing) {
253
+ const db = getDb();
254
+ const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(id) as Session | undefined;
255
+ if (!session) {
108
256
  return reply.code(404).send({ error: 'Session not found' });
109
257
  }
258
+ if (session.is_running) {
259
+ return reply.code(409).send({ error: 'Cannot rewind while a turn is running' });
260
+ }
261
+
262
+ const targetTurn = db.prepare('SELECT created_at FROM turns WHERE id = ? AND session_id = ?').get(turn_id, id) as
263
+ { created_at: string } | undefined;
264
+ if (!targetTurn) {
265
+ return reply.code(404).send({ error: 'Turn not found' });
266
+ }
110
267
 
268
+ // Delete turns after the target (cascade deletes items via FK)
269
+ db.prepare('DELETE FROM turns WHERE session_id = ? AND created_at > ?').run(id, targetTurn.created_at);
270
+
271
+ // Null out external_thread_id — next turn creates a fresh Codex thread
111
272
  const now = new Date().toISOString();
112
- const updates: string[] = ['updated_at = ?'];
113
- const params: (string | number | null)[] = [now];
273
+ db.prepare('UPDATE sessions SET external_thread_id = NULL, updated_at = ? WHERE id = ?').run(now, id);
274
+
275
+ // Clear in-memory thread entry
276
+ getProvider(session.runtime_type).clearThread(id);
114
277
 
115
- if (title !== undefined) {
116
- updates.push('title = ?');
117
- params.push(title);
278
+ const updated = db.prepare('SELECT * FROM sessions WHERE id = ?').get(id) as Session;
279
+ return updated;
280
+ },
281
+ );
282
+
283
+ // POST /api/sessions/:id/branch — create new session with turns up to and including target turn
284
+ fastify.post<{ Params: { id: string }; Body: { turn_id: string; title?: string } }>(
285
+ '/api/sessions/:id/branch',
286
+ {
287
+ schema: {
288
+ body: {
289
+ type: 'object',
290
+ required: ['turn_id'],
291
+ properties: {
292
+ turn_id: { type: 'string' },
293
+ title: { type: 'string' },
294
+ },
295
+ },
296
+ },
297
+ },
298
+ async (request, reply) => {
299
+ const { id } = request.params;
300
+ const { turn_id, title } = request.body || {};
301
+ if (!turn_id) {
302
+ return reply.code(400).send({ error: 'turn_id is required' });
118
303
  }
119
- if (lifecycle !== undefined) {
120
- updates.push('lifecycle = ?');
121
- params.push(lifecycle);
304
+
305
+ const db = getDb();
306
+ const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(id) as Session | undefined;
307
+ if (!session) {
308
+ return reply.code(404).send({ error: 'Session not found' });
122
309
  }
123
- if (has_unreviewed !== undefined) {
124
- updates.push('has_unreviewed = ?');
125
- params.push(has_unreviewed);
310
+
311
+ const targetTurn = db.prepare('SELECT created_at FROM turns WHERE id = ? AND session_id = ?').get(turn_id, id) as
312
+ { created_at: string } | undefined;
313
+ if (!targetTurn) {
314
+ return reply.code(404).send({ error: 'Turn not found' });
126
315
  }
127
316
 
128
- db.prepare(`UPDATE sessions SET ${updates.join(', ')} WHERE id = ?`).run(...params, id);
317
+ // Create new session
318
+ const newSessionId = uuidv4();
319
+ const now = new Date().toISOString();
320
+ const newTitle = title || `Branch of ${session.title || session.id.slice(0, 8)}`;
321
+
322
+ db.prepare(
323
+ `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)
324
+ VALUES (?, ?, ?, ?, ?, ?, NULL, 'active', 0, 'working', 0, ?, ?, ?)`,
325
+ ).run(
326
+ newSessionId,
327
+ newTitle,
328
+ session.runtime_type,
329
+ session.model,
330
+ session.model_reasoning_effort,
331
+ session.cwd,
332
+ now,
333
+ now,
334
+ session.runtime_config,
335
+ );
336
+
337
+ // Copy turns up to and including the target turn
338
+ const turnRows = db
339
+ .prepare('SELECT * FROM turns WHERE session_id = ? AND created_at <= ? ORDER BY created_at ASC')
340
+ .all(id, targetTurn.created_at) as TurnRow[];
341
+
342
+ for (const turn of turnRows) {
343
+ const newTurnId = uuidv4();
344
+ db.prepare(
345
+ `INSERT INTO turns (id, session_id, user_prompt, agent_response, thoughts, token_usage, external_turn_id, status, error, created_at, updated_at)
346
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
347
+ ).run(
348
+ newTurnId,
349
+ newSessionId,
350
+ turn.user_prompt,
351
+ turn.agent_response,
352
+ turn.thoughts,
353
+ turn.token_usage,
354
+ turn.external_turn_id,
355
+ turn.status,
356
+ turn.error,
357
+ turn.created_at,
358
+ turn.updated_at,
359
+ );
129
360
 
130
- const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(id) as Session;
131
- return session;
361
+ // Copy items for this turn
362
+ const itemRows = db
363
+ .prepare('SELECT * FROM items WHERE turn_id = ? ORDER BY created_at ASC')
364
+ .all(turn.id) as ItemRow[];
365
+ for (const item of itemRows) {
366
+ const newItemId = uuidv4();
367
+ db.prepare(
368
+ `INSERT INTO items (id, turn_id, external_item_id, type, data, status, created_at, updated_at)
369
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
370
+ ).run(
371
+ newItemId,
372
+ newTurnId,
373
+ item.external_item_id,
374
+ item.type,
375
+ item.data,
376
+ item.status,
377
+ item.created_at,
378
+ item.updated_at,
379
+ );
380
+ }
381
+ }
382
+
383
+ const newSession = db.prepare('SELECT * FROM sessions WHERE id = ?').get(newSessionId) as Session;
384
+ return newSession;
132
385
  },
133
386
  );
134
- }
387
+ }
@@ -0,0 +1,114 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import Fastify, { type FastifyInstance } from 'fastify';
3
+
4
+ // Mock shared-app-server to return a mock client
5
+ const mockRequest = vi.fn();
6
+
7
+ vi.mock('../codex/shared-app-server.ts', () => ({
8
+ getSharedAppServer: vi.fn(() => ({
9
+ request: mockRequest,
10
+ isClosed: vi.fn(() => false),
11
+ initialize: vi.fn(() => Promise.resolve()),
12
+ close: vi.fn(() => Promise.resolve()),
13
+ })),
14
+ }));
15
+
16
+ // Mock node:fs for readFileSync used in GET /api/skills/:name
17
+ vi.mock('node:fs', () => ({
18
+ readFileSync: vi.fn(),
19
+ }));
20
+
21
+ import { registerSkillRoutes } from './skills.ts';
22
+ import { readFileSync } from 'node:fs';
23
+ import type { SkillsListResult } from '../codex/app-server-types.ts';
24
+
25
+ const MOCK_SKILLS_RESULT: SkillsListResult = {
26
+ data: [
27
+ {
28
+ cwd: '/tmp',
29
+ skills: [
30
+ {
31
+ name: 'cloudflare',
32
+ description: 'Cloudflare skill',
33
+ path: '/home/.codex/skills/cloudflare/SKILL.md',
34
+ scope: 'user',
35
+ enabled: true,
36
+ },
37
+ {
38
+ name: 'imagegen',
39
+ description: 'Image generation skill',
40
+ path: '/home/.codex/skills/.system/imagegen/SKILL.md',
41
+ scope: 'system',
42
+ enabled: true,
43
+ interface: {
44
+ displayName: 'Image Gen',
45
+ shortDescription: 'Generate images',
46
+ },
47
+ },
48
+ ],
49
+ errors: [],
50
+ },
51
+ ],
52
+ };
53
+
54
+ describe('Skill routes', () => {
55
+ let app: FastifyInstance;
56
+
57
+ beforeEach(async () => {
58
+ vi.clearAllMocks();
59
+ mockRequest.mockResolvedValue(MOCK_SKILLS_RESULT);
60
+ app = Fastify();
61
+ await registerSkillRoutes(app);
62
+ });
63
+
64
+ describe('GET /api/skills', () => {
65
+ it('returns skills from app-server', async () => {
66
+ const res = await app.inject({ method: 'GET', url: '/api/skills' });
67
+ expect(res.statusCode).toBe(200);
68
+ const skills = res.json();
69
+ expect(skills).toHaveLength(2);
70
+ expect(skills[0].name).toBe('cloudflare');
71
+ expect(skills[0].type).toBe('skill');
72
+ expect(skills[0].metadata.scope).toBe('user');
73
+ expect(skills[1].name).toBe('Image Gen');
74
+ expect(skills[1].metadata.scope).toBe('system');
75
+ });
76
+
77
+ it('calls skills/list with cwd from query', async () => {
78
+ await app.inject({ method: 'GET', url: '/api/skills?cwd=/custom/path' });
79
+ expect(mockRequest).toHaveBeenCalledWith('skills/list', { cwds: ['/custom/path'] });
80
+ });
81
+
82
+ it('defaults cwd to process.cwd()', async () => {
83
+ await app.inject({ method: 'GET', url: '/api/skills' });
84
+ expect(mockRequest).toHaveBeenCalledWith('skills/list', {
85
+ cwds: [process.cwd()],
86
+ });
87
+ });
88
+
89
+ it('returns empty array when app-server returns no skills', async () => {
90
+ mockRequest.mockResolvedValue({ data: [{ cwd: '/tmp', skills: [], errors: [] }] });
91
+ const res = await app.inject({ method: 'GET', url: '/api/skills' });
92
+ expect(res.statusCode).toBe(200);
93
+ expect(res.json()).toEqual([]);
94
+ });
95
+ });
96
+
97
+ describe('GET /api/skills/:name', () => {
98
+ it('returns 404 for unknown skill', async () => {
99
+ mockRequest.mockResolvedValue(MOCK_SKILLS_RESULT);
100
+ const res = await app.inject({ method: 'GET', url: '/api/skills/nonexistent' });
101
+ expect(res.statusCode).toBe(404);
102
+ expect(res.json()).toEqual({ error: 'Skill not found' });
103
+ });
104
+
105
+ it('returns SKILL.md content for known skill', async () => {
106
+ vi.mocked(readFileSync).mockReturnValue('# Cloudflare\nSkill content here');
107
+ const res = await app.inject({ method: 'GET', url: '/api/skills/cloudflare' });
108
+ expect(res.statusCode).toBe(200);
109
+ expect(res.json().name).toBe('cloudflare');
110
+ expect(res.json().content).toContain('# Cloudflare');
111
+ expect(res.json().path).toBe('/home/.codex/skills/cloudflare/SKILL.md');
112
+ });
113
+ });
114
+ });
@@ -0,0 +1,70 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import { readFileSync } from 'node:fs';
3
+ import type { Extension } from '@agentpane/shared';
4
+ import { getSharedAppServer } from '../codex/shared-app-server.ts';
5
+ import type { SkillsListResult, AppServerSkill } from '../codex/app-server-types.ts';
6
+
7
+ /** Map an app-server skill to the shared Extension type. */
8
+ function mapSkill(skill: AppServerSkill): Extension {
9
+ return {
10
+ id: skill.name,
11
+ name: skill.interface?.displayName ?? skill.name,
12
+ description: skill.shortDescription ?? skill.interface?.shortDescription ?? skill.description,
13
+ type: 'skill',
14
+ enabled: skill.enabled,
15
+ metadata: {
16
+ scope: skill.scope,
17
+ path: skill.path,
18
+ },
19
+ };
20
+ }
21
+
22
+ /** Register skill routes. */
23
+ export async function registerSkillRoutes(fastify: FastifyInstance): Promise<void> {
24
+ // GET /api/skills — list all skills from the app-server
25
+ fastify.get<{ Querystring: { cwd?: string } }>('/api/skills', async (request) => {
26
+ const cwd = request.query.cwd ?? process.cwd();
27
+ const result = await getSharedAppServer().request<SkillsListResult>('skills/list', { cwds: [cwd] });
28
+
29
+ // Flatten all skills from all cwds in the response
30
+ const allSkills: Extension[] = [];
31
+ for (const entry of result.data) {
32
+ for (const skill of entry.skills) {
33
+ allSkills.push(mapSkill(skill));
34
+ }
35
+ }
36
+ return allSkills;
37
+ });
38
+
39
+ // GET /api/skills/:name — get full SKILL.md content
40
+ fastify.get<{ Params: { name: string }; Querystring: { cwd?: string } }>(
41
+ '/api/skills/:name',
42
+ async (request, reply) => {
43
+ const { name } = request.params;
44
+ const cwd = request.query.cwd ?? process.cwd();
45
+
46
+ // Fetch skills list to find the path for this skill
47
+ const result = await getSharedAppServer().request<SkillsListResult>('skills/list', { cwds: [cwd] });
48
+
49
+ let skillPath: string | null = null;
50
+ for (const entry of result.data) {
51
+ const skill = entry.skills.find((s) => s.name === name);
52
+ if (skill) {
53
+ skillPath = skill.path;
54
+ break;
55
+ }
56
+ }
57
+
58
+ if (!skillPath) {
59
+ return reply.code(404).send({ error: 'Skill not found' });
60
+ }
61
+
62
+ try {
63
+ const content = readFileSync(skillPath, 'utf-8');
64
+ return { name, content, path: skillPath };
65
+ } catch {
66
+ return reply.code(500).send({ error: 'Failed to read skill' });
67
+ }
68
+ },
69
+ );
70
+ }