@aiworker/web 1.22.2 → 1.23.1

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 (217) hide show
  1. package/bin/cli.js +7 -0
  2. package/dist/assets/{CodeMirrorCommentWidgets-CSICpWQD.js → CodeMirrorCommentWidgets-teB3HmBT.js} +1 -1
  3. package/dist/assets/DiffView-DC9RX3MF.js +4 -0
  4. package/dist/assets/{DirtyBranchSwitchDialog-C22HdWj4.js → DirtyBranchSwitchDialog-DwPkzXsS.js} +1 -1
  5. package/dist/assets/FilesView-BFZkuu4O.js +13 -0
  6. package/dist/assets/{GitView-VQQkYuUw.js → GitView-B_x52MHY.js} +4 -4
  7. package/dist/assets/{JsonTreeView-6lrhwD3q.js → JsonTreeView-cmAzUTiw.js} +1 -1
  8. package/dist/assets/{LinearIssuesView-Ba6Q9569.js → LinearIssuesView-Dehh5afm.js} +1 -1
  9. package/dist/assets/MarkdownImageGallery-DgNOqqhE.js +1 -0
  10. package/dist/assets/MarkdownRendererImpl-DusmMZ_p.js +12 -0
  11. package/dist/assets/OnboardingScreen-B8fm1H4m.js +1 -0
  12. package/dist/assets/{PatchDiff-BzcZPlOq.js → PatchDiff-BVr7gJU0.js} +1 -1
  13. package/dist/assets/PierreDiffViewer-DzLt-nel.js +161 -0
  14. package/dist/assets/PlanView-BmQUYT_z.js +5 -0
  15. package/dist/assets/SettingsView-ChWmAcZT.js +28 -0
  16. package/dist/assets/{SettingsWindow-DDQYaKWQ.js → SettingsWindow-Dy3M6ZB9.js} +1 -1
  17. package/dist/assets/SymbolsNerdFontMono-Regular-aK5vsLov.woff2 +0 -0
  18. package/dist/assets/{ToolOutputDialog-iYF8Y2bz.js → ToolOutputDialog-cTl_eLIH.js} +5 -5
  19. package/dist/assets/{ToolPartDiffPreview-CdjbOVMM.js → ToolPartDiffPreview-C_3iYNJQ.js} +1 -1
  20. package/dist/assets/{VirtualizedFileDiff-fFFm2QgH.js → VirtualizedFileDiff-CAWthkoy.js} +4 -4
  21. package/dist/assets/WalkthroughView-C7AHOpCA.js +4 -0
  22. package/dist/assets/{computeVirtualFileMetrics-CzgLum_K.js → computeVirtualFileMetrics-BkiDSlTW.js} +2 -2
  23. package/dist/assets/contextPanelEmbeddedChat-BdA6bdky.js +1 -0
  24. package/dist/assets/de-DJsQr-p7.js +10 -0
  25. package/dist/assets/es-BuYMlA9g.js +16 -0
  26. package/dist/assets/fr-ePDay33C.js +14 -0
  27. package/dist/assets/ghostty-vt-DdA0Zryv.wasm +0 -0
  28. package/dist/assets/index-BD8ILbxF.js +40 -0
  29. package/dist/assets/{index-Bl3LSbjn.js → index-n6O3hklM.js} +1 -1
  30. package/dist/assets/index-zhgg-SDz.css +1 -0
  31. package/dist/assets/{isDiffPlainText-Dx-dB_8l.js → isDiffPlainText-GMdPz-71.js} +1 -1
  32. package/dist/assets/{isFilePlainText-CWFPwmnU.js → isFilePlainText-BVuH1zH9.js} +1 -1
  33. package/dist/assets/ja-BzR9xOY_.js +16 -0
  34. package/dist/assets/ko-BEjHAm14.js +16 -0
  35. package/dist/assets/main-BydQ1tr_.js +930 -0
  36. package/dist/assets/{main-DnBAoktx.js → main-D3ZDQglc.js} +2 -2
  37. package/dist/assets/markdownCore-Cb2MdZnV.js +5 -0
  38. package/dist/assets/{miniChat-BKyvLseN.js → miniChat-d01k801U.js} +2 -2
  39. package/dist/assets/{mobile-DKjeFgMN.js → mobile-D7VV-Ufz.js} +2 -2
  40. package/dist/assets/patchFileDiff-CdztVMz0.js +13 -0
  41. package/dist/assets/pl-rQmpxNIF.js +16 -0
  42. package/dist/assets/pt-BR-CorbsSW-.js +16 -0
  43. package/dist/assets/renderElectronMiniChatApp-BGG9UIlh.js +2 -0
  44. package/dist/assets/renderMobileApp-BRP3J1iv.js +25 -0
  45. package/dist/assets/runtimeEndpointReset-BhT_-c52.js +41 -0
  46. package/dist/assets/scheduledTasksApi-BEuUF_cj.js +1 -0
  47. package/dist/assets/{shikiHighlight-TEQCmzW8.js → shikiHighlight-BzXUjlby.js} +1 -1
  48. package/dist/assets/surface-COKb77Ua.js +1 -0
  49. package/dist/assets/tr-CoAMXPtI.js +7 -0
  50. package/dist/assets/uk-YlAX0rRU.js +16 -0
  51. package/dist/assets/{useAppFontEffects-D7sNpuL2.js → useAppFontEffects-Cwyhjfl5.js} +141 -140
  52. package/dist/assets/useGitComparison-BCZVefdE.js +1 -0
  53. package/dist/assets/useInlineCommentController-DDkpBEU8.js +2 -0
  54. package/dist/assets/useRootScrollLock-CwjoWuC4.js +1 -0
  55. package/dist/assets/{vendor-base-ui-BeMD8X_w.js → vendor-base-ui-BY_5qla_.js} +1 -1
  56. package/dist/assets/{vendor-codemirror-language-data-Dp3KMRQG.js → vendor-codemirror-language-data-Cb1YnICj.js} +2 -2
  57. package/dist/assets/vendor-legendapp-list-BuBPBC3R.js +1 -0
  58. package/dist/assets/vendor-zod-B6551dwG.js +39 -0
  59. package/dist/assets/{web-components-_mNEMjFY.js → web-components-LV3vjTxy.js} +1 -1
  60. package/dist/assets/zh-CN-DDYg0Jl7.js +16 -0
  61. package/dist/assets/zh-TW-CQKaCayr.js +16 -0
  62. package/dist/index.html +5 -43
  63. package/dist/mini-chat.html +4 -4
  64. package/dist/mobile.html +4 -4
  65. package/package.json +2 -3
  66. package/server/index.d.ts +15 -0
  67. package/server/index.js +48 -22
  68. package/server/lib/agent-memory/runtime.js +7 -2
  69. package/server/lib/agent-memory/runtime.test.js +22 -0
  70. package/server/lib/data-dir-migration.js +40 -0
  71. package/server/lib/data-dir-migration.test.js +59 -0
  72. package/server/lib/event-stream/DOCUMENTATION.md +3 -0
  73. package/server/lib/event-stream/global-hub.js +29 -4
  74. package/server/lib/event-stream/global-hub.test.js +22 -0
  75. package/server/lib/event-stream/global-ws-bridge.js +15 -18
  76. package/server/lib/event-stream/protocol.js +21 -7
  77. package/server/lib/event-stream/runtime.js +9 -6
  78. package/server/lib/event-stream/runtime.test.js +72 -12
  79. package/server/lib/fs/DOCUMENTATION.md +3 -0
  80. package/server/lib/fs/routes.js +31 -0
  81. package/server/lib/fs/routes.test.js +110 -1
  82. package/server/lib/git/DOCUMENTATION.md +39 -6
  83. package/server/lib/git/routes.js +45 -12
  84. package/server/lib/git/routes.test.js +78 -0
  85. package/server/lib/git/service.js +343 -202
  86. package/server/lib/git/service.test.js +496 -25
  87. package/server/lib/github/DOCUMENTATION.md +5 -0
  88. package/server/lib/github/routes.js +2 -2
  89. package/server/lib/inherited-env.js +16 -9
  90. package/server/lib/inherited-env.test.js +8 -9
  91. package/server/lib/message-queue/DOCUMENTATION.md +11 -1
  92. package/server/lib/message-queue/runtime.js +13 -0
  93. package/server/lib/message-queue/runtime.test.js +31 -1
  94. package/server/lib/network-defaults.js +23 -0
  95. package/server/lib/network-defaults.test.js +52 -0
  96. package/server/lib/notifications/DOCUMENTATION.md +1 -1
  97. package/server/lib/notifications/emitter-runtime.js +4 -3
  98. package/server/lib/opencode/DOCUMENTATION.md +35 -5
  99. package/server/lib/opencode/bootstrap-runtime.js +2 -0
  100. package/server/lib/opencode/env-runtime.js +5 -1
  101. package/server/lib/opencode/env-runtime.test.js +72 -0
  102. package/server/lib/opencode/feature-routes-runtime.js +17 -1
  103. package/server/lib/opencode/lifecycle.js +4 -3
  104. package/server/lib/opencode/lifecycle.test.js +29 -0
  105. package/server/lib/opencode/managed-plugin-config.js +1 -1
  106. package/server/lib/opencode/openchamber-routes.js +64 -4
  107. package/server/lib/opencode/openchamber-routes.test.js +128 -1
  108. package/server/lib/opencode/proxy.js +33 -0
  109. package/server/lib/opencode/routes.js +5 -3
  110. package/server/lib/opencode/settings-files.js +214 -0
  111. package/server/lib/opencode/settings-helpers.js +88 -41
  112. package/server/lib/opencode/settings-helpers.test.js +176 -1
  113. package/server/lib/opencode/settings-registry.json +750 -0
  114. package/server/lib/opencode/settings-runtime.js +175 -27
  115. package/server/lib/opencode/settings-runtime.test.js +321 -7
  116. package/server/lib/project-context/DOCUMENTATION.md +38 -4
  117. package/server/lib/project-context/routes.http.test.js +18 -0
  118. package/server/lib/project-context/routes.js +15 -0
  119. package/server/lib/project-context/runtime.js +256 -25
  120. package/server/lib/project-context/runtime.test.js +160 -3
  121. package/server/lib/projects/DOCUMENTATION.md +77 -0
  122. package/server/lib/projects/project-config.js +179 -17
  123. package/server/lib/projects/project-config.test.js +97 -0
  124. package/server/lib/projects/project-id.js +43 -0
  125. package/server/lib/projects/project-setup.js +409 -0
  126. package/server/lib/projects/project-setup.test.js +557 -0
  127. package/server/lib/projects/routes.js +59 -0
  128. package/server/lib/quota/DOCUMENTATION.md +31 -1
  129. package/server/lib/quota/providers/cline-pass.js +125 -0
  130. package/server/lib/quota/providers/cline-pass.test.js +103 -0
  131. package/server/lib/quota/providers/index.js +7 -0
  132. package/server/lib/quota/providers/neuralwatt.js +3 -6
  133. package/server/lib/quota/providers/neuralwatt.test.js +7 -7
  134. package/server/lib/quota/providers/ollama-cloud.js +31 -0
  135. package/server/lib/quota/providers/ollama-cloud.test.js +33 -1
  136. package/server/lib/quota/providers/opencode-go.js +1 -1
  137. package/server/lib/quota/providers/opencode-go.test.js +26 -1
  138. package/server/lib/quota/providers/openrouter.js +119 -24
  139. package/server/lib/quota/providers/openrouter.test.js +330 -0
  140. package/server/lib/relay/DOCUMENTATION.md +75 -0
  141. package/server/lib/relay/cross-compat.test.js +26 -0
  142. package/server/lib/relay/downstream-scheduler.js +154 -0
  143. package/server/lib/relay/downstream-scheduler.test.js +162 -0
  144. package/server/lib/relay/e2ee.js +10 -8
  145. package/server/lib/relay/flow-control.test.js +177 -0
  146. package/server/lib/relay/host-client.js +44 -8
  147. package/server/lib/relay/host-client.test.js +14 -0
  148. package/server/lib/relay/tunnel-codec.js +9 -0
  149. package/server/lib/relay/tunnel-host.js +66 -17
  150. package/server/lib/scheduled-tasks/DOCUMENTATION.md +9 -1
  151. package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +28 -0
  152. package/server/lib/scheduled-tasks/runtime.js +12 -2
  153. package/server/lib/scheduled-tasks/runtime.test.js +53 -0
  154. package/server/lib/session-assist/DOCUMENTATION.md +131 -66
  155. package/server/lib/session-assist/context.js +127 -0
  156. package/server/lib/session-assist/context.test.js +132 -0
  157. package/server/lib/session-assist/prompt.js +54 -0
  158. package/server/lib/session-assist/runtime.js +117 -267
  159. package/server/lib/session-assist/runtime.test.js +203 -0
  160. package/server/lib/session-goal/runtime.js +4 -9
  161. package/server/lib/small-model/DOCUMENTATION.md +3 -2
  162. package/server/lib/small-model/call.js +9 -4
  163. package/server/lib/small-model/call.test.js +29 -0
  164. package/server/lib/small-model/index.js +5 -10
  165. package/server/lib/small-model/runtime-providers.js +15 -7
  166. package/server/lib/small-model/runtime-providers.test.js +7 -1
  167. package/server/lib/terminal/DOCUMENTATION.md +4 -4
  168. package/server/lib/terminal/runtime.js +11 -6
  169. package/server/lib/terminal/runtime.test.js +20 -13
  170. package/server/lib/walkthrough/DOCUMENTATION.md +49 -20
  171. package/server/lib/walkthrough/model-settings.js +8 -12
  172. package/server/lib/walkthrough/pr-source.test.js +21 -0
  173. package/server/lib/walkthrough/prompt.js +3 -1
  174. package/server/lib/walkthrough/pull-request.js +7 -4
  175. package/server/lib/walkthrough/pull-request.test.js +16 -0
  176. package/server/lib/walkthrough/routes.js +19 -0
  177. package/server/lib/walkthrough/routes.test.js +26 -0
  178. package/server/lib/walkthrough/sources.js +36 -4
  179. package/server/opencode-worktree-readiness.test.js +130 -0
  180. package/dist/assets/DiffView-hDBKesA_.js +0 -4
  181. package/dist/assets/FilesView-CdWzeOp9.js +0 -17
  182. package/dist/assets/MarkdownImageGallery-Czv_mhvS.js +0 -1
  183. package/dist/assets/MarkdownRendererImpl-BrMcswiy.js +0 -12
  184. package/dist/assets/OnboardingScreen-ChxQoAkx.js +0 -1
  185. package/dist/assets/PierreDiffViewer-BchBtZlp.js +0 -157
  186. package/dist/assets/PlanView-DyDt_nZ_.js +0 -5
  187. package/dist/assets/SettingsView-BKf5xBsP.js +0 -28
  188. package/dist/assets/WalkthroughView-DOeCsMju.js +0 -4
  189. package/dist/assets/contextPanelEmbeddedChat-C0ZLAepL.js +0 -1
  190. package/dist/assets/de-B5JaA3SA.js +0 -10
  191. package/dist/assets/es-EPIH88ii.js +0 -16
  192. package/dist/assets/fr-CAMfxUdZ.js +0 -14
  193. package/dist/assets/index-DF5kMP-i.js +0 -39
  194. package/dist/assets/index-Dl3xbAB_.css +0 -1
  195. package/dist/assets/ja-CifOE1VN.js +0 -16
  196. package/dist/assets/ko-BBDyAyYQ.js +0 -16
  197. package/dist/assets/main-MhSaOZoB.js +0 -930
  198. package/dist/assets/markdownCore-BrBwOOMn.js +0 -3
  199. package/dist/assets/patchFileDiff-o8ms1me8.js +0 -10
  200. package/dist/assets/pl-DgEHYfFM.js +0 -16
  201. package/dist/assets/pt-BR-DuWHojUw.js +0 -16
  202. package/dist/assets/renderElectronMiniChatApp-CDgBwwCS.js +0 -2
  203. package/dist/assets/renderMobileApp-DJIKIK1r.js +0 -25
  204. package/dist/assets/runtimeEndpointReset-CYJ9O_xX.js +0 -17
  205. package/dist/assets/scheduledTasksApi-D8yamh1b.js +0 -1
  206. package/dist/assets/tr-D5m2yIWL.js +0 -7
  207. package/dist/assets/uk-D_q60oog.js +0 -16
  208. package/dist/assets/useInlineCommentController-Brz_cLcQ.js +0 -2
  209. package/dist/assets/useRootScrollLock-lXEnWlaU.js +0 -1
  210. package/dist/assets/vendor-ghostty-web-CrxinGGH.js +0 -13
  211. package/dist/assets/vendor-legendapp-list-gg7YLRW7.js +0 -1
  212. package/dist/assets/vendor-zod-CK21ihK-.js +0 -39
  213. package/dist/assets/zh-CN-D8QvsVeg.js +0 -16
  214. package/dist/assets/zh-TW-B9t7rQQO.js +0 -16
  215. package/server/lib/mcp-reconnect/DOCUMENTATION.md +0 -55
  216. package/server/lib/mcp-reconnect/runtime.js +0 -122
  217. package/server/lib/mcp-reconnect/runtime.test.js +0 -140
