@baique/pi-web-sky 0.1.55 → 0.1.57

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 (396) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +8 -4
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/prerender-manifest.json +2 -2
  5. package/.next/react-loadable-manifest.json +2 -2
  6. package/.next/required-server-files.js +3 -2
  7. package/.next/required-server-files.json +3 -2
  8. package/.next/routes-manifest.json +28 -0
  9. package/.next/server/app/_global-error/page.js +2 -2
  10. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  11. package/.next/server/app/_global-error.html +1 -1
  12. package/.next/server/app/_global-error.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  14. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  15. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/.next/server/app/_not-found/page.js +1 -1
  17. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/server/app/_not-found.html +1 -1
  19. package/.next/server/app/_not-found.rsc +4 -3
  20. package/.next/server/app/_not-found.segments/_full.segment.rsc +4 -3
  21. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +4 -3
  22. package/.next/server/app/_not-found.segments/_tree.segment.rsc +3 -2
  23. package/.next/server/app/api/agent/[id]/bash-output/route.js +2 -2
  24. package/.next/server/app/api/agent/[id]/events/route.js +2 -2
  25. package/.next/server/app/api/agent/[id]/events/route.js.nft.json +1 -1
  26. package/.next/server/app/api/agent/[id]/route.js +1 -1
  27. package/.next/server/app/api/agent/[id]/route.js.nft.json +1 -1
  28. package/.next/server/app/api/agent/new/route.js +1 -1
  29. package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
  30. package/.next/server/app/api/agent/running/route.js +1 -1
  31. package/.next/server/app/api/agent/running/route.js.nft.json +1 -1
  32. package/.next/server/app/api/app-update/route.js +1 -1
  33. package/.next/server/app/api/auth/all-providers/route.js +1 -1
  34. package/.next/server/app/api/auth/api-key/[provider]/route.js +1 -1
  35. package/.next/server/app/api/auth/login/[provider]/route.js +1 -1
  36. package/.next/server/app/api/auth/logout/[provider]/route.js +1 -1
  37. package/.next/server/app/api/auth/providers/route.js +1 -1
  38. package/.next/server/app/api/board-assets/[...path]/route.js +2 -2
  39. package/.next/server/app/api/board-assets/route.js +2 -2
  40. package/.next/server/app/api/boards/[id]/add-session/route.js +1 -0
  41. package/.next/server/app/api/boards/[id]/add-session/route.js.nft.json +1 -0
  42. package/.next/server/app/api/boards/[id]/add-session/route_client-reference-manifest.js +1 -0
  43. package/.next/server/app/api/boards/[id]/reconcile/route.js +1 -1
  44. package/.next/server/app/api/boards/[id]/route.js +1 -1
  45. package/.next/server/app/api/boards/reorder/route.js +2 -2
  46. package/.next/server/app/api/boards/route.js +1 -1
  47. package/.next/server/app/api/cwd/browse/route.js +1 -1
  48. package/.next/server/app/api/cwd/validate/route.js +2 -2
  49. package/.next/server/app/api/default-cwd/route.js +2 -2
  50. package/.next/server/app/api/file-index/route.js +2 -2
  51. package/.next/server/app/api/files/[...path]/route.js +3 -3
  52. package/.next/server/app/api/git/diff/route.js +1 -1
  53. package/.next/server/app/api/git/status/route.js +1 -1
  54. package/.next/server/app/api/home/route.js +1 -1
  55. package/.next/server/app/api/mcp/route.js +2 -2
  56. package/.next/server/app/api/models/route.js +2 -2
  57. package/.next/server/app/api/models-config/catalog/route.js +1 -1
  58. package/.next/server/app/api/models-config/discover/route.js +1 -1
  59. package/.next/server/app/api/models-config/route.js +1 -1
  60. package/.next/server/app/api/models-config/test/route.js +1 -1
  61. package/.next/server/app/api/plugins/route.js +2 -2
  62. package/.next/server/app/api/project-trust/route.js +1 -1
  63. package/.next/server/app/api/project-trust/route.js.nft.json +1 -1
  64. package/.next/server/app/api/quota/route.js +1 -1
  65. package/.next/server/app/api/search/route.js +3 -3
  66. package/.next/server/app/api/sessions/[id]/auto-name/route.js +2 -2
  67. package/.next/server/app/api/sessions/[id]/auto-name/route.js.nft.json +1 -1
  68. package/.next/server/app/api/sessions/[id]/context/route.js +1 -1
  69. package/.next/server/app/api/sessions/[id]/context/route.js.nft.json +1 -1
  70. package/.next/server/app/api/sessions/[id]/entries/[entryId]/thinking/route.js +2 -2
  71. package/.next/server/app/api/sessions/[id]/export/route.js +2 -2
  72. package/.next/server/app/api/sessions/[id]/minimap/route.js +1 -1
  73. package/.next/server/app/api/sessions/[id]/minimap/route.js.nft.json +1 -1
  74. package/.next/server/app/api/sessions/[id]/route.js +1 -1
  75. package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  76. package/.next/server/app/api/sessions/[id]/state/route.js +1 -1
  77. package/.next/server/app/api/sessions/[id]/state/route.js.nft.json +1 -1
  78. package/.next/server/app/api/sessions/[id]/todos/route.js +1 -1
  79. package/.next/server/app/api/sessions/[id]/todos/route.js.nft.json +1 -1
  80. package/.next/server/app/api/sessions/route.js +1 -1
  81. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  82. package/.next/server/app/api/sessions/summary/route.js +2 -2
  83. package/.next/server/app/api/skills/check/route.js +1 -1
  84. package/.next/server/app/api/skills/install/route.js +2 -2
  85. package/.next/server/app/api/skills/route.js +1 -1
  86. package/.next/server/app/api/skills/search/route.js +1 -1
  87. package/.next/server/app/api/skills/update/route.js +1 -1
  88. package/.next/server/app/api/subagents/[id]/route.js +1 -0
  89. package/.next/server/app/api/subagents/[id]/route.js.nft.json +1 -0
  90. package/.next/server/app/api/subagents/[id]/route_client-reference-manifest.js +1 -0
  91. package/.next/server/app/api/subagents/profiles/route.js +30 -0
  92. package/.next/server/app/api/subagents/profiles/route.js.nft.json +1 -0
  93. package/.next/server/app/api/subagents/profiles/route_client-reference-manifest.js +1 -0
  94. package/.next/server/app/api/subagents/settings/route.js +1 -0
  95. package/.next/server/app/api/subagents/settings/route.js.nft.json +1 -0
  96. package/.next/server/app/api/subagents/settings/route_client-reference-manifest.js +1 -0
  97. package/.next/server/app/api/task-card-questions/[id]/answer/route.js +2 -2
  98. package/.next/server/app/api/task-card-questions/route.js +2 -2
  99. package/.next/server/app/api/task-cards/[id]/route.js +1 -1
  100. package/.next/server/app/api/task-cards/route.js +1 -1
  101. package/.next/server/app/api/task-cards/unbind/route.js +3 -3
  102. package/.next/server/app/api/task-scheduler/status/route.js +1 -1
  103. package/.next/server/app/api/task-scheduler/status/route.js.nft.json +1 -1
  104. package/.next/server/app/api/tasks/[id]/assign-session/route.js +2 -12
  105. package/.next/server/app/api/tasks/[id]/board/route.js +1 -1
  106. package/.next/server/app/api/tasks/[id]/route.js +1 -1
  107. package/.next/server/app/api/tasks/[id]/route.js.nft.json +1 -1
  108. package/.next/server/app/api/tasks/reorder/route.js +2 -12
  109. package/.next/server/app/api/tasks/route.js +2 -12
  110. package/.next/server/app/api/terminal/[id]/events/route.js +2 -2
  111. package/.next/server/app/api/terminal/[id]/route.js +2 -2
  112. package/.next/server/app/api/terminal/route.js +2 -2
  113. package/.next/server/app/api/worktrees/route.js +2 -2
  114. package/.next/server/app/api/yjs-version/route.js +1 -1
  115. package/.next/server/app/favicon.ico/route.js +1 -1
  116. package/.next/server/app/index.html +1 -1
  117. package/.next/server/app/index.rsc +6 -5
  118. package/.next/server/app/index.segments/__PAGE__.segment.rsc +6 -5
  119. package/.next/server/app/index.segments/_full.segment.rsc +6 -5
  120. package/.next/server/app/index.segments/_tree.segment.rsc +3 -2
  121. package/.next/server/app/manifest.webmanifest/route.js +2 -2
  122. package/.next/server/app/page.js +175 -8
  123. package/.next/server/app/page_client-reference-manifest.js +1 -1
  124. package/.next/server/app-paths-manifest.json +8 -4
  125. package/.next/server/chunks/1937.js +14 -13
  126. package/.next/server/chunks/2524.js +2 -2
  127. package/.next/server/chunks/3379.js +1 -1
  128. package/.next/server/chunks/3500.js +1 -1
  129. package/.next/server/chunks/402.js +28 -7
  130. package/.next/server/chunks/426.js +14 -0
  131. package/.next/server/chunks/4367.js +1 -1
  132. package/.next/server/chunks/4457.js +3 -13
  133. package/.next/server/chunks/4980.js +1 -1
  134. package/.next/server/chunks/542.js +23 -4
  135. package/.next/server/chunks/5794.js +2 -2
  136. package/.next/server/chunks/5940.js +1 -1
  137. package/.next/server/chunks/6337.js +1 -1
  138. package/.next/server/chunks/6429.js +15 -2
  139. package/.next/server/chunks/6525.js +25 -4
  140. package/.next/server/chunks/6583.js +12 -11
  141. package/.next/server/chunks/7048.js +6 -0
  142. package/.next/server/instrumentation.js +1 -1
  143. package/.next/server/middleware-build-manifest.js +1 -1
  144. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  145. package/.next/server/middleware.js +3 -3
  146. package/.next/server/pages/404.html +1 -1
  147. package/.next/server/pages/500.html +1 -1
  148. package/.next/static/MPBMmQqmDG8zo8tM_bkDy/_buildManifest.js +1 -0
  149. package/.next/static/chunks/752.bafff304e6b00987.js +1 -0
  150. package/.next/static/chunks/8088.39866dc5f1d15868.js +1 -0
  151. package/.next/static/chunks/app/_global-error/page-ad6e695deb9de65c.js +1 -0
  152. package/.next/static/chunks/app/api/agent/[id]/bash-output/route-ad6e695deb9de65c.js +1 -0
  153. package/.next/static/chunks/app/api/agent/[id]/events/route-ad6e695deb9de65c.js +1 -0
  154. package/.next/static/chunks/app/api/agent/[id]/route-ad6e695deb9de65c.js +1 -0
  155. package/.next/static/chunks/app/api/agent/new/route-ad6e695deb9de65c.js +1 -0
  156. package/.next/static/chunks/app/api/agent/running/route-ad6e695deb9de65c.js +1 -0
  157. package/.next/static/chunks/app/api/app-update/route-ad6e695deb9de65c.js +1 -0
  158. package/.next/static/chunks/app/api/auth/all-providers/route-ad6e695deb9de65c.js +1 -0
  159. package/.next/static/chunks/app/api/auth/api-key/[provider]/route-ad6e695deb9de65c.js +1 -0
  160. package/.next/static/chunks/app/api/auth/login/[provider]/route-ad6e695deb9de65c.js +1 -0
  161. package/.next/static/chunks/app/api/auth/logout/[provider]/route-ad6e695deb9de65c.js +1 -0
  162. package/.next/static/chunks/app/api/auth/providers/route-ad6e695deb9de65c.js +1 -0
  163. package/.next/static/chunks/app/api/board-assets/[...path]/route-ad6e695deb9de65c.js +1 -0
  164. package/.next/static/chunks/app/api/board-assets/route-ad6e695deb9de65c.js +1 -0
  165. package/.next/static/chunks/app/api/boards/[id]/add-session/route-ad6e695deb9de65c.js +1 -0
  166. package/.next/static/chunks/app/api/boards/[id]/reconcile/route-ad6e695deb9de65c.js +1 -0
  167. package/.next/static/chunks/app/api/boards/[id]/route-ad6e695deb9de65c.js +1 -0
  168. package/.next/static/chunks/app/api/boards/reorder/route-ad6e695deb9de65c.js +1 -0
  169. package/.next/static/chunks/app/api/boards/route-ad6e695deb9de65c.js +1 -0
  170. package/.next/static/chunks/app/api/cwd/browse/route-ad6e695deb9de65c.js +1 -0
  171. package/.next/static/chunks/app/api/cwd/validate/route-ad6e695deb9de65c.js +1 -0
  172. package/.next/static/chunks/app/api/default-cwd/route-ad6e695deb9de65c.js +1 -0
  173. package/.next/static/chunks/app/api/file-index/route-ad6e695deb9de65c.js +1 -0
  174. package/.next/static/chunks/app/api/files/[...path]/route-ad6e695deb9de65c.js +1 -0
  175. package/.next/static/chunks/app/api/git/diff/route-ad6e695deb9de65c.js +1 -0
  176. package/.next/static/chunks/app/api/git/status/route-ad6e695deb9de65c.js +1 -0
  177. package/.next/static/chunks/app/api/home/route-ad6e695deb9de65c.js +1 -0
  178. package/.next/static/chunks/app/api/mcp/route-ad6e695deb9de65c.js +1 -0
  179. package/.next/static/chunks/app/api/models/route-ad6e695deb9de65c.js +1 -0
  180. package/.next/static/chunks/app/api/models-config/catalog/route-ad6e695deb9de65c.js +1 -0
  181. package/.next/static/chunks/app/api/models-config/discover/route-ad6e695deb9de65c.js +1 -0
  182. package/.next/static/chunks/app/api/models-config/route-ad6e695deb9de65c.js +1 -0
  183. package/.next/static/chunks/app/api/models-config/test/route-ad6e695deb9de65c.js +1 -0
  184. package/.next/static/chunks/app/api/plugins/route-ad6e695deb9de65c.js +1 -0
  185. package/.next/static/chunks/app/api/project-trust/route-ad6e695deb9de65c.js +1 -0
  186. package/.next/static/chunks/app/api/quota/route-ad6e695deb9de65c.js +1 -0
  187. package/.next/static/chunks/app/api/search/route-ad6e695deb9de65c.js +1 -0
  188. package/.next/static/chunks/app/api/sessions/[id]/auto-name/route-ad6e695deb9de65c.js +1 -0
  189. package/.next/static/chunks/app/api/sessions/[id]/context/route-ad6e695deb9de65c.js +1 -0
  190. package/.next/static/chunks/app/api/sessions/[id]/entries/[entryId]/thinking/route-ad6e695deb9de65c.js +1 -0
  191. package/.next/static/chunks/app/api/sessions/[id]/export/route-ad6e695deb9de65c.js +1 -0
  192. package/.next/static/chunks/app/api/sessions/[id]/minimap/route-ad6e695deb9de65c.js +1 -0
  193. package/.next/static/chunks/app/api/sessions/[id]/route-ad6e695deb9de65c.js +1 -0
  194. package/.next/static/chunks/app/api/sessions/[id]/state/route-ad6e695deb9de65c.js +1 -0
  195. package/.next/static/chunks/app/api/sessions/[id]/todos/route-ad6e695deb9de65c.js +1 -0
  196. package/.next/static/chunks/app/api/sessions/route-ad6e695deb9de65c.js +1 -0
  197. package/.next/static/chunks/app/api/sessions/summary/route-ad6e695deb9de65c.js +1 -0
  198. package/.next/static/chunks/app/api/skills/check/route-ad6e695deb9de65c.js +1 -0
  199. package/.next/static/chunks/app/api/skills/install/route-ad6e695deb9de65c.js +1 -0
  200. package/.next/static/chunks/app/api/skills/route-ad6e695deb9de65c.js +1 -0
  201. package/.next/static/chunks/app/api/skills/search/route-ad6e695deb9de65c.js +1 -0
  202. package/.next/static/chunks/app/api/skills/update/route-ad6e695deb9de65c.js +1 -0
  203. package/.next/static/chunks/app/api/subagents/[id]/route-ad6e695deb9de65c.js +1 -0
  204. package/.next/static/chunks/app/api/subagents/profiles/route-ad6e695deb9de65c.js +1 -0
  205. package/.next/static/chunks/app/api/subagents/settings/route-ad6e695deb9de65c.js +1 -0
  206. package/.next/static/chunks/app/api/task-card-questions/[id]/answer/route-ad6e695deb9de65c.js +1 -0
  207. package/.next/static/chunks/app/api/task-card-questions/route-ad6e695deb9de65c.js +1 -0
  208. package/.next/static/chunks/app/api/task-cards/[id]/route-ad6e695deb9de65c.js +1 -0
  209. package/.next/static/chunks/app/api/task-cards/route-ad6e695deb9de65c.js +1 -0
  210. package/.next/static/chunks/app/api/task-cards/unbind/route-ad6e695deb9de65c.js +1 -0
  211. package/.next/static/chunks/app/api/task-scheduler/status/route-ad6e695deb9de65c.js +1 -0
  212. package/.next/static/chunks/app/api/tasks/[id]/assign-session/route-ad6e695deb9de65c.js +1 -0
  213. package/.next/static/chunks/app/api/tasks/[id]/board/route-ad6e695deb9de65c.js +1 -0
  214. package/.next/static/chunks/app/api/tasks/[id]/route-ad6e695deb9de65c.js +1 -0
  215. package/.next/static/chunks/app/api/tasks/reorder/route-ad6e695deb9de65c.js +1 -0
  216. package/.next/static/chunks/app/api/tasks/route-ad6e695deb9de65c.js +1 -0
  217. package/.next/static/chunks/app/api/terminal/[id]/events/route-ad6e695deb9de65c.js +1 -0
  218. package/.next/static/chunks/app/api/terminal/[id]/route-ad6e695deb9de65c.js +1 -0
  219. package/.next/static/chunks/app/api/terminal/route-ad6e695deb9de65c.js +1 -0
  220. package/.next/static/chunks/app/api/worktrees/route-ad6e695deb9de65c.js +1 -0
  221. package/.next/static/chunks/app/api/yjs-version/route-ad6e695deb9de65c.js +1 -0
  222. package/.next/static/chunks/app/layout-fae2b226b3c78448.js +1 -0
  223. package/.next/static/chunks/app/manifest.webmanifest/route-ad6e695deb9de65c.js +1 -0
  224. package/.next/static/chunks/app/page-d5af3bd535589daf.js +256 -0
  225. package/.next/static/chunks/next/dist/client/components/builtin/app-error-ad6e695deb9de65c.js +1 -0
  226. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-ad6e695deb9de65c.js +1 -0
  227. package/.next/static/chunks/next/dist/client/components/builtin/not-found-ad6e695deb9de65c.js +1 -0
  228. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-ad6e695deb9de65c.js +1 -0
  229. package/.next/static/chunks/{webpack-b0effde51c1651b8.js → webpack-1ce35e1249cfd931.js} +1 -1
  230. package/.next/static/css/bb9361c7f832a89d.css +1 -0
  231. package/.next/static/css/c7d127e07b1a786c.css +1 -0
  232. package/.next/trace +8 -7
  233. package/.next/trace-build +1 -1
  234. package/.next/types/app/api/boards/[id]/add-session/route.ts +353 -0
  235. package/.next/types/app/api/subagents/[id]/route.ts +353 -0
  236. package/.next/types/app/api/subagents/profiles/route.ts +353 -0
  237. package/.next/types/app/api/subagents/settings/route.ts +353 -0
  238. package/.next/types/routes.d.ts +5 -1
  239. package/.next/types/validator.ts +36 -0
  240. package/lib/api-types.ts +10 -0
  241. package/lib/board-reconcile.test.mjs +46 -8
  242. package/lib/board-reconcile.ts +57 -1
  243. package/lib/board-store.test.mjs +2 -2
  244. package/lib/chat-lazy-load.test.mjs +0 -62
  245. package/lib/chat-lazy-load.ts +0 -13
  246. package/lib/chat-only.ts +23 -0
  247. package/lib/draft-stash.ts +4 -5
  248. package/lib/file-types.ts +16 -1
  249. package/lib/i18n/messages/en.ts +91 -0
  250. package/lib/i18n/messages/zh-CN.ts +91 -0
  251. package/lib/id.test.mjs +38 -0
  252. package/lib/id.ts +17 -0
  253. package/lib/ime-guard.test.mjs +52 -0
  254. package/lib/ime-guard.ts +54 -0
  255. package/lib/message-text.ts +24 -0
  256. package/lib/models-cache.ts +1 -1
  257. package/lib/pi-types.ts +19 -1
  258. package/lib/pin-store.ts +85 -0
  259. package/lib/powershell-settings.ts +106 -0
  260. package/lib/quoted-selection.ts +4 -0
  261. package/lib/rpc-manager.activity.test.mjs +223 -0
  262. package/lib/rpc-manager.test.mjs +22 -0
  263. package/lib/rpc-manager.title.test.mjs +152 -0
  264. package/lib/rpc-manager.ts +301 -28
  265. package/lib/session-activity.test.mjs +123 -0
  266. package/lib/session-activity.ts +77 -0
  267. package/lib/session-delete.test.mjs +45 -41
  268. package/lib/session-delete.ts +8 -34
  269. package/lib/session-family.test.mjs +49 -0
  270. package/lib/session-family.ts +80 -0
  271. package/lib/session-index-scanner.test.mjs +364 -2
  272. package/lib/session-index-scanner.ts +199 -55
  273. package/lib/session-reader.noscan.test.mjs +371 -0
  274. package/lib/session-reader.project.test.mjs +30 -52
  275. package/lib/session-reader.test.mjs +2 -65
  276. package/lib/session-reader.ts +177 -213
  277. package/lib/session-scanner.test.mjs +151 -38
  278. package/lib/session-scanner.ts +140 -61
  279. package/lib/session-search.ts +6 -5
  280. package/lib/session-time-group.test.mjs +38 -0
  281. package/lib/session-time-group.ts +55 -0
  282. package/lib/settings-navigation.ts +124 -0
  283. package/lib/sqlite-db.test.mjs +26 -0
  284. package/lib/sqlite-db.ts +10 -1
  285. package/lib/subagent-extension.test.mjs +424 -0
  286. package/lib/subagent-extension.ts +309 -0
  287. package/lib/subagent-input.test.mjs +63 -0
  288. package/lib/subagent-input.ts +71 -0
  289. package/lib/subagent-profile-precedence.ts +20 -0
  290. package/lib/subagent-prompt.test.mjs +64 -0
  291. package/lib/subagent-prompt.ts +31 -0
  292. package/lib/subagent-queue.test.mjs +35 -0
  293. package/lib/subagent-queue.ts +74 -0
  294. package/lib/subagent-runtime.test.mjs +150 -0
  295. package/lib/subagent-runtime.ts +629 -0
  296. package/lib/subagent-session-title.test.mjs +120 -0
  297. package/lib/subagent-settings.test.mjs +50 -0
  298. package/lib/subagent-settings.ts +93 -0
  299. package/lib/subagents.test.mjs +601 -0
  300. package/lib/subagents.ts +602 -0
  301. package/lib/task-scheduler.test.mjs +27 -0
  302. package/lib/task-scheduler.ts +8 -1
  303. package/lib/task-store.activity.test.mjs +141 -0
  304. package/lib/task-store.meta-row.test.mjs +25 -5
  305. package/lib/task-store.partial-row.test.mjs +198 -0
  306. package/lib/task-store.subtree.test.mjs +172 -0
  307. package/lib/task-store.test.mjs +46 -20
  308. package/lib/task-store.ts +344 -68
  309. package/lib/tool-presets.ts +4 -0
  310. package/lib/types.ts +19 -0
  311. package/lib/worktree-isolation.test.mjs +51 -0
  312. package/next.config.ts +3 -1
  313. package/package.json +1 -1
  314. package/.next/static/bsW3fla0-BlBHVMKtAM5I/_buildManifest.js +0 -1
  315. package/.next/static/chunks/752.0ad92735d5f38143.js +0 -1
  316. package/.next/static/chunks/8088.53d9cdaf15634d4b.js +0 -1
  317. package/.next/static/chunks/app/_global-error/page-2d5f6296c7e693c6.js +0 -1
  318. package/.next/static/chunks/app/api/agent/[id]/bash-output/route-2d5f6296c7e693c6.js +0 -1
  319. package/.next/static/chunks/app/api/agent/[id]/events/route-2d5f6296c7e693c6.js +0 -1
  320. package/.next/static/chunks/app/api/agent/[id]/route-2d5f6296c7e693c6.js +0 -1
  321. package/.next/static/chunks/app/api/agent/new/route-2d5f6296c7e693c6.js +0 -1
  322. package/.next/static/chunks/app/api/agent/running/route-2d5f6296c7e693c6.js +0 -1
  323. package/.next/static/chunks/app/api/app-update/route-2d5f6296c7e693c6.js +0 -1
  324. package/.next/static/chunks/app/api/auth/all-providers/route-2d5f6296c7e693c6.js +0 -1
  325. package/.next/static/chunks/app/api/auth/api-key/[provider]/route-2d5f6296c7e693c6.js +0 -1
  326. package/.next/static/chunks/app/api/auth/login/[provider]/route-2d5f6296c7e693c6.js +0 -1
  327. package/.next/static/chunks/app/api/auth/logout/[provider]/route-2d5f6296c7e693c6.js +0 -1
  328. package/.next/static/chunks/app/api/auth/providers/route-2d5f6296c7e693c6.js +0 -1
  329. package/.next/static/chunks/app/api/board-assets/[...path]/route-2d5f6296c7e693c6.js +0 -1
  330. package/.next/static/chunks/app/api/board-assets/route-2d5f6296c7e693c6.js +0 -1
  331. package/.next/static/chunks/app/api/boards/[id]/reconcile/route-2d5f6296c7e693c6.js +0 -1
  332. package/.next/static/chunks/app/api/boards/[id]/route-2d5f6296c7e693c6.js +0 -1
  333. package/.next/static/chunks/app/api/boards/reorder/route-2d5f6296c7e693c6.js +0 -1
  334. package/.next/static/chunks/app/api/boards/route-2d5f6296c7e693c6.js +0 -1
  335. package/.next/static/chunks/app/api/cwd/browse/route-2d5f6296c7e693c6.js +0 -1
  336. package/.next/static/chunks/app/api/cwd/validate/route-2d5f6296c7e693c6.js +0 -1
  337. package/.next/static/chunks/app/api/default-cwd/route-2d5f6296c7e693c6.js +0 -1
  338. package/.next/static/chunks/app/api/file-index/route-2d5f6296c7e693c6.js +0 -1
  339. package/.next/static/chunks/app/api/files/[...path]/route-2d5f6296c7e693c6.js +0 -1
  340. package/.next/static/chunks/app/api/git/diff/route-2d5f6296c7e693c6.js +0 -1
  341. package/.next/static/chunks/app/api/git/status/route-2d5f6296c7e693c6.js +0 -1
  342. package/.next/static/chunks/app/api/home/route-2d5f6296c7e693c6.js +0 -1
  343. package/.next/static/chunks/app/api/mcp/route-2d5f6296c7e693c6.js +0 -1
  344. package/.next/static/chunks/app/api/models/route-2d5f6296c7e693c6.js +0 -1
  345. package/.next/static/chunks/app/api/models-config/catalog/route-2d5f6296c7e693c6.js +0 -1
  346. package/.next/static/chunks/app/api/models-config/discover/route-2d5f6296c7e693c6.js +0 -1
  347. package/.next/static/chunks/app/api/models-config/route-2d5f6296c7e693c6.js +0 -1
  348. package/.next/static/chunks/app/api/models-config/test/route-2d5f6296c7e693c6.js +0 -1
  349. package/.next/static/chunks/app/api/plugins/route-2d5f6296c7e693c6.js +0 -1
  350. package/.next/static/chunks/app/api/project-trust/route-2d5f6296c7e693c6.js +0 -1
  351. package/.next/static/chunks/app/api/quota/route-2d5f6296c7e693c6.js +0 -1
  352. package/.next/static/chunks/app/api/search/route-2d5f6296c7e693c6.js +0 -1
  353. package/.next/static/chunks/app/api/sessions/[id]/auto-name/route-2d5f6296c7e693c6.js +0 -1
  354. package/.next/static/chunks/app/api/sessions/[id]/context/route-2d5f6296c7e693c6.js +0 -1
  355. package/.next/static/chunks/app/api/sessions/[id]/entries/[entryId]/thinking/route-2d5f6296c7e693c6.js +0 -1
  356. package/.next/static/chunks/app/api/sessions/[id]/export/route-2d5f6296c7e693c6.js +0 -1
  357. package/.next/static/chunks/app/api/sessions/[id]/minimap/route-2d5f6296c7e693c6.js +0 -1
  358. package/.next/static/chunks/app/api/sessions/[id]/route-2d5f6296c7e693c6.js +0 -1
  359. package/.next/static/chunks/app/api/sessions/[id]/state/route-2d5f6296c7e693c6.js +0 -1
  360. package/.next/static/chunks/app/api/sessions/[id]/todos/route-2d5f6296c7e693c6.js +0 -1
  361. package/.next/static/chunks/app/api/sessions/route-2d5f6296c7e693c6.js +0 -1
  362. package/.next/static/chunks/app/api/sessions/summary/route-2d5f6296c7e693c6.js +0 -1
  363. package/.next/static/chunks/app/api/skills/check/route-2d5f6296c7e693c6.js +0 -1
  364. package/.next/static/chunks/app/api/skills/install/route-2d5f6296c7e693c6.js +0 -1
  365. package/.next/static/chunks/app/api/skills/route-2d5f6296c7e693c6.js +0 -1
  366. package/.next/static/chunks/app/api/skills/search/route-2d5f6296c7e693c6.js +0 -1
  367. package/.next/static/chunks/app/api/skills/update/route-2d5f6296c7e693c6.js +0 -1
  368. package/.next/static/chunks/app/api/task-card-questions/[id]/answer/route-2d5f6296c7e693c6.js +0 -1
  369. package/.next/static/chunks/app/api/task-card-questions/route-2d5f6296c7e693c6.js +0 -1
  370. package/.next/static/chunks/app/api/task-cards/[id]/route-2d5f6296c7e693c6.js +0 -1
  371. package/.next/static/chunks/app/api/task-cards/route-2d5f6296c7e693c6.js +0 -1
  372. package/.next/static/chunks/app/api/task-cards/unbind/route-2d5f6296c7e693c6.js +0 -1
  373. package/.next/static/chunks/app/api/task-scheduler/status/route-2d5f6296c7e693c6.js +0 -1
  374. package/.next/static/chunks/app/api/tasks/[id]/assign-session/route-2d5f6296c7e693c6.js +0 -1
  375. package/.next/static/chunks/app/api/tasks/[id]/board/route-2d5f6296c7e693c6.js +0 -1
  376. package/.next/static/chunks/app/api/tasks/[id]/route-2d5f6296c7e693c6.js +0 -1
  377. package/.next/static/chunks/app/api/tasks/reorder/route-2d5f6296c7e693c6.js +0 -1
  378. package/.next/static/chunks/app/api/tasks/route-2d5f6296c7e693c6.js +0 -1
  379. package/.next/static/chunks/app/api/terminal/[id]/events/route-2d5f6296c7e693c6.js +0 -1
  380. package/.next/static/chunks/app/api/terminal/[id]/route-2d5f6296c7e693c6.js +0 -1
  381. package/.next/static/chunks/app/api/terminal/route-2d5f6296c7e693c6.js +0 -1
  382. package/.next/static/chunks/app/api/worktrees/route-2d5f6296c7e693c6.js +0 -1
  383. package/.next/static/chunks/app/api/yjs-version/route-2d5f6296c7e693c6.js +0 -1
  384. package/.next/static/chunks/app/layout-dace09825b2d3857.js +0 -1
  385. package/.next/static/chunks/app/manifest.webmanifest/route-2d5f6296c7e693c6.js +0 -1
  386. package/.next/static/chunks/app/page-64b9f4b3567a2f73.js +0 -89
  387. package/.next/static/chunks/next/dist/client/components/builtin/app-error-2d5f6296c7e693c6.js +0 -1
  388. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-2d5f6296c7e693c6.js +0 -1
  389. package/.next/static/chunks/next/dist/client/components/builtin/not-found-2d5f6296c7e693c6.js +0 -1
  390. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-2d5f6296c7e693c6.js +0 -1
  391. package/.next/static/css/fd25673802257262.css +0 -1
  392. package/lib/extension-command.test.mjs +0 -63
  393. package/lib/extension-command.ts +0 -81
  394. package/lib/subagent-widget.test.mjs +0 -87
  395. package/lib/subagent-widget.ts +0 -109
  396. /package/.next/static/{bsW3fla0-BlBHVMKtAM5I → MPBMmQqmDG8zo8tM_bkDy}/_ssgManifest.js +0 -0
