@baique/pi-web-sky 0.1.35 → 0.1.36

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 (294) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +16 -16
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/react-loadable-manifest.json +1 -1
  5. package/.next/required-server-files.js +1 -1
  6. package/.next/required-server-files.json +1 -1
  7. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/server/app/_global-error.html +1 -1
  9. package/.next/server/app/_global-error.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  14. package/.next/server/app/_not-found.html +1 -1
  15. package/.next/server/app/_not-found.rsc +3 -3
  16. package/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  17. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +3 -3
  18. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  19. package/.next/server/app/api/agent/[id]/bash-output/route.js +1 -1
  20. package/.next/server/app/api/agent/[id]/events/route.js +1 -1
  21. package/.next/server/app/api/agent/[id]/route.js +1 -1
  22. package/.next/server/app/api/app-update/route.js +1 -1
  23. package/.next/server/app/api/auth/login/[provider]/route.js +1 -1
  24. package/.next/server/app/api/cwd/validate/route.js +1 -1
  25. package/.next/server/app/api/default-cwd/route.js +1 -1
  26. package/.next/server/app/api/file-index/route.js +1 -1
  27. package/.next/server/app/api/files/[...path]/route.js +1 -1
  28. package/.next/server/app/api/mcp/route.js +1 -1
  29. package/.next/server/app/api/models/route.js +1 -1
  30. package/.next/server/app/api/plugins/route.js +1 -1
  31. package/.next/server/app/api/project-trust/route.js +1 -1
  32. package/.next/server/app/api/search/route.js +1 -1
  33. package/.next/server/app/api/sessions/[id]/auto-name/route.js +1 -1
  34. package/.next/server/app/api/sessions/[id]/context/route.js +1 -1
  35. package/.next/server/app/api/sessions/[id]/entries/[entryId]/thinking/route.js +1 -1
  36. package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
  37. package/.next/server/app/api/sessions/[id]/state/route.js +1 -1
  38. package/.next/server/app/api/sessions/[id]/todos/route.js +1 -1
  39. package/.next/server/app/api/sessions/route.js +1 -1
  40. package/.next/server/app/api/skills/check/route.js +1 -1
  41. package/.next/server/app/api/skills/check/route.js.nft.json +1 -1
  42. package/.next/server/app/api/skills/install/route.js +1 -1
  43. package/.next/server/app/api/skills/route.js +2 -31
  44. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  45. package/.next/server/app/api/skills/update/route.js +1 -1
  46. package/.next/server/app/api/skills/update/route.js.nft.json +1 -1
  47. package/.next/server/app/api/worktrees/route.js +1 -1
  48. package/.next/server/app/index.html +1 -1
  49. package/.next/server/app/index.rsc +5 -5
  50. package/.next/server/app/index.segments/__PAGE__.segment.rsc +5 -5
  51. package/.next/server/app/index.segments/_full.segment.rsc +5 -5
  52. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  53. package/.next/server/app/page.js +2 -2
  54. package/.next/server/app/page_client-reference-manifest.js +1 -1
  55. package/.next/server/app-paths-manifest.json +16 -16
  56. package/.next/server/chunks/2404.js +1 -1
  57. package/.next/server/chunks/3500.js +30 -0
  58. package/.next/server/chunks/4226.js +2 -2
  59. package/.next/server/chunks/542.js +10 -5
  60. package/.next/server/chunks/5794.js +1 -1
  61. package/.next/server/chunks/6429.js +1 -1
  62. package/.next/server/chunks/898.js +1 -1
  63. package/.next/server/middleware-build-manifest.js +1 -1
  64. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  65. package/.next/server/pages/404.html +1 -1
  66. package/.next/server/pages/500.html +1 -1
  67. package/.next/static/chunks/5315.7e72d26a3ad3bd9a.js +4 -0
  68. package/.next/static/chunks/app/{layout-1d9a7a44fbc92d2e.js → layout-ebbc0d287267b07b.js} +1 -1
  69. package/.next/static/chunks/app/{page-6d6779a81a0a7639.js → page-e568623a44c67606.js} +2 -2
  70. package/.next/static/chunks/{webpack-fa4f40085b1ec1ac.js → webpack-2e4e348867a555cd.js} +1 -1
  71. package/.next/static/css/422c7dc0faa04d89.css +1 -0
  72. package/.next/trace +8 -9
  73. package/.next/trace-build +1 -1
  74. package/bin/pi-web-sky.js +15 -20
  75. package/lib/agent-client.test.mjs +64 -0
  76. package/lib/agent-client.ts +53 -0
  77. package/lib/agent-event-connection.test.mjs +193 -0
  78. package/lib/agent-event-connection.ts +202 -0
  79. package/lib/agent-event-stream.test.mjs +155 -0
  80. package/lib/agent-event-stream.ts +132 -0
  81. package/lib/agent-event-wire.test.mjs +278 -0
  82. package/lib/agent-event-wire.ts +107 -0
  83. package/lib/agent-phase.ts +24 -0
  84. package/lib/allowed-roots.ts +31 -0
  85. package/lib/ansi.test.mjs +52 -0
  86. package/lib/ansi.ts +211 -0
  87. package/lib/api-types.ts +139 -0
  88. package/lib/app-update.test.mjs +27 -0
  89. package/lib/app-update.ts +28 -0
  90. package/lib/atomic-file.test.mjs +37 -0
  91. package/lib/atomic-file.ts +34 -0
  92. package/lib/audit-session.ts +235 -0
  93. package/lib/bash-output.test.mjs +63 -0
  94. package/lib/bash-output.ts +53 -0
  95. package/lib/bg-image.ts +129 -0
  96. package/lib/board-events.ts +83 -0
  97. package/lib/board-purge.test.mjs +66 -0
  98. package/lib/board-purge.ts +65 -0
  99. package/lib/board-search.test.mjs +110 -0
  100. package/lib/board-search.ts +63 -0
  101. package/lib/board-store.test.mjs +542 -0
  102. package/lib/board-store.ts +794 -0
  103. package/lib/board-types.ts +96 -0
  104. package/lib/board-utils.ts +17 -0
  105. package/lib/bounded-form-data.test.mjs +54 -0
  106. package/lib/bounded-form-data.ts +49 -0
  107. package/lib/browser-notifications.test.mjs +167 -0
  108. package/lib/browser-notifications.ts +104 -0
  109. package/lib/chat-lazy-load.test.mjs +140 -0
  110. package/lib/chat-lazy-load.ts +64 -0
  111. package/lib/clipboard-paths.test.mjs +140 -0
  112. package/lib/clipboard-paths.ts +192 -0
  113. package/lib/clipboard.ts +18 -0
  114. package/lib/compaction-summary.test.mjs +44 -0
  115. package/lib/compaction-summary.ts +34 -0
  116. package/lib/custom-ui-terminal.test.mjs +32 -0
  117. package/lib/custom-ui-terminal.ts +27 -0
  118. package/lib/deepseek-pricing.test.mjs +120 -0
  119. package/lib/deepseek-pricing.ts +71 -0
  120. package/lib/directory-browser.test.mjs +60 -0
  121. package/lib/directory-browser.ts +83 -0
  122. package/lib/draft-stash.test.mjs +91 -0
  123. package/lib/draft-stash.ts +102 -0
  124. package/lib/draft-store.ts +105 -0
  125. package/lib/dropdown-direction.ts +27 -0
  126. package/lib/extension-command.test.mjs +63 -0
  127. package/lib/extension-command.ts +81 -0
  128. package/lib/file-access.test.mjs +31 -0
  129. package/lib/file-access.ts +59 -0
  130. package/lib/file-dirent.test.mjs +56 -0
  131. package/lib/file-dirent.ts +17 -0
  132. package/lib/file-explorer-state.test.mjs +63 -0
  133. package/lib/file-explorer-state.ts +36 -0
  134. package/lib/file-fuzzy.test.mjs +29 -0
  135. package/lib/file-fuzzy.ts +189 -0
  136. package/lib/file-links.test.mjs +92 -0
  137. package/lib/file-links.ts +148 -0
  138. package/lib/file-paths.ts +43 -0
  139. package/lib/file-types.test.mjs +34 -0
  140. package/lib/file-types.ts +73 -0
  141. package/lib/file-upload.test.mjs +70 -0
  142. package/lib/file-upload.ts +59 -0
  143. package/lib/file-viewer-state.test.mjs +20 -0
  144. package/lib/file-viewer-state.ts +15 -0
  145. package/lib/frontmatter.test.mjs +55 -0
  146. package/lib/frontmatter.ts +87 -0
  147. package/lib/git-changes.test.mjs +52 -0
  148. package/lib/git-changes.ts +232 -0
  149. package/lib/git-status.ts +47 -0
  150. package/lib/git-types.ts +29 -0
  151. package/lib/http-dispatcher.test.mjs +83 -0
  152. package/lib/http-dispatcher.ts +86 -0
  153. package/lib/i18n/format.test.mjs +19 -0
  154. package/lib/i18n/format.ts +60 -0
  155. package/lib/i18n/messages/en.ts +662 -0
  156. package/lib/i18n/messages/zh-CN.ts +661 -0
  157. package/lib/i18n/registry.test.mjs +32 -0
  158. package/lib/i18n/registry.ts +43 -0
  159. package/lib/i18n/types.ts +15 -0
  160. package/lib/image-attachments.test.mjs +27 -0
  161. package/lib/image-attachments.ts +56 -0
  162. package/lib/initial-navigation.test.mjs +65 -0
  163. package/lib/initial-navigation.ts +15 -0
  164. package/lib/markdown.test.mjs +104 -0
  165. package/lib/markdown.ts +347 -0
  166. package/lib/message-display.test.mjs +130 -0
  167. package/lib/message-display.ts +47 -0
  168. package/lib/model-catalog.test.mjs +327 -0
  169. package/lib/model-catalog.ts +414 -0
  170. package/lib/model-discovery-auth.ts +58 -0
  171. package/lib/model-discovery.test.mjs +50 -0
  172. package/lib/model-discovery.ts +75 -0
  173. package/lib/model-id-preference.ts +46 -0
  174. package/lib/model-scope.test.mjs +193 -0
  175. package/lib/model-scope.ts +198 -0
  176. package/lib/models-cache.test.mjs +122 -0
  177. package/lib/models-cache.ts +90 -0
  178. package/lib/models-config-store.test.mjs +200 -0
  179. package/lib/models-config-store.ts +84 -0
  180. package/lib/next-config-esm.test.mjs +14 -0
  181. package/lib/next-config.test.mjs +13 -0
  182. package/lib/node-version.test.mjs +33 -0
  183. package/lib/normalize.test.mjs +54 -0
  184. package/lib/normalize.ts +63 -0
  185. package/lib/npx.ts +62 -0
  186. package/lib/panel-layout.test.mjs +56 -0
  187. package/lib/panel-layout.ts +52 -0
  188. package/lib/patch.test.mjs +110 -0
  189. package/lib/patch.ts +134 -0
  190. package/lib/path-security.ts +42 -0
  191. package/lib/paths.test.mjs +58 -0
  192. package/lib/paths.ts +71 -0
  193. package/lib/pi-types.ts +183 -0
  194. package/lib/pi-web-options.test.mjs +63 -0
  195. package/lib/process-lifecycle.test.mjs +83 -0
  196. package/lib/project-command-env.test.mjs +298 -0
  197. package/lib/project-command-env.ts +129 -0
  198. package/lib/project-groups.test.mjs +60 -0
  199. package/lib/project-groups.ts +53 -0
  200. package/lib/project-identity.test.mjs +37 -0
  201. package/lib/project-identity.ts +26 -0
  202. package/lib/project-tree.test.mjs +115 -0
  203. package/lib/project-tree.ts +176 -0
  204. package/lib/project-trust.test.mjs +117 -0
  205. package/lib/project-trust.ts +48 -0
  206. package/lib/prompt-recovery.test.mjs +33 -0
  207. package/lib/prompt-recovery.ts +47 -0
  208. package/lib/provider-api-key-route.test.mjs +12 -0
  209. package/lib/provider-credential-store.test.mjs +126 -0
  210. package/lib/provider-credential-store.ts +114 -0
  211. package/lib/provider-listing-runtime.ts +37 -0
  212. package/lib/provider-listing.test.mjs +114 -0
  213. package/lib/provider-listing.ts +118 -0
  214. package/lib/provider-services.ts +64 -0
  215. package/lib/request-security.test.mjs +256 -0
  216. package/lib/request-security.ts +136 -0
  217. package/lib/rpc-manager-shutdown.test.mjs +372 -0
  218. package/lib/rpc-manager-widgets.test.mjs +342 -0
  219. package/lib/rpc-manager.test.mjs +139 -0
  220. package/lib/rpc-manager.ts +1703 -0
  221. package/lib/rpc-session-info.test.mjs +93 -0
  222. package/lib/session-delete.test.mjs +53 -0
  223. package/lib/session-delete.ts +86 -0
  224. package/lib/session-file-references-core.ts +82 -0
  225. package/lib/session-file-references.test.mjs +91 -0
  226. package/lib/session-file-references.ts +29 -0
  227. package/lib/session-path.test.mjs +24 -0
  228. package/lib/session-path.ts +11 -0
  229. package/lib/session-reader.pagination.test.mjs +115 -0
  230. package/lib/session-reader.test.mjs +446 -0
  231. package/lib/session-reader.ts +548 -0
  232. package/lib/session-resolve.test.mjs +65 -0
  233. package/lib/session-row-context-menu.test.mjs +46 -0
  234. package/lib/session-row-context-menu.ts +29 -0
  235. package/lib/session-scanner.test.mjs +151 -0
  236. package/lib/session-scanner.ts +238 -0
  237. package/lib/session-search.test.mjs +173 -0
  238. package/lib/session-search.ts +284 -0
  239. package/lib/session-stats.ts +26 -0
  240. package/lib/session-timing.test.mjs +107 -0
  241. package/lib/session-timing.ts +45 -0
  242. package/lib/session-title.test.mjs +284 -0
  243. package/lib/session-title.ts +255 -0
  244. package/lib/skill-frontmatter.test.mjs +75 -0
  245. package/lib/skill-frontmatter.ts +40 -0
  246. package/lib/skill-lock.test.mjs +182 -0
  247. package/lib/skill-lock.ts +146 -0
  248. package/lib/skill-updates.test.mjs +182 -0
  249. package/lib/skill-updates.ts +263 -0
  250. package/lib/skills-service.ts +16 -0
  251. package/lib/slash-display.test.mjs +122 -0
  252. package/lib/slash-display.ts +30 -0
  253. package/lib/sqlite-db.test.mjs +140 -0
  254. package/lib/sqlite-db.ts +205 -0
  255. package/lib/startup-preferences.test.mjs +130 -0
  256. package/lib/startup-preferences.ts +65 -0
  257. package/lib/streaming-message.test.mjs +212 -0
  258. package/lib/streaming-message.ts +151 -0
  259. package/lib/subagent-widget.test.mjs +87 -0
  260. package/lib/subagent-widget.ts +109 -0
  261. package/lib/sync-room-server.mjs +137 -0
  262. package/lib/task-card-store.test.mjs +309 -0
  263. package/lib/task-card-store.ts +559 -0
  264. package/lib/task-scheduler.ts +498 -0
  265. package/lib/task-store.test.mjs +216 -0
  266. package/lib/task-store.ts +271 -0
  267. package/lib/terminal-input.test.mjs +46 -0
  268. package/lib/terminal-input.ts +63 -0
  269. package/lib/terminal-manager.test.mjs +71 -0
  270. package/lib/terminal-manager.ts +245 -0
  271. package/lib/tool-execution-progress.test.mjs +32 -0
  272. package/lib/tool-execution-progress.ts +29 -0
  273. package/lib/tool-names.ts +25 -0
  274. package/lib/tool-preset-preference.test.mjs +72 -0
  275. package/lib/tool-preset-preference.ts +41 -0
  276. package/lib/tool-presets.test.mjs +56 -0
  277. package/lib/tool-presets.ts +42 -0
  278. package/lib/turn-written-files.test.mjs +137 -0
  279. package/lib/turn-written-files.ts +60 -0
  280. package/lib/types.ts +349 -0
  281. package/lib/wallpaper-settings.ts +191 -0
  282. package/lib/web-auth.test.mjs +50 -0
  283. package/lib/web-auth.ts +45 -0
  284. package/lib/workspace-memory.test.mjs +129 -0
  285. package/lib/workspace-memory.ts +98 -0
  286. package/lib/worktree.test.mjs +50 -0
  287. package/lib/worktree.ts +250 -0
  288. package/package.json +3 -1
  289. package/server.mjs +34 -0
  290. package/.next/server/chunks/7513.js +0 -30
  291. package/.next/static/chunks/5315.03de3c6abd8b283e.js +0 -4
  292. package/.next/static/css/cf25164cb7c04d95.css +0 -1
  293. /package/.next/static/{Ud6Y2ONkGuPVFbo1xZQvk → EoBw3VYlygpr1EqlVOa5b}/_buildManifest.js +0 -0
  294. /package/.next/static/{Ud6Y2ONkGuPVFbo1xZQvk → EoBw3VYlygpr1EqlVOa5b}/_ssgManifest.js +0 -0
