@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
@@ -0,0 +1,65 @@
1
+ // 孤儿看板卡片清理:左栏删除会话后,看板上对应卡片(refId 指向已删会话的
2
+ // board_nodes 节点)与指向它的连线成为孤儿数据。进入看板时懒加载清理一次。
3
+ //
4
+ // 孤儿判定(防误删"创建中"会话):
5
+ // - refId 非空(未绑定会话的节点不动)
6
+ // - 会话文件不存在(resolveSessionPath === null;listAllSessions 已填充路径缓存)
7
+ // - session_meta 无该会话记录(任务会话创建即写 meta,文件延迟落盘也安全)
8
+ // 运行中会话文件存在,天然不被删。
9
+
10
+ import { getDb } from "./sqlite-db";
11
+ import { listAllSessions, resolveSessionPath } from "./session-reader";
12
+ import { deleteNodesByIds } from "./board-store";
13
+ import { SYSTEM_RUNNING_BOARD_ID } from "./board-types";
14
+
15
+ export interface PurgeResult {
16
+ /** 实际删除的卡片数 */
17
+ deletedNodes: number;
18
+ /** 级联删除的连线数 */
19
+ deletedEdges: number;
20
+ /** 受影响的看板 id 列表 */
21
+ boards: string[];
22
+ }
23
+
24
+ /** 清理全部看板上的孤儿卡片(级联删连线)。无孤儿时返回全 0。 */
25
+ export async function purgeOrphanBoardCards(): Promise<PurgeResult> {
26
+ const db = getDb();
27
+ // 1a. 会话节点(kind != 'taskcard'):孤儿判定基于会话文件 / session_meta 存在性
28
+ // (防误删"创建中"会话:任务会话创建即写 meta,文件延迟落盘也安全)
29
+ const sessionNodes = db
30
+ .prepare("SELECT id, board_id AS boardId, ref_id AS refId FROM board_nodes WHERE ref_id IS NOT NULL AND board_id != ? AND kind != 'taskcard'")
31
+ .all(SYSTEM_RUNNING_BOARD_ID) as Array<{ id: string; boardId: string; refId: string }>;
32
+ // 1b. 任务卡节点(kind='taskcard'):孤儿判定 = refId 不在 task_cards 表
33
+ // (任务卡已删但画布节点残留,如 deleteTask 未级联清;refId 是 task_cards.id 非会话 id)
34
+ const taskCardNodes = db
35
+ .prepare(
36
+ "SELECT id, board_id AS boardId, ref_id AS refId FROM board_nodes n " +
37
+ "WHERE kind = 'taskcard' AND ref_id IS NOT NULL AND board_id != ? " +
38
+ "AND NOT EXISTS (SELECT 1 FROM task_cards c WHERE c.id = n.ref_id)",
39
+ )
40
+ .all(SYSTEM_RUNNING_BOARD_ID) as Array<{ id: string; boardId: string; refId: string }>;
41
+
42
+ // 2. 会话孤儿逐个判定(resolveSessionPath 走路径缓存);taskcard 孤儿直接入列
43
+ const orphanIds: string[] = taskCardNodes.map((n) => n.id);
44
+ if (sessionNodes.length > 0) {
45
+ const sessions = await listAllSessions();
46
+ const aliveIds = new Set(sessions.map((s) => s.id));
47
+ const metaIds = new Set(
48
+ (db.prepare("SELECT session_id FROM session_meta").all() as Array<{ session_id: string }>).map((r) => r.session_id),
49
+ );
50
+ for (const n of sessionNodes) {
51
+ if (aliveIds.has(n.refId) || metaIds.has(n.refId)) continue;
52
+ const p = await resolveSessionPath(n.refId);
53
+ if (p === null) orphanIds.push(n.id);
54
+ }
55
+ }
56
+ if (orphanIds.length === 0) return { deletedNodes: 0, deletedEdges: 0, boards: [] };
57
+
58
+ // 3. 批量删除(级联删边 + bump 看板 updated,单事务)
59
+ const result = deleteNodesByIds(orphanIds);
60
+ return {
61
+ deletedNodes: result.deletedNodes,
62
+ deletedEdges: result.deletedEdges,
63
+ boards: result.boards,
64
+ };
65
+ }
@@ -0,0 +1,110 @@
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, { interopDefault: true, moduleCache: false });
6
+ const { collectSearchable, filterMatches, makeSnippet } = await jiti.import("./board-search.ts");
7
+
8
+ /** 构造 fake tldraw editor(单测只依赖 getCurrentPageShapes/getShape/getShapePageBounds) */
9
+ function fakeEditor(shapes) {
10
+ return {
11
+ getCurrentPageShapes: () => shapes,
12
+ getShape: (id) => shapes.find((s) => s.id === id),
13
+ getShapePageBounds: (id) => {
14
+ const s = shapes.find((x) => x.id === id);
15
+ return s ? { x: s.x ?? 0, y: s.y ?? 0, w: s.w ?? 100, h: s.h ?? 100 } : undefined;
16
+ },
17
+ };
18
+ }
19
+
20
+ const shape = (id, type, props) => ({ id, type, props });
21
+
22
+ test("collectSearchable 收集会话卡标题 + 便笺正文,跳过空文本与无关 shape", () => {
23
+ const editor = fakeEditor([
24
+ shape("shape:a", "session-card", { title: "登录重构", sessionId: "s1" }),
25
+ shape("shape:b", "session-card", { title: " ", sessionId: "s2" }), // 空标题跳过
26
+ shape("shape:c", "sticky-note", { text: "便笺:token 校验" }),
27
+ shape("shape:d", "sticky-note", { text: "" }), // 空便笺跳过
28
+ shape("shape:e", "arrow", { label: "不能搜连线" }), // 无关 type 跳过
29
+ shape("shape:f", "text", { text: "普通文本不搜" }), // 无关 type 跳过
30
+ ]);
31
+ const items = collectSearchable(editor);
32
+ assert.deepEqual(items.map((i) => i.text), ["登录重构", "便笺:token 校验"]);
33
+ assert.deepEqual(items.map((i) => i.kind), ["session-card", "sticky-note"]);
34
+ });
35
+
36
+ test("filterMatches 中文子串命中", () => {
37
+ const editor = fakeEditor([
38
+ shape("a", "session-card", { title: "修复登录接口的 bug" }),
39
+ shape("b", "sticky-note", { text: "记录 token 过期逻辑" }),
40
+ ]);
41
+ const items = collectSearchable(editor);
42
+ const hits = filterMatches(items, "登录");
43
+ assert.equal(hits.length, 1);
44
+ assert.equal(hits[0].item.shapeId, "a");
45
+ });
46
+
47
+ test("filterMatches 大小写不敏感(英文)", () => {
48
+ const editor = fakeEditor([
49
+ shape("a", "session-card", { title: "Refactor Token Logic" }),
50
+ shape("b", "sticky-note", { text: "another note" }),
51
+ ]);
52
+ const items = collectSearchable(editor);
53
+ assert.equal(filterMatches(items, "token").length, 1);
54
+ assert.equal(filterMatches(items, "TOKEN").length, 1);
55
+ assert.equal(filterMatches(items, "Token").length, 1);
56
+ assert.equal(filterMatches(items, "tokenz").length, 0);
57
+ });
58
+
59
+ test("filterMatches 空 query 返回空", () => {
60
+ const editor = fakeEditor([shape("a", "session-card", { title: "anything" })]);
61
+ const items = collectSearchable(editor);
62
+ assert.equal(filterMatches(items, "").length, 0);
63
+ assert.equal(filterMatches(items, " ").length, 0);
64
+ });
65
+
66
+ test("filterMatches 多命中保持画布遍历顺序", () => {
67
+ const editor = fakeEditor([
68
+ shape("a", "sticky-note", { text: "zzz 搜索 zzz" }),
69
+ shape("b", "session-card", { title: "aaa 搜索 bbb" }),
70
+ shape("c", "sticky-note", { text: "搜索收尾" }),
71
+ ]);
72
+ const items = collectSearchable(editor);
73
+ const hits = filterMatches(items, "搜索");
74
+ assert.deepEqual(hits.map((h) => h.item.shapeId), ["a", "b", "c"]);
75
+ });
76
+
77
+ test("filterMatches 多关键字空格分隔 AND 关系", () => {
78
+ const editor = fakeEditor([
79
+ shape("a", "session-card", { title: "修复登录接口 token 过期" }),
80
+ shape("b", "sticky-note", { text: "登录流程重构" }),
81
+ shape("c", "session-card", { title: "部署配置调整" }),
82
+ shape("d", "session-card", { title: "登录接口优化" }),
83
+ ]);
84
+ const items = collectSearchable(editor);
85
+ // 两个词都在同一节点文本里才命中(AND)
86
+ assert.deepEqual(filterMatches(items, "登录 token").map((h) => h.item.shapeId), ["a"]);
87
+ assert.deepEqual(filterMatches(items, "登录 重构").map((h) => h.item.shapeId), ["b"]);
88
+ assert.deepEqual(filterMatches(items, "登录 接口").map((h) => h.item.shapeId), ["a", "d"]);
89
+ // 缺任一词不命中
90
+ assert.equal(filterMatches(items, "登录 不存在词").length, 0);
91
+ // 多个空格/首尾空格容忍
92
+ assert.deepEqual(filterMatches(items, " 登录 接口 ").map((h) => h.item.shapeId), ["a", "d"]);
93
+ });
94
+
95
+ test("makeSnippet 命中居中截断 + 两端省略号", () => {
96
+ const long = "这是一段很长的便笺文本,中间藏着关键词,后面还有更多内容需要被截断显示以便下拉列表保持整洁。";
97
+ const s = makeSnippet(long, "关键词", 20);
98
+ assert.ok(s.includes("关键词"), `snippet should contain the hit: ${s}`);
99
+ assert.ok(s.startsWith("…"), "head ellipsis");
100
+ assert.ok(s.endsWith("…"), "tail ellipsis");
101
+ assert.ok(s.length <= 24, `bounded length: ${s.length}`);
102
+ });
103
+
104
+ test("makeSnippet 无命中时首段截断", () => {
105
+ const short = "hello";
106
+ assert.equal(makeSnippet(short, "zzz"), "hello");
107
+ const long = "x".repeat(100);
108
+ const s = makeSnippet(long, "zzz");
109
+ assert.equal(s, `${"x".repeat(44)}…`);
110
+ });
@@ -0,0 +1,63 @@
1
+ /**
2
+ * 看板内搜索定位(Ctrl+F)的纯函数。
3
+ *
4
+ * 只遍历当前画布上的 shape(session-card 标题 + sticky-note 便笺文字),
5
+ * 不做会话正文索引(正文搜索属另一需求,见 .agent/spec/2026-08-30-board-search.md 不做项)。
6
+ * 画布节点量级小(几十个),includes 子串匹配足够,无需索引。
7
+ */
8
+
9
+ import type { Editor, TLShapeId } from "tldraw";
10
+
11
+ export type SearchableKind = "session-card" | "sticky-note";
12
+
13
+ export interface SearchableItem {
14
+ shapeId: TLShapeId;
15
+ kind: SearchableKind;
16
+ /** 完整原文(匹配用全文) */
17
+ text: string;
18
+ }
19
+
20
+ export interface SearchMatch {
21
+ item: SearchableItem;
22
+ /** 命中片段(下拉展示用,前后截断) */
23
+ snippet: string;
24
+ }
25
+
26
+ /** 收集当前画布上可搜索的节点。空文本节点跳过(无搜索价值)。 */
27
+ export function collectSearchable(editor: Editor): SearchableItem[] {
28
+ const out: SearchableItem[] = [];
29
+ for (const shape of editor.getCurrentPageShapes()) {
30
+ if (shape.type === "session-card") {
31
+ const title = String((shape.props as { title?: unknown }).title ?? "").trim();
32
+ if (title) out.push({ shapeId: shape.id, kind: "session-card", text: title });
33
+ } else if (shape.type === "sticky-note") {
34
+ const text = String((shape.props as { text?: unknown }).text ?? "").trim();
35
+ if (text) out.push({ shapeId: shape.id, kind: "sticky-note", text });
36
+ }
37
+ }
38
+ return out;
39
+ }
40
+
41
+ /** 按 query 过滤:空格分隔多个关键字,全部命中(AND)才算匹配。空 query 返回空结果。 */
42
+ export function filterMatches(items: SearchableItem[], query: string): SearchMatch[] {
43
+ const terms = query.trim().toLowerCase().split(/\s+/).filter(Boolean);
44
+ if (terms.length === 0) return [];
45
+ return items
46
+ .filter((it) => {
47
+ const text = it.text.toLowerCase();
48
+ return terms.every((term) => text.includes(term));
49
+ })
50
+ .map((it) => ({ item: it, snippet: makeSnippet(it.text, terms[0]) }));
51
+ }
52
+
53
+ /** 命中片段:以首个命中位置为中心截取 ~44 字符,两端加省略号。 */
54
+ export function makeSnippet(text: string, query: string, maxLen = 44): string {
55
+ const q = query.trim().toLowerCase();
56
+ const idx = text.toLowerCase().indexOf(q);
57
+ if (idx < 0) return text.length > maxLen ? `${text.slice(0, maxLen)}…` : text;
58
+ const start = Math.max(0, idx - Math.floor((maxLen - q.length) / 2));
59
+ const end = Math.min(text.length, start + maxLen);
60
+ const head = start > 0 ? "…" : "";
61
+ const tail = end < text.length ? "…" : "";
62
+ return `${head}${text.slice(start, end)}${tail}`;
63
+ }