@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,284 @@
1
+ import { readFileSync } from "fs";
2
+ import { getDb } from "./sqlite-db";
3
+ import type { SessionInfo } from "./types";
4
+
5
+ // ============================================================================
6
+ // Session full-text search over a SQLite FTS5 (trigram) index.
7
+ //
8
+ // Key properties (verified locally):
9
+ // - trigram tokenizer matches CJK substrings, unlike unicode61 which treats a
10
+ // whole CJK run as one token.
11
+ // - queries shorter than 3 chars never match via MATCH -> LIKE fallback on
12
+ // the same FTS table, which is index-accelerated for trigram.
13
+ // - FTS5 MATCH syntax needs special chars escaped -> build a quoted phrase.
14
+ //
15
+ // Text is extracted from the .jsonl directly (lightweight parser, no SDK) so
16
+ // this module stays loadable in plain-node unit tests.
17
+ // ============================================================================
18
+
19
+ const BODY_CAP = 256 * 1024;
20
+ const TOOL_INPUT_CAP = 500;
21
+ const BASH_OUTPUT_CAP = 4 * 1024;
22
+ const TITLE_CAP = 200;
23
+
24
+ interface SearchRow {
25
+ session_id: string;
26
+ titleMatch: number;
27
+ snip: string | null;
28
+ }
29
+
30
+ interface RawEntry {
31
+ type: string;
32
+ message?: {
33
+ role: string;
34
+ content?: unknown;
35
+ command?: string;
36
+ output?: string;
37
+ };
38
+ summary?: string;
39
+ }
40
+
41
+ export interface SearchResult {
42
+ session: SessionInfo;
43
+ titleMatch: boolean;
44
+ snippet: string;
45
+ }
46
+
47
+ function appendText(parts: string[], text: string, budgetLeft: () => number) {
48
+ const t = typeof text === "string" ? text.trim() : "";
49
+ if (!t) return;
50
+ const room = budgetLeft();
51
+ parts.push(t.length <= room ? t : t.slice(0, room));
52
+ }
53
+
54
+ function contentText(content: unknown, omitImages: boolean): string {
55
+ if (typeof content === "string") return content;
56
+ if (!Array.isArray(content)) return "";
57
+ const parts: string[] = [];
58
+ for (const block of content) {
59
+ const b = block as { type?: string; text?: string; thinking?: string; toolCallId?: string; toolName?: string; input?: unknown };
60
+ if (!b || typeof b !== "object") continue;
61
+ if (b.type === "text" && typeof b.text === "string") parts.push(b.text);
62
+ else if (b.type === "thinking" && typeof b.thinking === "string") parts.push(b.thinking);
63
+ else if (b.type === "toolCall" && typeof b.toolName === "string") {
64
+ let input = "";
65
+ try {
66
+ input = JSON.stringify(b.input ?? {});
67
+ } catch {
68
+ input = "";
69
+ }
70
+ parts.push(`[tool:${b.toolName}] ${input.slice(0, TOOL_INPUT_CAP)}`);
71
+ } else if (b.type === "image") {
72
+ if (!omitImages) parts.push("[image]");
73
+ }
74
+ }
75
+ return parts.join("\n");
76
+ }
77
+
78
+ /**
79
+ * Extract searchable title + body from a session's jsonl file. Kept separate
80
+ * from indexing so tests can exercise extraction without touching the DB.
81
+ */
82
+ export function extractSessionText(session: SessionInfo): { title: string; body: string } {
83
+ const title = (session.name || (session.firstMessage || "").slice(0, TITLE_CAP)).slice(0, 400);
84
+ const parts: string[] = [];
85
+ let size = 0;
86
+ const budgetLeft = () => BODY_CAP - size;
87
+ const push = (text: string) => {
88
+ if (size >= BODY_CAP) return;
89
+ const before = parts.length;
90
+ appendText(parts, text, budgetLeft);
91
+ size += parts.slice(before).join("\n").length;
92
+ };
93
+
94
+ let entries: RawEntry[] = [];
95
+ try {
96
+ entries = parseSessionFile(session.path);
97
+ } catch {
98
+ // Unreadable session (deleted mid-scan): index with empty body so it
99
+ // still appears in title-only search.
100
+ }
101
+ for (const entry of entries) {
102
+ if (entry.type === "message" && entry.message) {
103
+ const { role, content } = entry.message;
104
+ if (role === "user") push(contentText(content, false));
105
+ else if (role === "assistant") push(contentText(content, false));
106
+ else if (role === "toolResult") {
107
+ const text = contentText(content, true);
108
+ // Keep tool errors searchable, drop everything else once big.
109
+ push(text.length > 600 ? text.slice(0, 600) : text);
110
+ } else if (role === "bashExecution") {
111
+ if (typeof entry.message?.command === "string") push(`> ${entry.message.command}`);
112
+ if (typeof entry.message?.output === "string") push(entry.message.output.slice(0, BASH_OUTPUT_CAP));
113
+ }
114
+ } else if (entry.type === "compaction" && typeof entry.summary === "string") {
115
+ push(entry.summary);
116
+ } else if (entry.type === "custom_message") {
117
+ const content = (entry as unknown as { content?: unknown }).content;
118
+ if (typeof content === "string") push(content);
119
+ }
120
+ }
121
+ return { title, body: parts.join("\n") };
122
+ }
123
+
124
+ /** Lightweight per-line JSONL parse — only fields the search needs. */
125
+ function parseSessionFile(path: string): RawEntry[] {
126
+ const text = readFileSync(path, "utf8");
127
+ const entries: RawEntry[] = [];
128
+ for (const line of text.split("\n")) {
129
+ if (!line.trim()) continue;
130
+ try {
131
+ const entry = JSON.parse(line) as RawEntry;
132
+ if (entry && typeof entry.type === "string") entries.push(entry);
133
+ } catch {
134
+ // Skip malformed lines.
135
+ }
136
+ }
137
+ return entries;
138
+ }
139
+
140
+ /** Escape a user term into an FTS5 quoted-phrase token. */
141
+ export function fuzzyQueryForTerm(q: string): string {
142
+ return `"${q.replace(/"/g, '""')}"`;
143
+ }
144
+
145
+ /**
146
+ * Bring the index in sync with the (optionally injected) session list:
147
+ * re-index changed/new sessions by modified-mtime, drop vanished ones.
148
+ * Returns whether any write happened this round (frontend "indexing" hint).
149
+ */
150
+ export async function ensureSearchIndex(
151
+ sessionsOverride?: SessionInfo[],
152
+ ): Promise<{ indexing: boolean }> {
153
+ let sessions = sessionsOverride;
154
+ if (!sessions) {
155
+ const { listAllSessions } = await import("./session-reader");
156
+ sessions = await listAllSessions();
157
+ }
158
+ const db = getDb();
159
+
160
+ const stateRows = db.prepare("SELECT session_id, mtime FROM search_state").all() as {
161
+ session_id: string;
162
+ mtime: string;
163
+ }[];
164
+ const state = new Map(stateRows.map((r) => [r.session_id, r.mtime]));
165
+ const known = new Set<string>();
166
+ let changed = false;
167
+
168
+ const upsertState = db.prepare(
169
+ "INSERT INTO search_state (session_id, mtime, title) VALUES (?, ?, ?) " +
170
+ "ON CONFLICT(session_id) DO UPDATE SET mtime = excluded.mtime, title = excluded.title",
171
+ );
172
+ const insertFts = db.prepare(
173
+ "INSERT INTO session_search (session_id, title, body) VALUES (?, ?, ?)",
174
+ );
175
+ const deleteFts = db.prepare("DELETE FROM session_search WHERE session_id = ?");
176
+
177
+ for (const session of sessions) {
178
+ known.add(session.id);
179
+ if (state.get(session.id) === session.modified) continue;
180
+ const { title, body } = extractSessionText(session);
181
+ db.exec("BEGIN");
182
+ try {
183
+ deleteFts.run(session.id);
184
+ if (body || title) insertFts.run(session.id, title, body);
185
+ upsertState.run(session.id, session.modified, title);
186
+ db.exec("COMMIT");
187
+ } catch (error) {
188
+ db.exec("ROLLBACK");
189
+ throw error;
190
+ }
191
+ changed = true;
192
+ }
193
+ db.exec("BEGIN");
194
+ try {
195
+ for (const sessionId of state.keys()) {
196
+ if (!known.has(sessionId)) {
197
+ deleteFts.run(sessionId);
198
+ db.prepare("DELETE FROM search_state WHERE session_id = ?").run(sessionId);
199
+ changed = true;
200
+ }
201
+ }
202
+ db.exec("COMMIT");
203
+ } catch (error) {
204
+ db.exec("ROLLBACK");
205
+ throw error;
206
+ }
207
+ return { indexing: changed };
208
+ }
209
+
210
+ /**
211
+ * Full-text search over all sessions. Queries of any length; ≥3 chars go
212
+ * through FTS5 trigram MATCH (CJK substring safe), shorter ones through LIKE.
213
+ * Results: title hits first, then most recently modified. `sessionsOverride`
214
+ * injects the session list (tests) instead of loading the real catalogue.
215
+ */
216
+ export async function searchSessions(
217
+ q: string,
218
+ limit = 30,
219
+ sessionsOverride?: SessionInfo[],
220
+ ): Promise<{ indexing: boolean; results: SearchResult[] }> {
221
+ const query = q.trim();
222
+ if (!query) return { indexing: false, results: [] };
223
+ const sessions = sessionsOverride ?? (await loadAllSessions());
224
+ const indexed = await ensureSearchIndex(sessions);
225
+
226
+ const cap = Math.min(Math.max(limit, 1), 50);
227
+ const db = getDb();
228
+ const sessionsById = new Map(sessions.map((s) => [s.id, s]));
229
+
230
+ let rows: SearchRow[];
231
+ const titleIds = new Set<string>();
232
+ // 多关键字:空格分隔,全部命中(AND)。
233
+ // FTS5 trigram 对 1-2 字符词不可靠:任一词 <3 字符 → 整条退化为 LIKE 逐词 AND。
234
+ const terms = query.split(/\s+/).filter(Boolean);
235
+ const allLong = terms.every((term) => term.length >= 3);
236
+ if (allLong) {
237
+ // 每个词一个 FTS 短语,AND 组合(各词均为子串命中)
238
+ const phrase = terms.map(fuzzyQueryForTerm).join(" AND ");
239
+ rows = db
240
+ .prepare(
241
+ "SELECT session_id, ifnull(snippet(session_search, 2, '[', ']', '…', 12), '') AS snip " +
242
+ "FROM session_search WHERE session_search MATCH ? LIMIT ?",
243
+ )
244
+ .all(phrase, cap) as unknown as SearchRow[];
245
+ for (const r of db.prepare("SELECT session_id FROM session_search WHERE title MATCH ?").all(phrase) as { session_id: string }[]) {
246
+ titleIds.add(r.session_id);
247
+ }
248
+ } else {
249
+ // LIKE 逐词 AND:每个词 (title LIKE ? OR body LIKE ?),组合成 AND
250
+ const where = terms.map(() => "(title LIKE ? OR body LIKE ?)").join(" AND ");
251
+ const params = terms.flatMap((term) => [`%${term}%`, `%${term}%`]);
252
+ rows = db
253
+ .prepare(`SELECT session_id, '' AS snip FROM session_search WHERE ${where} LIMIT ?`)
254
+ .all(...params, cap) as unknown as SearchRow[];
255
+ const titleWhere = terms.map(() => "title LIKE ?").join(" AND ");
256
+ const titleParams = terms.map((term) => `%${term}%`);
257
+ for (const r of db.prepare(`SELECT session_id FROM session_search WHERE ${titleWhere}`).all(...titleParams) as { session_id: string }[]) {
258
+ titleIds.add(r.session_id);
259
+ }
260
+ }
261
+
262
+ // Title hits first, then most recently modified.
263
+ const results: SearchResult[] = [];
264
+ for (const row of rows) {
265
+ const session = sessionsById.get(row.session_id);
266
+ if (!session) continue;
267
+ results.push({
268
+ session,
269
+ titleMatch: titleIds.has(row.session_id),
270
+ snippet: row.snip ?? "",
271
+ });
272
+ }
273
+ results.sort(
274
+ (a, b) =>
275
+ Number(b.titleMatch) - Number(a.titleMatch)
276
+ || b.session.modified.localeCompare(a.session.modified),
277
+ );
278
+ return { indexing: indexed.indexing, results };
279
+ }
280
+
281
+ async function loadAllSessions(): Promise<SessionInfo[]> {
282
+ const { listAllSessions } = await import("./session-reader");
283
+ return listAllSessions();
284
+ }
@@ -0,0 +1,26 @@
1
+ import type { SessionStatsInfo } from "@/lib/pi-types";
2
+
3
+ /**
4
+ * 会话统计行:in | out | cache read | cache write | cost | context。
5
+ * 普通会话顶栏 Session info 按钮 tooltip 与看板统计弹层第一行共用同一格式,避免双实现。
6
+ */
7
+ export function formatSessionStatsLine(
8
+ stats: SessionStatsInfo | null,
9
+ contextUsage: { percent: number | null; contextWindow: number; tokens: number | null } | null,
10
+ locale?: string,
11
+ ): string {
12
+ if (!stats) return "";
13
+ const tokens = stats.tokens;
14
+ const cost = stats.cost ?? 0;
15
+ const parts: string[] = [];
16
+ parts.push(`in: ${tokens.input.toLocaleString(locale)}`);
17
+ parts.push(`out: ${tokens.output.toLocaleString(locale)}`);
18
+ parts.push(`cache read: ${tokens.cacheRead.toLocaleString(locale)}`);
19
+ parts.push(`cache write: ${tokens.cacheWrite.toLocaleString(locale)}`);
20
+ if (cost > 0) parts.push(`cost: $${cost.toFixed(4)}`);
21
+ if (contextUsage?.contextWindow) {
22
+ const percent = contextUsage.percent;
23
+ parts.push(`context: ${percent !== null ? percent.toFixed(1) + "%" : "unknown"} of ${contextUsage.contextWindow.toLocaleString(locale)} tokens`);
24
+ }
25
+ return parts.join(" | ");
26
+ }
@@ -0,0 +1,107 @@
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, { tsconfigPaths: true });
6
+ const { computeSessionTotalActiveMs } = await jiti.import("./session-timing.ts");
7
+
8
+ const base = Date.parse("2026-01-01T00:00:00.000Z");
9
+ const timestamp = (ms) => new Date(base + ms).toISOString();
10
+ let nextId = 0;
11
+
12
+ function entry(type, ms, extra = {}) {
13
+ return {
14
+ type,
15
+ id: String(++nextId),
16
+ parentId: null,
17
+ timestamp: timestamp(ms),
18
+ ...extra,
19
+ };
20
+ }
21
+ function message(role, ms, extra = {}) {
22
+ return entry("message", ms, { ...extra, message: { role, content: [] } });
23
+ }
24
+
25
+ test("empty or single-entry session yields zero", () => {
26
+ assert.equal(computeSessionTotalActiveMs([]), 0);
27
+ assert.equal(computeSessionTotalActiveMs([message("user", 1000)]), 0);
28
+ });
29
+
30
+ test("counts active gaps within a turn", () => {
31
+ const entries = [
32
+ message("user", 1000),
33
+ message("assistant", 4000),
34
+ message("toolResult", 7000),
35
+ message("assistant", 9000),
36
+ ];
37
+ assert.equal(computeSessionTotalActiveMs(entries), 8000);
38
+ });
39
+
40
+ test("drops human idle before the next user message", () => {
41
+ const entries = [
42
+ message("user", 0),
43
+ message("assistant", 3000),
44
+ message("user", 20000),
45
+ message("assistant", 23000),
46
+ ];
47
+ assert.equal(computeSessionTotalActiveMs(entries), 6000);
48
+ });
49
+
50
+ test("keeps compacted history and compaction time in append order", () => {
51
+ const entries = [
52
+ message("user", 0),
53
+ message("assistant", 3000),
54
+ message("user", 20000),
55
+ message("assistant", 25000),
56
+ entry("compaction", 27000),
57
+ message("assistant", 30000),
58
+ ];
59
+ assert.equal(computeSessionTotalActiveMs(entries), 13000);
60
+ });
61
+
62
+ test("counts work appended on every branch exactly once", () => {
63
+ const root = message("user", 0);
64
+ const shared = message("assistant", 2000, { parentId: root.id });
65
+ const branchAUser = message("user", 10000, { parentId: shared.id });
66
+ const branchAResult = message("assistant", 13000, { parentId: branchAUser.id });
67
+ const branchBUser = message("user", 20000, { parentId: shared.id });
68
+ const branchBResult = message("assistant", 24000, { parentId: branchBUser.id });
69
+ const entries = [root, shared, branchAUser, branchAResult, branchBUser, branchBResult];
70
+ assert.equal(computeSessionTotalActiveMs(entries), 9000);
71
+ });
72
+
73
+ test("treats user-initiated bash as a boundary", () => {
74
+ const entries = [
75
+ message("user", 0),
76
+ message("assistant", 2000),
77
+ message("bashExecution", 100000),
78
+ message("user", 110000),
79
+ message("assistant", 113000),
80
+ ];
81
+ assert.equal(computeSessionTotalActiveMs(entries), 5000);
82
+ });
83
+
84
+ test("ignores metadata without breaking the active interval", () => {
85
+ const entries = [
86
+ message("user", 0),
87
+ entry("model_change", 1000),
88
+ message("assistant", 4000),
89
+ ];
90
+ assert.equal(computeSessionTotalActiveMs(entries), 4000);
91
+ });
92
+
93
+ test("counts branch summaries and custom messages", () => {
94
+ const entries = [
95
+ message("user", 0),
96
+ entry("branch_summary", 2000),
97
+ entry("custom_message", 3000),
98
+ message("assistant", 5000),
99
+ ];
100
+ assert.equal(computeSessionTotalActiveMs(entries), 5000);
101
+ });
102
+
103
+ test("skips invalid timestamps and ignores negative gaps", () => {
104
+ const invalid = { ...message("assistant", 2000), timestamp: "invalid" };
105
+ const entries = [message("user", 5000), invalid, message("assistant", 3000)];
106
+ assert.equal(computeSessionTotalActiveMs(entries), 0);
107
+ });
@@ -0,0 +1,45 @@
1
+ interface TimingEntry {
2
+ type: string;
3
+ timestamp: string;
4
+ message?: { role?: string };
5
+ }
6
+
7
+ /**
8
+ * Estimate active wall-clock time from the append-only session log.
9
+ *
10
+ * Raw entries preserve compacted history and every executed branch exactly
11
+ * once. Gaps ending at user messages are treated as human idle. User-initiated
12
+ * bash entries are also boundaries because the log records only their finish
13
+ * time, so counting the incoming gap could include arbitrary human idle.
14
+ */
15
+ export function computeSessionTotalActiveMs(entries: readonly TimingEntry[]): number {
16
+ let totalActiveMs = 0;
17
+ let previousTimestamp: number | undefined;
18
+
19
+ for (const entry of entries) {
20
+ if (!isTimingEntry(entry.type)) continue;
21
+
22
+ const timestamp = Date.parse(entry.timestamp);
23
+ if (!Number.isFinite(timestamp)) continue;
24
+
25
+ const role = entry.type === "message" ? entry.message?.role : undefined;
26
+ if (role === "user" || role === "bashExecution") {
27
+ previousTimestamp = timestamp;
28
+ continue;
29
+ }
30
+
31
+ if (previousTimestamp !== undefined && timestamp > previousTimestamp) {
32
+ totalActiveMs += timestamp - previousTimestamp;
33
+ }
34
+ previousTimestamp = timestamp;
35
+ }
36
+
37
+ return totalActiveMs;
38
+ }
39
+
40
+ function isTimingEntry(type: string): boolean {
41
+ return type === "message"
42
+ || type === "compaction"
43
+ || type === "branch_summary"
44
+ || type === "custom_message";
45
+ }