@@ -1,8 +1,18 @@
1
1
  import assert from "node:assert/strict";
2
- import test from "node:test";
2
+ import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import test, { after } from "node:test";
3
6
  import { DatabaseSync } from "node:sqlite";
4
7
  import { createJiti } from "jiti";
5
8
 
9
+ // getAgentDir() 每次调用都读环境变量 → 指向临时树,测试绝不碰 ~/.pi/agent。
10
+ // sessions 子目录先建好:无行会话补行时会按 id 在这里找文件,miss 不回落扫开发机真实目录。
11
+ const agentDir = mkdtempSync(join(tmpdir(), "pi-web-task-store-"));
12
+ mkdirSync(join(agentDir, "sessions"), { recursive: true });
13
+ process.env.PI_CODING_AGENT_DIR = agentDir;
14
+ after(() => rmSync(agentDir, { recursive: true, force: true }));
15
+
6
16
  const jiti = createJiti(import.meta.url, { interopDefault: true, moduleCache: false });
7
17
  const { getDb, setDbForTesting } = await jiti.import("./sqlite-db.ts");
8
18
  const { createTask, deleteTask, listTasks, updateTask, taskForSession, assignSessionToTask, reorderTasks, setSessionTitle } = await jiti.import("./task-store.ts");
@@ -112,26 +122,26 @@ test("updateTask returns null for unknown id and rejects empty rename", () => {
112
122
  assert.throws(() => updateTask(a.id, { name: " " }), /name must not be empty/);
113
123
  });