package/.next/trace-build CHANGED
@@ -1 +1 @@
1
- [{"name":"run-webpack","duration":27382115,"timestamp":19454917504,"id":14,"parentId":1,"tags":{},"startTime":1788280988635,"traceId":"9678d98c14895f8d"},{"name":"run-typescript","duration":19825586,"timestamp":19482307922,"id":1004,"parentId":1,"tags":{},"startTime":1788281016025,"traceId":"9678d98c14895f8d"},{"name":"static-check","duration":3922639,"timestamp":19502214102,"id":1007,"parentId":1,"tags":{},"startTime":1788281035931,"traceId":"9678d98c14895f8d"},{"name":"static-generation","duration":6090787,"timestamp":19506566895,"id":1153,"parentId":1,"tags":{},"startTime":1788281040284,"traceId":"9678d98c14895f8d"},{"name":"collect-build-traces","duration":54685097,"timestamp":19506140492,"id":1150,"parentId":1,"tags":{},"startTime":1788281039858,"traceId":"9678d98c14895f8d"},{"name":"telemetry-flush","duration":70,"timestamp":19560830811,"id":1162,"parentId":1,"tags":{},"startTime":1788281094548,"traceId":"9678d98c14895f8d"},{"name":"next-build","duration":106024469,"timestamp":19454806423,"id":1,"tags":{"buildMode":"default","version":"16.3.1","bundler":"webpack","has-custom-webpack-config":"false","use-build-worker":"true"},"startTime":1788280988523,"traceId":"9678d98c14895f8d"}]
1
+ [{"name":"run-webpack","duration":22650232,"timestamp":22590332853,"id":14,"parentId":1,"tags":{},"startTime":1788284124050,"traceId":"7bb8ae0dfe4d6fb8"},{"name":"run-typescript","duration":18061477,"timestamp":22612988890,"id":779,"parentId":1,"tags":{},"startTime":1788284146706,"traceId":"7bb8ae0dfe4d6fb8"},{"name":"static-check","duration":3027832,"timestamp":22631130170,"id":782,"parentId":1,"tags":{},"startTime":1788284164847,"traceId":"7bb8ae0dfe4d6fb8"},{"name":"static-generation","duration":5570297,"timestamp":22634582835,"id":928,"parentId":1,"tags":{},"startTime":1788284168300,"traceId":"7bb8ae0dfe4d6fb8"},{"name":"collect-build-traces","duration":52542703,"timestamp":22634161856,"id":925,"parentId":1,"tags":{},"startTime":1788284167879,"traceId":"7bb8ae0dfe4d6fb8"},{"name":"telemetry-flush","duration":52,"timestamp":22686708604,"id":937,"parentId":1,"tags":{},"startTime":1788284220426,"traceId":"7bb8ae0dfe4d6fb8"},{"name":"next-build","duration":96575927,"timestamp":22590132737,"id":1,"tags":{"buildMode":"default","version":"16.3.1","bundler":"webpack","has-custom-webpack-config":"false","use-build-worker":"true"},"startTime":1788284123850,"traceId":"7bb8ae0dfe4d6fb8"}]
package/bin/pi-web-sky.js CHANGED
@@ -23,19 +23,10 @@ const { wireChildProcessLifecycle } = require("./process-lifecycle");
23
23
  const pkgDir = path.join(__dirname, "..");