@@ -1,19 +1,22 @@
1
- export function registerGitRoutes(app) {
1
+ export function registerGitRoutes(app, { emitWorktreeChanged } = {}) {
2
2
  let gitLibraries = null;
3
3
  const getGitLibraries = async () => {
4
4
  if (!gitLibraries) {
5
5
  gitLibraries = await import('./index.js');
6
+ if (emitWorktreeChanged) {
7
+ gitLibraries.subscribeWorktreeTopologyChanges(emitWorktreeChanged);
8
+ }
6
9
  }
7
10
  return gitLibraries;
8
11
  };
9
12
 
10
- const resolveDirectoryQuery = (value) => {
13
+ const resolveDirectoryQuery = (value, preserveWhitespace = false) => {
11
14
  const raw = Array.isArray(value) ? value[0] : value;
12
15
  if (typeof raw !== 'string') {
13
16
  return null;
14
17
  }
15
- const trimmed = raw.trim();
16
- return trimmed || null;
18
+ const normalized = preserveWhitespace ? raw : raw.trim();
19
+ return normalized || null;
17
20
  };
18
21
 
19
22
  const extractGitErrorText = (error) => {
@@ -220,7 +223,7 @@ export function registerGitRoutes(app) {
220
223
  });
221
224
 
222
225
  app.get('/api/git/status', async (req, res) => {
223
- const { getStatus, isGitRepository } = await getGitLibraries();
226
+ const { getStatus, isGitRepository, observeWorktreeTopology } = await getGitLibraries();
224
227
 
225
228
  try {
226
229
  const directory = resolveDirectoryQuery(req.query.directory);
@@ -233,6 +236,11 @@ export function registerGitRoutes(app) {
233
236
  return res.json(nonRepoStatusPayload());
234
237
  }
235
238
 
239
+ // Clients ask for status while they work in a repository, so this is
240
+ // where an externally added or removed worktree gets noticed. It runs
241
+ // beside the status call and never delays or fails the response.
242
+ void observeWorktreeTopology(directory);
243
+
236
244
  const mode = req.query.mode === 'light' ? 'light' : undefined;
237
245
  const status = await getStatus(directory, { mode });
238
246
  res.json(status);
@@ -417,6 +425,7 @@ export function registerGitRoutes(app) {
417
425
  const diff = await getRangeDiff(directory, {
418
426
  base,
419
427
  head,
428
+ includeWorkingTree: req.query.includeWorkingTree === 'true',
420
429
  path: pathParam,
421
430
  contextLines: Number.isFinite(context) ? context : 3,
422
431
  });
@@ -463,7 +472,7 @@ export function registerGitRoutes(app) {
463
472
  return res.status(400).json({ error: 'base and head parameters are required' });
464
473
  }
465
474
 
466
- const files = await getRangeFiles(directory, { base, head });
475
+ const files = await getRangeFiles(directory, { base, head, includeWorkingTree: req.query.includeWorkingTree === 'true' });
467
476
  res.json({ files });
468
477
  } catch (error) {
469
478
  console.error('Failed to get git range files:', error);
@@ -1077,7 +1086,7 @@ export function registerGitRoutes(app) {
1077
1086
  });
1078
1087
 
1079
1088
  app.get('/api/git/worktrees', async (req, res) => {
1080
- const { getWorktrees } = await getGitLibraries();
1089
+ const { getWorktrees, observeWorktreeTopology } = await getGitLibraries();
1081
1090
  try {
1082
1091
  const directory = req.query.directory;
1083
1092
  if (!directory) {
@@ -1085,13 +1094,18 @@ export function registerGitRoutes(app) {
1085
1094
  }
1086
1095
 
1087
1096
  const worktrees = await getWorktrees(directory);
1097
+ // A repository always lists at least its primary worktree; an empty
1098
+ // list means "not a repository" and has no topology to track.
1099
+ if (worktrees.length > 0) {
1100
+ void observeWorktreeTopology(directory);
1101
+ }
1088
1102
  res.json(worktrees);
1089
1103
  } catch (error) {
1090
- // Worktrees are an optional feature. Avoid repeated 500s (and repeated client retries)
1091
- // when the directory isn't a git repo or uses shell shorthand like "~/".
1092
- console.warn('Failed to get worktrees, returning empty list:', error?.message || error);
1093
- res.setHeader('X-AiWorker-Warning', 'git worktrees unavailable');
1094
- res.json([]);
1104
+ // A directory outside any repository still answers `[]` from getWorktrees.
1105
+ // Anything else is a real failure the client must not mistake for "no
1106
+ // worktrees", or it would drop the ones it already knows.
1107
+ console.error('Failed to get worktrees:', error);
1108
+ res.status(500).json({ error: error.message || 'Failed to get worktrees' });
1095
1109
  }
1096
1110
  });
1097
1111
 
@@ -1300,6 +1314,25 @@ export function registerGitRoutes(app) {
1300
1314
  }
1301
1315
  });
1302
1316
 
1317
+ app.get('/api/git/commit-diff', async (req, res) => {
1318
+ const { getCommitDiff } = await getGitLibraries();
1319
+ try {
1320
+ const directory = resolveDirectoryQuery(req.query.directory);
1321
+ const hash = resolveDirectoryQuery(req.query.hash);
1322
+ if (!directory || !hash) return res.status(400).json({ error: 'directory and hash are required' });
1323
+ const context = Number(req.query.context ?? 3);
1324
+ const diff = await getCommitDiff(directory, {
1325
+ hash,
1326
+ path: resolveDirectoryQuery(req.query.path, true) ?? undefined,
1327
+ previousPath: resolveDirectoryQuery(req.query.previousPath, true) ?? undefined,
1328
+ contextLines: Number.isFinite(context) ? context : 3,
1329
+ });
1330
+ res.json({ diff });
1331
+ } catch (error) {
1332
+ res.status(500).json({ error: error.message || 'Failed to get commit diff' });
1333
+ }
1334
+ });
1335
+
1303
1336
  app.get('/api/git/commit-file-diff', async (req, res) => {
1304
1337
  const { getCommitFileDiff } = await getGitLibraries();
1305
1338
  try {
@@ -5,6 +5,9 @@ const gitLibraries = {
5
5
  unstageFiles: vi.fn(),
6
6
  isGitRepository: vi.fn(),
7
7
  getStatus: vi.fn(),
8
+ getWorktrees: vi.fn(),
9
+ observeWorktreeTopology: vi.fn(),
10
+ subscribeWorktreeTopologyChanges: vi.fn(),
8
11
  };
9
12
 
10
13
  vi.mock('./index.js', () => ({
@@ -12,6 +15,9 @@ vi.mock('./index.js', () => ({
12
15
  unstageFiles: gitLibraries.unstageFiles,
13
16
  isGitRepository: gitLibraries.isGitRepository,
14
17
  getStatus: gitLibraries.getStatus,
18
+ getWorktrees: gitLibraries.getWorktrees,
19
+ observeWorktreeTopology: gitLibraries.observeWorktreeTopology,
20
+ subscribeWorktreeTopologyChanges: gitLibraries.subscribeWorktreeTopologyChanges,
15
21
  }));
16
22
 
17
23
  const { registerGitRoutes } = await import('./routes.js');
@@ -141,6 +147,78 @@ describe('git routes index mutations', () => {
141
147
  });
142
148
  });
143
149
 
150
+ describe('git worktree topology routes', () => {
151
+ beforeEach(() => {
152
+ gitLibraries.isGitRepository.mockReset();
153
+ gitLibraries.getStatus.mockReset();
154
+ gitLibraries.getWorktrees.mockReset();
155
+ gitLibraries.observeWorktreeTopology.mockReset();
156
+ gitLibraries.subscribeWorktreeTopologyChanges.mockReset();
157
+ gitLibraries.observeWorktreeTopology.mockResolvedValue(undefined);
158
+ });
159
+
160
+ it('observes the repository topology while serving status, never for non-repositories', async () => {
161
+ gitLibraries.isGitRepository.mockResolvedValueOnce(true).mockResolvedValueOnce(false);
162
+ gitLibraries.getStatus.mockResolvedValue({ current: 'main' });
163
+ const { app, getRoute } = createRouteRegistry();
164
+ registerGitRoutes(app);
165
+ const route = getRoute('GET', '/api/git/status');
166
+
167
+ const repoResponse = createMockResponse();
168
+ await route({ query: { directory: '/repo' } }, repoResponse);
169
+ expect(repoResponse.body).toEqual({ current: 'main' });
170
+ expect(gitLibraries.observeWorktreeTopology).toHaveBeenCalledWith('/repo');
171
+
172
+ await route({ query: { directory: '/plain-folder' } }, createMockResponse());
173
+ expect(gitLibraries.observeWorktreeTopology).toHaveBeenCalledTimes(1);
174
+ });
175
+
176
+ it('observes topology after a repository listing and reports listing failures', async () => {
177
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
178
+ gitLibraries.getWorktrees
179
+ .mockResolvedValueOnce([{ path: '/repo', branch: 'main' }])
180
+ .mockResolvedValueOnce([])
181
+ .mockRejectedValueOnce(new Error('git failed'));
182
+ const { app, getRoute } = createRouteRegistry();
183
+ registerGitRoutes(app);
184
+ const route = getRoute('GET', '/api/git/worktrees');
185
+
186
+ const listed = createMockResponse();
187
+ await route({ query: { directory: '/repo' } }, listed);
188
+ expect(listed.body).toEqual([{ path: '/repo', branch: 'main' }]);
189
+ expect(gitLibraries.observeWorktreeTopology).toHaveBeenCalledWith('/repo');
190
+
191
+ await route({ query: { directory: '/plain-folder' } }, createMockResponse());
192
+ expect(gitLibraries.observeWorktreeTopology).toHaveBeenCalledTimes(1);
193
+
194
+ const failed = createMockResponse();
195
+ await route({ query: { directory: '/repo' } }, failed);
196
+ expect(failed.statusCode).toBe(500);
197
+ expect(failed.body).toEqual({ error: 'git failed' });
198
+ errorSpy.mockRestore();
199
+ });
200
+
201
+ it('forwards topology changes to the control event emitter once', async () => {
202
+ let listener = null;
203
+ gitLibraries.subscribeWorktreeTopologyChanges.mockImplementation((next) => {
204
+ listener = next;
205
+ return () => undefined;
206
+ });
207
+ gitLibraries.getWorktrees.mockResolvedValue([]);
208
+ const emitWorktreeChanged = vi.fn();
209
+ const { app, getRoute } = createRouteRegistry();
210
+ registerGitRoutes(app, { emitWorktreeChanged });
211
+ const route = getRoute('GET', '/api/git/worktrees');
212
+
213
+ await route({ query: { directory: '/repo' } }, createMockResponse());
214
+ await route({ query: { directory: '/repo' } }, createMockResponse());
215
+ expect(gitLibraries.subscribeWorktreeTopologyChanges).toHaveBeenCalledTimes(1);
216
+
217
+ listener({ directories: ['/repo'], at: 123 });
218
+ expect(emitWorktreeChanged).toHaveBeenCalledWith({ directories: ['/repo'], at: 123 });
219
+ });
220
+ });
221
+
144
222
  describe('git routes status discovery', () => {
145
223
  beforeEach(() => {
146
224
  gitLibraries.isGitRepository.mockReset();