114
124
 
115
- test("assignSessionToTask attaches a session atomically and bumps the task's updated", () => {
125
+ test("assignSessionToTask attaches a session atomically and bumps the task's updated", async () => {
116
126
  freshDb();
117
127
  const a = createTask("p", "A");
118
128
  seedSession("s1", null); // temp
119
129
  seedSession("s2", a.id); // already a member
120
130
 
121
- assert.equal(assignSessionToTask("s1", a.id), true);
131
+ assert.equal(await assignSessionToTask("s1", a.id), true);
122
132
  assert.equal(taskForSession("s1"), a.id);
123
133
  // 重复归属幂等:已是成员再归一次仍成功且不重复。
124
- assert.equal(assignSessionToTask("s2", a.id), true);
134
+ assert.equal(await assignSessionToTask("s2", a.id), true);
125
135
 
126
136
  const task = listTasks("p")[0];
127
137
  assert.deepEqual([...task.sessionIds].sort(), ["s1", "s2"]);
128
138
  assert.ok(task.updated >= task.created, "归属会刷新任务的 updated(排序用)");
129
139
  });
130
140
 
131
- test("assignSessionToTask returns false for a missing task without writing anything", () => {
141
+ test("assignSessionToTask returns false for a missing task without writing anything", async () => {
132
142
  freshDb();
133
143
  seedSession("s1", null);
134
- assert.equal(assignSessionToTask("s1", "no-such-task"), false);
144
+ assert.equal(await assignSessionToTask("s1", "no-such-task"), false);
135
145
  assert.equal(taskForSession("s1"), null);
136
146
  assert.equal(listTasks("p").length, 0);
137
147
  });
@@ -177,18 +187,38 @@ test("updateTask supports sortOrder patch", () => {
177
187
  assert.equal(b.sortOrder < 0, true);
178
188
  });
179
189
 
180
- test("assignSessionToTask moves a session out of its previous task", () => {
190
+ test("assignSessionToTask moves a session out of its previous task", async () => {
181
191
  freshDb();
182
192
  const a = createTask("p", "A");
183
193
  const b = createTask("p", "B");
184
194
  seedSession("s1", null);
185
- assignSessionToTask("s1", a.id);
186
- assignSessionToTask("s1", b.id); // 移动到 B
195
+ await assignSessionToTask("s1", a.id);
196
+ await assignSessionToTask("s1", b.id); // 移动到 B
187
197
  assert.equal(taskForSession("s1"), b.id);
188
198
  const taskA = listTasks("p").find((t) => t.id === a.id);
189
199
  assert.deepEqual(taskA.sessionIds, []);
190
200
  });
191
201
 
202
+ test("updateTask 不再为无行会话插缺列局部行(加侧只 UPDATE)", () => {
203
+ freshDb();
204
+ const a = createTask("p", "A");
205
+ updateTask(a.id, { sessionIds: ["ghost-no-row"] });
206
+ // 行由路由层 updateTask 之前调 ensureSessionRows 建(那里才能 await 文件 I/O)。
207
+ assert.equal(
208
+ getDb().prepare("SELECT COUNT(*) c FROM session_meta WHERE session_id='ghost-no-row'").get().c,
209
+ 0,
210
+ );
211
+ assert.deepEqual(listTasks("p")[0].sessionIds, []);
212
+ });
213
+
214
+ test("updateTask 对已有行的成员照常写归属(加侧 UPDATE 不丢写)", () => {
215
+ freshDb();
216
+ const a = createTask("p", "A");
217
+ seedSession("s1", null);
218
+ updateTask(a.id, { sessionIds: ["s1"] });
219
+ assert.equal(taskForSession("s1"), a.id);
220
+ });
221
+
192
222
  test("deleteTask cascades to its task board (nodes/edges/view gone)", () => {
193
223
  freshDb();
194
224
  const a = createTask("p", "A");
@@ -218,21 +248,17 @@ test("updateTask rename syncs task board name", () => {
218
248
  updateTask(b.id, { name: "B2" });
219
249
  });
220
250
 
221
- test("setSessionTitle:写 title;老会话无行则 INSERT 首改,不影响归属/索引列", () => {
251
+ test("setSessionTitle:已有行只改名(不覆盖归属/索引列)", async () => {
222
252
  freshDb();
223
253
  // 场景1:已有归属行(任务会话),改名写 title 保留 task_id
224
254
  seedSession("s-task", "task-1");
225
- setSessionTitle("s-task", "新名字");
255
+ await setSessionTitle("s-task", "新名字");
226
256
  let row = getDb().prepare("SELECT title, task_id FROM session_meta WHERE session_id='s-task'").get();
227
257
  assert.equal(row.title, "新名字");
228
258
  assert.equal(row.task_id, "task-1", "不覆盖已有归属");
229
- // 场景2:无行老会话首改 → INSERT,title 写入、task_id NULL
230
- setSessionTitle("s-orphan", "老会话改名");
231
- row = getDb().prepare("SELECT title, task_id, pinned FROM session_meta WHERE session_id='s-orphan'").get();
232
- assert.equal(row.title, "老会话改名");
233
- assert.equal(row.task_id, null);
234
- assert.equal(row.pinned, 0);
235
- // 场景3:再次改名覆盖
236
- setSessionTitle("s-orphan", "再改");
237
- assert.equal(getDb().prepare("SELECT title FROM session_meta WHERE session_id='s-orphan'").get().title, "再改");
259
+ // 场景2:再次改名覆盖
260
+ await setSessionTitle("s-task", "再改");
261
+ assert.equal(getDb().prepare("SELECT title FROM session_meta WHERE session_id='s-task'").get().title, "再改");
262
+ // 场景3(无行 + 磁盘无文件 → 不写缺列局部行)在 task-store.partial-row.test.mjs 覆盖。
263
+ assert.equal(getDb().prepare("SELECT COUNT(*) c FROM session_meta").get().c, 1);
238
264
  });
package/lib/task-store.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import { randomUUID } from "crypto";
2
2
  import { getDb } from "./sqlite-db";
3
3
  import { deleteBoardCascade, renameTaskBoard } from "./board-store";
4
+ import { projectIdentityKey } from "./project-identity";
5
+ import { scanOneSessionHead } from "./session-scanner";
6
+ import { resolveProject } from "./worktree";
4
7
 
5
8
  export interface Task {
6
9
  id: string;
@@ -60,6 +63,60 @@ export function listAllTaskSessionIds(): Set<string> {
60
63
  return new Set(rows.map((r) => r.session_id));
61
64
  }
62
65
 
66
+ /** 一次全表读出的父子链(parentOf 上溯、childrenOf 下探)。
67
+ * updateTask 的多个根共用同一份快照——否则每个入参 id 都调一次 listDescendantIds,
68
+ * 每次一条全表 SELECT(O(成员数 × 全表))。 */
69
+ function readParentLinks(): { parentOf: Map<string, string | null>; childrenOf: Map<string, string[]> } {
70
+ const parentOf = new Map<string, string | null>();
71
+ const childrenOf = new Map<string, string[]>();
72
+ for (const r of getDb().prepare("SELECT session_id, parent_id FROM session_meta").all() as Array<{ session_id: string; parent_id: string | null }>) {
73
+ parentOf.set(r.session_id, r.parent_id);
74
+ if (r.parent_id !== null) {
75
+ const arr = childrenOf.get(r.parent_id) ?? [];
76
+ arr.push(r.session_id);
77
+ childrenOf.set(r.parent_id, arr);
78
+ }
79
+ }
80
+ return { parentOf, childrenOf };
81
+ }
82
+
83
+ /** 按 childrenOf 递归收集后代 id(不含自身)。广度优先、visited 去重防环。 */
84
+ function collectDescendants(childrenOf: Map<string, string[]>, sessionId: string): string[] {
85
+ const out: string[] = [];
86
+ const seen = new Set<string>([sessionId]);
87
+ const queue = [...(childrenOf.get(sessionId) ?? [])];
88
+ while (queue.length) {
89
+ const cur = queue.shift()!;
90
+ if (seen.has(cur)) continue;
91
+ seen.add(cur);
92
+ out.push(cur);
93
+ queue.push(...(childrenOf.get(cur) ?? []));
94
+ }
95
+ return out;
96
+ }
97
+
98
+ /** 按 parent_id 递归收集后代 id(不含自身)。 */
99
+ export function listDescendantIds(sessionId: string): string[] {
100
+ return collectDescendants(readParentLinks().childrenOf, sessionId);
101
+ }
102
+
103
+ /** 成员集合归一化:若某成员在库里还有祖先、而该祖先不在集合里,则它也不能留
104
+ * (归属按整棵子树存,父子不跨任务)。循环到不动点,兼容多层子树。
105
+ * 祖先行已不存在(外部删除留下的悬空 parent_id)不算「祖先在库内」——此时它
106
+ * 就是本任务的根,剪掉会把合法成员误踢出任务。就地修改传入的集合。 */
107
+ function normalizeMemberSet(members: Set<string>, parentOf: Map<string, string | null>): void {
108
+ let pruned = true;
109
+ while (pruned) {
110
+ pruned = false;
111
+ for (const sessionId of members) {
112
+ const parent = parentOf.get(sessionId);
113
+ if (!parent || members.has(parent) || !parentOf.has(parent)) continue;
114
+ members.delete(sessionId);
115
+ pruned = true;
116
+ }
117
+ }
118
+ }
119
+
63
120
  function getTaskRow(id: string): TaskRow | undefined {
64
121
  return getDb()
65
122
  .prepare(
@@ -120,10 +177,20 @@ export function createTask(projectKey: string, name: string): Task {
120
177
  }
121
178
 
122
179
  /**
123
- * Rename and/or replace task membership. `sessionIds` is a full-replace list:
124
- * the diff only touches this task's members — added ids get task_id set (or
125
- * moved from another task), removed ids fall back to temp (NULL), members of
126
- * other tasks are never modified. Returns null when the task does not exist.
180
+ * Rename and/or replace task membership. `sessionIds` is a full-replace list of
181
+ * subtree roots: each id and all of its descendants (session_meta.parent_id) are
182
+ * added; members whose ancestor falls outside that closure fall back to temp
183
+ * (NULL). Membership is stored per subtree, so a PATCH whose roots close over
184
+ * another task's nodes moves them here (子树压过单节点归属):两个任务的闭包重叠时,
185
+ * 以最后一个 PATCH 的闭包为准,而不是「永不碰其它任务」。
186
+ *
187
+ * 不变量:子会话不能单独移出任务——祖先仍是本任务成员时,子会话就保持在任务内
188
+ * (提交 [根] 时闭包会连带子孙;宁可不对「单独移出子会话」作出响应,也不让聊天区
189
+ * 出现父不在、子单飞的孤儿根)。
190
+ *
191
+ * 「移出任务」由这里的成员规范化承担:提交的剩余成员列表里,祖先已不在集合的成员
192
+ * 一律被剪掉(整棵子树一起走),不需要单独的 unassign 子树入口。
193
+ * Returns null when the task does not exist.
127
194
  */
128
195
  export function updateTask(
129
196
  id: string,
@@ -157,22 +224,26 @@ export function updateTask(
157
224
  );
158
225
  }
159
226
  if (patch.sessionIds !== undefined) {
160
- const current = new Set(listTaskSessionIds(id));
161
- const next = new Set(patch.sessionIds);
162
- const ts = now();
163
- const assign = db.prepare(
164
- "INSERT INTO session_meta (session_id, task_id, updated) VALUES (?, ?, ?) " +
165
- "ON CONFLICT(session_id) DO UPDATE SET task_id = excluded.task_id, updated = excluded.updated",
166
- );
167
- const unassign = db.prepare(
168
- "UPDATE session_meta SET task_id = NULL, updated = ? WHERE session_id = ?",
169
- );
227
+ // 入参是「根 id 列表」的闭包视图:加/减都以整棵子树为单位,避免子会话留在任务里当孤儿根。
228
+ // 父子链只读一次(多个根的展开 + 归一化共用同一份快照)。
229
+ const { parentOf, childrenOf } = readParentLinks();
230
+ const next = new Set<string>();
170
231
  for (const sessionId of patch.sessionIds) {
171
- if (!current.has(sessionId)) assign.run(sessionId, id, ts);
172
- }
173
- for (const sessionId of current) {
174
- if (!next.has(sessionId)) unassign.run(ts, sessionId);
232
+ next.add(sessionId);
233
+ for (const d of collectDescendants(childrenOf, sessionId)) next.add(d);
175
234
  }
235
+ // 归一化:入参里「祖先已被剔除」的成员不能留。前端拖出父会话时提交的是
236
+ // flat 剩余成员列表(仍含子会话 id),不归一化就会父走子留、子会话在任务区当根。
237
+ normalizeMemberSet(next, parentOf);
238
+ const current = new Set(listTaskSessionIds(id));
239
+ const ts = now();
240
+ // 加侧只 UPDATE:行由创建链路(ensureSessionMetaRow)/扫描器/路由层
241
+ // (ensureSessionRows)建全列行。此处不能 INSERT——同步事务里 await 不了文件 I/O,
242
+ // 而插缺列局部行(缺 path/project_key/created)会让会话从聊天区消失、任务区显示 1970。
243
+ const assign = db.prepare("UPDATE session_meta SET task_id = ?, updated = ? WHERE session_id = ?");
244
+ const unassign = db.prepare("UPDATE session_meta SET task_id = NULL, updated = ? WHERE session_id = ?");
245
+ for (const sessionId of next) if (!current.has(sessionId)) assign.run(id, ts, sessionId);
246
+ for (const sessionId of current) if (!next.has(sessionId)) unassign.run(ts, sessionId);
176
247
  db.prepare("UPDATE tasks SET updated = ? WHERE id = ?").run(ts, id);
177
248
  }
178
249
  db.exec("COMMIT");
@@ -256,15 +327,13 @@ export function taskNameForSession(sessionId: string): string | null {
256
327
  return row?.name ?? null;
257
328
  }
258
329
 
259
- /**
260
- * 把一个会话原子归属到任务(任务不存在返回 false)。会刷新任务的 updated
261
- * 使置顶/最近排序生效;会话原本在其他任务下则移动(ON CONFLICT 更新)。
262
- * 会话创建完成时由服务端调用,避免前端两跳 PATCH 造成的“先临时区后任务”窗口。
263
- */
264
330
  /** 新建会话落盘即建全列索引行(先于会话真正运行)。
265
331
  * 调用方拿到的 path/cwd/project_key/parent 等此时全部已知——会话从出生就是
266
332
  * 完整索引行,读取(列表/任务区/看板)不再依赖扫描器补列,也不退化扫盘。
267
- * ON CONFLICT 只补索引列、不碰 task_id/pinned/title(归属/置顶/改名各自管)。 */
333
+ * ON CONFLICT 只补索引列、不碰 task_id/pinned/title(归属/置顶/改名各自管);
334
+ * `first_message` 只补空(COALESCE)——调用方不传就保留库里已有的首条消息。
335
+ * `taskId` 只在 INSERT 时写入(归属继承用):已存在的行归属各自管,upsert 不覆盖。
336
+ * INSERT 列清单显式列全 13 列(`last_reply` 显式 NULL)——不依赖建表默认值。 */
268
337
  export function ensureSessionMetaRow(sessionId: string, row: {
269
338
  path: string;
270
339
  cwd: string;
@@ -272,46 +341,154 @@ export function ensureSessionMetaRow(sessionId: string, row: {
272
341
  parentId?: string;
273
342
  firstMessage?: string;
274
343
  title?: string;
344
+ taskId?: string | null;
275
345
  }): void {
276
- const db = getDb();
277
- db.prepare(
278
- `INSERT INTO session_meta
279
- (session_id, task_id, updated, pinned, path, cwd, project_key, title, first_message, parent_id, created, modified)
280
- VALUES (?, NULL, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?)
281
- ON CONFLICT(session_id) DO UPDATE SET
282
- path = excluded.path,
283
- cwd = excluded.cwd,
284
- project_key = excluded.project_key,
285
- first_message = excluded.first_message,
286
- parent_id = excluded.parent_id,
287
- created = excluded.created,
288
- modified = excluded.modified`,
289
- ).run(
290
- sessionId,
291
- now(),
292
- row.path,
293
- row.cwd,
294
- row.projectKey,
295
- row.title ?? null,
296
- row.firstMessage ?? null,
297
- row.parentId ?? null,
298
- now(),
299
- now(),
300
- );
346
+ getDb()
347
+ .prepare(
348
+ `INSERT INTO session_meta
349
+ (session_id, task_id, updated, pinned, path, cwd, project_key, title, first_message, parent_id, created, modified, last_reply)
350
+ VALUES (?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, NULL)
351
+ ON CONFLICT(session_id) DO UPDATE SET
352
+ path = excluded.path,
353
+ cwd = excluded.cwd,
354
+ project_key = excluded.project_key,
355
+ -- first_message 只补空:调用方不传(undefined)时不再把已有的首条消息擦成 NULL。
356
+ first_message = COALESCE(session_meta.first_message, excluded.first_message),
357
+ parent_id = excluded.parent_id,
358
+ created = excluded.created,
359
+ modified = excluded.modified`,
360
+ )
361
+ .run(
362
+ sessionId,
363
+ row.taskId ?? null,
364
+ now(),
365
+ row.path,
366
+ row.cwd,
367
+ row.projectKey,
368
+ row.title ?? null,
369
+ row.firstMessage ?? null,
370
+ row.parentId ?? null,
371
+ now(),
372
+ now(),
373
+ );
301
374
  invalidateSessionListCache();
302
375
  }
303
376
 
304
- export function assignSessionToTask(sessionId: string, taskId: string): boolean {
377
+ /**
378
+ * 给「会话已经在磁盘上、库里却还没行」的场景补一条**全列**行:定位文件 → 读 header → upsert。
379
+ * 先查行(有行直接返回,完全不碰文件);无行才按 id 解析文件。
380
+ *
381
+ * 为什么不能退回插「局部行」:只写 session_id/updated/title(pinned) 的行缺 path/project_key/created,
382
+ * 会被聊天区的 `project_key = ?` 过滤掉(会话消失到下一轮扫描)、任务区显示 1970-01-01(V4)。
383
+ * 同时补 `parent_id`(磁盘 header.parentSession 反查):归属守卫(hasForeignTaskAncestor)
384
+ * 只看库内 parent_id——补行时不写父,「库里还没行」的会话在守卫眼里就是无祖先的顶层行
385
+ * (带上跨任务祖先拖入别的任务会被错误放行)。反查不到父(父行未建/父文件已消失)则留空。
386
+ * 文件也找不到时返回 false 且报错可见——调用方保持原语义(不抛错),但绝不写半行。
387
+ */
388
+ async function ensureRowForSession(sessionId: string): Promise<boolean> {
389
+ if (getDb().prepare("SELECT 1 FROM session_meta WHERE session_id = ?").get(sessionId)) return true;
390
+ // 动态导入避开 session-reader ↔ task-store 的循环加载(session-reader 反向动态引本模块)。
391
+ const { resolveSessionPath, resolveSessionIdByPath } = await import("./session-reader");
392
+ const filePath = await resolveSessionPath(sessionId);
393
+ if (!filePath) {
394
+ console.error(`[pi-web] 会话行补建失败:找不到会话文件 session=${sessionId}`);
395
+ return false;
396
+ }
397
+ const head = scanOneSessionHead(filePath);
398
+ if (!head) {
399
+ console.error(`[pi-web] 会话行补建失败:会话文件无有效 header session=${sessionId} path=${filePath}`);
400
+ return false;
401
+ }
402
+ const project = await resolveProject(head.cwd);
403
+ const parentId = head.parentSessionPath
404
+ ? ((await resolveSessionIdByPath(head.parentSessionPath)) ?? undefined)
405
+ : undefined;
406
+ ensureSessionMetaRow(sessionId, {
407
+ path: filePath,
408
+ cwd: head.cwd,
409
+ projectKey: projectIdentityKey(project?.projectRoot ?? head.cwd),
410
+ parentId,
411
+ firstMessage: head.firstMessage || undefined,
412
+ });
413
+ return true;
414
+ }
415
+
416
+ /** 批量为「库内无行」的会话补全列行(PATCH 全量替换前由路由层调用)。
417
+ * 为什么在事务外:建行要读会话文件(`resolveSessionPath` miss 时会目录扫描),
418
+ * 在 `updateTask` 的事务里 await 文件 I/O 会长时间持写锁。
419
+ * 单个 id 失败不抛错也不阻塞其余 id(失败已由 ensureRowForSession 报错可见)。 */
420
+ export async function ensureSessionRows(sessionIds: string[]): Promise<void> {
421
+ for (const sessionId of sessionIds) {
422
+ try {
423
+ await ensureRowForSession(sessionId);
424
+ } catch (error) {
425
+ console.error(
426
+ `[pi-web] 会话行补建异常 session=${sessionId}:`,
427
+ error instanceof Error ? error.message : String(error),
428
+ );
429
+ }
430
+ }
431
+ }
432
+
433
+ /** 归属写入前的祖先守卫:该会话沿 session_meta.parent_id 上溯的任一祖先,是否带着
434
+ * **非空且不属于 taskId** 的归属。
435
+ *
436
+ * 为什么归属入口必须查:「归属按整棵子树存」是写库侧的不变量,但把「父在任务 A 的
437
+ * 会话」归到任务 B 会造出跨任务父子——而 updateTask 的成员规范化与扫描器的归属收敛
438
+ * 会反过来把它剪掉/拉回,两个方向互相纠正、永不收敛(看板画布落点、聊天区拖拽都是
439
+ * 可达入口)。上溯时用 visited 防脏环;祖先行不在库里(悬空 parent_id)不算冲突,
440
+ * 它就是本任务的根。
441
+ * 导出供路由在 assign* 拒绝后给出准确的状态码/原因(409 vs 404)。 */
442
+ export function hasForeignTaskAncestor(sessionId: string, taskId: string): boolean {
443
+ // 一次全表读同时拿 parent 链与归属(本函数在每次归属写入前跑,别读两遍库)。
444
+ const parentOf = new Map<string, string | null>();
445
+ const taskOf = new Map<string, string | null>();
446
+ for (const r of getDb().prepare("SELECT session_id, parent_id, task_id FROM session_meta").all() as Array<{ session_id: string; parent_id: string | null; task_id: string | null }>) {
447
+ parentOf.set(r.session_id, r.parent_id);
448
+ taskOf.set(r.session_id, r.task_id);
449
+ }
450
+ const seen = new Set<string>([sessionId]);
451
+ let current = parentOf.get(sessionId) ?? null;
452
+ while (current && !seen.has(current)) {
453
+ seen.add(current);
454
+ const ancestorTask = taskOf.get(current) ?? null;
455
+ if (ancestorTask !== null && ancestorTask !== taskId) return true;
456
+ current = parentOf.get(current) ?? null;
457
+ }
458
+ return false;
459
+ }
460
+
461
+ /**
462
+ * 把一个会话原子归属到任务(任务不存在返回 false)。会刷新任务的 updated
463
+ * 使置顶/最近排序生效;会话原本在其他任务下则移动。
464
+ * 会话创建完成时由服务端调用,避免前端两跳 PATCH 造成的“先临时区后任务”窗口。
465
+ *
466
+ * 无行会话先在**同一调用内**补全列行再归属(见 ensureRowForSession);补不出行返回 false,
467
+ * 保证「返回 true」永远等价于「归属已落库」。
468
+ * 祖先已属于其它任务时拒绝(返回 false + console.error,不写任何行):见 hasForeignTaskAncestor。
469
+ * 补行**先于**守卫:库里原本没行的会话,其父链只能靠补行(读磁盘 header)写进 parent_id,
470
+ * 顺序反了守卫就是盲的(无行会话会被当成无祖先的顶层行放行)。
471
+ */
472
+ export async function assignSessionToTask(sessionId: string, taskId: string): Promise<boolean> {
305
473
  const db = getDb();
306
474
  const task = getTaskRow(taskId);
307
475
  if (!task) return false;
476
+ // 归属不能靠「先插行为空、等扫描补列」:那正是 V4 的缺列局部行,也是 V1 的病灶。
477
+ if (!(await ensureRowForSession(sessionId))) return false;
478
+ if (hasForeignTaskAncestor(sessionId, taskId)) {
479
+ console.error(
480
+ `[pi-web] 归属被拒绝:祖先属于其它任务 session=${sessionId} task=${taskId}`,
481
+ );
482
+ return false;
483
+ }
308
484
  const ts = now();
309
485
  db.exec("BEGIN");
310
486
  try {
311
- db.prepare(
312
- "INSERT INTO session_meta (session_id, task_id, updated) VALUES (?, ?, ?) " +
313
- "ON CONFLICT(session_id) DO UPDATE SET task_id = excluded.task_id, updated = excluded.updated",
314
- ).run(sessionId, taskId, ts);
487
+ db.prepare("UPDATE session_meta SET task_id = ?, updated = ? WHERE session_id = ?").run(
488
+ taskId,
489
+ ts,
490
+ sessionId,
491
+ );
315
492
  db.prepare("UPDATE tasks SET updated = ? WHERE id = ?").run(ts, taskId);
316
493
  db.exec("COMMIT");
317
494
  } catch (error) {
@@ -322,24 +499,79 @@ export function assignSessionToTask(sessionId: string, taskId: string): boolean
322
499
  return true;
323
500
  }
324
501
 
325
- /** Pin / unpin a session (region-relative: inside its task group, or in chat). */
326
- export function setSessionPinned(sessionId: string, pinned: boolean): void {
502
+ /** 归属一条会话及其整棵子树(写库先于任何 UI 刷新;一次事务)。
503
+ * 子孙一律跟随父:否则「父在任务、子 task_id 为空」会让子会话同时出现在
504
+ * 任务区(按树渲染)与聊天区(按 task_id 过滤),即 V1 的两区重复。
505
+ * 根会话无行时同样先补全列行(UPDATE 对无行会话影响 0 行 = 静默写 0 行)。
506
+ * 根的祖先已属于其它任务时拒绝(返回 false + console.error,不写任何行):否则会造出
507
+ * 跨任务父子,被规范化/收敛反向拉回。补行先于守卫(与 assignSessionToTask 同规则):
508
+ * 无行会话的父链只能靠补行写进 parent_id,守卫才有得看。 */
509
+ export async function assignSessionSubtreeToTask(sessionId: string, taskId: string): Promise<boolean> {
510
+ if (!getTaskRow(taskId)) return false;
511
+ if (!(await ensureRowForSession(sessionId))) return false;
512
+ if (hasForeignTaskAncestor(sessionId, taskId)) {
513
+ console.error(
514
+ `[pi-web] 子树归属被拒绝:祖先属于其它任务 session=${sessionId} task=${taskId}`,
515
+ );
516
+ return false;
517
+ }
518
+ const ts = now();
327
519
  const db = getDb();
328
- db.prepare(
329
- "INSERT INTO session_meta (session_id, task_id, updated, pinned) VALUES (?, NULL, ?, ?) " +
330
- "ON CONFLICT(session_id) DO UPDATE SET pinned = excluded.pinned",
331
- ).run(sessionId, now(), pinned ? 1 : 0);
520
+ db.exec("BEGIN");
521
+ try {
522
+ const stmt = db.prepare("UPDATE session_meta SET task_id = ?, updated = ? WHERE session_id = ?");
523
+ for (const id of [sessionId, ...listDescendantIds(sessionId)]) stmt.run(taskId, ts, id);
524
+ db.prepare("UPDATE tasks SET updated = ? WHERE id = ?").run(ts, taskId);
525
+ db.exec("COMMIT");
526
+ } catch (error) {
527
+ db.exec("ROLLBACK");
528
+ throw error;
529
+ }
332
530
  invalidateSessionListCache();
531
+ return true;
333
532
  }
334
533
 
335
- /** 改名同步写 session_meta.title(列表索引的标题源)。无行则 INSERT(老会话首改,
336
- * 索引列 path/project_key 等由扫描器下一轮补全)。 */
337
- export function setSessionTitle(sessionId: string, title: string): void {
338
- const db = getDb();
339
- db.prepare(
340
- "INSERT INTO session_meta (session_id, task_id, updated, pinned, title) VALUES (?, NULL, ?, 0, ?) " +
341
- "ON CONFLICT(session_id) DO UPDATE SET title = excluded.title, updated = excluded.updated",
342
- ).run(sessionId, now(), title);
534
+ /** 库内直接子行在磁盘上的文件路径(只用非空 path)。
535
+ * 给「删父会话时同步改写子行磁盘 header」用:子会话文件可能在子目录里
536
+ * (`<proj>/<session>/forks/*.jsonl` 等),只平铺一层 `readdirSync(dirname(父文件))`
537
+ * 会漏掉它们,漏斗就会「库已写祖父、磁盘仍指已删父」——下一轮扫描按磁盘把它改回 NULL。
538
+ * 库里有父链与 path,路径就是事实(不新写目录递归)。 */
539
+ export function listChildPaths(sessionId: string): string[] {
540
+ const rows = getDb()
541
+ .prepare("SELECT path FROM session_meta WHERE parent_id = ? AND path IS NOT NULL AND path != ''")
542
+ .all(sessionId) as Array<{ path: string }>;
543
+ return rows.map((row) => row.path);
544
+ }
545
+
546
+ /** 会话被删除时,把库内直接子行的 parent_id 改写为祖父(拿不到写入 NULL)。
547
+ * 为什么必须同请求写库:T5 之后「根」判定全来自 session_meta.parent_id——只管磁盘
548
+ * header 的话,库里子行仍指向已删 id,子会话会以根行呈现(≤30s 的窗口期)。 */
549
+ export function reparentSessionChildren(sessionId: string, newParentId: string | null): void {
550
+ getDb()
551
+ .prepare("UPDATE session_meta SET parent_id = ? WHERE parent_id = ?")
552
+ .run(newParentId, sessionId);
553
+ invalidateSessionListCache();
554
+ }
555
+
556
+ /** Pin / unpin a session (region-relative: inside its task group, or in chat).
557
+ * 无行老会话先补全列行(缺列行会被聊天区 project_key 过滤掉),补不出行则不写。 */
558
+ export async function setSessionPinned(sessionId: string, pinned: boolean): Promise<void> {
559
+ if (!(await ensureRowForSession(sessionId))) return;
560
+ // 只写 pinned,不刷 updated——与原先 ON CONFLICT SET pinned 的语义一致
561
+ // (updated 是扫描器「年轻行保护」的依据,置顶不该给幽灵行续命)。
562
+ getDb()
563
+ .prepare("UPDATE session_meta SET pinned = ? WHERE session_id = ?")
564
+ .run(pinned ? 1 : 0, sessionId);
565
+ invalidateSessionListCache();
566
+ }
567
+
568
+ /** 改名同步写 session_meta.title(列表索引的标题源)。无行老会话先补全列行
569
+ * (path/project_key/created 齐全),补不出行则只报错不写半行。 */
570
+ export async function setSessionTitle(sessionId: string, title: string): Promise<void> {
571
+ if (!(await ensureRowForSession(sessionId))) return;
572
+ getDb()
573
+ .prepare("UPDATE session_meta SET title = ?, updated = ? WHERE session_id = ?")
574
+ .run(title, now(), sessionId);
343
575
  invalidateSessionListCache();
344
576
  }
345
577
 
@@ -347,4 +579,48 @@ export function setSessionTitle(sessionId: string, title: string): void {
347
579
  export function unassignSession(sessionId: string): void {
348
580
  getDb().prepare("DELETE FROM session_meta WHERE session_id = ?").run(sessionId);
349
581
  invalidateSessionListCache();
582
+ }
583
+
584
+ // ── 事件链路写入(agent_start / agent_settled)───────────────────────────────
585
+ // 三个函数都**只 UPDATE、不 INSERT**:会话行由创建链路(ensureSessionMetaRow)
586
+ // 或扫描器建立,事件不负责补行——否则会留下缺列局部行(path/project_key 为空的
587
+ // 1970 行),读取期就得再回退扫盘。库里没行时静默 no-op(UPDATE 影响 0 行)。
588
+
589
+ /** last_reply 入库上限(与 types.ts 的「截断」语义一致,防大消息撑爆行)。 */
590
+ export const LAST_REPLY_MAX = 4000;
591
+
592
+ /** 会话活跃时间前移(agent_start:运行中会话立刻浮顶)。 */
593
+ export function touchSessionActivity(sessionId: string, at: number = now()): void {
594
+ getDb().prepare("UPDATE session_meta SET modified = ? WHERE session_id = ?").run(at, sessionId);
595
+ invalidateSessionListCache();
596
+ }
597
+
598
+ /**
599
+ * 一轮循环真正结束(agent_settled,含用户取消)时落最后一条 assistant 文本 + 活跃时间。
600
+ * 空文本(本轮只有工具调用 / 刚发出就被取消)**不清库**:退化为只刷 modified,
601
+ * 保留库里已有的 last_reply——空字符串不是「一条消息」。
602
+ */
603
+ export function recordSessionOutcome(sessionId: string, outcome: { lastReply: string; at: number }): void {
604
+ const text = outcome.lastReply.trim();
605
+ if (!text) {
606
+ touchSessionActivity(sessionId, outcome.at);
607
+ return;
608
+ }
609
+ const stored = text.length > LAST_REPLY_MAX ? text.slice(0, LAST_REPLY_MAX) : text;
610
+ getDb()
611
+ .prepare("UPDATE session_meta SET last_reply = ?, modified = ? WHERE session_id = ?")
612
+ .run(stored, outcome.at, sessionId);
613
+ invalidateSessionListCache();
614
+ }
615
+
616
+ /** 首条用户消息回填(读取路径不再读文件,改由事件补齐;只补空行)。 */
617
+ export function fillFirstMessageIfEmpty(sessionId: string, firstMessage: string): void {
618
+ const text = firstMessage.trim();
619
+ if (!text) return;
620
+ getDb()
621
+ .prepare(
622
+ "UPDATE session_meta SET first_message = ? WHERE session_id = ? AND (first_message IS NULL OR first_message = '')",
623
+ )
624
+ .run(text, sessionId);
625
+ invalidateSessionListCache();
350
626
  }
@@ -2,6 +2,8 @@ export interface ToolEntry {
2
2
  name: string;
3
3
  description: string;
4
4
  active: boolean;
5
+ parameters?: Record<string, unknown>;
6
+ promptGuidelines?: string[];
5
7
  }
6
8
 
7
9
  export const TOOL_PRESET_VALUES = ["none", "read-only", "default", "full"] as const;
@@ -11,6 +13,8 @@ export const PRESET_NONE: string[] = [];
11
13
  export const PRESET_READ_ONLY: string[] = ["read", "grep", "find", "ls"];
12
14
  export const PRESET_DEFAULT: string[] = ["read", "bash", "edit", "write"];
13
15
  export const PRESET_FULL: string[] = ["bash", "read", "edit", "write", "grep", "find", "ls"];
16
+ /** 子代理默认工具集(subagents.ts 与 AgentsConfig 共用,客户端安全模块)。 */
17
+ export const DEFAULT_TOOLS: string[] = ["read", "bash", "edit", "write", "grep", "find", "ls"];
14
18
 
15
19
  const BUILTIN_TOOL_NAMES = new Set(PRESET_FULL);
16
20
 
package/lib/types.ts CHANGED
@@ -271,6 +271,15 @@ export interface SessionInfoEntry extends SessionEntryBase {
271
271
  name?: string;
272
272
  }
273
273
 
274
+ export type SubagentSessionStatus =
275
+ | "starting"
276
+ | "queued"
277
+ | "running"
278
+ | "completed"
279
+ | "failed"
280
+ | "aborted"
281
+ | "interrupted";
282
+
274
283
  export type SessionEntry =
275
284
  | SessionMessageEntry
276
285
  | ThinkingLevelChangeEntry
@@ -309,6 +318,16 @@ export interface SessionInfo {
309
318
  /** 最后一条 assistant 回复文本(看板卡片中间区展示用,截断),无则为空串 */
310
319
  lastReply?: string;
311
320
  parentSessionId?: string; // set if this session was forked from another
321
+ /** 会话间关系:fork 在 UI 保持顶层;仅 subagent 关系形成可见的父子树。 */
322
+ relation?:
323
+ | { kind: "fork"; originSessionId?: string }
324
+ | {
325
+ kind: "subagent";
326
+ parentSessionId: string;
327
+ profile: string;
328
+ description: string;
329
+ status: SubagentSessionStatus;
330
+ };
312
331
  /** Main repo root shared by all worktrees of this cwd (cwd itself for non-git dirs).
313
332
  * Always set by the server; optional because the client builds transient
314
333
  * SessionInfo objects before the first refresh. Fall back to cwd. */