24
24
  const nextDir = path.join(pkgDir, ".next");
25
25
 
26
- // Resolve next's CLI entry directly to avoid relying on .bin symlinks (which
27
- // may not exist when installed via npx).
28
- let nextBin;
29
- try {
30
- nextBin = require.resolve("next/dist/bin/next", { paths: [pkgDir] });
31
- } catch {
32
- // Fallback: locate next package root and derive the bin path manually.
33
- try {
34
- const nextPkg = require.resolve("next/package.json", { paths: [pkgDir] });
35
- nextBin = path.join(path.dirname(nextPkg), "dist", "bin", "next");
36
- } catch {
37
- nextBin = path.join(pkgDir, "node_modules", "next", "dist", "bin", "next");
38
- }
26
+ const serverEntry = path.join(pkgDir, "server.mjs");
27
+ if (!fs.existsSync(serverEntry)) {
28
+ console.error("Server entry not found. Please report this issue.");
29
+ process.exit(1);
39
30
  }
40
31
 
41
32
  const { port, hostname, openBrowser } = parseLaunchOptions();
@@ -59,15 +50,19 @@ if (!loopbackHostnames.has(hostname)) {
59
50
  }
60
51
  }
61
52
 
62
- const nextArgs = ["start", "-p", port];
63
- nextArgs.push("-H", hostname);
64
-
65
- // Always run next's JS entry with node directly — avoids .bin symlink issues
66
- // and path-with-spaces problems on Windows when shell: true is used.
67
- const child = spawn(process.execPath, [nextBin, ...nextArgs], {
53
+ // 直接跑自定义 server(server.mjs):HTTP Next,WebSocket upgrade 分流到 tldraw
54
+ // sync 房间(/connect/:boardId)。不能只跑 `next start`——那样 WS 后台不启动,
55
+ // 看板永远 loading(0.1.35 生产 bug 根因)。
56
+ const child = spawn(process.execPath, [serverEntry], {
68
57
  cwd: pkgDir,
69
58
  stdio: ["inherit", "pipe", "inherit"],
70
- env: { ...process.env, PI_WEB_HOSTNAME: hostname },
59
+ env: {
60
+ ...process.env,
61
+ NODE_ENV: "production",
62
+ PORT: String(port),
63
+ HOSTNAME: hostname,
64
+ PI_WEB_HOSTNAME: hostname,
65
+ },
71
66
  });
72
67
  wireChildProcessLifecycle(child);
73
68
 
@@ -0,0 +1,64 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createJiti } from "jiti";
4
+
5
+ const jiti = createJiti(import.meta.url, {
6
+ interopDefault: true,
7
+ moduleCache: false,
8
+ });
9
+ const { AgentCommandError, isPromptRejectedError, sendAgentCommand } = await jiti.import("./agent-client.ts");
10
+
11
+ test("agent command HTTP rejections are distinguishable from transport failures", async (t) => {
12
+ const originalFetch = globalThis.fetch;
13
+ t.after(() => {
14
+ globalThis.fetch = originalFetch;
15
+ });
16
+
17
+ globalThis.fetch = async () => new Response(
18
+ JSON.stringify({
19
+ error: "Authentication failed",
20
+ code: "prompt_rejected",
21
+ accepted: false,
22
+ }),
23
+ { status: 500, headers: { "Content-Type": "application/json" } },
24
+ );
25
+
26
+ await assert.rejects(
27
+ sendAgentCommand("session-id", { type: "prompt", message: "hello" }),
28
+ (error) => {
29
+ assert.equal(error instanceof AgentCommandError, true);
30
+ assert.equal(error.status, 500);
31
+ assert.equal(error.message, "Authentication failed");
32
+ assert.equal(error.code, "prompt_rejected");
33
+ assert.equal(error.accepted, false);
34
+ assert.equal(isPromptRejectedError(error), true);
35
+ return true;
36
+ },
37
+ );
38
+
39
+ const transportError = new TypeError("connection reset");
40
+ globalThis.fetch = async () => {
41
+ throw transportError;
42
+ };
43
+
44
+ await assert.rejects(
45
+ sendAgentCommand("session-id", { type: "prompt", message: "hello" }),
46
+ (error) => {
47
+ assert.equal(error, transportError);
48
+ assert.equal(error instanceof AgentCommandError, false);
49
+ assert.equal(isPromptRejectedError(error), false);
50
+ return true;
51
+ },
52
+ );
53
+ });
54
+
55
+ test("only an explicit negative prompt acknowledgement is definitive", () => {
56
+ assert.equal(
57
+ isPromptRejectedError(new AgentCommandError("proxy failure", 502)),
58
+ false,
59
+ );
60
+ assert.equal(
61
+ isPromptRejectedError(new AgentCommandError("generic API failure", 500, "internal_error", false)),
62
+ false,
63
+ );
64
+ });
@@ -0,0 +1,53 @@
1
+ // Client-side helper for POST /api/agent/[id].
2
+ //
3
+ // Every /api/agent/[id] route returns one of:
4
+ // { success: true, data: <result> }
5
+ // { error: string } (non-2xx)
6
+ //
7
+ // Call sites previously repeated the same 5-line fetch block 13× in
8
+ // hooks/useAgentSession.ts. This helper collapses that down to one line.
9
+
10
+ export class AgentCommandError extends Error {
11
+ constructor(
12
+ message: string,
13
+ public readonly status: number,
14
+ public readonly code?: string,
15
+ public readonly accepted?: boolean,
16
+ ) {
17
+ super(message);
18
+ this.name = "AgentCommandError";
19
+ }
20
+ }
21
+
22
+ export function isPromptRejectedError(error: unknown): error is AgentCommandError {
23
+ return error instanceof AgentCommandError
24
+ && error.code === "prompt_rejected"
25
+ && error.accepted === false;
26
+ }
27
+
28
+ export async function sendAgentCommand<T = unknown>(
29
+ sessionId: string,
30
+ command: Record<string, unknown>,
31
+ ): Promise<T> {
32
+ const res = await fetch(`/api/agent/${encodeURIComponent(sessionId)}`, {
33
+ method: "POST",
34
+ headers: { "Content-Type": "application/json" },
35
+ body: JSON.stringify(command),
36
+ });
37
+ const body = (await res.json().catch(() => ({}))) as {
38
+ success?: boolean;
39
+ data?: T;
40
+ error?: string;
41
+ code?: string;
42
+ accepted?: boolean;
43
+ };
44
+ if (!res.ok || body.error) {
45
+ throw new AgentCommandError(
46
+ body.error ?? `HTTP ${res.status}`,
47
+ res.status,
48
+ body.code,
49
+ body.accepted,
50
+ );
51
+ }
52
+ return body.data as T;
53
+ }
@@ -0,0 +1,193 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createJiti } from "jiti";
4
+
5
+ const jiti = createJiti(import.meta.url);
6
+ const { AgentEventConnection } = await jiti.import("./agent-event-connection.ts");
7
+
8
+ const CONNECTING = 0;
9
+ const OPEN = 1;
10
+ const CLOSED = 2;
11
+
12
+ class FakeEventSource {
13
+ readyState = CONNECTING;
14
+ onmessage = null;
15
+ onerror = null;
16
+ closeCount = 0;
17
+
18
+ open() { this.readyState = OPEN; }
19
+ message(event) { this.onmessage?.({ data: JSON.stringify(event) }); }
20
+ error() {
21
+ this.readyState = CLOSED;
22
+ this.onerror?.(new Event("error"));
23
+ }
24
+ close() {
25
+ this.readyState = CLOSED;
26
+ this.closeCount += 1;
27
+ }
28
+ }
29
+
30
+ function createHarness({ readinessTimeoutMs = 100, reconnectDelayMs = 5 } = {}) {
31
+ const sources = [];
32
+ const events = [];
33
+ const unexpectedErrors = [];
34
+ let activeSession = "session-a";
35
+ let demand = false;
36
+ const connection = new AgentEventConnection({
37
+ createSource(sessionId) {
38
+ const source = new FakeEventSource();
39
+ source.sessionId = sessionId;
40
+ sources.push(source);
41
+ return source;
42
+ },
43
+ onEvent(event) { events.push(event); },
44
+ shouldMaintain: (sessionId) => demand && sessionId === activeSession,
45
+ readinessTimeoutMs,
46
+ reconnectDelayMs,
47
+ onUnexpectedError(error) { unexpectedErrors.push(error); },
48
+ });
49
+ return {
50
+ connection,
51
+ sources,
52
+ events,
53
+ unexpectedErrors,
54
+ setActiveSession(sessionId) { activeSession = sessionId; },
55
+ setDemand(value) { demand = value; },
56
+ };
57
+ }
58
+
59
+ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
60
+ const nextTurn = () => new Promise((resolve) => setImmediate(resolve));
61
+
62
+ test("shares one source and waits past the former transport deadline", async () => {
63
+ const { connection, sources, events } = createHarness();
64
+ const first = connection.ensureConnected("session-a");
65
+ const second = connection.ensureConnected("session-a");
66
+
67
+ assert.equal(sources.length, 1);
68
+ sources[0].open();
69
+ await wait(20);
70
+ sources[0].message({ type: "connected" });
71
+ await Promise.all([first, second]);
72
+
73
+ sources[0].message({ type: "agent_start" });
74
+ assert.deepEqual(events.map((event) => event.type), ["connected", "agent_start"]);
75
+ });
76
+
77
+ test("times out all waiters and closes the stalled source", async () => {
78
+ const { connection, sources } = createHarness({ readinessTimeoutMs: 10 });
79
+ const first = connection.ensureConnected("session-a");
80
+ const second = connection.ensureConnected("session-a");
81
+
82
+ await assert.rejects(first, (error) => error.status === "ready_timeout");
83
+ await assert.rejects(second, (error) => error.status === "ready_timeout");
84
+ assert.equal(sources[0].closeCount, 1);
85
+ });
86
+
87
+ test("manual close rejects waiters and isolates late events", async () => {
88
+ const { connection, sources, events } = createHarness();
89
+ const abandoned = connection.ensureConnected("session-a");
90
+ const oldSource = sources[0];
91
+ connection.close();
92
+ await assert.rejects(abandoned, (error) => error.status === "closed");
93
+
94
+ const replacement = connection.ensureConnected("session-a");
95
+ oldSource.open();
96
+ oldSource.message({ type: "connected" });
97
+ await nextTurn();
98
+ assert.deepEqual(events, []);
99
+
100
+ sources[1].open();
101
+ sources[1].message({ type: "connected" });
102
+ await replacement;
103
+ });
104
+
105
+ test("surfaces startup errors without retrying or dispatching them", async () => {
106
+ const { connection, sources, events } = createHarness();
107
+ const ready = connection.ensureConnected("session-a");
108
+ sources[0].open();
109
+ sources[0].message({ type: "startup_error", errorMessage: "broken config" });
110
+
111
+ await assert.rejects(ready, (error) => (
112
+ error.status === "startup_error" && error.message === "broken config"
113
+ ));
114
+ await wait(10);
115
+ assert.equal(sources.length, 1);
116
+ assert.deepEqual(events, []);
117
+ });
118
+
119
+ test("replaces a previously connected CONNECTING source only once", async () => {
120
+ const { connection, sources } = createHarness();
121
+ const initial = connection.ensureConnected("session-a");
122
+ sources[0].open();
123
+ sources[0].message({ type: "connected" });
124
+ await initial;
125
+
126
+ sources[0].readyState = CONNECTING;
127
+ const first = connection.ensureConnected("session-a");
128
+ const second = connection.ensureConnected("session-a");
129
+ await nextTurn();
130
+ assert.equal(sources.length, 2);
131
+
132
+ sources[1].open();
133
+ sources[1].message({ type: "connected" });
134
+ await Promise.all([first, second]);
135
+ });
136
+
137
+ test("passive maintenance retries a transient failure once", async () => {
138
+ const { connection, sources, setDemand } = createHarness();
139
+ setDemand(true);
140
+ connection.maintain("session-a");
141
+ sources[0].error();
142
+ await wait(10);
143
+ assert.equal(sources.length, 2);
144
+
145
+ sources[1].open();
146
+ sources[1].message({ type: "connected" });
147
+ await wait(10);
148
+ assert.equal(sources.length, 2);
149
+ });
150
+
151
+ test("an explicit startup error cancels a pending passive retry", async () => {
152
+ const { connection, sources, setDemand } = createHarness();
153
+ setDemand(true);
154
+ connection.maintain("session-a");
155
+ sources[0].error();
156
+
157
+ const explicit = connection.ensureConnected("session-a");
158
+ sources[1].open();
159
+ sources[1].message({ type: "startup_error", errorMessage: "terminal" });
160
+ await assert.rejects(explicit, (error) => error.status === "startup_error");
161
+ await wait(10);
162
+ assert.equal(sources.length, 2);
163
+ });
164
+
165
+ test("dropped demand, session switches, and close cannot revive an old retry", async () => {
166
+ const first = createHarness();
167
+ first.setDemand(true);
168
+ first.connection.maintain("session-a");
169
+ first.sources[0].error();
170
+ first.setDemand(false);
171
+ await wait(10);
172
+ assert.equal(first.sources.length, 1);
173
+
174
+ const second = createHarness();
175
+ second.setDemand(true);
176
+ second.connection.maintain("session-a");
177
+ second.sources[0].error();
178
+ second.setActiveSession("session-b");
179
+ const ready = second.connection.ensureConnected("session-b");
180
+ second.sources[1].open();
181
+ second.sources[1].message({ type: "connected" });
182
+ await ready;
183
+ await wait(10);
184
+ assert.equal(second.sources.length, 2);
185
+
186
+ const closed = createHarness();
187
+ closed.setDemand(true);
188
+ closed.connection.maintain("session-a");
189
+ closed.sources[0].error();
190
+ closed.connection.close();
191
+ await wait(10);
192
+ assert.equal(closed.sources.length, 1);
193
+ });
@@ -0,0 +1,202 @@
1
+ import type { AgentEventLike } from "./agent-event-wire";
2
+
3
+ export interface AgentEventSourceLike {
4
+ readonly readyState: number;
5
+ onmessage: ((event: MessageEvent<string>) => void) | null;
6
+ onerror: ((event: Event) => void) | null;
7
+ close(): void;
8
+ }
9
+
10
+ export type AgentEventConnectionStatus = "ready_timeout" | "startup_error" | "closed";
11
+
12
+ export class AgentEventConnectionError extends Error {
13
+ constructor(public readonly status: AgentEventConnectionStatus, message?: string) {
14
+ super(message ?? (
15
+ status === "ready_timeout"
16
+ ? "Timed out starting the agent session. Please try again."
17
+ : "Failed to connect to the agent event stream. Please try again."
18
+ ));
19
+ this.name = "AgentEventConnectionError";
20
+ }
21
+ }
22
+
23
+ type Attempt = {
24
+ promise: Promise<void>;
25
+ ready: boolean;
26
+ succeed(): void;
27
+ fail(error: AgentEventConnectionError): void;
28
+ };
29
+
30
+ type Connection = {
31
+ sessionId: string;
32
+ source: AgentEventSourceLike;
33
+ attempt: Attempt;
34
+ };
35
+
36
+ export interface AgentEventConnectionOptions {
37
+ createSource(sessionId: string): AgentEventSourceLike;
38
+ onEvent(event: AgentEventLike): void;
39
+ shouldMaintain(sessionId: string): boolean;
40
+ readinessTimeoutMs: number;
41
+ reconnectDelayMs: number;
42
+ onUnexpectedError?(error: unknown): void;
43
+ }
44
+
45
+ const EVENT_SOURCE_OPEN = 1;
46
+
47
+ /** Owns the EventSource, agent-readiness handshake, and passive reconnect. */
48
+ export class AgentEventConnection {
49
+ private current: Connection | null = null;
50
+ private retry: { sessionId: string; timer: ReturnType<typeof setTimeout> } | null = null;
51
+ private retryGeneration = 0;
52
+
53
+ constructor(private readonly options: AgentEventConnectionOptions) {}
54
+
55
+ close(): void {
56
+ this.stopRetrying();
57
+ if (this.current) this.discard(this.current, new AgentEventConnectionError("closed"));
58
+ }
59
+
60
+ maintain(sessionId: string): void {
61
+ if (!this.options.shouldMaintain(sessionId)) return;
62
+ const retryGeneration = this.retryGeneration;
63
+ void this.ensureConnected(sessionId).catch((error) => {
64
+ if (retryGeneration !== this.retryGeneration) return;
65
+ if (error instanceof AgentEventConnectionError) {
66
+ if (error.status === "startup_error") this.stopRetrying();
67
+ else this.scheduleRetry(sessionId);
68
+ } else {
69
+ this.options.onUnexpectedError?.(error);
70
+ }
71
+ });
72
+ }
73
+
74
+ async ensureConnected(sessionId: string): Promise<void> {
75
+ while (true) {
76
+ let connection = this.current;
77
+ if (!connection || connection.sessionId !== sessionId) {
78
+ connection = this.open(sessionId);
79
+ } else if (
80
+ connection.attempt.ready
81
+ && connection.source.readyState === EVENT_SOURCE_OPEN
82
+ ) {
83
+ return;
84
+ }
85
+
86
+ await connection.attempt.promise;
87
+ if (this.current !== connection) {
88
+ if (this.current?.sessionId === sessionId) continue;
89
+ throw new AgentEventConnectionError("closed");
90
+ }
91
+ if (connection.source.readyState === EVENT_SOURCE_OPEN) return;
92
+
93
+ // A once-ready EventSource may otherwise remain CONNECTING indefinitely.
94
+ this.discard(connection, new AgentEventConnectionError("closed"));
95
+ }
96
+ }
97
+
98
+ private open(sessionId: string): Connection {
99
+ if (this.current) this.discard(this.current, new AgentEventConnectionError("closed"));
100
+
101
+ let source: AgentEventSourceLike;
102
+ try {
103
+ source = this.options.createSource(sessionId);
104
+ } catch (error) {
105
+ throw new AgentEventConnectionError(
106
+ "closed",
107
+ error instanceof Error ? error.message : String(error),
108
+ );
109
+ }
110
+
111
+ let settled = false;
112
+ let resolve!: () => void;
113
+ let reject!: (error: AgentEventConnectionError) => void;
114
+ const promise = new Promise<void>((resolvePromise, rejectPromise) => {
115
+ resolve = resolvePromise;
116
+ reject = rejectPromise;
117
+ });
118
+ const timeout = setTimeout(() => {
119
+ this.fail(connection, new AgentEventConnectionError("ready_timeout"));
120
+ }, this.options.readinessTimeoutMs);
121
+ const attempt: Attempt = {
122
+ promise,
123
+ ready: false,
124
+ succeed() {
125
+ if (settled) return;
126
+ settled = true;
127
+ clearTimeout(timeout);
128
+ resolve();
129
+ },
130
+ fail(error) {
131
+ if (settled) return;
132
+ settled = true;
133
+ clearTimeout(timeout);
134
+ reject(error);
135
+ },
136
+ };
137
+ const connection: Connection = { sessionId, source, attempt };
138
+ this.current = connection;
139
+
140
+ source.onmessage = (message) => {
141
+ if (this.current !== connection) return;
142
+ let event: AgentEventLike;
143
+ try {
144
+ event = JSON.parse(message.data) as AgentEventLike;
145
+ } catch {
146
+ return;
147
+ }
148
+
149
+ if (event.type === "connected") {
150
+ attempt.ready = true;
151
+ attempt.succeed();
152
+ this.stopRetrying();
153
+ } else if (event.type === "startup_error") {
154
+ const message = typeof event.errorMessage === "string" ? event.errorMessage : undefined;
155
+ this.fail(connection, new AgentEventConnectionError("startup_error", message));
156
+ return;
157
+ }
158
+ this.options.onEvent(event);
159
+ };
160
+ source.onerror = () => {
161
+ this.fail(connection, new AgentEventConnectionError("closed"));
162
+ };
163
+
164
+ return connection;
165
+ }
166
+
167
+ private fail(connection: Connection, error: AgentEventConnectionError): void {
168
+ if (this.current !== connection) return;
169
+ this.discard(connection, error);
170
+ if (error.status === "startup_error") this.stopRetrying();
171
+ else this.scheduleRetry(connection.sessionId);
172
+ }
173
+
174
+ private discard(connection: Connection, error: AgentEventConnectionError): void {
175
+ connection.attempt.fail(error);
176
+ connection.source.close();
177
+ if (this.current === connection) this.current = null;
178
+ }
179
+
180
+ private scheduleRetry(sessionId: string): void {
181
+ if (!this.options.shouldMaintain(sessionId)) return;
182
+ if (this.retry?.sessionId === sessionId) return;
183
+ this.clearRetry();
184
+
185
+ const timer = setTimeout(() => {
186
+ if (this.retry?.timer !== timer) return;
187
+ this.retry = null;
188
+ this.maintain(sessionId);
189
+ }, this.options.reconnectDelayMs);
190
+ this.retry = { sessionId, timer };
191
+ }
192
+
193
+ private stopRetrying(): void {
194
+ this.retryGeneration += 1;
195
+ this.clearRetry();
196
+ }
197
+
198
+ private clearRetry(): void {
199
+ if (this.retry) clearTimeout(this.retry.timer);
200
+ this.retry = null;
201
+ }
202
+ }