@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,446 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import test from "node:test";
6
+ import { createJiti } from "jiti";
7
+
8
+ const jiti = createJiti(import.meta.url);
9
+ const { sessionPathKey } = await jiti.import("./session-path.ts");
10
+ const {
11
+ listAllSessions,
12
+ mergeSessionLists,
13
+ buildSessionContext,
14
+ cacheSessionPath,
15
+ invalidateSessionListCache,
16
+ invalidateSessionPathCache,
17
+ readSessionHeader,
18
+ resolveSessionIdByPath,
19
+ } = await jiti.import("./session-reader.ts");
20
+ const { SessionManager } = await jiti.import("@earendil-works/pi-coding-agent");
21
+ const scannerMod = await jiti.import("./session-scanner.ts");
22
+
23
+ function resetSessionListState() {
24
+ globalThis.__piSessionListCache = undefined;
25
+ globalThis.__piSessionListPromise = undefined;
26
+ globalThis.__piSessionListPromiseGeneration = undefined;
27
+ globalThis.__piSessionListGeneration = 0;
28
+ }
29
+
30
+ function userEntry(id, parentId, content, timestamp = "2026-01-01T00:00:00.000Z") {
31
+ return {
32
+ type: "message",
33
+ id,
34
+ parentId,
35
+ timestamp,
36
+ message: {
37
+ role: "user",
38
+ content,
39
+ },
40
+ };
41
+ }
42
+
43
+ function assistantEntry(id, parentId, text, timestamp = "2026-01-01T00:00:00.000Z") {
44
+ return {
45
+ type: "message",
46
+ id,
47
+ parentId,
48
+ timestamp,
49
+ message: {
50
+ role: "assistant",
51
+ provider: "test",
52
+ model: "test-model",
53
+ content: [{ type: "text", text }],
54
+ },
55
+ };
56
+ }
57
+
58
+ test("renders the SDK compaction-aware context with aligned entry IDs", () => {
59
+ const entries = [
60
+ userEntry("u1", null, "old user request"),
61
+ assistantEntry("a1", "u1", "old assistant answer"),
62
+ userEntry("u2", "a1", "kept user request"),
63
+ {
64
+ type: "compaction",
65
+ id: "cmp",
66
+ parentId: "u2",
67
+ timestamp: "2026-01-01T00:00:03.000Z",
68
+ summary: "old exchange summary",
69
+ firstKeptEntryId: "u2",
70
+ tokensBefore: 123,
71
+ },
72
+ userEntry("u3", "cmp", "after compaction"),
73
+ ];
74
+
75
+ const context = buildSessionContext(entries);
76
+
77
+ assert.deepEqual(context.entryIds, ["cmp", "u2", "u3"]);
78
+ // parentIds are file-authoritative tree parents (unaffected by the
79
+ // compaction-aware display projection). u3's rollback point is the
80
+ // compaction entry; u2's is the assistant before the compaction — the same
81
+ // leaves pi's navigateTree would resolve for those user messages.
82
+ assert.deepEqual(context.parentIds, ["u2", "a1", "cmp"]);
83
+ assert.deepEqual(context.parentIds.length, context.entryIds.length);
84
+ assert.deepEqual(
85
+ context.messages.map((message) => [message.role, message.customType, message.content]),
86
+ [
87
+ ["custom", "compaction", "old exchange summary"],
88
+ ["user", undefined, "kept user request"],
89
+ ["user", undefined, "after compaction"],
90
+ ],
91
+ );
92
+ });
93
+
94
+ test("uses only the latest compaction on the active path", () => {
95
+ const entries = [
96
+ userEntry("u1", null, "old request"),
97
+ assistantEntry("a1", "u1", "old answer"),
98
+ userEntry("u2", "a1", "first kept request"),
99
+ {
100
+ type: "compaction",
101
+ id: "cmp1",
102
+ parentId: "u2",
103
+ timestamp: "2026-01-01T00:00:03.000Z",
104
+ summary: "first summary",
105
+ firstKeptEntryId: "u2",
106
+ tokensBefore: 100,
107
+ },
108
+ assistantEntry("a2", "cmp1", "second kept answer"),
109
+ userEntry("u3", "a2", "second kept request"),
110
+ {
111
+ type: "compaction",
112
+ id: "cmp2",
113
+ parentId: "u3",
114
+ timestamp: "2026-01-01T00:00:06.000Z",
115
+ summary: "latest summary",
116
+ firstKeptEntryId: "a2",
117
+ tokensBefore: 200,
118
+ },
119
+ assistantEntry("a3", "cmp2", "latest answer"),
120
+ ];
121
+
122
+ const context = buildSessionContext(entries);
123
+
124
+ assert.deepEqual(context.entryIds, ["cmp2", "a2", "u3", "a3"]);
125
+ assert.equal(context.messages[0].role, "custom");
126
+ assert.equal(context.messages[0].content, "latest summary");
127
+ assert.equal(context.messages.length, context.entryIds.length);
128
+ });
129
+
130
+ test("uses the selected leaf's path before a later compaction", () => {
131
+ const entries = [
132
+ userEntry("u1", null, "root request"),
133
+ assistantEntry("a1", "u1", "root answer"),
134
+ userEntry("u2", "a1", "main branch"),
135
+ {
136
+ type: "compaction",
137
+ id: "cmp",
138
+ parentId: "u2",
139
+ timestamp: "2026-01-01T00:00:03.000Z",
140
+ summary: "main branch summary",
141
+ firstKeptEntryId: "u2",
142
+ tokensBefore: 100,
143
+ },
144
+ userEntry("alt", "a1", "alternate branch"),
145
+ ];
146
+
147
+ const context = buildSessionContext(entries, "alt");
148
+
149
+ assert.deepEqual(context.entryIds, ["u1", "a1", "alt"]);
150
+ assert.equal(context.messages.some((message) => message.role === "custom"), false);
151
+ });
152
+
153
+ test("returns an empty context for a null leaf", () => {
154
+ const context = buildSessionContext([
155
+ userEntry("u1", null, "not active"),
156
+ ], null);
157
+
158
+ assert.deepEqual(context.messages, []);
159
+ assert.deepEqual(context.entryIds, []);
160
+ assert.deepEqual(context.parentIds, []);
161
+ });
162
+
163
+ test("defers historical thinking without changing live-session content", () => {
164
+ const entries = [
165
+ userEntry("u1", null, "start"),
166
+ {
167
+ ...assistantEntry("a1", "u1", "answer"),
168
+ message: {
169
+ role: "assistant",
170
+ provider: "test",
171
+ model: "test-model",
172
+ content: [
173
+ { type: "thinking", thinking: "large reasoning" },
174
+ { type: "text", text: "answer" },
175
+ ],
176
+ },
177
+ },
178
+ ];
179
+
180
+ const deferred = buildSessionContext(entries, undefined, { deferThinking: true });
181
+ assert.deepEqual(deferred.messages[1].content[0], {
182
+ type: "thinking",
183
+ thinking: "",
184
+ deferred: true,
185
+ });
186
+
187
+ const full = buildSessionContext(entries);
188
+ assert.equal(full.messages[1].content[0].thinking, "large reasoning");
189
+ });
190
+
191
+ test("does not defer empty historical thinking blocks", () => {
192
+ const entries = [
193
+ userEntry("u1", null, "start"),
194
+ {
195
+ ...assistantEntry("a1", "u1", "answer"),
196
+ message: {
197
+ role: "assistant",
198
+ provider: "test",
199
+ model: "test-model",
200
+ content: [
201
+ { type: "thinking", thinking: "" },
202
+ { type: "text", text: "answer" },
203
+ ],
204
+ },
205
+ },
206
+ ];
207
+
208
+ const context = buildSessionContext(entries, undefined, { deferThinking: true });
209
+ assert.deepEqual(context.messages[1].content[0], { type: "thinking", thinking: "" });
210
+ });
211
+
212
+ test("defers only base64 images from historical tool results", () => {
213
+ const userImage = {
214
+ type: "image",
215
+ source: { type: "base64", media_type: "image/png", data: "QUJDRA==" },
216
+ };
217
+ const toolImage = {
218
+ type: "image",
219
+ source: { type: "base64", media_type: "image/jpeg", data: "QUJDRA==" },
220
+ };
221
+ const toolUrlImage = {
222
+ type: "image",
223
+ source: { type: "url", url: "https://example.com/result.png" },
224
+ };
225
+ const flatToolImage = {
226
+ type: "image",
227
+ data: "QUJDRA==",
228
+ mimeType: "image/png",
229
+ };
230
+ const entries = [
231
+ userEntry("u1", null, [{ type: "text", text: "inspect this" }, userImage]),
232
+ assistantEntry("a1", "u1", "reading"),
233
+ {
234
+ type: "message",
235
+ id: "tr1",
236
+ parentId: "a1",
237
+ timestamp: "2026-01-01T00:00:01.000Z",
238
+ message: {
239
+ role: "toolResult",
240
+ toolCallId: "call1",
241
+ content: [
242
+ { type: "text", text: "Read image file" },
243
+ toolImage,
244
+ flatToolImage,
245
+ toolUrlImage,
246
+ ],
247
+ },
248
+ },
249
+ ];
250
+
251
+ const deferred = buildSessionContext(entries, undefined, { deferToolResultImages: true });
252
+ assert.deepEqual(deferred.messages[0].content[1], userImage);
253
+ assert.deepEqual(deferred.messages[2].content[0], { type: "text", text: "Read image file" });
254
+ assert.deepEqual(deferred.messages[2].content[1], toolUrlImage);
255
+ assert.match(deferred.messages[2].content[2].text, /2 tool result images omitted.*image\/jpeg, image\/png.*~8 bytes/);
256
+
257
+ const full = buildSessionContext(entries);
258
+ assert.deepEqual(full.messages[2].content[1], toolImage);
259
+ assert.deepEqual(full.messages[2].content[2], flatToolImage);
260
+ assert.deepEqual(full.messages[2].content[3], toolUrlImage);
261
+ });
262
+
263
+ test("preserves hidden custom messages so the UI can render them collapsed", () => {
264
+ const entries = [
265
+ userEntry("u1", null, "start"),
266
+ {
267
+ type: "custom_message",
268
+ id: "c1",
269
+ parentId: "u1",
270
+ timestamp: "2026-01-01T00:00:01.000Z",
271
+ customType: "extension_debug",
272
+ content: "hidden extension payload",
273
+ display: false,
274
+ details: { source: "test" },
275
+ },
276
+ assistantEntry("a1", "c1", "done"),
277
+ ];
278
+
279
+ const context = buildSessionContext(entries);
280
+
281
+ assert.deepEqual(context.entryIds, ["u1", "c1", "a1"]);
282
+ assert.equal(context.messages[1].role, "custom");
283
+ assert.equal(context.messages[1].customType, "extension_debug");
284
+ assert.equal(context.messages[1].display, false);
285
+ assert.equal(context.messages[1].content, "hidden extension payload");
286
+ });
287
+
288
+ test("preserves valid epoch timestamps on synthetic UI messages", () => {
289
+ const entries = [
290
+ userEntry("u1", null, "start"),
291
+ {
292
+ type: "compaction",
293
+ id: "cmp",
294
+ parentId: "u1",
295
+ timestamp: "1970-01-01T00:00:00.000Z",
296
+ summary: "epoch summary",
297
+ firstKeptEntryId: "u1",
298
+ tokensBefore: 10,
299
+ },
300
+ ];
301
+
302
+ const context = buildSessionContext(entries);
303
+
304
+ assert.equal(context.messages[0].role, "custom");
305
+ assert.equal(context.messages[0].customType, "compaction");
306
+ assert.equal(context.messages[0].timestamp, 0);
307
+ });
308
+
309
+ test("reads only a bounded session header, including headers larger than 4 KiB", () => {
310
+ const dir = mkdtempSync(join(tmpdir(), "pi-web-header-"));
311
+ const filePath = join(dir, "session.jsonl");
312
+ const parentSession = `/tmp/${"p".repeat(5_000)}.jsonl`;
313
+ writeFileSync(filePath, `${JSON.stringify({
314
+ type: "session",
315
+ version: 3,
316
+ id: "session",
317
+ timestamp: "2026-01-01T00:00:00.000Z",
318
+ cwd: dir,
319
+ parentSession,
320
+ })}\n${JSON.stringify(userEntry("u1", null, "message"))}\n`);
321
+
322
+ try {
323
+ assert.equal(readSessionHeader(filePath)?.parentSession, parentSession);
324
+ } finally {
325
+ rmSync(dir, { recursive: true, force: true });
326
+ }
327
+ });
328
+
329
+ test("returns null for malformed or unbounded session headers", () => {
330
+ const dir = mkdtempSync(join(tmpdir(), "pi-web-header-invalid-"));
331
+ const malformedPath = join(dir, "malformed.jsonl");
332
+ const oversizedPath = join(dir, "oversized.jsonl");
333
+ writeFileSync(malformedPath, "{not-json}\n");
334
+ writeFileSync(oversizedPath, "x".repeat(64 * 1024));
335
+
336
+ try {
337
+ assert.equal(readSessionHeader(malformedPath), null);
338
+ assert.equal(readSessionHeader(oversizedPath), null);
339
+ } finally {
340
+ rmSync(dir, { recursive: true, force: true });
341
+ }
342
+ });
343
+
344
+ test("keeps forward and reverse session path caches in sync", async () => {
345
+ const sessionId = "cache-test-session";
346
+ const filePath = join(tmpdir(), "pi-web-cache-test", "..", "cache-test", "session.jsonl");
347
+
348
+ cacheSessionPath(sessionId, filePath);
349
+ try {
350
+ assert.equal(
351
+ await resolveSessionIdByPath(filePath),
352
+ sessionId,
353
+ );
354
+ } finally {
355
+ invalidateSessionPathCache(sessionId);
356
+ }
357
+
358
+ assert.equal(globalThis.__piSessionPathCache?.has(sessionId), false);
359
+ assert.equal(globalThis.__piPathToSessionIdCache?.has(sessionPathKey(filePath)), false);
360
+ });
361
+
362
+ test("forced session listing bypasses the fresh server cache", async (t) => {
363
+ const originalScan = scannerMod.sessionScanner.scan;
364
+ let scans = 0;
365
+ scannerMod.sessionScanner.scan = async () => {
366
+ scans += 1;
367
+ return [];
368
+ };
369
+ resetSessionListState();
370
+ t.after(() => {
371
+ scannerMod.sessionScanner.scan = originalScan;
372
+ resetSessionListState();
373
+ });
374
+
375
+ await listAllSessions({ force: true });
376
+ await listAllSessions();
377
+ assert.equal(scans, 1);
378
+
379
+ await listAllSessions({ force: true });
380
+ assert.equal(scans, 2);
381
+ });
382
+
383
+ test("a scan invalidated in flight retries before returning to its caller", async (t) => {
384
+ const originalScan = scannerMod.sessionScanner.scan;
385
+ let scans = 0;
386
+ let releaseFirstScan;
387
+ let markFirstScanStarted;
388
+ const firstScanStarted = new Promise((resolve) => {
389
+ markFirstScanStarted = resolve;
390
+ });
391
+ const firstScanGate = new Promise((resolve) => {
392
+ releaseFirstScan = resolve;
393
+ });
394
+ scannerMod.sessionScanner.scan = async () => {
395
+ scans += 1;
396
+ if (scans === 1) {
397
+ markFirstScanStarted();
398
+ await firstScanGate;
399
+ }
400
+ return [];
401
+ };
402
+ resetSessionListState();
403
+ t.after(() => {
404
+ scannerMod.sessionScanner.scan = originalScan;
405
+ resetSessionListState();
406
+ });
407
+
408
+ const listing = listAllSessions({ force: true });
409
+ await firstScanStarted;
410
+ invalidateSessionListCache();
411
+ releaseFirstScan();
412
+ await listing;
413
+
414
+ assert.equal(scans, 2);
415
+ });
416
+
417
+ test("disk sessions replace runtime snapshots with the same id", () => {
418
+ const base = {
419
+ path: "/tmp/session.jsonl",
420
+ id: "same-id",
421
+ cwd: "/tmp",
422
+ created: "2026-01-01T00:00:00.000Z",
423
+ modified: "2026-01-01T00:00:01.000Z",
424
+ messageCount: 2,
425
+ firstMessage: "persisted",
426
+ };
427
+ const persisted = { ...base };
428
+ const runtime = {
429
+ ...base,
430
+ path: "/tmp/not-written-yet.jsonl",
431
+ modified: "2026-01-01T00:00:02.000Z",
432
+ firstMessage: "runtime",
433
+ transient: true,
434
+ };
435
+ const runtimeOnly = {
436
+ ...runtime,
437
+ id: "runtime-only",
438
+ modified: "2026-01-01T00:00:03.000Z",
439
+ };
440
+
441
+ const merged = mergeSessionLists([persisted], [runtime, runtimeOnly]);
442
+
443
+ assert.deepEqual(merged.map((session) => session.id), ["runtime-only", "same-id"]);
444
+ assert.equal(merged[1], persisted);
445
+ assert.equal(merged[1].transient, undefined);
446
+ });