@geminixiang/mikan 0.3.1 → 0.4.0-beta.0

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 (371) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/adapter.d.ts +1 -138
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/adapter.js.map +1 -1
  5. package/dist/adapters/discord/bot.d.ts +1 -4
  6. package/dist/adapters/discord/bot.d.ts.map +1 -1
  7. package/dist/adapters/discord/bot.js +25 -33
  8. package/dist/adapters/discord/bot.js.map +1 -1
  9. package/dist/adapters/discord/context.d.ts.map +1 -1
  10. package/dist/adapters/discord/context.js +28 -0
  11. package/dist/adapters/discord/context.js.map +1 -1
  12. package/dist/adapters/discord/types.d.ts +6 -0
  13. package/dist/adapters/discord/types.d.ts.map +1 -0
  14. package/dist/adapters/discord/types.js +2 -0
  15. package/dist/adapters/discord/types.js.map +1 -0
  16. package/dist/adapters/intake.d.ts +11 -0
  17. package/dist/adapters/intake.d.ts.map +1 -0
  18. package/dist/adapters/intake.js +42 -0
  19. package/dist/adapters/intake.js.map +1 -0
  20. package/dist/adapters/shared.d.ts +7 -31
  21. package/dist/adapters/shared.d.ts.map +1 -1
  22. package/dist/adapters/shared.js +18 -2
  23. package/dist/adapters/shared.js.map +1 -1
  24. package/dist/adapters/slack/bot.d.ts +14 -33
  25. package/dist/adapters/slack/bot.d.ts.map +1 -1
  26. package/dist/adapters/slack/bot.js +148 -116
  27. package/dist/adapters/slack/bot.js.map +1 -1
  28. package/dist/adapters/slack/context.d.ts +3 -4
  29. package/dist/adapters/slack/context.d.ts.map +1 -1
  30. package/dist/adapters/slack/context.js +97 -14
  31. package/dist/adapters/slack/context.js.map +1 -1
  32. package/dist/adapters/slack/session.d.ts +5 -20
  33. package/dist/adapters/slack/session.d.ts.map +1 -1
  34. package/dist/adapters/slack/session.js.map +1 -1
  35. package/dist/adapters/slack/types.d.ts +84 -0
  36. package/dist/adapters/slack/types.d.ts.map +1 -0
  37. package/dist/adapters/slack/types.js +2 -0
  38. package/dist/adapters/slack/types.js.map +1 -0
  39. package/dist/adapters/streaming.d.ts +18 -0
  40. package/dist/adapters/streaming.d.ts.map +1 -0
  41. package/dist/adapters/streaming.js +44 -0
  42. package/dist/adapters/streaming.js.map +1 -0
  43. package/dist/adapters/telegram/bot.d.ts +1 -4
  44. package/dist/adapters/telegram/bot.d.ts.map +1 -1
  45. package/dist/adapters/telegram/bot.js +32 -39
  46. package/dist/adapters/telegram/bot.js.map +1 -1
  47. package/dist/adapters/telegram/context.d.ts.map +1 -1
  48. package/dist/adapters/telegram/context.js +33 -0
  49. package/dist/adapters/telegram/context.js.map +1 -1
  50. package/dist/adapters/telegram/types.d.ts +6 -0
  51. package/dist/adapters/telegram/types.d.ts.map +1 -0
  52. package/dist/adapters/telegram/types.js +2 -0
  53. package/dist/adapters/telegram/types.js.map +1 -0
  54. package/dist/adapters/types.d.ts +58 -0
  55. package/dist/adapters/types.d.ts.map +1 -0
  56. package/dist/adapters/types.js +2 -0
  57. package/dist/adapters/types.js.map +1 -0
  58. package/dist/agent.d.ts +4 -16
  59. package/dist/agent.d.ts.map +1 -1
  60. package/dist/agent.js +31 -22
  61. package/dist/agent.js.map +1 -1
  62. package/dist/commands/admin.d.ts.map +1 -1
  63. package/dist/commands/admin.js +1 -1
  64. package/dist/commands/admin.js.map +1 -1
  65. package/dist/commands/auto-reply.d.ts.map +1 -1
  66. package/dist/commands/auto-reply.js +1 -8
  67. package/dist/commands/auto-reply.js.map +1 -1
  68. package/dist/commands/login.d.ts.map +1 -1
  69. package/dist/commands/login.js +3 -3
  70. package/dist/commands/login.js.map +1 -1
  71. package/dist/commands/model.d.ts +5 -8
  72. package/dist/commands/model.d.ts.map +1 -1
  73. package/dist/commands/model.js +15 -20
  74. package/dist/commands/model.js.map +1 -1
  75. package/dist/commands/new.d.ts.map +1 -1
  76. package/dist/commands/new.js +5 -10
  77. package/dist/commands/new.js.map +1 -1
  78. package/dist/commands/parse.d.ts.map +1 -1
  79. package/dist/commands/parse.js +1 -4
  80. package/dist/commands/parse.js.map +1 -1
  81. package/dist/commands/registry.d.ts +1 -0
  82. package/dist/commands/registry.d.ts.map +1 -1
  83. package/dist/commands/registry.js +23 -0
  84. package/dist/commands/registry.js.map +1 -1
  85. package/dist/commands/sandbox.d.ts +2 -5
  86. package/dist/commands/sandbox.d.ts.map +1 -1
  87. package/dist/commands/sandbox.js +11 -16
  88. package/dist/commands/sandbox.js.map +1 -1
  89. package/dist/commands/session-view.d.ts.map +1 -1
  90. package/dist/commands/session-view.js +10 -15
  91. package/dist/commands/session-view.js.map +1 -1
  92. package/dist/commands/types.d.ts +11 -2
  93. package/dist/commands/types.d.ts.map +1 -1
  94. package/dist/commands/types.js.map +1 -1
  95. package/dist/config.d.ts +6 -28
  96. package/dist/config.d.ts.map +1 -1
  97. package/dist/config.js +43 -41
  98. package/dist/config.js.map +1 -1
  99. package/dist/context.d.ts +1 -15
  100. package/dist/context.d.ts.map +1 -1
  101. package/dist/context.js.map +1 -1
  102. package/dist/events.d.ts +3 -44
  103. package/dist/events.d.ts.map +1 -1
  104. package/dist/events.js +2 -9
  105. package/dist/events.js.map +1 -1
  106. package/dist/execution-resolver.d.ts +3 -7
  107. package/dist/execution-resolver.d.ts.map +1 -1
  108. package/dist/execution-resolver.js +8 -8
  109. package/dist/execution-resolver.js.map +1 -1
  110. package/dist/index.d.ts +3 -3
  111. package/dist/index.d.ts.map +1 -1
  112. package/dist/index.js +2 -2
  113. package/dist/index.js.map +1 -1
  114. package/dist/log.d.ts +2 -6
  115. package/dist/log.d.ts.map +1 -1
  116. package/dist/log.js +1 -37
  117. package/dist/log.js.map +1 -1
  118. package/dist/main.d.ts +1 -1
  119. package/dist/main.d.ts.map +1 -1
  120. package/dist/main.js +16 -16
  121. package/dist/main.js.map +1 -1
  122. package/dist/observability/instrument.d.ts.map +1 -0
  123. package/dist/{instrument.js → observability/instrument.js} +2 -2
  124. package/dist/observability/instrument.js.map +1 -0
  125. package/dist/{sentry.d.ts → observability/sentry.d.ts} +2 -30
  126. package/dist/observability/sentry.d.ts.map +1 -0
  127. package/dist/observability/sentry.js.map +1 -0
  128. package/dist/observability/types.d.ts +31 -0
  129. package/dist/observability/types.d.ts.map +1 -0
  130. package/dist/observability/types.js +2 -0
  131. package/dist/observability/types.js.map +1 -0
  132. package/dist/{ui-copy.d.ts → platform-messages.d.ts} +1 -1
  133. package/dist/platform-messages.d.ts.map +1 -0
  134. package/dist/{ui-copy.js → platform-messages.js} +1 -1
  135. package/dist/platform-messages.js.map +1 -0
  136. package/dist/portal-shell.d.ts +2 -28
  137. package/dist/portal-shell.d.ts.map +1 -1
  138. package/dist/portal-shell.js +2 -2
  139. package/dist/portal-shell.js.map +1 -1
  140. package/dist/provisioner.d.ts +2 -23
  141. package/dist/provisioner.d.ts.map +1 -1
  142. package/dist/provisioner.js +1 -1
  143. package/dist/provisioner.js.map +1 -1
  144. package/dist/runtime/conversation-orchestrator.d.ts +4 -19
  145. package/dist/runtime/conversation-orchestrator.d.ts.map +1 -1
  146. package/dist/runtime/conversation-orchestrator.js +3 -3
  147. package/dist/runtime/conversation-orchestrator.js.map +1 -1
  148. package/dist/runtime/session-runtime.d.ts +2 -23
  149. package/dist/runtime/session-runtime.d.ts.map +1 -1
  150. package/dist/runtime/session-runtime.js +7 -9
  151. package/dist/runtime/session-runtime.js.map +1 -1
  152. package/dist/runtime/types.d.ts +35 -0
  153. package/dist/runtime/types.d.ts.map +1 -0
  154. package/dist/runtime/types.js +2 -0
  155. package/dist/runtime/types.js.map +1 -0
  156. package/dist/sandbox/cloudflare.d.ts.map +1 -1
  157. package/dist/sandbox/cloudflare.js +1 -1
  158. package/dist/sandbox/cloudflare.js.map +1 -1
  159. package/dist/sandbox/container.d.ts.map +1 -1
  160. package/dist/sandbox/container.js +1 -4
  161. package/dist/sandbox/container.js.map +1 -1
  162. package/dist/sessions/chat-session-manager.d.ts +2 -46
  163. package/dist/sessions/chat-session-manager.d.ts.map +1 -1
  164. package/dist/sessions/chat-session-manager.js +12 -40
  165. package/dist/sessions/chat-session-manager.js.map +1 -1
  166. package/dist/sessions/metadata.d.ts +1 -13
  167. package/dist/sessions/metadata.d.ts.map +1 -1
  168. package/dist/sessions/metadata.js.map +1 -1
  169. package/dist/sessions/policy.d.ts +3 -10
  170. package/dist/sessions/policy.d.ts.map +1 -1
  171. package/dist/sessions/policy.js.map +1 -1
  172. package/dist/sessions/store.d.ts +1 -12
  173. package/dist/sessions/store.d.ts.map +1 -1
  174. package/dist/sessions/store.js +4 -7
  175. package/dist/sessions/store.js.map +1 -1
  176. package/dist/sessions/types.d.ts +76 -0
  177. package/dist/sessions/types.d.ts.map +1 -0
  178. package/dist/sessions/types.js +2 -0
  179. package/dist/sessions/types.js.map +1 -0
  180. package/dist/store.d.ts +2 -19
  181. package/dist/store.d.ts.map +1 -1
  182. package/dist/store.js +1 -1
  183. package/dist/store.js.map +1 -1
  184. package/dist/tools/event.d.ts +30 -36
  185. package/dist/tools/event.d.ts.map +1 -1
  186. package/dist/tools/event.js +207 -26
  187. package/dist/tools/event.js.map +1 -1
  188. package/dist/tools/index.d.ts +2 -2
  189. package/dist/tools/index.d.ts.map +1 -1
  190. package/dist/tools/index.js.map +1 -1
  191. package/dist/tools/sandbox.d.ts.map +1 -1
  192. package/dist/tools/sandbox.js +1 -1
  193. package/dist/tools/sandbox.js.map +1 -1
  194. package/dist/tools/truncate.d.ts +2 -26
  195. package/dist/tools/truncate.d.ts.map +1 -1
  196. package/dist/tools/truncate.js.map +1 -1
  197. package/dist/tools/types.d.ts +54 -0
  198. package/dist/tools/types.d.ts.map +1 -0
  199. package/dist/tools/types.js +2 -0
  200. package/dist/tools/types.js.map +1 -0
  201. package/dist/trigger.d.ts +2 -13
  202. package/dist/trigger.d.ts.map +1 -1
  203. package/dist/trigger.js.map +1 -1
  204. package/dist/types.d.ts +307 -0
  205. package/dist/types.d.ts.map +1 -0
  206. package/dist/types.js +4 -0
  207. package/dist/types.js.map +1 -0
  208. package/dist/utils/date.d.ts +10 -0
  209. package/dist/utils/date.d.ts.map +1 -0
  210. package/dist/utils/date.js +23 -0
  211. package/dist/utils/date.js.map +1 -0
  212. package/dist/utils/env.d.ts.map +1 -0
  213. package/dist/utils/env.js.map +1 -0
  214. package/dist/utils/file-guards.d.ts.map +1 -0
  215. package/dist/utils/file-guards.js.map +1 -0
  216. package/dist/utils/fs-atomic.d.ts.map +1 -0
  217. package/dist/utils/fs-atomic.js.map +1 -0
  218. package/dist/utils/html.d.ts.map +1 -0
  219. package/dist/utils/html.js.map +1 -0
  220. package/dist/utils/http-body.d.ts +10 -0
  221. package/dist/utils/http-body.d.ts.map +1 -0
  222. package/dist/utils/http-body.js +34 -0
  223. package/dist/utils/http-body.js.map +1 -0
  224. package/dist/vault/index.d.ts +34 -0
  225. package/dist/vault/index.d.ts.map +1 -0
  226. package/dist/{vault.js → vault/index.js} +4 -4
  227. package/dist/vault/index.js.map +1 -0
  228. package/dist/{vault-routing.d.ts → vault/routing.d.ts} +2 -2
  229. package/dist/vault/routing.d.ts.map +1 -0
  230. package/dist/{vault-routing.js → vault/routing.js} +2 -2
  231. package/dist/vault/routing.js.map +1 -0
  232. package/dist/{vault.d.ts → vault/types.d.ts} +3 -34
  233. package/dist/vault/types.d.ts.map +1 -0
  234. package/dist/vault/types.js +2 -0
  235. package/dist/vault/types.js.map +1 -0
  236. package/dist/web/admin/portal.d.ts +5 -0
  237. package/dist/web/admin/portal.d.ts.map +1 -0
  238. package/dist/{admin → web/admin}/portal.js +140 -52
  239. package/dist/web/admin/portal.js.map +1 -0
  240. package/dist/web/admin/store.d.ts +13 -0
  241. package/dist/web/admin/store.d.ts.map +1 -0
  242. package/dist/web/admin/store.js +23 -0
  243. package/dist/web/admin/store.js.map +1 -0
  244. package/dist/web/admin/types.d.ts +28 -0
  245. package/dist/web/admin/types.d.ts.map +1 -0
  246. package/dist/web/admin/types.js +2 -0
  247. package/dist/web/admin/types.js.map +1 -0
  248. package/dist/web/login/oauth.d.ts +6 -0
  249. package/dist/web/login/oauth.d.ts.map +1 -0
  250. package/dist/{login/index.js → web/login/oauth.js} +33 -30
  251. package/dist/web/login/oauth.js.map +1 -0
  252. package/dist/{login → web/login}/portal.d.ts +5 -5
  253. package/dist/web/login/portal.d.ts.map +1 -0
  254. package/dist/{login → web/login}/portal.js +16 -35
  255. package/dist/web/login/portal.js.map +1 -0
  256. package/dist/web/login/store.d.ts +12 -0
  257. package/dist/web/login/store.d.ts.map +1 -0
  258. package/dist/web/login/store.js +28 -0
  259. package/dist/web/login/store.js.map +1 -0
  260. package/dist/web/login/types.d.ts +50 -0
  261. package/dist/web/login/types.d.ts.map +1 -0
  262. package/dist/web/login/types.js +2 -0
  263. package/dist/web/login/types.js.map +1 -0
  264. package/dist/web/session-view/command.d.ts +4 -0
  265. package/dist/web/session-view/command.d.ts.map +1 -0
  266. package/dist/{session-view → web/session-view}/command.js +1 -1
  267. package/dist/web/session-view/command.js.map +1 -0
  268. package/dist/{session-view → web/session-view}/portal.d.ts +2 -5
  269. package/dist/web/session-view/portal.d.ts.map +1 -0
  270. package/dist/{session-view → web/session-view}/portal.js +5 -5
  271. package/dist/web/session-view/portal.js.map +1 -0
  272. package/dist/web/session-view/service.d.ts +6 -0
  273. package/dist/web/session-view/service.d.ts.map +1 -0
  274. package/dist/{session-view → web/session-view}/service.js +6 -36
  275. package/dist/web/session-view/service.js.map +1 -0
  276. package/dist/web/session-view/store.d.ts +8 -0
  277. package/dist/web/session-view/store.d.ts.map +1 -0
  278. package/dist/web/session-view/store.js +20 -0
  279. package/dist/web/session-view/store.js.map +1 -0
  280. package/dist/{session-view/service.d.ts → web/session-view/types.d.ts} +20 -4
  281. package/dist/web/session-view/types.d.ts.map +1 -0
  282. package/dist/web/session-view/types.js +2 -0
  283. package/dist/web/session-view/types.js.map +1 -0
  284. package/dist/web/token-store.d.ts +19 -0
  285. package/dist/web/token-store.d.ts.map +1 -0
  286. package/dist/web/token-store.js +45 -0
  287. package/dist/web/token-store.js.map +1 -0
  288. package/dist/web/types.d.ts +5 -0
  289. package/dist/web/types.d.ts.map +1 -0
  290. package/dist/web/types.js +2 -0
  291. package/dist/web/types.js.map +1 -0
  292. package/package.json +1 -1
  293. package/dist/adapters/discord/index.d.ts +0 -3
  294. package/dist/adapters/discord/index.d.ts.map +0 -1
  295. package/dist/adapters/discord/index.js +0 -3
  296. package/dist/adapters/discord/index.js.map +0 -1
  297. package/dist/adapters/slack/index.d.ts +0 -3
  298. package/dist/adapters/slack/index.d.ts.map +0 -1
  299. package/dist/adapters/slack/index.js +0 -3
  300. package/dist/adapters/slack/index.js.map +0 -1
  301. package/dist/adapters/slack/thread-manager.d.ts +0 -19
  302. package/dist/adapters/slack/thread-manager.d.ts.map +0 -1
  303. package/dist/adapters/slack/thread-manager.js +0 -11
  304. package/dist/adapters/slack/thread-manager.js.map +0 -1
  305. package/dist/adapters/telegram/index.d.ts +0 -3
  306. package/dist/adapters/telegram/index.d.ts.map +0 -1
  307. package/dist/adapters/telegram/index.js +0 -3
  308. package/dist/adapters/telegram/index.js.map +0 -1
  309. package/dist/admin/portal.d.ts +0 -27
  310. package/dist/admin/portal.d.ts.map +0 -1
  311. package/dist/admin/portal.js.map +0 -1
  312. package/dist/admin/store.d.ts +0 -22
  313. package/dist/admin/store.d.ts.map +0 -1
  314. package/dist/admin/store.js +0 -39
  315. package/dist/admin/store.js.map +0 -1
  316. package/dist/commands/index.d.ts +0 -5
  317. package/dist/commands/index.d.ts.map +0 -1
  318. package/dist/commands/index.js +0 -20
  319. package/dist/commands/index.js.map +0 -1
  320. package/dist/env.d.ts.map +0 -1
  321. package/dist/env.js.map +0 -1
  322. package/dist/file-guards.d.ts.map +0 -1
  323. package/dist/file-guards.js.map +0 -1
  324. package/dist/fs-atomic.d.ts.map +0 -1
  325. package/dist/fs-atomic.js.map +0 -1
  326. package/dist/html.d.ts.map +0 -1
  327. package/dist/html.js.map +0 -1
  328. package/dist/instrument.d.ts.map +0 -1
  329. package/dist/instrument.js.map +0 -1
  330. package/dist/login/index.d.ts +0 -43
  331. package/dist/login/index.d.ts.map +0 -1
  332. package/dist/login/index.js.map +0 -1
  333. package/dist/login/portal.d.ts.map +0 -1
  334. package/dist/login/portal.js.map +0 -1
  335. package/dist/login/store.d.ts +0 -26
  336. package/dist/login/store.d.ts.map +0 -1
  337. package/dist/login/store.js +0 -56
  338. package/dist/login/store.js.map +0 -1
  339. package/dist/runtime/index.d.ts +0 -2
  340. package/dist/runtime/index.d.ts.map +0 -1
  341. package/dist/runtime/index.js +0 -2
  342. package/dist/runtime/index.js.map +0 -1
  343. package/dist/sentry.d.ts.map +0 -1
  344. package/dist/sentry.js.map +0 -1
  345. package/dist/session-view/command.d.ts +0 -5
  346. package/dist/session-view/command.d.ts.map +0 -1
  347. package/dist/session-view/command.js.map +0 -1
  348. package/dist/session-view/portal.d.ts.map +0 -1
  349. package/dist/session-view/portal.js.map +0 -1
  350. package/dist/session-view/service.d.ts.map +0 -1
  351. package/dist/session-view/service.js.map +0 -1
  352. package/dist/session-view/store.d.ts +0 -18
  353. package/dist/session-view/store.d.ts.map +0 -1
  354. package/dist/session-view/store.js +0 -36
  355. package/dist/session-view/store.js.map +0 -1
  356. package/dist/ui-copy.d.ts.map +0 -1
  357. package/dist/ui-copy.js.map +0 -1
  358. package/dist/vault-routing.d.ts.map +0 -1
  359. package/dist/vault-routing.js.map +0 -1
  360. package/dist/vault.d.ts.map +0 -1
  361. package/dist/vault.js.map +0 -1
  362. /package/dist/{instrument.d.ts → observability/instrument.d.ts} +0 -0
  363. /package/dist/{sentry.js → observability/sentry.js} +0 -0
  364. /package/dist/{env.d.ts → utils/env.d.ts} +0 -0
  365. /package/dist/{env.js → utils/env.js} +0 -0
  366. /package/dist/{file-guards.d.ts → utils/file-guards.d.ts} +0 -0
  367. /package/dist/{file-guards.js → utils/file-guards.js} +0 -0
  368. /package/dist/{fs-atomic.d.ts → utils/fs-atomic.d.ts} +0 -0
  369. /package/dist/{fs-atomic.js → utils/fs-atomic.js} +0 -0
  370. /package/dist/{html.d.ts → utils/html.d.ts} +0 -0
  371. /package/dist/{html.js → utils/html.js} +0 -0
@@ -1 +0,0 @@
1
- {"version":3,"file":"sentry.d.ts","sourceRoot":"","sources":["../src/sentry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAQpF,KAAK,eAAe,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAkDjD,MAAM,WAAW,qBAAqB;IACpC,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,KAAK,qBAAqB,GACtB,KAAK,GACL,eAAe,GACf,OAAO,GACP,SAAS,GACT,OAAO,GACP,QAAQ,GACR,cAAc,CAAC;AAEnB,KAAK,uBAAuB,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,CAAC;AAE7D,MAAM,WAAW,4BAA4B;IAC3C,MAAM,EAAE,qBAAqB,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,uBAAuB,CAAC;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,GAAG,SAAS,CAAC,CAAC;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,wBAAgB,uBAAuB,CAAC,GAAG,CAAC,EAAE,MAAM;;;;;;;;sBAS9B,UAAU,QAAQ,SAAS,GAAG,UAAU,GAAG,IAAI;iCAGpC,UAAU,GAAG,UAAU,GAAG,IAAI;EAI9D;AAED,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,4BAA4B,GACpC,MAAM,GAAG,SAAS,CAkCpB;AAMD,wBAAgB,aAAa,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,qBAAqB,GAAG,IAAI,CAuBhF;AAED,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,GAChE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAO3C;AAED,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,GAC3D,IAAI,CAON;AAED,wBAAgB,aAAa,CAAC,CAAC,SAAS,KAAK,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,SAAS,GAAG,CAAC,GAAG,IAAI,CA2CpF;AAED,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,UAAU,GAAG,UAAU,GAAG,IAAI,CAU5E;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,KAAK,SAAI,GAAG,OAAO,CAwB9E","sourcesContent":["import type { Breadcrumb, ErrorEvent, Event, EventHint, Scope } from \"@sentry/node\";\nimport * as Sentry from \"@sentry/node\";\n\nconst REDACTED = \"[REDACTED]\";\nconst REDACTED_PATH = \"[REDACTED_PATH]\";\nconst MAX_STRING_LENGTH = 256;\nconst MAX_DEPTH = 4;\n\ntype SentryPrimitive = string | number | boolean;\n\nconst SENSITIVE_KEYS = new Set([\n \"accesstoken\",\n \"apikey\",\n \"args\",\n \"attachment\",\n \"attachments\",\n \"authorization\",\n \"body\",\n \"code\",\n \"content\",\n \"contents\",\n \"cookie\",\n \"cookies\",\n \"credential\",\n \"filepath\",\n \"headers\",\n \"image\",\n \"imageattachments\",\n \"images\",\n \"localpath\",\n \"messages\",\n \"newusermessage\",\n \"password\",\n \"path\",\n \"paths\",\n \"prompt\",\n \"refreshtoken\",\n \"response\",\n \"result\",\n \"secret\",\n \"systemprompt\",\n \"text\",\n \"thinking\",\n \"token\",\n \"url\",\n \"uri\",\n \"workspacepath\",\n]);\n\nconst ABSOLUTE_PATH_PATTERN =\n /(?:\\/Users\\/[^\\s\"'`]+|\\/workspace\\/[^\\s\"'`]+|\\/tmp\\/[^\\s\"'`]+|\\/var\\/folders\\/[^\\s\"'`]+|[A-Za-z]:\\\\[^\\s\"'`]+)/;\nconst TOKEN_PATTERNS = [\n /\\bsk-[A-Za-z0-9_-]{12,}\\b/,\n /\\bxox[a-z]-[A-Za-z0-9-]{10,}\\b/,\n /\\bAIza[0-9A-Za-z_-]{20,}\\b/,\n /\\bgh[pousr]_[A-Za-z0-9]{20,}\\b/,\n];\n\nexport interface SentryRunScopeContext {\n conversationId: string;\n sessionKey: string;\n messageId: string;\n platform: string;\n userId: string;\n userName?: string;\n threadTs?: string;\n provider?: string;\n model?: string;\n}\n\ntype UserFacingErrorDomain =\n | \"llm\"\n | \"chat_platform\"\n | \"mikan\"\n | \"sandbox\"\n | \"login\"\n | \"events\"\n | \"session_view\";\n\ntype UserFacingErrorSeverity = \"warning\" | \"error\" | \"fatal\";\n\nexport interface ReportUserFacingErrorOptions {\n domain: UserFacingErrorDomain;\n surface: string;\n operation: string;\n severity?: UserFacingErrorSeverity;\n platform?: string;\n provider?: string;\n model?: string;\n toolName?: string;\n stopReason?: string;\n expected?: boolean;\n fingerprint?: string[];\n tags?: Record<string, SentryPrimitive | undefined>;\n context?: Record<string, unknown>;\n}\n\nexport function createSentryInitOptions(dsn?: string) {\n return {\n dsn,\n environment: process.env.SENTRY_ENVIRONMENT ?? \"production\",\n enabled: Boolean(dsn) && process.env.SENTRY_ENABLED !== \"false\",\n sendDefaultPii: false,\n tracesSampleRate: process.env.NODE_ENV === \"development\" ? 1.0 : 1.0,\n includeLocalVariables: false,\n enableLogs: true,\n beforeSend(event: ErrorEvent, hint: EventHint): ErrorEvent | null {\n return sanitizeEvent(event, hint);\n },\n beforeBreadcrumb(breadcrumb: Breadcrumb): Breadcrumb | null {\n return sanitizeBreadcrumb(breadcrumb);\n },\n };\n}\n\nexport function reportUserFacingError(\n error: unknown,\n options: ReportUserFacingErrorOptions,\n): string | undefined {\n if (options.expected) return undefined;\n\n const exception = error instanceof Error ? error : new Error(String(error));\n return Sentry.withScope((scope) => {\n scope.setLevel(options.severity ?? \"error\");\n scope.setTag(\"user_facing\", \"true\");\n scope.setTag(\"expected\", \"false\");\n scope.setTag(\"error_domain\", options.domain);\n scope.setTag(\"error_surface\", options.surface);\n scope.setTag(\"operation\", options.operation);\n setOptionalTag(scope, \"platform\", options.platform);\n setOptionalTag(scope, \"provider\", options.provider);\n setOptionalTag(scope, \"model\", options.model);\n setOptionalTag(scope, \"tool\", options.toolName);\n setOptionalTag(scope, \"stop_reason\", options.stopReason);\n for (const [key, value] of Object.entries(options.tags ?? {})) {\n if (value !== undefined) scope.setTag(key, String(value));\n }\n if (options.fingerprint) scope.setFingerprint(options.fingerprint);\n scope.setContext(\"user_facing_error\", {\n domain: options.domain,\n surface: options.surface,\n operation: options.operation,\n severity: options.severity ?? \"error\",\n platform: options.platform,\n provider: options.provider,\n model: options.model,\n toolName: options.toolName,\n stopReason: options.stopReason,\n ...(sanitizeValue(options.context ?? {}) as Record<string, unknown>),\n });\n return Sentry.captureException(exception);\n });\n}\n\nfunction setOptionalTag(scope: Scope, key: string, value: string | undefined): void {\n if (value !== undefined) scope.setTag(key, value);\n}\n\nexport function applyRunScope(scope: Scope, context: SentryRunScopeContext): void {\n scope.setTag(\"conversation_id\", context.conversationId);\n scope.setTag(\"channel_id\", context.conversationId);\n scope.setTag(\"session_key\", context.sessionKey);\n scope.setTag(\"platform\", context.platform);\n if (context.threadTs) scope.setTag(\"thread_ts\", context.threadTs);\n if (context.provider) scope.setTag(\"provider\", context.provider);\n if (context.model) scope.setTag(\"model\", context.model);\n\n scope.setUser({\n id: context.userId,\n username: context.userName,\n });\n scope.setContext(\"agent_run\", {\n conversationId: context.conversationId,\n channelId: context.conversationId,\n sessionKey: context.sessionKey,\n messageId: context.messageId,\n threadTs: context.threadTs,\n platform: context.platform,\n provider: context.provider,\n model: context.model,\n });\n}\n\nexport function metricAttributes(\n attributes: Record<string, string | number | boolean | undefined>,\n): Record<string, string | number | boolean> {\n return Object.fromEntries(\n Object.entries(attributes).filter((entry): entry is [string, string | number | boolean] => {\n const [, value] = entry;\n return value !== undefined;\n }),\n );\n}\n\nexport function addLifecycleBreadcrumb(\n message: string,\n data?: Record<string, string | number | boolean | undefined>,\n): void {\n Sentry.addBreadcrumb({\n category: \"agent.lifecycle\",\n message,\n level: \"info\",\n data: data ? metricAttributes(data) : undefined,\n });\n}\n\nexport function sanitizeEvent<T extends Event>(event: T, _hint?: EventHint): T | null {\n const sanitized: T = {\n ...event,\n breadcrumbs: event.breadcrumbs\n ?.map((breadcrumb) => sanitizeBreadcrumb(breadcrumb))\n .filter((breadcrumb): breadcrumb is Breadcrumb => breadcrumb !== null),\n extra: sanitizeValue(event.extra) as T[\"extra\"],\n contexts: sanitizeValue(event.contexts) as T[\"contexts\"],\n request: sanitizeRequest(event.request),\n user: undefined,\n server_name: undefined,\n };\n\n if (sanitized.message) {\n sanitized.message = sanitizeString(sanitized.message);\n }\n\n if (sanitized.logentry) {\n sanitized.logentry = {\n ...sanitized.logentry,\n message: sanitized.logentry.message ? sanitizeString(sanitized.logentry.message) : undefined,\n };\n }\n\n if (sanitized.exception?.values) {\n sanitized.exception.values = sanitized.exception.values.map((value) => ({\n ...value,\n value: value.value ? sanitizeString(value.value) : value.value,\n stacktrace: value.stacktrace\n ? {\n ...value.stacktrace,\n frames: value.stacktrace.frames?.map((frame) => ({\n ...frame,\n filename: frame.filename ? sanitizeString(frame.filename) : frame.filename,\n abs_path: frame.abs_path ? sanitizeString(frame.abs_path) : frame.abs_path,\n vars: undefined,\n })),\n }\n : value.stacktrace,\n }));\n }\n\n return sanitized;\n}\n\nexport function sanitizeBreadcrumb(breadcrumb: Breadcrumb): Breadcrumb | null {\n if (breadcrumb.category === \"console\") {\n return null;\n }\n\n return {\n ...breadcrumb,\n message: breadcrumb.message ? sanitizeString(breadcrumb.message) : breadcrumb.message,\n data: sanitizeValue(breadcrumb.data) as Breadcrumb[\"data\"],\n };\n}\n\nexport function sanitizeValue(value: unknown, key?: string, depth = 0): unknown {\n if (value == null) return value;\n if (depth > MAX_DEPTH) return \"[Truncated]\";\n\n if (isSensitiveKey(key)) {\n return summarizeValue(value, key);\n }\n\n if (typeof value === \"string\") {\n return sanitizeString(value);\n }\n\n if (Array.isArray(value)) {\n return value.slice(0, 20).map((entry) => sanitizeValue(entry, key, depth + 1));\n }\n\n if (typeof value === \"object\") {\n const entries = Object.entries(value as Record<string, unknown>).map(\n ([entryKey, entryValue]) => [entryKey, sanitizeValue(entryValue, entryKey, depth + 1)],\n );\n return Object.fromEntries(entries);\n }\n\n return value;\n}\n\nfunction sanitizeRequest(request: Event[\"request\"]): Event[\"request\"] {\n if (!request) return request;\n\n return {\n ...request,\n data: request.data ? summarizeValue(request.data, \"body\") : undefined,\n headers: undefined,\n cookies: undefined,\n };\n}\n\nfunction isSensitiveKey(key?: string): boolean {\n if (!key) return false;\n return SENSITIVE_KEYS.has(key.toLowerCase());\n}\n\nfunction summarizeValue(value: unknown, key?: string): string {\n const label = key ?? \"field\";\n if (typeof value === \"string\") {\n return `[Redacted ${label}; length=${value.length}]`;\n }\n if (Array.isArray(value)) {\n return `[Redacted ${label}; items=${value.length}]`;\n }\n if (value && typeof value === \"object\") {\n return `[Redacted ${label}; keys=${Object.keys(value as Record<string, unknown>).length}]`;\n }\n return `[Redacted ${label}]`;\n}\n\nfunction sanitizeString(value: string): string {\n let sanitized = value.replace(new RegExp(ABSOLUTE_PATH_PATTERN, \"g\"), REDACTED_PATH);\n for (const pattern of TOKEN_PATTERNS) {\n sanitized = sanitized.replace(new RegExp(pattern, \"g\"), REDACTED);\n }\n if (sanitized.length > MAX_STRING_LENGTH) {\n return `${sanitized.slice(0, MAX_STRING_LENGTH)}… [truncated ${sanitized.length - MAX_STRING_LENGTH} chars]`;\n }\n return sanitized;\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"sentry.js","sourceRoot":"","sources":["../src/sentry.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Breadcrumb, ErrorEvent, Event, EventHint, Scope } from \"@sentry/node\";\nimport * as Sentry from \"@sentry/node\";\n\nconst REDACTED = \"[REDACTED]\";\nconst REDACTED_PATH = \"[REDACTED_PATH]\";\nconst MAX_STRING_LENGTH = 256;\nconst MAX_DEPTH = 4;\n\ntype SentryPrimitive = string | number | boolean;\n\nconst SENSITIVE_KEYS = new Set([\n \"accesstoken\",\n \"apikey\",\n \"args\",\n \"attachment\",\n \"attachments\",\n \"authorization\",\n \"body\",\n \"code\",\n \"content\",\n \"contents\",\n \"cookie\",\n \"cookies\",\n \"credential\",\n \"filepath\",\n \"headers\",\n \"image\",\n \"imageattachments\",\n \"images\",\n \"localpath\",\n \"messages\",\n \"newusermessage\",\n \"password\",\n \"path\",\n \"paths\",\n \"prompt\",\n \"refreshtoken\",\n \"response\",\n \"result\",\n \"secret\",\n \"systemprompt\",\n \"text\",\n \"thinking\",\n \"token\",\n \"url\",\n \"uri\",\n \"workspacepath\",\n]);\n\nconst ABSOLUTE_PATH_PATTERN =\n /(?:\\/Users\\/[^\\s\"'`]+|\\/workspace\\/[^\\s\"'`]+|\\/tmp\\/[^\\s\"'`]+|\\/var\\/folders\\/[^\\s\"'`]+|[A-Za-z]:\\\\[^\\s\"'`]+)/;\nconst TOKEN_PATTERNS = [\n /\\bsk-[A-Za-z0-9_-]{12,}\\b/,\n /\\bxox[a-z]-[A-Za-z0-9-]{10,}\\b/,\n /\\bAIza[0-9A-Za-z_-]{20,}\\b/,\n /\\bgh[pousr]_[A-Za-z0-9]{20,}\\b/,\n];\n\nexport interface SentryRunScopeContext {\n conversationId: string;\n sessionKey: string;\n messageId: string;\n platform: string;\n userId: string;\n userName?: string;\n threadTs?: string;\n provider?: string;\n model?: string;\n}\n\ntype UserFacingErrorDomain =\n | \"llm\"\n | \"chat_platform\"\n | \"mikan\"\n | \"sandbox\"\n | \"login\"\n | \"events\"\n | \"session_view\";\n\ntype UserFacingErrorSeverity = \"warning\" | \"error\" | \"fatal\";\n\nexport interface ReportUserFacingErrorOptions {\n domain: UserFacingErrorDomain;\n surface: string;\n operation: string;\n severity?: UserFacingErrorSeverity;\n platform?: string;\n provider?: string;\n model?: string;\n toolName?: string;\n stopReason?: string;\n expected?: boolean;\n fingerprint?: string[];\n tags?: Record<string, SentryPrimitive | undefined>;\n context?: Record<string, unknown>;\n}\n\nexport function createSentryInitOptions(dsn?: string) {\n return {\n dsn,\n environment: process.env.SENTRY_ENVIRONMENT ?? \"production\",\n enabled: Boolean(dsn) && process.env.SENTRY_ENABLED !== \"false\",\n sendDefaultPii: false,\n tracesSampleRate: process.env.NODE_ENV === \"development\" ? 1.0 : 1.0,\n includeLocalVariables: false,\n enableLogs: true,\n beforeSend(event: ErrorEvent, hint: EventHint): ErrorEvent | null {\n return sanitizeEvent(event, hint);\n },\n beforeBreadcrumb(breadcrumb: Breadcrumb): Breadcrumb | null {\n return sanitizeBreadcrumb(breadcrumb);\n },\n };\n}\n\nexport function reportUserFacingError(\n error: unknown,\n options: ReportUserFacingErrorOptions,\n): string | undefined {\n if (options.expected) return undefined;\n\n const exception = error instanceof Error ? error : new Error(String(error));\n return Sentry.withScope((scope) => {\n scope.setLevel(options.severity ?? \"error\");\n scope.setTag(\"user_facing\", \"true\");\n scope.setTag(\"expected\", \"false\");\n scope.setTag(\"error_domain\", options.domain);\n scope.setTag(\"error_surface\", options.surface);\n scope.setTag(\"operation\", options.operation);\n setOptionalTag(scope, \"platform\", options.platform);\n setOptionalTag(scope, \"provider\", options.provider);\n setOptionalTag(scope, \"model\", options.model);\n setOptionalTag(scope, \"tool\", options.toolName);\n setOptionalTag(scope, \"stop_reason\", options.stopReason);\n for (const [key, value] of Object.entries(options.tags ?? {})) {\n if (value !== undefined) scope.setTag(key, String(value));\n }\n if (options.fingerprint) scope.setFingerprint(options.fingerprint);\n scope.setContext(\"user_facing_error\", {\n domain: options.domain,\n surface: options.surface,\n operation: options.operation,\n severity: options.severity ?? \"error\",\n platform: options.platform,\n provider: options.provider,\n model: options.model,\n toolName: options.toolName,\n stopReason: options.stopReason,\n ...(sanitizeValue(options.context ?? {}) as Record<string, unknown>),\n });\n return Sentry.captureException(exception);\n });\n}\n\nfunction setOptionalTag(scope: Scope, key: string, value: string | undefined): void {\n if (value !== undefined) scope.setTag(key, value);\n}\n\nexport function applyRunScope(scope: Scope, context: SentryRunScopeContext): void {\n scope.setTag(\"conversation_id\", context.conversationId);\n scope.setTag(\"channel_id\", context.conversationId);\n scope.setTag(\"session_key\", context.sessionKey);\n scope.setTag(\"platform\", context.platform);\n if (context.threadTs) scope.setTag(\"thread_ts\", context.threadTs);\n if (context.provider) scope.setTag(\"provider\", context.provider);\n if (context.model) scope.setTag(\"model\", context.model);\n\n scope.setUser({\n id: context.userId,\n username: context.userName,\n });\n scope.setContext(\"agent_run\", {\n conversationId: context.conversationId,\n channelId: context.conversationId,\n sessionKey: context.sessionKey,\n messageId: context.messageId,\n threadTs: context.threadTs,\n platform: context.platform,\n provider: context.provider,\n model: context.model,\n });\n}\n\nexport function metricAttributes(\n attributes: Record<string, string | number | boolean | undefined>,\n): Record<string, string | number | boolean> {\n return Object.fromEntries(\n Object.entries(attributes).filter((entry): entry is [string, string | number | boolean] => {\n const [, value] = entry;\n return value !== undefined;\n }),\n );\n}\n\nexport function addLifecycleBreadcrumb(\n message: string,\n data?: Record<string, string | number | boolean | undefined>,\n): void {\n Sentry.addBreadcrumb({\n category: \"agent.lifecycle\",\n message,\n level: \"info\",\n data: data ? metricAttributes(data) : undefined,\n });\n}\n\nexport function sanitizeEvent<T extends Event>(event: T, _hint?: EventHint): T | null {\n const sanitized: T = {\n ...event,\n breadcrumbs: event.breadcrumbs\n ?.map((breadcrumb) => sanitizeBreadcrumb(breadcrumb))\n .filter((breadcrumb): breadcrumb is Breadcrumb => breadcrumb !== null),\n extra: sanitizeValue(event.extra) as T[\"extra\"],\n contexts: sanitizeValue(event.contexts) as T[\"contexts\"],\n request: sanitizeRequest(event.request),\n user: undefined,\n server_name: undefined,\n };\n\n if (sanitized.message) {\n sanitized.message = sanitizeString(sanitized.message);\n }\n\n if (sanitized.logentry) {\n sanitized.logentry = {\n ...sanitized.logentry,\n message: sanitized.logentry.message ? sanitizeString(sanitized.logentry.message) : undefined,\n };\n }\n\n if (sanitized.exception?.values) {\n sanitized.exception.values = sanitized.exception.values.map((value) => ({\n ...value,\n value: value.value ? sanitizeString(value.value) : value.value,\n stacktrace: value.stacktrace\n ? {\n ...value.stacktrace,\n frames: value.stacktrace.frames?.map((frame) => ({\n ...frame,\n filename: frame.filename ? sanitizeString(frame.filename) : frame.filename,\n abs_path: frame.abs_path ? sanitizeString(frame.abs_path) : frame.abs_path,\n vars: undefined,\n })),\n }\n : value.stacktrace,\n }));\n }\n\n return sanitized;\n}\n\nexport function sanitizeBreadcrumb(breadcrumb: Breadcrumb): Breadcrumb | null {\n if (breadcrumb.category === \"console\") {\n return null;\n }\n\n return {\n ...breadcrumb,\n message: breadcrumb.message ? sanitizeString(breadcrumb.message) : breadcrumb.message,\n data: sanitizeValue(breadcrumb.data) as Breadcrumb[\"data\"],\n };\n}\n\nexport function sanitizeValue(value: unknown, key?: string, depth = 0): unknown {\n if (value == null) return value;\n if (depth > MAX_DEPTH) return \"[Truncated]\";\n\n if (isSensitiveKey(key)) {\n return summarizeValue(value, key);\n }\n\n if (typeof value === \"string\") {\n return sanitizeString(value);\n }\n\n if (Array.isArray(value)) {\n return value.slice(0, 20).map((entry) => sanitizeValue(entry, key, depth + 1));\n }\n\n if (typeof value === \"object\") {\n const entries = Object.entries(value as Record<string, unknown>).map(\n ([entryKey, entryValue]) => [entryKey, sanitizeValue(entryValue, entryKey, depth + 1)],\n );\n return Object.fromEntries(entries);\n }\n\n return value;\n}\n\nfunction sanitizeRequest(request: Event[\"request\"]): Event[\"request\"] {\n if (!request) return request;\n\n return {\n ...request,\n data: request.data ? summarizeValue(request.data, \"body\") : undefined,\n headers: undefined,\n cookies: undefined,\n };\n}\n\nfunction isSensitiveKey(key?: string): boolean {\n if (!key) return false;\n return SENSITIVE_KEYS.has(key.toLowerCase());\n}\n\nfunction summarizeValue(value: unknown, key?: string): string {\n const label = key ?? \"field\";\n if (typeof value === \"string\") {\n return `[Redacted ${label}; length=${value.length}]`;\n }\n if (Array.isArray(value)) {\n return `[Redacted ${label}; items=${value.length}]`;\n }\n if (value && typeof value === \"object\") {\n return `[Redacted ${label}; keys=${Object.keys(value as Record<string, unknown>).length}]`;\n }\n return `[Redacted ${label}]`;\n}\n\nfunction sanitizeString(value: string): string {\n let sanitized = value.replace(new RegExp(ABSOLUTE_PATH_PATTERN, \"g\"), REDACTED_PATH);\n for (const pattern of TOKEN_PATTERNS) {\n sanitized = sanitized.replace(new RegExp(pattern, \"g\"), REDACTED);\n }\n if (sanitized.length > MAX_STRING_LENGTH) {\n return `${sanitized.slice(0, MAX_STRING_LENGTH)}… [truncated ${sanitized.length - MAX_STRING_LENGTH} chars]`;\n }\n return sanitized;\n}\n"]}
@@ -1,5 +0,0 @@
1
- export interface ParsedSessionViewCommand {
2
- command: "session" | "/session" | "/pi-session";
3
- }
4
- export declare function parseSessionViewCommand(text: string): ParsedSessionViewCommand | null;
5
- //# sourceMappingURL=command.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../../src/session-view/command.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,SAAS,GAAG,UAAU,GAAG,aAAa,CAAC;CACjD;AAID,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,wBAAwB,GAAG,IAAI,CAGrF","sourcesContent":["import { matchCommand } from \"../commands/parse.js\";\n\nexport interface ParsedSessionViewCommand {\n command: \"session\" | \"/session\" | \"/pi-session\";\n}\n\nconst SESSION_VIEW_COMMANDS = [\"session\", \"/session\", \"/pi-session\"] as const;\n\nexport function parseSessionViewCommand(text: string): ParsedSessionViewCommand | null {\n const matched = matchCommand(text, SESSION_VIEW_COMMANDS);\n return matched ? { command: matched.command } : null;\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"command.js","sourceRoot":"","sources":["../../src/session-view/command.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAMpD,MAAM,qBAAqB,GAAG,CAAC,SAAS,EAAE,UAAU,EAAE,aAAa,CAAU,CAAC;AAE9E,MAAM,UAAU,uBAAuB,CAAC,IAAY;IAClD,MAAM,OAAO,GAAG,YAAY,CAAC,IAAI,EAAE,qBAAqB,CAAC,CAAC;IAC1D,OAAO,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AACvD,CAAC","sourcesContent":["import { matchCommand } from \"../commands/parse.js\";\n\nexport interface ParsedSessionViewCommand {\n command: \"session\" | \"/session\" | \"/pi-session\";\n}\n\nconst SESSION_VIEW_COMMANDS = [\"session\", \"/session\", \"/pi-session\"] as const;\n\nexport function parseSessionViewCommand(text: string): ParsedSessionViewCommand | null {\n const matched = matchCommand(text, SESSION_VIEW_COMMANDS);\n return matched ? { command: matched.command } : null;\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"portal.d.ts","sourceRoot":"","sources":["../../src/session-view/portal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,MAAM,CAAC;AAG5D,OAAO,KAAK,EAAE,GAAG,EAAyB,UAAU,EAAuB,MAAM,eAAe,CAAC;AAYjG,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,YAAY,CAAC;AA4DhE,MAAM,WAAW,6BAA6B;IAC5C,OAAO,EAAE,UAAU,CAAC;IACpB,cAAc,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;CAC9C;AAED,wBAAsB,wBAAwB,CAC5C,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,GAAG,EACR,qBAAqB,CAAC,EAAE,6BAA6B,EACrD,WAAW,CAAC,EAAE,6BAA6B,GAC1C,OAAO,CAAC,OAAO,CAAC,CA2GlB;AAmID,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG;IAC1C,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;CACjB,CA8BA","sourcesContent":["import type { IncomingMessage, ServerResponse } from \"http\";\nimport { basename } from \"path\";\nimport MarkdownIt from \"markdown-it\";\nimport type { Bot, BotAdapters, BotEvent, BotHandler, ChatResponseContext } from \"../adapter.js\";\nimport { escapeHtml } from \"../html.js\";\nimport * as log from \"../log.js\";\nimport { renderPortalShell } from \"../portal-shell.js\";\nimport { reportUserFacingError } from \"../sentry.js\";\nimport { inferConversationKind } from \"../sessions/policy.js\";\nimport {\n loadSessionViewModel,\n resolveRequestedSessionFile,\n type SessionViewItem,\n type SessionViewRelation,\n} from \"./service.js\";\nimport type { InMemorySessionViewTokenStore } from \"./store.js\";\n\nconst markdown = new MarkdownIt({\n html: false,\n linkify: true,\n breaks: true,\n});\n\nconst defaultLinkOpen = markdown.renderer.rules.link_open;\ntype LinkOpenRule = NonNullable<typeof defaultLinkOpen>;\nmarkdown.renderer.rules.link_open = (...args: Parameters<LinkOpenRule>) => {\n const [tokens, idx, options, env, self] = args;\n const token = tokens[idx];\n token.attrSet(\"target\", \"_blank\");\n token.attrSet(\"rel\", \"noreferrer noopener\");\n return defaultLinkOpen\n ? defaultLinkOpen(tokens, idx, options, env, self)\n : self.renderToken(tokens, idx, options);\n};\n\ntype SessionStreamEvent =\n | { type: \"status\"; running: boolean }\n | { type: \"user\"; html: string }\n | { type: \"assistant\"; html: string }\n | { type: \"assistant_remove\" }\n | { type: \"tool\"; html: string }\n | { type: \"system\"; html: string }\n | {\n type: \"refresh\";\n timelineHtml: string;\n updatedAt: string;\n entryCount: number;\n running: boolean;\n }\n | { type: \"error\"; message: string };\n\nclass SessionViewStreamHub {\n private listeners = new Map<string, Set<(event: SessionStreamEvent) => void>>();\n\n subscribe(key: string, listener: (event: SessionStreamEvent) => void): () => void {\n const set = this.listeners.get(key) ?? new Set<(event: SessionStreamEvent) => void>();\n set.add(listener);\n this.listeners.set(key, set);\n return () => {\n const current = this.listeners.get(key);\n if (!current) return;\n current.delete(listener);\n if (current.size === 0) this.listeners.delete(key);\n };\n }\n\n publish(key: string, event: SessionStreamEvent): void {\n const set = this.listeners.get(key);\n if (!set) return;\n for (const listener of set) listener(event);\n }\n}\n\nconst sessionViewStreamHub = new SessionViewStreamHub();\n\nexport interface SessionViewInteractiveOptions {\n handler: BotHandler;\n botsByPlatform: Partial<Record<string, Bot>>;\n}\n\nexport async function handleSessionViewRequest(\n req: IncomingMessage,\n res: ServerResponse,\n url: URL,\n sessionViewTokenStore?: InMemorySessionViewTokenStore,\n interactive?: SessionViewInteractiveOptions,\n): Promise<boolean> {\n if (req.method === \"POST\" && url.pathname === \"/session/message\") {\n await handleSessionMessageRequest(req, res, sessionViewTokenStore, interactive);\n return true;\n }\n\n if (req.method === \"GET\" && url.pathname === \"/session/stream\") {\n await handleSessionStreamRequest(req, res, url, sessionViewTokenStore, interactive);\n return true;\n }\n\n if (req.method !== \"GET\" || url.pathname !== \"/session\") {\n return false;\n }\n\n const token = url.searchParams.get(\"token\")?.trim();\n if (!token || !sessionViewTokenStore) {\n res.writeHead(400, { \"Content-Type\": \"text/html; charset=utf-8\" });\n res.end(\n renderStatusPage(\"Session unavailable\", \"This session link is invalid or has expired.\"),\n );\n return true;\n }\n\n const entry = sessionViewTokenStore.peek(token);\n if (!entry) {\n res.writeHead(400, { \"Content-Type\": \"text/html; charset=utf-8\" });\n res.end(\n renderStatusPage(\"Session unavailable\", \"This session link is invalid or has expired.\"),\n );\n return true;\n }\n\n const requestedSession = url.searchParams.get(\"session\");\n let targetSessionFile: string | null;\n try {\n targetSessionFile = resolveRequestedSessionFile(entry.sessionFile, requestedSession);\n } catch (error) {\n log.logWarning(\n `[${entry.conversationId}] Corrupted session file referenced for ${entry.sessionFile}`,\n error instanceof Error ? error.message : String(error),\n );\n reportUserFacingError(error, {\n domain: \"session_view\",\n surface: \"session_view\",\n operation: \"resolve_requested_session\",\n severity: \"error\",\n platform: entry.platform,\n context: {\n conversationId: entry.conversationId,\n sessionKey: entry.sessionKey,\n sessionFile: basename(entry.sessionFile),\n requestedSession,\n },\n });\n res.writeHead(500, { \"Content-Type\": \"text/html; charset=utf-8\" });\n res.end(\n renderStatusPage(\"Session unavailable\", \"The selected session file appears to be corrupted.\"),\n );\n return true;\n }\n if (!targetSessionFile) {\n res.writeHead(400, { \"Content-Type\": \"text/html; charset=utf-8\" });\n res.end(renderStatusPage(\"Session unavailable\", \"The selected session link is invalid.\"));\n return true;\n }\n\n try {\n const model = loadSessionViewModel(targetSessionFile);\n const displayedSessionKey = resolveDisplayedSessionKey(entry, targetSessionFile);\n const isRunning = interactive?.handler.isRunning(displayedSessionKey) ?? false;\n res.writeHead(200, {\n \"Content-Type\": \"text/html; charset=utf-8\",\n \"Cache-Control\": \"no-store\",\n });\n res.end(\n renderSessionPage(\n model,\n entry.token,\n entry.expiresAt,\n isRunning,\n displayedSessionKey,\n entry.conversationId,\n ),\n );\n } catch (error) {\n log.logWarning(\n `[${entry.conversationId}] Failed to render session ${entry.sessionFile}`,\n error instanceof Error ? error.message : String(error),\n );\n reportUserFacingError(error, {\n domain: \"session_view\",\n surface: \"session_view\",\n operation: \"render_session\",\n severity: \"error\",\n platform: entry.platform,\n context: {\n conversationId: entry.conversationId,\n sessionKey: entry.sessionKey,\n sessionFile: basename(targetSessionFile),\n },\n });\n res.writeHead(500, { \"Content-Type\": \"text/html; charset=utf-8\" });\n res.end(renderStatusPage(\"Session unavailable\", \"The session could not be loaded right now.\"));\n }\n\n return true;\n}\n\nfunction resolveDisplayedSessionKey(\n entry: { platform: string; conversationId: string; sessionKey: string },\n sessionFile: string,\n): string {\n if (entry.platform === \"slack\") {\n const fileName = basename(sessionFile, \".jsonl\");\n if (/^\\d+\\.\\d+$/.test(fileName)) {\n return `${entry.conversationId}:${fileName}`;\n }\n return entry.conversationId;\n }\n return entry.sessionKey;\n}\n\nfunction sessionStreamKey(entry: {\n platform: string;\n conversationId: string;\n sessionKey: string;\n}): string {\n return `${entry.platform}:${entry.conversationId}:${entry.sessionKey}`;\n}\n\nfunction renderTimelineItems(items: SessionViewItem[], token: string): string {\n return items.length > 0\n ? items.map((item) => renderItem(item, token)).join(\"\\n\")\n : `<div class=\"system-event\"><span class=\"event-dot\"></span><span class=\"event-text\">No messages yet — send one to the bot, then refresh.</span></div>`;\n}\n\nfunction renderSessionPage(\n model: {\n title: string;\n sessionId: string;\n fileName: string;\n createdAt: string;\n updatedAt: string;\n entryCount: number;\n items: SessionViewItem[];\n parent?: SessionViewRelation;\n threads: SessionViewRelation[];\n },\n token: string,\n expiresAt: number,\n isRunning: boolean,\n displayedSessionKey: string,\n conversationId: string,\n): string {\n const items = renderTimelineItems(model.items, token);\n\n const relatedSections = model.parent\n ? `<section class=\"related-card stack\">\n <p class=\"eyebrow\">Parent session</p>\n ${renderRelationCard(model.parent, token)}\n </section>`\n : \"\";\n\n const body = `<header class=\"page-head\">\n <div>\n <p class=\"eyebrow\">Session</p>\n <h2 class=\"page-title\">${esc(model.title)}</h2>\n <p class=\"page-desc\">\n <span>Created ${esc(formatDate(model.createdAt))}</span> ·\n <span>Updated <strong data-session-updated>${esc(formatDate(model.updatedAt))}</strong></span> ·\n <span><strong data-session-entries>${esc(String(model.entryCount))}</strong> entries</span>\n </p>\n </div>\n <div class=\"session-side\">\n <span class=\"session-badge session-badge-status${isRunning ? \" is-running\" : \"\"}\"><span class=\"session-badge-dot\"></span><strong data-session-status>${esc(isRunning ? \"Running\" : \"Idle\")}</strong></span>\n <span class=\"session-badge\">${esc(displayedSessionKey === conversationId ? \"Channel\" : \"Thread\")}</span>\n </div>\n </header>\n\n <div class=\"session-detail-row\">\n <span class=\"session-detail\"><span class=\"session-detail-label\">Session</span><code>${esc(model.sessionId.slice(0, 8))}</code></span>\n <span class=\"session-detail\"><span class=\"session-detail-label\">File</span><code>${esc(model.fileName)}</code></span>\n <span class=\"session-detail\"><span class=\"session-detail-label\">Expires</span><span>${esc(formatDate(new Date(expiresAt).toISOString()))}</span></span>\n </div>\n\n ${relatedSections}\n\n <div class=\"timeline-shell\">\n <div class=\"timeline-list\" data-timeline-list>\n ${items}\n </div>\n </div>\n\n <button class=\"jump-latest-btn\" type=\"button\" hidden data-jump-latest aria-label=\"Jump to latest\" title=\"Jump to latest\">↓</button>\n\n <section class=\"composer-card\">\n <form class=\"composer-form\" data-session-composer>\n <input type=\"hidden\" name=\"token\" value=\"${esc(token)}\">\n <input type=\"hidden\" name=\"session\" value=\"${esc(model.fileName)}\">\n <input type=\"hidden\" name=\"sessionKey\" value=\"${esc(displayedSessionKey)}\">\n <textarea name=\"text\" rows=\"1\" placeholder=\"Ask mikan in this session… (replies stay in Session View)\" required></textarea>\n <div class=\"composer-actions\">\n <span class=\"composer-status\" data-composer-status></span>\n <button class=\"composer-send-btn\" type=\"submit\" aria-label=\"Send\" title=\"Send\">↑</button>\n </div>\n </form>\n </section>`;\n\n return renderHtmlDocument(`${model.title} · Session Viewer`, body, isRunning);\n}\n\nfunction renderRelationCard(relation: SessionViewRelation, token: string): string {\n const href = `/session?token=${encodeURIComponent(token)}&session=${encodeURIComponent(relation.fileName)}`;\n const summary = relation.summary ? `<p class=\"related-summary\">${esc(relation.summary)}</p>` : \"\";\n return `<a class=\"related-link\" href=\"${href}\">\n <span class=\"related-copy\">\n <strong class=\"related-title\">${esc(relation.title)}</strong>\n ${summary}\n <span class=\"related-meta\">${esc(formatDate(relation.updatedAt))} · ${esc(String(relation.entryCount))} entries · ${esc(relation.fileName)}</span>\n </span>\n <span class=\"related-arrow\" aria-hidden=\"true\">→</span>\n </a>`;\n}\n\nfunction renderThreadLinks(relations: SessionViewRelation[] | undefined, token: string): string {\n if (!relations || relations.length === 0) return \"\";\n return `<div class=\"thread-links\">${relations\n .map((relation) => {\n const href = `/session?token=${encodeURIComponent(token)}&session=${encodeURIComponent(relation.fileName)}`;\n return `<a class=\"thread-link\" href=\"${href}\" title=\"Open ${esc(relation.title)}\">\n <span class=\"thread-dot\" aria-hidden=\"true\"></span>\n <span class=\"thread-text\">Thread</span>\n </a>`;\n })\n .join(\"\")}</div>`;\n}\n\nexport function parseUserBody(raw: string): {\n timestamp: string | null;\n username: string | null;\n threadTs: string | null;\n header: string | null;\n content: string;\n} {\n // [timestamp] [username] [in-thread:ts]: content\n let m = raw.match(\n /^\\[([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}[+-][0-9]{2}:[0-9]{2})\\]\\s*\\[([^\\]]+)\\](?:\\s*\\[in-thread:([^\\]]+)\\])?:\\s*([\\s\\S]*)$/,\n );\n if (m) {\n const header = [`[${m[1]}]`, `[${m[2]}]`, m[3] ? `[in-thread:${m[3]}]` : \"\"]\n .filter(Boolean)\n .join(\" \");\n return {\n timestamp: m[1],\n username: m[2],\n threadTs: m[3] ?? null,\n header,\n content: m[4],\n };\n }\n // [username] [in-thread:ts]: content\n m = raw.match(/^\\[([^\\]]+)\\](?:\\s*\\[in-thread:([^\\]]+)\\])?:\\s*([\\s\\S]*)$/);\n if (m) {\n const header = [`[${m[1]}]`, m[2] ? `[in-thread:${m[2]}]` : \"\"].filter(Boolean).join(\" \");\n return {\n timestamp: null,\n username: m[1],\n threadTs: m[2] ?? null,\n header,\n content: m[3],\n };\n }\n return { timestamp: null, username: null, threadTs: null, header: null, content: raw };\n}\n\ntype ParsedUserBody = ReturnType<typeof parseUserBody>;\n\nfunction renderCopyButton(label = \"Copy message\"): string {\n return `<div class=\"msg-actions\"><button class=\"copy-action-btn\" type=\"button\" data-copy-button data-copy-label=\"${esc(label)}\" aria-label=\"${esc(label)}\" title=\"${esc(label)}\"><svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\"><rect x=\"9\" y=\"9\" width=\"11\" height=\"11\" rx=\"2\" stroke=\"currentColor\" stroke-width=\"1.8\"></rect><path d=\"M6 15H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v1\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\"></path></svg></button></div>`;\n}\n\nfunction renderItem(item: SessionViewItem, token?: string): string {\n if (item.kind === \"system\") {\n const parts = [item.title, item.body].filter((x): x is string => Boolean(x)).map(esc);\n const time = item.meta\n ? ` · <time class=\"event-time\">${esc(formatDate(item.meta))}</time>`\n : \"\";\n return `<div class=\"system-event\"><span class=\"event-dot\"></span><span class=\"event-text\">${parts.join(\" — \")}</span>${time}</div>`;\n }\n\n if (item.kind === \"tool\") {\n const toneClass = item.tone === \"err\" ? \" tone-err\" : item.tone === \"ok\" ? \" tone-ok\" : \"\";\n const body = item.body ? `<pre class=\"tool-output${toneClass}\">${esc(item.body)}</pre>` : \"\";\n const time = item.meta ? `<time class=\"tool-time\">${esc(formatDate(item.meta))}</time>` : \"\";\n return `<div class=\"tool-block\">\n <div class=\"tool-header\">\n <span class=\"tool-icon\"><svg width=\"10\" height=\"10\" viewBox=\"0 0 10 10\" fill=\"none\"><path d=\"M1.5 2L5 5.5 1.5 9\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M6 9h2.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/></svg></span>\n <span class=\"tool-name\">${esc(item.title)}</span>\n ${time}\n </div>\n ${body}\n</div>`;\n }\n\n const time = item.meta ? `<time class=\"msg-time\">${esc(formatDate(item.meta))}</time>` : \"\";\n\n if (item.kind === \"user\") {\n const parsed: ParsedUserBody = item.body\n ? parseUserBody(item.body)\n : { timestamp: null, username: null, threadTs: null, header: null, content: \"\" };\n const { username, threadTs, header, content } = parsed;\n const initial = username ? esc(username.slice(0, 2).toUpperCase()) : \"U\";\n const rawHeader = header ? `<div class=\"msg-raw-header\">${esc(header)}</div>` : \"\";\n const body = content ? renderMarkdownBlock(content, \"user\") : \"\";\n const threadBadge = threadTs\n ? `<div class=\"thread-badge\" title=\"Thread ${esc(threadTs)}\">Thread · <code>${esc(threadTs)}</code></div>`\n : \"\";\n const threads = renderThreadLinks(item.threads, token ?? \"\");\n return `<div class=\"msg-row msg-user copy-host\">\n <div class=\"msg-main user-main\">\n <div class=\"user-bubble\">\n ${rawHeader}\n ${threadBadge}\n ${body}\n ${threads}\n ${time}\n </div>\n ${renderCopyButton()}\n </div>\n <div class=\"msg-avatar user-avatar\" title=\"${username ? esc(username) : \"User\"}\">${initial}</div>\n</div>`;\n }\n\n // assistant\n const body = item.body ? renderMarkdownBlock(item.body, \"assistant\") : \"\";\n const threads = renderThreadLinks(item.threads, token ?? \"\");\n return `<div class=\"msg-row msg-assistant copy-host\">\n <div class=\"msg-avatar asst-avatar\" aria-hidden=\"true\">A</div>\n <div class=\"msg-main asst-main\">\n <div class=\"asst-card\">\n ${body}\n ${threads}\n ${time}\n </div>\n ${renderCopyButton()}\n </div>\n</div>`;\n}\n\nfunction renderMarkdownBlock(text: string, variant: \"user\" | \"assistant\"): string {\n return `<div class=\"msg-body markdown-body markdown-${variant}\">${markdown.render(text)}</div>`;\n}\n\nfunction renderLiveUserMessage(text: string, userName: string): string {\n const initial = esc(userName.slice(0, 2).toUpperCase());\n return `<div class=\"msg-row msg-user copy-host\" data-live-item>\n <div class=\"msg-main user-main\">\n <div class=\"user-bubble\">\n ${renderMarkdownBlock(text, \"user\")}\n </div>\n ${renderCopyButton()}\n </div>\n <div class=\"msg-avatar user-avatar\" title=\"${esc(userName)}\">${initial}</div>\n</div>`;\n}\n\nfunction renderLiveAssistantMessage(text: string): string {\n return `<div class=\"msg-row msg-assistant copy-host\" data-live-assistant>\n <div class=\"msg-avatar asst-avatar\" aria-hidden=\"true\">A</div>\n <div class=\"msg-main asst-main\">\n <div class=\"asst-card\">\n ${renderMarkdownBlock(text, \"assistant\")}\n </div>\n ${renderCopyButton()}\n </div>\n</div>`;\n}\n\nfunction renderLiveToolResult(result: {\n toolName: string;\n result: string;\n isError: boolean;\n}): string {\n const toneClass = result.isError ? \" tone-err\" : \" tone-ok\";\n return `<div class=\"tool-block\" data-live-item>\n <div class=\"tool-header\">\n <span class=\"tool-icon\"><svg width=\"10\" height=\"10\" viewBox=\"0 0 10 10\" fill=\"none\"><path d=\"M1.5 2L5 5.5 1.5 9\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M6 9h2.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/></svg></span>\n <span class=\"tool-name\">${esc(result.toolName)}</span>\n </div>\n <pre class=\"tool-output${toneClass}\">${esc(result.result)}</pre>\n</div>`;\n}\n\nfunction renderLiveSystemEvent(text: string, tone: \"default\" | \"err\" = \"default\"): string {\n const cls = tone === \"err\" ? \" system-event-err\" : \"\";\n return `<div class=\"system-event${cls}\" data-live-item><span class=\"event-dot\"></span><span class=\"event-text\">${esc(text)}</span></div>`;\n}\n\nasync function handleSessionStreamRequest(\n req: IncomingMessage,\n res: ServerResponse,\n url: URL,\n sessionViewTokenStore?: InMemorySessionViewTokenStore,\n interactive?: SessionViewInteractiveOptions,\n): Promise<void> {\n const token = url.searchParams.get(\"token\")?.trim() ?? \"\";\n if (!token || !sessionViewTokenStore || !interactive) {\n res.writeHead(400, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(\"Session stream unavailable\");\n return;\n }\n\n const entry = sessionViewTokenStore.peek(token);\n if (!entry) {\n res.writeHead(400, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(\"Invalid session token\");\n return;\n }\n\n const requestedSession = url.searchParams.get(\"session\");\n let targetSessionFile: string | null;\n try {\n targetSessionFile = resolveRequestedSessionFile(entry.sessionFile, requestedSession);\n } catch (error) {\n reportUserFacingError(error, {\n domain: \"session_view\",\n surface: \"session_view\",\n operation: \"session_stream\",\n severity: \"error\",\n platform: entry.platform,\n context: {\n conversationId: entry.conversationId,\n sessionKey: entry.sessionKey,\n sessionFile: basename(entry.sessionFile),\n requestedSession,\n },\n });\n res.writeHead(500, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(\"Session stream unavailable\");\n return;\n }\n if (!targetSessionFile) {\n res.writeHead(400, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(\"Invalid session file\");\n return;\n }\n const activeSessionKey = resolveDisplayedSessionKey(entry, targetSessionFile);\n const streamKey = sessionStreamKey({ ...entry, sessionKey: activeSessionKey });\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-store\",\n Connection: \"keep-alive\",\n });\n res.write(\n `data: ${JSON.stringify({ type: \"status\", running: interactive.handler.isRunning(activeSessionKey) })}\\n\\n`,\n );\n\n const unsubscribe = sessionViewStreamHub.subscribe(streamKey, (event) => {\n res.write(`data: ${JSON.stringify(event)}\\n\\n`);\n });\n const heartbeat = setInterval(() => {\n res.write(\": keep-alive\\n\\n\");\n }, 15000);\n\n req.on(\"close\", () => {\n clearInterval(heartbeat);\n unsubscribe();\n });\n}\n\nasync function handleSessionMessageRequest(\n req: IncomingMessage,\n res: ServerResponse,\n sessionViewTokenStore?: InMemorySessionViewTokenStore,\n interactive?: SessionViewInteractiveOptions,\n): Promise<void> {\n if (!sessionViewTokenStore || !interactive) {\n json(res, 503, { ok: false, error: \"Session chat is not configured.\" });\n return;\n }\n\n let body: { token?: string; text?: string; session?: string; sessionKey?: string };\n try {\n body = JSON.parse(await readRequestBody(req)) as {\n token?: string;\n text?: string;\n session?: string;\n sessionKey?: string;\n };\n } catch {\n json(res, 400, { ok: false, error: \"Invalid request body.\" });\n return;\n }\n\n const token = body.token?.trim() ?? \"\";\n const text = body.text?.trim() ?? \"\";\n const requestedSession = body.session?.trim() || null;\n const requestedSessionKey = body.sessionKey?.trim() || \"\";\n if (!token || !text) {\n json(res, 400, { ok: false, error: \"Missing token or text.\" });\n return;\n }\n\n const entry = sessionViewTokenStore.peek(token);\n if (!entry) {\n json(res, 400, { ok: false, error: \"This session link is invalid or has expired.\" });\n return;\n }\n\n let targetSessionFile: string | null;\n try {\n targetSessionFile = resolveRequestedSessionFile(entry.sessionFile, requestedSession);\n } catch (error) {\n reportUserFacingError(error, {\n domain: \"session_view\",\n surface: \"session_view\",\n operation: \"session_message\",\n severity: \"error\",\n platform: entry.platform,\n context: {\n conversationId: entry.conversationId,\n sessionKey: entry.sessionKey,\n sessionFile: basename(entry.sessionFile),\n requestedSession,\n },\n });\n json(res, 500, { ok: false, error: \"Session file could not be loaded.\" });\n return;\n }\n if (!targetSessionFile) {\n json(res, 400, { ok: false, error: \"Invalid session file.\" });\n return;\n }\n const activeSessionKey = resolveDisplayedSessionKey(entry, targetSessionFile);\n if (requestedSessionKey && requestedSessionKey !== activeSessionKey) {\n json(res, 400, { ok: false, error: \"Session target mismatch.\" });\n return;\n }\n\n const bot = interactive.botsByPlatform[entry.platform];\n if (!bot) {\n json(res, 503, { ok: false, error: `No bot configured for ${entry.platform}.` });\n return;\n }\n\n const streamKey = sessionStreamKey({ ...entry, sessionKey: activeSessionKey });\n const conversationKind = inferConversationKind(entry.platform, entry.conversationId);\n const ts = (Date.now() / 1000).toFixed(6);\n const platformInfo = bot.getPlatformInfo();\n const platformUserName =\n entry.platformUserName ||\n platformInfo.users.find((user) => user.id === entry.platformUserId)?.userName ||\n platformInfo.users.find((user) => user.id === entry.platformUserId)?.displayName ||\n \"unknown\";\n const responseCtx = createSessionViewResponseContext((event) => {\n sessionViewStreamHub.publish(streamKey, event);\n });\n const event: BotEvent = {\n type: \"session_view\",\n conversationId: entry.conversationId,\n conversationKind,\n ts,\n user: entry.platformUserId,\n text,\n attachments: [],\n sessionKey: activeSessionKey,\n ...(activeSessionKey.includes(\":\")\n ? { thread_ts: activeSessionKey.split(\":\").slice(1).join(\":\") }\n : {}),\n };\n const adapters: BotAdapters = {\n message: {\n id: ts,\n sessionKey: activeSessionKey,\n conversationKind,\n userId: entry.platformUserId,\n userName: platformUserName,\n text,\n attachments: [],\n threadTs: event.thread_ts,\n },\n responseCtx,\n platform: { ...platformInfo, diagnostics: { showUsageSummary: false } },\n };\n\n sessionViewStreamHub.publish(streamKey, { type: \"status\", running: true });\n sessionViewStreamHub.publish(streamKey, {\n type: \"user\",\n html: renderLiveUserMessage(text, platformUserName),\n });\n\n void interactive.handler\n .handleEvent(event, bot, adapters)\n .then(() => {\n if (!targetSessionFile) {\n sessionViewStreamHub.publish(streamKey, { type: \"status\", running: false });\n return;\n }\n const model = loadSessionViewModel(targetSessionFile);\n sessionViewStreamHub.publish(streamKey, {\n type: \"refresh\",\n timelineHtml: renderTimelineItems(model.items, token),\n updatedAt: formatDate(model.updatedAt),\n entryCount: model.entryCount,\n running: false,\n });\n })\n .catch((error) => {\n log.logWarning(\n `[${entry.conversationId}] Session view message failed`,\n error instanceof Error ? error.message : String(error),\n );\n reportUserFacingError(error, {\n domain: \"session_view\",\n surface: \"session_view\",\n operation: \"interactive_message\",\n severity: \"error\",\n platform: entry.platform,\n context: {\n conversationId: entry.conversationId,\n sessionKey: activeSessionKey,\n messageId: ts,\n textLength: text.length,\n },\n });\n sessionViewStreamHub.publish(streamKey, {\n type: \"error\",\n message: error instanceof Error ? error.message : String(error),\n });\n sessionViewStreamHub.publish(streamKey, { type: \"status\", running: false });\n });\n\n json(res, 202, { ok: true, accepted: true });\n}\n\nfunction createSessionViewResponseContext(\n publish: (event: SessionStreamEvent) => void,\n): ChatResponseContext {\n let accumulatedText = \"\";\n\n return {\n respond: async (text: string) => {\n accumulatedText = accumulatedText ? `${accumulatedText}\\n${text}` : text;\n publish({ type: \"assistant\", html: renderLiveAssistantMessage(accumulatedText) });\n },\n replaceResponse: async (text: string) => {\n accumulatedText = text;\n publish({ type: \"assistant\", html: renderLiveAssistantMessage(accumulatedText) });\n },\n respondDiagnostic: async (text: string, options?: { style?: \"muted\" | \"error\" }) => {\n if (options?.style === \"error\") {\n publish({ type: \"system\", html: renderLiveSystemEvent(text, \"err\") });\n }\n },\n respondToolResult: async (result) => {\n publish({ type: \"tool\", html: renderLiveToolResult(result) });\n },\n setTyping: async () => {\n publish({ type: \"status\", running: true });\n },\n setWorking: async (working: boolean) => {\n publish({ type: \"status\", running: working });\n },\n uploadFile: async () => {},\n deleteResponse: async () => {\n accumulatedText = \"\";\n publish({ type: \"assistant_remove\" });\n },\n };\n}\n\nfunction readRequestBody(req: IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n let data = \"\";\n req.setEncoding(\"utf8\");\n req.on(\"data\", (chunk) => {\n data += chunk;\n if (data.length > 1024 * 1024) {\n reject(new Error(\"Request body too large\"));\n req.destroy();\n }\n });\n req.on(\"end\", () => resolve(data));\n req.on(\"error\", reject);\n });\n}\n\nfunction json(res: ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, {\n \"Content-Type\": \"application/json; charset=utf-8\",\n \"Cache-Control\": \"no-store\",\n });\n res.end(JSON.stringify(body));\n}\n\nfunction renderStatusPage(title: string, message: string): string {\n return renderHtmlDocument(\n title,\n `<section class=\"card stack\">\n <p class=\"eyebrow\">mikan</p>\n <h1 class=\"page-title\">${esc(title)}</h1>\n <div class=\"status err\">${esc(message)}</div>\n </section>`,\n false,\n );\n}\n\nfunction renderHtmlDocument(title: string, shellContent: string, isRunning: boolean): string {\n return renderPortalShell({\n activeView: \"session\",\n pageTitle: \"Session\",\n body: shellContent,\n extraStyles: sessionViewStyles,\n bodyAttributes: { \"data-session-running\": isRunning ? \"true\" : \"false\" },\n inlineScript: sessionViewScript,\n });\n}\n\nconst sessionViewScript = `\n const form = document.querySelector('[data-session-composer]');\n const timelineList = document.querySelector('[data-timeline-list]');\n const jumpLatestBtn = document.querySelector('[data-jump-latest]');\n const statusEl = document.querySelector('[data-session-status]');\n const updatedEl = document.querySelector('[data-session-updated]');\n const entriesEl = document.querySelector('[data-session-entries]');\n const composerStatus = form?.querySelector('[data-composer-status]');\n const textarea = form?.querySelector('textarea[name=\"text\"]');\n const submitButton = form?.querySelector('button[type=\"submit\"]');\n let liveAssistant = null;\n let running = document.body.dataset.sessionRunning === 'true';\n\n const isNearBottom = () => window.innerHeight + window.scrollY >= document.body.offsetHeight - 120;\n const scrollToLatest = (behavior = 'smooth') => window.scrollTo({ top: document.body.scrollHeight, behavior });\n const toggleJumpButton = () => {\n if (!jumpLatestBtn) return;\n jumpLatestBtn.hidden = isNearBottom();\n };\n const updateFollowState = () => {\n if (isNearBottom()) scrollToLatest('smooth');\n else toggleJumpButton();\n };\n const canSubmit = () => Boolean(textarea && textarea.value.trim()) && !running;\n const updateSubmitButtonState = () => {\n if (submitButton) submitButton.disabled = !canSubmit();\n };\n const setRunning = (value) => {\n running = value;\n document.body.dataset.sessionRunning = value ? 'true' : 'false';\n if (statusEl) statusEl.textContent = value ? 'Running' : 'Idle';\n updateSubmitButtonState();\n if (composerStatus && !value && composerStatus.textContent === 'Thinking…') {\n composerStatus.textContent = '';\n }\n };\n\n jumpLatestBtn?.addEventListener('click', () => {\n scrollToLatest('smooth');\n toggleJumpButton();\n });\n document.addEventListener('click', async (event) => {\n const button = event.target instanceof Element ? event.target.closest('[data-copy-button]') : null;\n if (!(button instanceof HTMLButtonElement)) return;\n const label = button.dataset.copyLabel || 'Copy message';\n const source = button.closest('.msg-actions')?.previousElementSibling;\n const text = source instanceof HTMLElement ? (source.innerText || source.textContent || '').trim() : '';\n if (!text) return;\n const setState = (state, transient) => {\n button.dataset.copyState = state;\n button.title = transient;\n button.setAttribute('aria-label', transient);\n window.setTimeout(() => {\n if (!button.isConnected) return;\n delete button.dataset.copyState;\n button.title = label;\n button.setAttribute('aria-label', label);\n }, 1200);\n };\n try {\n await navigator.clipboard.writeText(text);\n setState('done', 'Copied');\n } catch {\n setState('error', 'Copy failed');\n }\n });\n window.addEventListener('scroll', toggleJumpButton, { passive: true });\n\n if (textarea) {\n const resize = () => {\n textarea.style.height = 'auto';\n textarea.style.height = Math.min(textarea.scrollHeight, 240) + 'px';\n };\n textarea.addEventListener('input', () => {\n resize();\n updateSubmitButtonState();\n });\n textarea.addEventListener('keydown', (event) => {\n if (event.key !== 'Enter' || event.shiftKey) return;\n if (event.isComposing || event.keyCode === 229) return;\n event.preventDefault();\n if (!running) form?.requestSubmit();\n });\n resize();\n }\n\n setRunning(running);\n updateSubmitButtonState();\n\n const streamUrl = form\n ? '/session/stream?token=' + encodeURIComponent(form.token.value) + '&session=' + encodeURIComponent(form.session.value)\n : null;\n if (streamUrl) {\n const source = new EventSource(streamUrl);\n source.onmessage = (event) => {\n const payload = JSON.parse(event.data);\n switch (payload.type) {\n case 'status':\n setRunning(Boolean(payload.running));\n if (payload.running && composerStatus) composerStatus.textContent = 'Thinking…';\n break;\n case 'user':\n case 'tool':\n case 'system': {\n timelineList?.insertAdjacentHTML('beforeend', payload.html);\n updateFollowState();\n break;\n }\n case 'assistant': {\n if (!liveAssistant || !liveAssistant.isConnected) {\n timelineList?.insertAdjacentHTML('beforeend', payload.html);\n liveAssistant = timelineList?.querySelector('[data-live-assistant]:last-of-type') || null;\n } else {\n liveAssistant.outerHTML = payload.html;\n liveAssistant = timelineList?.querySelector('[data-live-assistant]:last-of-type') || null;\n }\n updateFollowState();\n break;\n }\n case 'assistant_remove':\n if (liveAssistant?.isConnected) liveAssistant.remove();\n liveAssistant = null;\n break;\n case 'refresh':\n if (timelineList) timelineList.innerHTML = payload.timelineHtml;\n liveAssistant = null;\n if (updatedEl) updatedEl.textContent = payload.updatedAt;\n if (entriesEl) entriesEl.textContent = String(payload.entryCount);\n setRunning(Boolean(payload.running));\n if (composerStatus) composerStatus.textContent = '';\n updateFollowState();\n break;\n case 'error':\n if (composerStatus) composerStatus.textContent = payload.message || 'Something went wrong';\n setRunning(false);\n break;\n }\n };\n }\n\n form?.addEventListener('submit', async (event) => {\n event.preventDefault();\n if (!textarea || !composerStatus) return;\n const text = textarea.value.trim();\n if (!text || running) return;\n composerStatus.textContent = 'Sending…';\n updateSubmitButtonState();\n try {\n const response = await fetch('/session/message', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ token: form.token.value, session: form.session.value, sessionKey: form.sessionKey.value, text }),\n });\n const payload = await response.json();\n if (!response.ok || !payload.ok) throw new Error(payload.error || 'Request failed');\n textarea.value = '';\n textarea.style.height = 'auto';\n composerStatus.textContent = 'Thinking…';\n setRunning(true);\n updateSubmitButtonState();\n scrollToLatest('smooth');\n } catch (err) {\n composerStatus.textContent = err && err.message ? err.message : String(err);\n submitButton.disabled = false;\n }\n });\n\n toggleJumpButton();\n`;\n\nfunction formatDate(value: string): string {\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) return value;\n return date.toLocaleString();\n}\n\nconst esc = escapeHtml;\n\nconst sessionViewStyles = `\n :root {\n --user-bg: #18181b;\n --user-text: #fafafa;\n --user-time: rgba(250, 250, 250, 0.5);\n\n --asst-border: #22c55e;\n --asst-avatar-bg: #f0fdf4;\n --asst-avatar-text: #16a34a;\n\n --tool-bg: #0d1117;\n --tool-header: #161b22;\n --tool-text: #c9d1d9;\n --tool-accent: #58a6ff;\n --tool-ok: #3fb950;\n --tool-err: #f85149;\n --tool-time: #484f58;\n }\n\n body {\n /* Extra bottom padding for the fixed composer */\n padding-bottom: calc(140px + env(safe-area-inset-bottom, 0px));\n overflow-x: hidden;\n }\n\n /* ── Session-specific page-head extras ─────────────────────────────── */\n\n .session-side {\n display: flex; flex-direction: column; align-items: flex-end; gap: 8px;\n flex-shrink: 0;\n }\n .session-badge {\n display: inline-flex; align-items: center; gap: 8px;\n padding: 6px 11px; border: 1px solid var(--border); border-radius: 999px;\n background: rgba(255,255,255,0.7); font-size: 0.78rem; color: var(--muted);\n line-height: 1;\n }\n .session-badge strong { color: var(--text); font-weight: 600; }\n .session-badge-status.is-running {\n background: #fff7ed; border-color: rgba(217, 119, 6, 0.18); color: #9a3412;\n }\n .session-badge-dot {\n width: 7px; height: 7px; border-radius: 50%;\n background: #a1a1aa; flex-shrink: 0;\n }\n .session-badge-status.is-running .session-badge-dot {\n background: #d97706; box-shadow: 0 0 0 4px rgba(217, 119, 6, 0.14);\n }\n\n .session-detail-row {\n display: flex; flex-wrap: wrap; gap: 8px;\n padding: 12px 14px; border: 1px solid var(--border); border-radius: 14px;\n background: rgba(255,255,255,0.6);\n }\n .session-detail {\n display: inline-flex; align-items: center; gap: 8px; min-width: 0;\n padding: 4px 10px; border-radius: 10px; background: rgba(0, 0, 0, 0.025);\n color: var(--muted); font-size: 0.78rem;\n }\n .session-detail-label {\n text-transform: uppercase; letter-spacing: 0.08em;\n font-size: 0.68rem; color: var(--subtle);\n }\n .session-detail code {\n min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n font-family: 'JetBrains Mono', ui-monospace, monospace; font-size: 0.74rem;\n color: var(--text); padding: 0;\n }\n\n /* ── Timeline shell ───────────────────────────────────────────────────── */\n\n .thread-links {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n margin-top: 10px;\n }\n\n .thread-link {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 5px 10px;\n border-radius: 999px;\n border: 1px solid rgba(239, 68, 68, 0.18);\n background: rgba(254, 242, 242, 0.95);\n color: #b91c1c;\n text-decoration: none;\n font-size: 0.74rem;\n font-weight: 600;\n line-height: 1;\n transition: transform 120ms, background 120ms, border-color 120ms;\n }\n\n .thread-link:hover {\n transform: translateY(-1px);\n background: #fff1f2;\n border-color: rgba(239, 68, 68, 0.28);\n }\n\n .thread-dot {\n width: 7px;\n height: 7px;\n border-radius: 50%;\n background: #ef4444;\n box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.12);\n flex-shrink: 0;\n }\n\n .thread-text {\n white-space: nowrap;\n }\n\n .related-card {\n padding: 18px 20px;\n border: 1px solid var(--border);\n border-radius: 18px;\n background: rgba(255,255,255,0.78);\n box-shadow: 0 1px 2px rgba(0,0,0,0.04), 0 4px 16px rgba(0,0,0,0.04);\n backdrop-filter: blur(12px);\n }\n\n .related-list {\n display: flex;\n flex-direction: column;\n gap: 10px;\n }\n\n .related-link {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n padding: 12px 14px;\n border-radius: 14px;\n border: 1px solid var(--border);\n background: rgba(255,255,255,0.82);\n color: inherit;\n text-decoration: none;\n transition: transform 120ms, border-color 120ms, box-shadow 120ms, background 120ms;\n }\n\n .related-link:hover {\n transform: translateY(-1px);\n border-color: rgba(0,0,0,0.16);\n background: #fff;\n box-shadow: 0 8px 18px rgba(0,0,0,0.05);\n }\n\n .related-copy {\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n }\n\n .related-title {\n color: var(--text);\n font-size: 0.94rem;\n line-height: 1.3;\n }\n\n .related-summary {\n color: var(--muted);\n font-size: 0.82rem;\n line-height: 1.45;\n }\n\n .related-meta {\n color: var(--subtle);\n font-size: 0.74rem;\n line-height: 1.4;\n }\n\n .related-arrow {\n flex-shrink: 0;\n color: var(--subtle);\n font-size: 1rem;\n }\n\n .timeline-shell {\n padding: 20px 0;\n }\n\n .timeline-list {\n display: flex;\n flex-direction: column;\n gap: 14px;\n min-width: 0;\n }\n\n .copy-host {\n position: relative;\n }\n\n .msg-actions {\n height: 32px;\n display: flex;\n align-items: center;\n gap: 8px;\n margin-top: 8px;\n opacity: 0;\n visibility: hidden;\n transition: opacity 140ms ease, visibility 140ms ease;\n }\n\n .copy-host:hover .msg-actions,\n .copy-host .msg-actions:hover,\n .copy-host:focus-within .msg-actions,\n .timeline-list > .copy-host:last-child .msg-actions,\n .copy-action-btn[data-copy-state] {\n opacity: 1;\n visibility: visible;\n }\n\n .copy-action-btn {\n width: 24px;\n height: 24px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: 0;\n border-radius: 0;\n background: transparent;\n color: rgba(63,63,70,0.8);\n transition: color 140ms ease, opacity 140ms ease;\n cursor: pointer;\n padding: 0;\n appearance: none;\n }\n\n .copy-action-btn:hover {\n background: transparent;\n color: rgba(24,24,27,0.96);\n border-color: transparent;\n }\n\n .copy-action-btn[data-copy-state='done'] {\n background: transparent;\n border-color: transparent;\n color: rgba(24,24,27,0.96);\n }\n\n .copy-action-btn[data-copy-state='done'] svg {\n position: absolute;\n opacity: 0;\n transform: scale(0.6);\n pointer-events: none;\n }\n\n .copy-action-btn svg {\n transition: opacity 140ms ease, transform 140ms ease;\n }\n\n .copy-action-btn[data-copy-state='done']::before {\n content: '';\n width: 14px;\n height: 14px;\n background-color: currentColor;\n -webkit-mask: url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><polyline points='4 12 10 18 20 6'/></svg>\") center / contain no-repeat;\n mask: url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><polyline points='4 12 10 18 20 6'/></svg>\") center / contain no-repeat;\n animation: copy-check-in 200ms ease-out both;\n }\n\n @keyframes copy-check-in {\n from { opacity: 0; transform: scale(0.6); }\n to { opacity: 1; transform: scale(1); }\n }\n\n @media (prefers-reduced-motion: reduce) {\n .copy-action-btn svg,\n .copy-action-btn[data-copy-state='done']::before {\n transition: none;\n animation: none;\n }\n }\n\n .copy-action-btn[data-copy-state='error'] {\n background: transparent;\n border-color: transparent;\n color: #b91c1c;\n }\n\n /* ── Message rows ─────────────────────────────────────────────────────── */\n\n .msg-row {\n display: flex;\n align-items: flex-end;\n gap: 8px;\n padding: 4px 0;\n min-width: 0;\n }\n\n /* ── User messages ────────────────────────────────────────────────────── */\n\n .msg-user {\n justify-content: flex-end;\n }\n\n .msg-main {\n min-width: 0;\n }\n\n .user-main {\n max-width: 85%;\n display: flex;\n flex-direction: column;\n align-items: flex-end;\n }\n\n .user-bubble {\n max-width: 100%;\n min-width: 0;\n padding: 12px 16px;\n border-radius: 18px 18px 4px 18px;\n background: var(--user-bg);\n color: var(--user-text);\n box-shadow: 0 1px 2px rgba(0,0,0,0.12);\n }\n\n .msg-raw-header {\n margin-bottom: 8px;\n color: rgba(250, 250, 250, 0.72);\n font-family: 'JetBrains Mono', ui-monospace, monospace;\n font-size: 0.72rem;\n line-height: 1.5;\n white-space: pre-wrap;\n word-break: break-word;\n }\n\n .thread-badge {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n margin-bottom: 8px;\n padding: 4px 10px;\n border-radius: 999px;\n background: rgba(255,255,255,0.22);\n color: var(--user-text);\n font-size: 0.68rem;\n font-weight: 700;\n letter-spacing: 0.01em;\n }\n\n .thread-badge code {\n font-family: 'JetBrains Mono', ui-monospace, monospace;\n font-size: 0.66rem;\n background: rgba(255,255,255,0.16);\n padding: 1px 6px;\n border-radius: 999px;\n color: inherit;\n }\n\n .msg-user .msg-body {\n color: var(--user-text);\n }\n\n .msg-user .msg-time {\n display: block;\n margin-top: 6px;\n font-size: 0.72rem;\n color: var(--user-time);\n text-align: right;\n }\n\n /* ── Avatars ──────────────────────────────────────────────────────────── */\n\n .msg-avatar {\n flex: 0 0 28px;\n width: 28px;\n height: 28px;\n border-radius: 50%;\n font-size: 0.68rem;\n font-weight: 700;\n display: flex;\n align-items: center;\n justify-content: center;\n letter-spacing: 0;\n flex-shrink: 0;\n }\n\n .user-avatar {\n background: #eff6ff;\n border: 1.5px solid #93c5fd;\n color: #1d4ed8;\n }\n\n .asst-avatar {\n background: var(--asst-avatar-bg);\n border: 1.5px solid var(--asst-border);\n color: var(--asst-avatar-text);\n margin-bottom: 2px;\n }\n\n /* ── Assistant messages ───────────────────────────────────────────────── */\n\n .msg-assistant {\n align-items: flex-end;\n gap: 8px;\n max-width: 85%;\n min-width: 0;\n }\n\n .asst-main {\n max-width: 100%;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n }\n\n .asst-card {\n min-width: 0;\n max-width: 100%;\n padding: 14px 18px;\n border: 1px solid var(--border);\n border-radius: 18px 18px 18px 4px;\n background: var(--surface);\n box-shadow: 0 1px 3px rgba(0,0,0,0.04);\n }\n\n .msg-assistant .msg-body {\n color: var(--text);\n }\n\n .msg-assistant .msg-time {\n display: block;\n margin-top: 8px;\n font-size: 0.72rem;\n color: var(--subtle);\n }\n\n /* ── Tool blocks ──────────────────────────────────────────────────────── */\n\n .tool-block {\n max-width: 92%;\n margin-left: 36px;\n border-radius: 10px;\n overflow: hidden;\n border: 1px solid rgba(255,255,255,0.06);\n box-shadow: 0 2px 8px rgba(0,0,0,0.16);\n margin: 2px 0;\n }\n\n .tool-header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 14px;\n background: var(--tool-header);\n border-bottom: 1px solid rgba(255,255,255,0.06);\n overflow: hidden;\n }\n\n .tool-icon {\n color: var(--tool-accent);\n flex-shrink: 0;\n display: flex;\n align-items: center;\n }\n\n .tool-name {\n flex: 1;\n font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;\n font-size: 0.75rem;\n font-weight: 500;\n color: var(--tool-accent);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n .tool-time {\n flex-shrink: 0;\n font-family: 'JetBrains Mono', ui-monospace, monospace;\n font-size: 0.7rem;\n color: var(--tool-time);\n }\n\n .tool-output {\n display: block;\n padding: 12px 14px;\n background: var(--tool-bg);\n color: var(--tool-text);\n font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;\n font-size: 0.78rem;\n line-height: 1.6;\n white-space: pre-wrap;\n word-break: break-word;\n overflow-x: auto;\n max-height: 400px;\n overflow-y: auto;\n }\n\n .tool-output.tone-ok { color: var(--tool-ok); }\n .tool-output.tone-err { color: var(--tool-err); }\n\n /* ── Markdown blocks ──────────────────────────────────────────────────── */\n\n .markdown-body {\n font-family: 'DM Sans', system-ui, sans-serif;\n font-size: 0.9rem;\n line-height: 1.65;\n word-break: break-word;\n }\n\n .markdown-body > *:first-child { margin-top: 0; }\n .markdown-body > *:last-child { margin-bottom: 0; }\n .markdown-body p,\n .markdown-body ul,\n .markdown-body ol,\n .markdown-body blockquote,\n .markdown-body pre,\n .markdown-body table,\n .markdown-body hr {\n margin: 0 0 0.85em;\n }\n\n .markdown-body h1,\n .markdown-body h2,\n .markdown-body h3,\n .markdown-body h4,\n .markdown-body h5,\n .markdown-body h6 {\n margin: 0 0 0.55em;\n line-height: 1.25;\n font-weight: 700;\n letter-spacing: -0.01em;\n }\n\n .markdown-body h1 { font-size: 1.4rem; }\n .markdown-body h2 { font-size: 1.22rem; }\n .markdown-body h3 { font-size: 1.08rem; }\n .markdown-body h4,\n .markdown-body h5,\n .markdown-body h6 { font-size: 0.95rem; }\n\n .markdown-body ul,\n .markdown-body ol {\n padding-left: 1.3em;\n }\n\n .markdown-body li + li {\n margin-top: 0.22em;\n }\n\n .markdown-body blockquote {\n padding-left: 12px;\n border-left: 3px solid rgba(34, 197, 94, 0.35);\n opacity: 0.95;\n }\n\n .markdown-body a {\n color: inherit;\n text-decoration: underline;\n text-underline-offset: 2px;\n }\n\n .markdown-body code {\n font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;\n font-size: 0.82em;\n padding: 0.16em 0.38em;\n border-radius: 6px;\n }\n\n .markdown-body pre {\n overflow-x: auto;\n border-radius: 12px;\n padding: 12px 14px;\n }\n\n .markdown-body pre code {\n display: block;\n padding: 0;\n border-radius: 0;\n background: transparent;\n font-size: 0.82rem;\n line-height: 1.6;\n }\n\n .markdown-body table {\n width: 100%;\n border-collapse: collapse;\n font-size: 0.85rem;\n }\n\n .markdown-body th,\n .markdown-body td {\n padding: 8px 10px;\n border: 1px solid rgba(0, 0, 0, 0.08);\n text-align: left;\n vertical-align: top;\n }\n\n .markdown-body img {\n max-width: 100%;\n border-radius: 12px;\n }\n\n .markdown-user code {\n background: rgba(255,255,255,0.14);\n color: var(--user-text);\n }\n\n .markdown-user pre {\n background: rgba(255,255,255,0.08);\n border: 1px solid rgba(255,255,255,0.08);\n }\n\n .markdown-user table th,\n .markdown-user table td {\n border-color: rgba(255,255,255,0.16);\n }\n\n .markdown-assistant code {\n background: #f4f4f5;\n color: #27272a;\n }\n\n .markdown-assistant pre {\n background: #0f172a;\n color: #e5e7eb;\n }\n\n .markdown-assistant pre code {\n background: transparent;\n color: inherit;\n }\n\n .markdown-assistant table th,\n .markdown-assistant table td {\n border-color: rgba(0, 0, 0, 0.08);\n }\n\n /* ── System events ────────────────────────────────────────────────────── */\n\n .system-event {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n padding: 10px 0;\n color: var(--subtle);\n font-size: 0.775rem;\n }\n\n .event-dot {\n width: 4px;\n height: 4px;\n border-radius: 50%;\n background: var(--subtle);\n flex-shrink: 0;\n opacity: 0.6;\n }\n\n .event-text {\n color: var(--muted);\n }\n\n .system-event-err .event-text {\n color: var(--err-text);\n }\n\n .event-time {\n color: var(--subtle);\n font-style: normal;\n }\n\n /* ── Status page ──────────────────────────────────────────────────────── */\n\n .stack > * + * { margin-top: 14px; }\n\n p { color: var(--muted); font-size: 0.9rem; line-height: 1.5; }\n\n .status {\n padding: 12px 16px;\n border-radius: 10px;\n font-size: 0.9rem;\n }\n\n .status.err {\n background: var(--err-bg);\n color: var(--err-text);\n border: 1px solid rgba(185, 28, 28, 0.12);\n }\n\n /* ── Composer ─────────────────────────────────────────────────────────── */\n\n .composer-card {\n position: fixed;\n left: calc(72px + (100vw - 72px) / 2);\n bottom: calc(16px + env(safe-area-inset-bottom, 0px));\n transform: translateX(-50%);\n width: min(960px, calc(100vw - 96px));\n padding: 10px 12px 10px 14px;\n border: 1px solid var(--border);\n border-radius: 22px;\n background: rgba(250, 248, 244, 0.92);\n box-shadow: 0 12px 36px rgba(0,0,0,0.10), 0 2px 6px rgba(0,0,0,0.04);\n backdrop-filter: blur(14px);\n -webkit-backdrop-filter: blur(14px);\n z-index: 20;\n }\n\n .composer-form {\n display: flex;\n flex-direction: column;\n gap: 6px;\n }\n\n .jump-latest-btn {\n position: fixed;\n left: calc(72px + (100vw - 72px) / 2);\n bottom: calc(env(safe-area-inset-bottom, 0px) + 120px);\n z-index: 25;\n width: 42px;\n height: 42px;\n border: 1px solid var(--border);\n border-radius: 999px;\n background: var(--bg);\n color: var(--text);\n font: 700 1rem/1 'DM Sans', sans-serif;\n box-shadow: 0 10px 30px rgba(0,0,0,0.12);\n cursor: pointer;\n backdrop-filter: blur(10px);\n transform: translateX(-50%);\n outline: none;\n appearance: none;\n -webkit-tap-highlight-color: transparent;\n }\n\n .jump-latest-btn:hover {\n transform: translateX(-50%) translateY(-1px);\n background: #e8e3d9;\n }\n\n .jump-latest-btn:focus,\n .jump-latest-btn:active {\n outline: none;\n }\n\n .jump-latest-btn:focus-visible {\n box-shadow: 0 10px 30px rgba(0,0,0,0.12), 0 0 0 3px rgba(0,0,0,0.08);\n }\n\n .composer-copy { margin-bottom: 12px; color: var(--muted); }\n\n .composer-form textarea {\n width: 100%;\n resize: none;\n overflow-y: auto;\n min-height: 28px;\n max-height: 200px;\n padding: 6px 6px 2px;\n border: 0;\n border-radius: 0;\n font: inherit;\n color: var(--text);\n background: transparent;\n }\n\n .composer-form textarea::placeholder {\n color: rgba(63,63,70,0.55);\n }\n\n .composer-form textarea:focus {\n outline: none;\n border: 0;\n }\n\n .composer-actions {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n margin-top: 0;\n }\n\n .composer-status { color: var(--muted); font-size: 13px; }\n .composer-actions button:disabled { opacity: 0.55; cursor: wait; }\n\n .composer-send-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 32px;\n height: 32px;\n border: none;\n border-radius: 999px;\n background: #d97706;\n color: #ffffff;\n font: 700 1rem/1 'DM Sans', sans-serif;\n cursor: pointer;\n box-shadow: 0 10px 24px rgba(217, 119, 6, 0.26);\n transition: transform 120ms, filter 120ms, box-shadow 120ms, background 120ms;\n }\n\n .composer-send-btn:hover:not(:disabled) {\n transform: translateY(-1px);\n filter: saturate(1.06) brightness(0.98);\n box-shadow: 0 12px 28px rgba(217, 119, 6, 0.32);\n }\n\n .composer-send-btn:focus-visible {\n outline: 2px solid rgba(217, 119, 6, 0.28);\n outline-offset: 3px;\n }\n\n .composer-send-btn:disabled {\n background: #d4d4d8;\n color: rgba(24, 24, 27, 0.45);\n box-shadow: none;\n transform: none;\n filter: none;\n cursor: not-allowed;\n opacity: 1;\n }\n\n /* ── Responsive ───────────────────────────────────────────────────────── */\n\n @media (max-width: 900px) {\n .composer-card {\n left: 50%;\n width: min(960px, calc(100vw - 24px));\n }\n .jump-latest-btn { left: 50%; }\n }\n\n @media (max-width: 600px) {\n body { padding-bottom: calc(130px + env(safe-area-inset-bottom, 0px)); }\n\n .composer-card {\n width: calc(100vw - 16px);\n bottom: calc(8px + env(safe-area-inset-bottom, 0px));\n padding: 8px 10px;\n border-radius: 18px;\n }\n\n .session-side { align-items: flex-start; flex-direction: row; }\n\n .user-bubble,\n .msg-assistant,\n .tool-block { max-width: 100%; }\n\n .asst-avatar { display: none; }\n\n .asst-card { border-radius: 4px 14px 14px 14px; }\n }\n`;\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"portal.js","sourceRoot":"","sources":["../../src/session-view/portal.ts"],"names":[],"mappings":"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","sourcesContent":["import type { IncomingMessage, ServerResponse } from \"http\";\nimport { basename } from \"path\";\nimport MarkdownIt from \"markdown-it\";\nimport type { Bot, BotAdapters, BotEvent, BotHandler, ChatResponseContext } from \"../adapter.js\";\nimport { escapeHtml } from \"../html.js\";\nimport * as log from \"../log.js\";\nimport { renderPortalShell } from \"../portal-shell.js\";\nimport { reportUserFacingError } from \"../sentry.js\";\nimport { inferConversationKind } from \"../sessions/policy.js\";\nimport {\n loadSessionViewModel,\n resolveRequestedSessionFile,\n type SessionViewItem,\n type SessionViewRelation,\n} from \"./service.js\";\nimport type { InMemorySessionViewTokenStore } from \"./store.js\";\n\nconst markdown = new MarkdownIt({\n html: false,\n linkify: true,\n breaks: true,\n});\n\nconst defaultLinkOpen = markdown.renderer.rules.link_open;\ntype LinkOpenRule = NonNullable<typeof defaultLinkOpen>;\nmarkdown.renderer.rules.link_open = (...args: Parameters<LinkOpenRule>) => {\n const [tokens, idx, options, env, self] = args;\n const token = tokens[idx];\n token.attrSet(\"target\", \"_blank\");\n token.attrSet(\"rel\", \"noreferrer noopener\");\n return defaultLinkOpen\n ? defaultLinkOpen(tokens, idx, options, env, self)\n : self.renderToken(tokens, idx, options);\n};\n\ntype SessionStreamEvent =\n | { type: \"status\"; running: boolean }\n | { type: \"user\"; html: string }\n | { type: \"assistant\"; html: string }\n | { type: \"assistant_remove\" }\n | { type: \"tool\"; html: string }\n | { type: \"system\"; html: string }\n | {\n type: \"refresh\";\n timelineHtml: string;\n updatedAt: string;\n entryCount: number;\n running: boolean;\n }\n | { type: \"error\"; message: string };\n\nclass SessionViewStreamHub {\n private listeners = new Map<string, Set<(event: SessionStreamEvent) => void>>();\n\n subscribe(key: string, listener: (event: SessionStreamEvent) => void): () => void {\n const set = this.listeners.get(key) ?? new Set<(event: SessionStreamEvent) => void>();\n set.add(listener);\n this.listeners.set(key, set);\n return () => {\n const current = this.listeners.get(key);\n if (!current) return;\n current.delete(listener);\n if (current.size === 0) this.listeners.delete(key);\n };\n }\n\n publish(key: string, event: SessionStreamEvent): void {\n const set = this.listeners.get(key);\n if (!set) return;\n for (const listener of set) listener(event);\n }\n}\n\nconst sessionViewStreamHub = new SessionViewStreamHub();\n\nexport interface SessionViewInteractiveOptions {\n handler: BotHandler;\n botsByPlatform: Partial<Record<string, Bot>>;\n}\n\nexport async function handleSessionViewRequest(\n req: IncomingMessage,\n res: ServerResponse,\n url: URL,\n sessionViewTokenStore?: InMemorySessionViewTokenStore,\n interactive?: SessionViewInteractiveOptions,\n): Promise<boolean> {\n if (req.method === \"POST\" && url.pathname === \"/session/message\") {\n await handleSessionMessageRequest(req, res, sessionViewTokenStore, interactive);\n return true;\n }\n\n if (req.method === \"GET\" && url.pathname === \"/session/stream\") {\n await handleSessionStreamRequest(req, res, url, sessionViewTokenStore, interactive);\n return true;\n }\n\n if (req.method !== \"GET\" || url.pathname !== \"/session\") {\n return false;\n }\n\n const token = url.searchParams.get(\"token\")?.trim();\n if (!token || !sessionViewTokenStore) {\n res.writeHead(400, { \"Content-Type\": \"text/html; charset=utf-8\" });\n res.end(\n renderStatusPage(\"Session unavailable\", \"This session link is invalid or has expired.\"),\n );\n return true;\n }\n\n const entry = sessionViewTokenStore.peek(token);\n if (!entry) {\n res.writeHead(400, { \"Content-Type\": \"text/html; charset=utf-8\" });\n res.end(\n renderStatusPage(\"Session unavailable\", \"This session link is invalid or has expired.\"),\n );\n return true;\n }\n\n const requestedSession = url.searchParams.get(\"session\");\n let targetSessionFile: string | null;\n try {\n targetSessionFile = resolveRequestedSessionFile(entry.sessionFile, requestedSession);\n } catch (error) {\n log.logWarning(\n `[${entry.conversationId}] Corrupted session file referenced for ${entry.sessionFile}`,\n error instanceof Error ? error.message : String(error),\n );\n reportUserFacingError(error, {\n domain: \"session_view\",\n surface: \"session_view\",\n operation: \"resolve_requested_session\",\n severity: \"error\",\n platform: entry.platform,\n context: {\n conversationId: entry.conversationId,\n sessionKey: entry.sessionKey,\n sessionFile: basename(entry.sessionFile),\n requestedSession,\n },\n });\n res.writeHead(500, { \"Content-Type\": \"text/html; charset=utf-8\" });\n res.end(\n renderStatusPage(\"Session unavailable\", \"The selected session file appears to be corrupted.\"),\n );\n return true;\n }\n if (!targetSessionFile) {\n res.writeHead(400, { \"Content-Type\": \"text/html; charset=utf-8\" });\n res.end(renderStatusPage(\"Session unavailable\", \"The selected session link is invalid.\"));\n return true;\n }\n\n try {\n const model = loadSessionViewModel(targetSessionFile);\n const displayedSessionKey = resolveDisplayedSessionKey(entry, targetSessionFile);\n const isRunning = interactive?.handler.isRunning(displayedSessionKey) ?? false;\n res.writeHead(200, {\n \"Content-Type\": \"text/html; charset=utf-8\",\n \"Cache-Control\": \"no-store\",\n });\n res.end(\n renderSessionPage(\n model,\n entry.token,\n entry.expiresAt,\n isRunning,\n displayedSessionKey,\n entry.conversationId,\n ),\n );\n } catch (error) {\n log.logWarning(\n `[${entry.conversationId}] Failed to render session ${entry.sessionFile}`,\n error instanceof Error ? error.message : String(error),\n );\n reportUserFacingError(error, {\n domain: \"session_view\",\n surface: \"session_view\",\n operation: \"render_session\",\n severity: \"error\",\n platform: entry.platform,\n context: {\n conversationId: entry.conversationId,\n sessionKey: entry.sessionKey,\n sessionFile: basename(targetSessionFile),\n },\n });\n res.writeHead(500, { \"Content-Type\": \"text/html; charset=utf-8\" });\n res.end(renderStatusPage(\"Session unavailable\", \"The session could not be loaded right now.\"));\n }\n\n return true;\n}\n\nfunction resolveDisplayedSessionKey(\n entry: { platform: string; conversationId: string; sessionKey: string },\n sessionFile: string,\n): string {\n if (entry.platform === \"slack\") {\n const fileName = basename(sessionFile, \".jsonl\");\n if (/^\\d+\\.\\d+$/.test(fileName)) {\n return `${entry.conversationId}:${fileName}`;\n }\n return entry.conversationId;\n }\n return entry.sessionKey;\n}\n\nfunction sessionStreamKey(entry: {\n platform: string;\n conversationId: string;\n sessionKey: string;\n}): string {\n return `${entry.platform}:${entry.conversationId}:${entry.sessionKey}`;\n}\n\nfunction renderTimelineItems(items: SessionViewItem[], token: string): string {\n return items.length > 0\n ? items.map((item) => renderItem(item, token)).join(\"\\n\")\n : `<div class=\"system-event\"><span class=\"event-dot\"></span><span class=\"event-text\">No messages yet — send one to the bot, then refresh.</span></div>`;\n}\n\nfunction renderSessionPage(\n model: {\n title: string;\n sessionId: string;\n fileName: string;\n createdAt: string;\n updatedAt: string;\n entryCount: number;\n items: SessionViewItem[];\n parent?: SessionViewRelation;\n threads: SessionViewRelation[];\n },\n token: string,\n expiresAt: number,\n isRunning: boolean,\n displayedSessionKey: string,\n conversationId: string,\n): string {\n const items = renderTimelineItems(model.items, token);\n\n const relatedSections = model.parent\n ? `<section class=\"related-card stack\">\n <p class=\"eyebrow\">Parent session</p>\n ${renderRelationCard(model.parent, token)}\n </section>`\n : \"\";\n\n const body = `<header class=\"page-head\">\n <div>\n <p class=\"eyebrow\">Session</p>\n <h2 class=\"page-title\">${esc(model.title)}</h2>\n <p class=\"page-desc\">\n <span>Created ${esc(formatDate(model.createdAt))}</span> ·\n <span>Updated <strong data-session-updated>${esc(formatDate(model.updatedAt))}</strong></span> ·\n <span><strong data-session-entries>${esc(String(model.entryCount))}</strong> entries</span>\n </p>\n </div>\n <div class=\"session-side\">\n <span class=\"session-badge session-badge-status${isRunning ? \" is-running\" : \"\"}\"><span class=\"session-badge-dot\"></span><strong data-session-status>${esc(isRunning ? \"Running\" : \"Idle\")}</strong></span>\n <span class=\"session-badge\">${esc(displayedSessionKey === conversationId ? \"Channel\" : \"Thread\")}</span>\n </div>\n </header>\n\n <div class=\"session-detail-row\">\n <span class=\"session-detail\"><span class=\"session-detail-label\">Session</span><code>${esc(model.sessionId.slice(0, 8))}</code></span>\n <span class=\"session-detail\"><span class=\"session-detail-label\">File</span><code>${esc(model.fileName)}</code></span>\n <span class=\"session-detail\"><span class=\"session-detail-label\">Expires</span><span>${esc(formatDate(new Date(expiresAt).toISOString()))}</span></span>\n </div>\n\n ${relatedSections}\n\n <div class=\"timeline-shell\">\n <div class=\"timeline-list\" data-timeline-list>\n ${items}\n </div>\n </div>\n\n <button class=\"jump-latest-btn\" type=\"button\" hidden data-jump-latest aria-label=\"Jump to latest\" title=\"Jump to latest\">↓</button>\n\n <section class=\"composer-card\">\n <form class=\"composer-form\" data-session-composer>\n <input type=\"hidden\" name=\"token\" value=\"${esc(token)}\">\n <input type=\"hidden\" name=\"session\" value=\"${esc(model.fileName)}\">\n <input type=\"hidden\" name=\"sessionKey\" value=\"${esc(displayedSessionKey)}\">\n <textarea name=\"text\" rows=\"1\" placeholder=\"Ask mikan in this session… (replies stay in Session View)\" required></textarea>\n <div class=\"composer-actions\">\n <span class=\"composer-status\" data-composer-status></span>\n <button class=\"composer-send-btn\" type=\"submit\" aria-label=\"Send\" title=\"Send\">↑</button>\n </div>\n </form>\n </section>`;\n\n return renderHtmlDocument(`${model.title} · Session Viewer`, body, isRunning);\n}\n\nfunction renderRelationCard(relation: SessionViewRelation, token: string): string {\n const href = `/session?token=${encodeURIComponent(token)}&session=${encodeURIComponent(relation.fileName)}`;\n const summary = relation.summary ? `<p class=\"related-summary\">${esc(relation.summary)}</p>` : \"\";\n return `<a class=\"related-link\" href=\"${href}\">\n <span class=\"related-copy\">\n <strong class=\"related-title\">${esc(relation.title)}</strong>\n ${summary}\n <span class=\"related-meta\">${esc(formatDate(relation.updatedAt))} · ${esc(String(relation.entryCount))} entries · ${esc(relation.fileName)}</span>\n </span>\n <span class=\"related-arrow\" aria-hidden=\"true\">→</span>\n </a>`;\n}\n\nfunction renderThreadLinks(relations: SessionViewRelation[] | undefined, token: string): string {\n if (!relations || relations.length === 0) return \"\";\n return `<div class=\"thread-links\">${relations\n .map((relation) => {\n const href = `/session?token=${encodeURIComponent(token)}&session=${encodeURIComponent(relation.fileName)}`;\n return `<a class=\"thread-link\" href=\"${href}\" title=\"Open ${esc(relation.title)}\">\n <span class=\"thread-dot\" aria-hidden=\"true\"></span>\n <span class=\"thread-text\">Thread</span>\n </a>`;\n })\n .join(\"\")}</div>`;\n}\n\nexport function parseUserBody(raw: string): {\n timestamp: string | null;\n username: string | null;\n threadTs: string | null;\n header: string | null;\n content: string;\n} {\n // [timestamp] [username] [in-thread:ts]: content\n let m = raw.match(\n /^\\[([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}[+-][0-9]{2}:[0-9]{2})\\]\\s*\\[([^\\]]+)\\](?:\\s*\\[in-thread:([^\\]]+)\\])?:\\s*([\\s\\S]*)$/,\n );\n if (m) {\n const header = [`[${m[1]}]`, `[${m[2]}]`, m[3] ? `[in-thread:${m[3]}]` : \"\"]\n .filter(Boolean)\n .join(\" \");\n return {\n timestamp: m[1],\n username: m[2],\n threadTs: m[3] ?? null,\n header,\n content: m[4],\n };\n }\n // [username] [in-thread:ts]: content\n m = raw.match(/^\\[([^\\]]+)\\](?:\\s*\\[in-thread:([^\\]]+)\\])?:\\s*([\\s\\S]*)$/);\n if (m) {\n const header = [`[${m[1]}]`, m[2] ? `[in-thread:${m[2]}]` : \"\"].filter(Boolean).join(\" \");\n return {\n timestamp: null,\n username: m[1],\n threadTs: m[2] ?? null,\n header,\n content: m[3],\n };\n }\n return { timestamp: null, username: null, threadTs: null, header: null, content: raw };\n}\n\ntype ParsedUserBody = ReturnType<typeof parseUserBody>;\n\nfunction renderCopyButton(label = \"Copy message\"): string {\n return `<div class=\"msg-actions\"><button class=\"copy-action-btn\" type=\"button\" data-copy-button data-copy-label=\"${esc(label)}\" aria-label=\"${esc(label)}\" title=\"${esc(label)}\"><svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\"><rect x=\"9\" y=\"9\" width=\"11\" height=\"11\" rx=\"2\" stroke=\"currentColor\" stroke-width=\"1.8\"></rect><path d=\"M6 15H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v1\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\"></path></svg></button></div>`;\n}\n\nfunction renderItem(item: SessionViewItem, token?: string): string {\n if (item.kind === \"system\") {\n const parts = [item.title, item.body].filter((x): x is string => Boolean(x)).map(esc);\n const time = item.meta\n ? ` · <time class=\"event-time\">${esc(formatDate(item.meta))}</time>`\n : \"\";\n return `<div class=\"system-event\"><span class=\"event-dot\"></span><span class=\"event-text\">${parts.join(\" — \")}</span>${time}</div>`;\n }\n\n if (item.kind === \"tool\") {\n const toneClass = item.tone === \"err\" ? \" tone-err\" : item.tone === \"ok\" ? \" tone-ok\" : \"\";\n const body = item.body ? `<pre class=\"tool-output${toneClass}\">${esc(item.body)}</pre>` : \"\";\n const time = item.meta ? `<time class=\"tool-time\">${esc(formatDate(item.meta))}</time>` : \"\";\n return `<div class=\"tool-block\">\n <div class=\"tool-header\">\n <span class=\"tool-icon\"><svg width=\"10\" height=\"10\" viewBox=\"0 0 10 10\" fill=\"none\"><path d=\"M1.5 2L5 5.5 1.5 9\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M6 9h2.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/></svg></span>\n <span class=\"tool-name\">${esc(item.title)}</span>\n ${time}\n </div>\n ${body}\n</div>`;\n }\n\n const time = item.meta ? `<time class=\"msg-time\">${esc(formatDate(item.meta))}</time>` : \"\";\n\n if (item.kind === \"user\") {\n const parsed: ParsedUserBody = item.body\n ? parseUserBody(item.body)\n : { timestamp: null, username: null, threadTs: null, header: null, content: \"\" };\n const { username, threadTs, header, content } = parsed;\n const initial = username ? esc(username.slice(0, 2).toUpperCase()) : \"U\";\n const rawHeader = header ? `<div class=\"msg-raw-header\">${esc(header)}</div>` : \"\";\n const body = content ? renderMarkdownBlock(content, \"user\") : \"\";\n const threadBadge = threadTs\n ? `<div class=\"thread-badge\" title=\"Thread ${esc(threadTs)}\">Thread · <code>${esc(threadTs)}</code></div>`\n : \"\";\n const threads = renderThreadLinks(item.threads, token ?? \"\");\n return `<div class=\"msg-row msg-user copy-host\">\n <div class=\"msg-main user-main\">\n <div class=\"user-bubble\">\n ${rawHeader}\n ${threadBadge}\n ${body}\n ${threads}\n ${time}\n </div>\n ${renderCopyButton()}\n </div>\n <div class=\"msg-avatar user-avatar\" title=\"${username ? esc(username) : \"User\"}\">${initial}</div>\n</div>`;\n }\n\n // assistant\n const body = item.body ? renderMarkdownBlock(item.body, \"assistant\") : \"\";\n const threads = renderThreadLinks(item.threads, token ?? \"\");\n return `<div class=\"msg-row msg-assistant copy-host\">\n <div class=\"msg-avatar asst-avatar\" aria-hidden=\"true\">A</div>\n <div class=\"msg-main asst-main\">\n <div class=\"asst-card\">\n ${body}\n ${threads}\n ${time}\n </div>\n ${renderCopyButton()}\n </div>\n</div>`;\n}\n\nfunction renderMarkdownBlock(text: string, variant: \"user\" | \"assistant\"): string {\n return `<div class=\"msg-body markdown-body markdown-${variant}\">${markdown.render(text)}</div>`;\n}\n\nfunction renderLiveUserMessage(text: string, userName: string): string {\n const initial = esc(userName.slice(0, 2).toUpperCase());\n return `<div class=\"msg-row msg-user copy-host\" data-live-item>\n <div class=\"msg-main user-main\">\n <div class=\"user-bubble\">\n ${renderMarkdownBlock(text, \"user\")}\n </div>\n ${renderCopyButton()}\n </div>\n <div class=\"msg-avatar user-avatar\" title=\"${esc(userName)}\">${initial}</div>\n</div>`;\n}\n\nfunction renderLiveAssistantMessage(text: string): string {\n return `<div class=\"msg-row msg-assistant copy-host\" data-live-assistant>\n <div class=\"msg-avatar asst-avatar\" aria-hidden=\"true\">A</div>\n <div class=\"msg-main asst-main\">\n <div class=\"asst-card\">\n ${renderMarkdownBlock(text, \"assistant\")}\n </div>\n ${renderCopyButton()}\n </div>\n</div>`;\n}\n\nfunction renderLiveToolResult(result: {\n toolName: string;\n result: string;\n isError: boolean;\n}): string {\n const toneClass = result.isError ? \" tone-err\" : \" tone-ok\";\n return `<div class=\"tool-block\" data-live-item>\n <div class=\"tool-header\">\n <span class=\"tool-icon\"><svg width=\"10\" height=\"10\" viewBox=\"0 0 10 10\" fill=\"none\"><path d=\"M1.5 2L5 5.5 1.5 9\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M6 9h2.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/></svg></span>\n <span class=\"tool-name\">${esc(result.toolName)}</span>\n </div>\n <pre class=\"tool-output${toneClass}\">${esc(result.result)}</pre>\n</div>`;\n}\n\nfunction renderLiveSystemEvent(text: string, tone: \"default\" | \"err\" = \"default\"): string {\n const cls = tone === \"err\" ? \" system-event-err\" : \"\";\n return `<div class=\"system-event${cls}\" data-live-item><span class=\"event-dot\"></span><span class=\"event-text\">${esc(text)}</span></div>`;\n}\n\nasync function handleSessionStreamRequest(\n req: IncomingMessage,\n res: ServerResponse,\n url: URL,\n sessionViewTokenStore?: InMemorySessionViewTokenStore,\n interactive?: SessionViewInteractiveOptions,\n): Promise<void> {\n const token = url.searchParams.get(\"token\")?.trim() ?? \"\";\n if (!token || !sessionViewTokenStore || !interactive) {\n res.writeHead(400, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(\"Session stream unavailable\");\n return;\n }\n\n const entry = sessionViewTokenStore.peek(token);\n if (!entry) {\n res.writeHead(400, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(\"Invalid session token\");\n return;\n }\n\n const requestedSession = url.searchParams.get(\"session\");\n let targetSessionFile: string | null;\n try {\n targetSessionFile = resolveRequestedSessionFile(entry.sessionFile, requestedSession);\n } catch (error) {\n reportUserFacingError(error, {\n domain: \"session_view\",\n surface: \"session_view\",\n operation: \"session_stream\",\n severity: \"error\",\n platform: entry.platform,\n context: {\n conversationId: entry.conversationId,\n sessionKey: entry.sessionKey,\n sessionFile: basename(entry.sessionFile),\n requestedSession,\n },\n });\n res.writeHead(500, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(\"Session stream unavailable\");\n return;\n }\n if (!targetSessionFile) {\n res.writeHead(400, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(\"Invalid session file\");\n return;\n }\n const activeSessionKey = resolveDisplayedSessionKey(entry, targetSessionFile);\n const streamKey = sessionStreamKey({ ...entry, sessionKey: activeSessionKey });\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-store\",\n Connection: \"keep-alive\",\n });\n res.write(\n `data: ${JSON.stringify({ type: \"status\", running: interactive.handler.isRunning(activeSessionKey) })}\\n\\n`,\n );\n\n const unsubscribe = sessionViewStreamHub.subscribe(streamKey, (event) => {\n res.write(`data: ${JSON.stringify(event)}\\n\\n`);\n });\n const heartbeat = setInterval(() => {\n res.write(\": keep-alive\\n\\n\");\n }, 15000);\n\n req.on(\"close\", () => {\n clearInterval(heartbeat);\n unsubscribe();\n });\n}\n\nasync function handleSessionMessageRequest(\n req: IncomingMessage,\n res: ServerResponse,\n sessionViewTokenStore?: InMemorySessionViewTokenStore,\n interactive?: SessionViewInteractiveOptions,\n): Promise<void> {\n if (!sessionViewTokenStore || !interactive) {\n json(res, 503, { ok: false, error: \"Session chat is not configured.\" });\n return;\n }\n\n let body: { token?: string; text?: string; session?: string; sessionKey?: string };\n try {\n body = JSON.parse(await readRequestBody(req)) as {\n token?: string;\n text?: string;\n session?: string;\n sessionKey?: string;\n };\n } catch {\n json(res, 400, { ok: false, error: \"Invalid request body.\" });\n return;\n }\n\n const token = body.token?.trim() ?? \"\";\n const text = body.text?.trim() ?? \"\";\n const requestedSession = body.session?.trim() || null;\n const requestedSessionKey = body.sessionKey?.trim() || \"\";\n if (!token || !text) {\n json(res, 400, { ok: false, error: \"Missing token or text.\" });\n return;\n }\n\n const entry = sessionViewTokenStore.peek(token);\n if (!entry) {\n json(res, 400, { ok: false, error: \"This session link is invalid or has expired.\" });\n return;\n }\n\n let targetSessionFile: string | null;\n try {\n targetSessionFile = resolveRequestedSessionFile(entry.sessionFile, requestedSession);\n } catch (error) {\n reportUserFacingError(error, {\n domain: \"session_view\",\n surface: \"session_view\",\n operation: \"session_message\",\n severity: \"error\",\n platform: entry.platform,\n context: {\n conversationId: entry.conversationId,\n sessionKey: entry.sessionKey,\n sessionFile: basename(entry.sessionFile),\n requestedSession,\n },\n });\n json(res, 500, { ok: false, error: \"Session file could not be loaded.\" });\n return;\n }\n if (!targetSessionFile) {\n json(res, 400, { ok: false, error: \"Invalid session file.\" });\n return;\n }\n const activeSessionKey = resolveDisplayedSessionKey(entry, targetSessionFile);\n if (requestedSessionKey && requestedSessionKey !== activeSessionKey) {\n json(res, 400, { ok: false, error: \"Session target mismatch.\" });\n return;\n }\n\n const bot = interactive.botsByPlatform[entry.platform];\n if (!bot) {\n json(res, 503, { ok: false, error: `No bot configured for ${entry.platform}.` });\n return;\n }\n\n const streamKey = sessionStreamKey({ ...entry, sessionKey: activeSessionKey });\n const conversationKind = inferConversationKind(entry.platform, entry.conversationId);\n const ts = (Date.now() / 1000).toFixed(6);\n const platformInfo = bot.getPlatformInfo();\n const platformUserName =\n entry.platformUserName ||\n platformInfo.users.find((user) => user.id === entry.platformUserId)?.userName ||\n platformInfo.users.find((user) => user.id === entry.platformUserId)?.displayName ||\n \"unknown\";\n const responseCtx = createSessionViewResponseContext((event) => {\n sessionViewStreamHub.publish(streamKey, event);\n });\n const event: BotEvent = {\n type: \"session_view\",\n conversationId: entry.conversationId,\n conversationKind,\n ts,\n user: entry.platformUserId,\n text,\n attachments: [],\n sessionKey: activeSessionKey,\n ...(activeSessionKey.includes(\":\")\n ? { thread_ts: activeSessionKey.split(\":\").slice(1).join(\":\") }\n : {}),\n };\n const adapters: BotAdapters = {\n message: {\n id: ts,\n sessionKey: activeSessionKey,\n conversationKind,\n userId: entry.platformUserId,\n userName: platformUserName,\n text,\n attachments: [],\n threadTs: event.thread_ts,\n },\n responseCtx,\n platform: { ...platformInfo, diagnostics: { showUsageSummary: false } },\n };\n\n sessionViewStreamHub.publish(streamKey, { type: \"status\", running: true });\n sessionViewStreamHub.publish(streamKey, {\n type: \"user\",\n html: renderLiveUserMessage(text, platformUserName),\n });\n\n void interactive.handler\n .handleEvent(event, bot, adapters)\n .then(() => {\n if (!targetSessionFile) {\n sessionViewStreamHub.publish(streamKey, { type: \"status\", running: false });\n return;\n }\n const model = loadSessionViewModel(targetSessionFile);\n sessionViewStreamHub.publish(streamKey, {\n type: \"refresh\",\n timelineHtml: renderTimelineItems(model.items, token),\n updatedAt: formatDate(model.updatedAt),\n entryCount: model.entryCount,\n running: false,\n });\n })\n .catch((error) => {\n log.logWarning(\n `[${entry.conversationId}] Session view message failed`,\n error instanceof Error ? error.message : String(error),\n );\n reportUserFacingError(error, {\n domain: \"session_view\",\n surface: \"session_view\",\n operation: \"interactive_message\",\n severity: \"error\",\n platform: entry.platform,\n context: {\n conversationId: entry.conversationId,\n sessionKey: activeSessionKey,\n messageId: ts,\n textLength: text.length,\n },\n });\n sessionViewStreamHub.publish(streamKey, {\n type: \"error\",\n message: error instanceof Error ? error.message : String(error),\n });\n sessionViewStreamHub.publish(streamKey, { type: \"status\", running: false });\n });\n\n json(res, 202, { ok: true, accepted: true });\n}\n\nfunction createSessionViewResponseContext(\n publish: (event: SessionStreamEvent) => void,\n): ChatResponseContext {\n let accumulatedText = \"\";\n\n return {\n respond: async (text: string) => {\n accumulatedText = accumulatedText ? `${accumulatedText}\\n${text}` : text;\n publish({ type: \"assistant\", html: renderLiveAssistantMessage(accumulatedText) });\n },\n replaceResponse: async (text: string) => {\n accumulatedText = text;\n publish({ type: \"assistant\", html: renderLiveAssistantMessage(accumulatedText) });\n },\n respondDiagnostic: async (text: string, options?: { style?: \"muted\" | \"error\" }) => {\n if (options?.style === \"error\") {\n publish({ type: \"system\", html: renderLiveSystemEvent(text, \"err\") });\n }\n },\n respondToolResult: async (result) => {\n publish({ type: \"tool\", html: renderLiveToolResult(result) });\n },\n setTyping: async () => {\n publish({ type: \"status\", running: true });\n },\n setWorking: async (working: boolean) => {\n publish({ type: \"status\", running: working });\n },\n uploadFile: async () => {},\n deleteResponse: async () => {\n accumulatedText = \"\";\n publish({ type: \"assistant_remove\" });\n },\n };\n}\n\nfunction readRequestBody(req: IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n let data = \"\";\n req.setEncoding(\"utf8\");\n req.on(\"data\", (chunk) => {\n data += chunk;\n if (data.length > 1024 * 1024) {\n reject(new Error(\"Request body too large\"));\n req.destroy();\n }\n });\n req.on(\"end\", () => resolve(data));\n req.on(\"error\", reject);\n });\n}\n\nfunction json(res: ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, {\n \"Content-Type\": \"application/json; charset=utf-8\",\n \"Cache-Control\": \"no-store\",\n });\n res.end(JSON.stringify(body));\n}\n\nfunction renderStatusPage(title: string, message: string): string {\n return renderHtmlDocument(\n title,\n `<section class=\"card stack\">\n <p class=\"eyebrow\">mikan</p>\n <h1 class=\"page-title\">${esc(title)}</h1>\n <div class=\"status err\">${esc(message)}</div>\n </section>`,\n false,\n );\n}\n\nfunction renderHtmlDocument(title: string, shellContent: string, isRunning: boolean): string {\n return renderPortalShell({\n activeView: \"session\",\n pageTitle: \"Session\",\n body: shellContent,\n extraStyles: sessionViewStyles,\n bodyAttributes: { \"data-session-running\": isRunning ? \"true\" : \"false\" },\n inlineScript: sessionViewScript,\n });\n}\n\nconst sessionViewScript = `\n const form = document.querySelector('[data-session-composer]');\n const timelineList = document.querySelector('[data-timeline-list]');\n const jumpLatestBtn = document.querySelector('[data-jump-latest]');\n const statusEl = document.querySelector('[data-session-status]');\n const updatedEl = document.querySelector('[data-session-updated]');\n const entriesEl = document.querySelector('[data-session-entries]');\n const composerStatus = form?.querySelector('[data-composer-status]');\n const textarea = form?.querySelector('textarea[name=\"text\"]');\n const submitButton = form?.querySelector('button[type=\"submit\"]');\n let liveAssistant = null;\n let running = document.body.dataset.sessionRunning === 'true';\n\n const isNearBottom = () => window.innerHeight + window.scrollY >= document.body.offsetHeight - 120;\n const scrollToLatest = (behavior = 'smooth') => window.scrollTo({ top: document.body.scrollHeight, behavior });\n const toggleJumpButton = () => {\n if (!jumpLatestBtn) return;\n jumpLatestBtn.hidden = isNearBottom();\n };\n const updateFollowState = () => {\n if (isNearBottom()) scrollToLatest('smooth');\n else toggleJumpButton();\n };\n const canSubmit = () => Boolean(textarea && textarea.value.trim()) && !running;\n const updateSubmitButtonState = () => {\n if (submitButton) submitButton.disabled = !canSubmit();\n };\n const setRunning = (value) => {\n running = value;\n document.body.dataset.sessionRunning = value ? 'true' : 'false';\n if (statusEl) statusEl.textContent = value ? 'Running' : 'Idle';\n updateSubmitButtonState();\n if (composerStatus && !value && composerStatus.textContent === 'Thinking…') {\n composerStatus.textContent = '';\n }\n };\n\n jumpLatestBtn?.addEventListener('click', () => {\n scrollToLatest('smooth');\n toggleJumpButton();\n });\n document.addEventListener('click', async (event) => {\n const button = event.target instanceof Element ? event.target.closest('[data-copy-button]') : null;\n if (!(button instanceof HTMLButtonElement)) return;\n const label = button.dataset.copyLabel || 'Copy message';\n const source = button.closest('.msg-actions')?.previousElementSibling;\n const text = source instanceof HTMLElement ? (source.innerText || source.textContent || '').trim() : '';\n if (!text) return;\n const setState = (state, transient) => {\n button.dataset.copyState = state;\n button.title = transient;\n button.setAttribute('aria-label', transient);\n window.setTimeout(() => {\n if (!button.isConnected) return;\n delete button.dataset.copyState;\n button.title = label;\n button.setAttribute('aria-label', label);\n }, 1200);\n };\n try {\n await navigator.clipboard.writeText(text);\n setState('done', 'Copied');\n } catch {\n setState('error', 'Copy failed');\n }\n });\n window.addEventListener('scroll', toggleJumpButton, { passive: true });\n\n if (textarea) {\n const resize = () => {\n textarea.style.height = 'auto';\n textarea.style.height = Math.min(textarea.scrollHeight, 240) + 'px';\n };\n textarea.addEventListener('input', () => {\n resize();\n updateSubmitButtonState();\n });\n textarea.addEventListener('keydown', (event) => {\n if (event.key !== 'Enter' || event.shiftKey) return;\n if (event.isComposing || event.keyCode === 229) return;\n event.preventDefault();\n if (!running) form?.requestSubmit();\n });\n resize();\n }\n\n setRunning(running);\n updateSubmitButtonState();\n\n const streamUrl = form\n ? '/session/stream?token=' + encodeURIComponent(form.token.value) + '&session=' + encodeURIComponent(form.session.value)\n : null;\n if (streamUrl) {\n const source = new EventSource(streamUrl);\n source.onmessage = (event) => {\n const payload = JSON.parse(event.data);\n switch (payload.type) {\n case 'status':\n setRunning(Boolean(payload.running));\n if (payload.running && composerStatus) composerStatus.textContent = 'Thinking…';\n break;\n case 'user':\n case 'tool':\n case 'system': {\n timelineList?.insertAdjacentHTML('beforeend', payload.html);\n updateFollowState();\n break;\n }\n case 'assistant': {\n if (!liveAssistant || !liveAssistant.isConnected) {\n timelineList?.insertAdjacentHTML('beforeend', payload.html);\n liveAssistant = timelineList?.querySelector('[data-live-assistant]:last-of-type') || null;\n } else {\n liveAssistant.outerHTML = payload.html;\n liveAssistant = timelineList?.querySelector('[data-live-assistant]:last-of-type') || null;\n }\n updateFollowState();\n break;\n }\n case 'assistant_remove':\n if (liveAssistant?.isConnected) liveAssistant.remove();\n liveAssistant = null;\n break;\n case 'refresh':\n if (timelineList) timelineList.innerHTML = payload.timelineHtml;\n liveAssistant = null;\n if (updatedEl) updatedEl.textContent = payload.updatedAt;\n if (entriesEl) entriesEl.textContent = String(payload.entryCount);\n setRunning(Boolean(payload.running));\n if (composerStatus) composerStatus.textContent = '';\n updateFollowState();\n break;\n case 'error':\n if (composerStatus) composerStatus.textContent = payload.message || 'Something went wrong';\n setRunning(false);\n break;\n }\n };\n }\n\n form?.addEventListener('submit', async (event) => {\n event.preventDefault();\n if (!textarea || !composerStatus) return;\n const text = textarea.value.trim();\n if (!text || running) return;\n composerStatus.textContent = 'Sending…';\n updateSubmitButtonState();\n try {\n const response = await fetch('/session/message', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ token: form.token.value, session: form.session.value, sessionKey: form.sessionKey.value, text }),\n });\n const payload = await response.json();\n if (!response.ok || !payload.ok) throw new Error(payload.error || 'Request failed');\n textarea.value = '';\n textarea.style.height = 'auto';\n composerStatus.textContent = 'Thinking…';\n setRunning(true);\n updateSubmitButtonState();\n scrollToLatest('smooth');\n } catch (err) {\n composerStatus.textContent = err && err.message ? err.message : String(err);\n submitButton.disabled = false;\n }\n });\n\n toggleJumpButton();\n`;\n\nfunction formatDate(value: string): string {\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) return value;\n return date.toLocaleString();\n}\n\nconst esc = escapeHtml;\n\nconst sessionViewStyles = `\n :root {\n --user-bg: #18181b;\n --user-text: #fafafa;\n --user-time: rgba(250, 250, 250, 0.5);\n\n --asst-border: #22c55e;\n --asst-avatar-bg: #f0fdf4;\n --asst-avatar-text: #16a34a;\n\n --tool-bg: #0d1117;\n --tool-header: #161b22;\n --tool-text: #c9d1d9;\n --tool-accent: #58a6ff;\n --tool-ok: #3fb950;\n --tool-err: #f85149;\n --tool-time: #484f58;\n }\n\n body {\n /* Extra bottom padding for the fixed composer */\n padding-bottom: calc(140px + env(safe-area-inset-bottom, 0px));\n overflow-x: hidden;\n }\n\n /* ── Session-specific page-head extras ─────────────────────────────── */\n\n .session-side {\n display: flex; flex-direction: column; align-items: flex-end; gap: 8px;\n flex-shrink: 0;\n }\n .session-badge {\n display: inline-flex; align-items: center; gap: 8px;\n padding: 6px 11px; border: 1px solid var(--border); border-radius: 999px;\n background: rgba(255,255,255,0.7); font-size: 0.78rem; color: var(--muted);\n line-height: 1;\n }\n .session-badge strong { color: var(--text); font-weight: 600; }\n .session-badge-status.is-running {\n background: #fff7ed; border-color: rgba(217, 119, 6, 0.18); color: #9a3412;\n }\n .session-badge-dot {\n width: 7px; height: 7px; border-radius: 50%;\n background: #a1a1aa; flex-shrink: 0;\n }\n .session-badge-status.is-running .session-badge-dot {\n background: #d97706; box-shadow: 0 0 0 4px rgba(217, 119, 6, 0.14);\n }\n\n .session-detail-row {\n display: flex; flex-wrap: wrap; gap: 8px;\n padding: 12px 14px; border: 1px solid var(--border); border-radius: 14px;\n background: rgba(255,255,255,0.6);\n }\n .session-detail {\n display: inline-flex; align-items: center; gap: 8px; min-width: 0;\n padding: 4px 10px; border-radius: 10px; background: rgba(0, 0, 0, 0.025);\n color: var(--muted); font-size: 0.78rem;\n }\n .session-detail-label {\n text-transform: uppercase; letter-spacing: 0.08em;\n font-size: 0.68rem; color: var(--subtle);\n }\n .session-detail code {\n min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n font-family: 'JetBrains Mono', ui-monospace, monospace; font-size: 0.74rem;\n color: var(--text); padding: 0;\n }\n\n /* ── Timeline shell ───────────────────────────────────────────────────── */\n\n .thread-links {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n margin-top: 10px;\n }\n\n .thread-link {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 5px 10px;\n border-radius: 999px;\n border: 1px solid rgba(239, 68, 68, 0.18);\n background: rgba(254, 242, 242, 0.95);\n color: #b91c1c;\n text-decoration: none;\n font-size: 0.74rem;\n font-weight: 600;\n line-height: 1;\n transition: transform 120ms, background 120ms, border-color 120ms;\n }\n\n .thread-link:hover {\n transform: translateY(-1px);\n background: #fff1f2;\n border-color: rgba(239, 68, 68, 0.28);\n }\n\n .thread-dot {\n width: 7px;\n height: 7px;\n border-radius: 50%;\n background: #ef4444;\n box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.12);\n flex-shrink: 0;\n }\n\n .thread-text {\n white-space: nowrap;\n }\n\n .related-card {\n padding: 18px 20px;\n border: 1px solid var(--border);\n border-radius: 18px;\n background: rgba(255,255,255,0.78);\n box-shadow: 0 1px 2px rgba(0,0,0,0.04), 0 4px 16px rgba(0,0,0,0.04);\n backdrop-filter: blur(12px);\n }\n\n .related-list {\n display: flex;\n flex-direction: column;\n gap: 10px;\n }\n\n .related-link {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n padding: 12px 14px;\n border-radius: 14px;\n border: 1px solid var(--border);\n background: rgba(255,255,255,0.82);\n color: inherit;\n text-decoration: none;\n transition: transform 120ms, border-color 120ms, box-shadow 120ms, background 120ms;\n }\n\n .related-link:hover {\n transform: translateY(-1px);\n border-color: rgba(0,0,0,0.16);\n background: #fff;\n box-shadow: 0 8px 18px rgba(0,0,0,0.05);\n }\n\n .related-copy {\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n }\n\n .related-title {\n color: var(--text);\n font-size: 0.94rem;\n line-height: 1.3;\n }\n\n .related-summary {\n color: var(--muted);\n font-size: 0.82rem;\n line-height: 1.45;\n }\n\n .related-meta {\n color: var(--subtle);\n font-size: 0.74rem;\n line-height: 1.4;\n }\n\n .related-arrow {\n flex-shrink: 0;\n color: var(--subtle);\n font-size: 1rem;\n }\n\n .timeline-shell {\n padding: 20px 0;\n }\n\n .timeline-list {\n display: flex;\n flex-direction: column;\n gap: 14px;\n min-width: 0;\n }\n\n .copy-host {\n position: relative;\n }\n\n .msg-actions {\n height: 32px;\n display: flex;\n align-items: center;\n gap: 8px;\n margin-top: 8px;\n opacity: 0;\n visibility: hidden;\n transition: opacity 140ms ease, visibility 140ms ease;\n }\n\n .copy-host:hover .msg-actions,\n .copy-host .msg-actions:hover,\n .copy-host:focus-within .msg-actions,\n .timeline-list > .copy-host:last-child .msg-actions,\n .copy-action-btn[data-copy-state] {\n opacity: 1;\n visibility: visible;\n }\n\n .copy-action-btn {\n width: 24px;\n height: 24px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: 0;\n border-radius: 0;\n background: transparent;\n color: rgba(63,63,70,0.8);\n transition: color 140ms ease, opacity 140ms ease;\n cursor: pointer;\n padding: 0;\n appearance: none;\n }\n\n .copy-action-btn:hover {\n background: transparent;\n color: rgba(24,24,27,0.96);\n border-color: transparent;\n }\n\n .copy-action-btn[data-copy-state='done'] {\n background: transparent;\n border-color: transparent;\n color: rgba(24,24,27,0.96);\n }\n\n .copy-action-btn[data-copy-state='done'] svg {\n position: absolute;\n opacity: 0;\n transform: scale(0.6);\n pointer-events: none;\n }\n\n .copy-action-btn svg {\n transition: opacity 140ms ease, transform 140ms ease;\n }\n\n .copy-action-btn[data-copy-state='done']::before {\n content: '';\n width: 14px;\n height: 14px;\n background-color: currentColor;\n -webkit-mask: url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><polyline points='4 12 10 18 20 6'/></svg>\") center / contain no-repeat;\n mask: url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><polyline points='4 12 10 18 20 6'/></svg>\") center / contain no-repeat;\n animation: copy-check-in 200ms ease-out both;\n }\n\n @keyframes copy-check-in {\n from { opacity: 0; transform: scale(0.6); }\n to { opacity: 1; transform: scale(1); }\n }\n\n @media (prefers-reduced-motion: reduce) {\n .copy-action-btn svg,\n .copy-action-btn[data-copy-state='done']::before {\n transition: none;\n animation: none;\n }\n }\n\n .copy-action-btn[data-copy-state='error'] {\n background: transparent;\n border-color: transparent;\n color: #b91c1c;\n }\n\n /* ── Message rows ─────────────────────────────────────────────────────── */\n\n .msg-row {\n display: flex;\n align-items: flex-end;\n gap: 8px;\n padding: 4px 0;\n min-width: 0;\n }\n\n /* ── User messages ────────────────────────────────────────────────────── */\n\n .msg-user {\n justify-content: flex-end;\n }\n\n .msg-main {\n min-width: 0;\n }\n\n .user-main {\n max-width: 85%;\n display: flex;\n flex-direction: column;\n align-items: flex-end;\n }\n\n .user-bubble {\n max-width: 100%;\n min-width: 0;\n padding: 12px 16px;\n border-radius: 18px 18px 4px 18px;\n background: var(--user-bg);\n color: var(--user-text);\n box-shadow: 0 1px 2px rgba(0,0,0,0.12);\n }\n\n .msg-raw-header {\n margin-bottom: 8px;\n color: rgba(250, 250, 250, 0.72);\n font-family: 'JetBrains Mono', ui-monospace, monospace;\n font-size: 0.72rem;\n line-height: 1.5;\n white-space: pre-wrap;\n word-break: break-word;\n }\n\n .thread-badge {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n margin-bottom: 8px;\n padding: 4px 10px;\n border-radius: 999px;\n background: rgba(255,255,255,0.22);\n color: var(--user-text);\n font-size: 0.68rem;\n font-weight: 700;\n letter-spacing: 0.01em;\n }\n\n .thread-badge code {\n font-family: 'JetBrains Mono', ui-monospace, monospace;\n font-size: 0.66rem;\n background: rgba(255,255,255,0.16);\n padding: 1px 6px;\n border-radius: 999px;\n color: inherit;\n }\n\n .msg-user .msg-body {\n color: var(--user-text);\n }\n\n .msg-user .msg-time {\n display: block;\n margin-top: 6px;\n font-size: 0.72rem;\n color: var(--user-time);\n text-align: right;\n }\n\n /* ── Avatars ──────────────────────────────────────────────────────────── */\n\n .msg-avatar {\n flex: 0 0 28px;\n width: 28px;\n height: 28px;\n border-radius: 50%;\n font-size: 0.68rem;\n font-weight: 700;\n display: flex;\n align-items: center;\n justify-content: center;\n letter-spacing: 0;\n flex-shrink: 0;\n }\n\n .user-avatar {\n background: #eff6ff;\n border: 1.5px solid #93c5fd;\n color: #1d4ed8;\n }\n\n .asst-avatar {\n background: var(--asst-avatar-bg);\n border: 1.5px solid var(--asst-border);\n color: var(--asst-avatar-text);\n margin-bottom: 2px;\n }\n\n /* ── Assistant messages ───────────────────────────────────────────────── */\n\n .msg-assistant {\n align-items: flex-end;\n gap: 8px;\n max-width: 85%;\n min-width: 0;\n }\n\n .asst-main {\n max-width: 100%;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n }\n\n .asst-card {\n min-width: 0;\n max-width: 100%;\n padding: 14px 18px;\n border: 1px solid var(--border);\n border-radius: 18px 18px 18px 4px;\n background: var(--surface);\n box-shadow: 0 1px 3px rgba(0,0,0,0.04);\n }\n\n .msg-assistant .msg-body {\n color: var(--text);\n }\n\n .msg-assistant .msg-time {\n display: block;\n margin-top: 8px;\n font-size: 0.72rem;\n color: var(--subtle);\n }\n\n /* ── Tool blocks ──────────────────────────────────────────────────────── */\n\n .tool-block {\n max-width: 92%;\n margin-left: 36px;\n border-radius: 10px;\n overflow: hidden;\n border: 1px solid rgba(255,255,255,0.06);\n box-shadow: 0 2px 8px rgba(0,0,0,0.16);\n margin: 2px 0;\n }\n\n .tool-header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 14px;\n background: var(--tool-header);\n border-bottom: 1px solid rgba(255,255,255,0.06);\n overflow: hidden;\n }\n\n .tool-icon {\n color: var(--tool-accent);\n flex-shrink: 0;\n display: flex;\n align-items: center;\n }\n\n .tool-name {\n flex: 1;\n font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;\n font-size: 0.75rem;\n font-weight: 500;\n color: var(--tool-accent);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n .tool-time {\n flex-shrink: 0;\n font-family: 'JetBrains Mono', ui-monospace, monospace;\n font-size: 0.7rem;\n color: var(--tool-time);\n }\n\n .tool-output {\n display: block;\n padding: 12px 14px;\n background: var(--tool-bg);\n color: var(--tool-text);\n font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;\n font-size: 0.78rem;\n line-height: 1.6;\n white-space: pre-wrap;\n word-break: break-word;\n overflow-x: auto;\n max-height: 400px;\n overflow-y: auto;\n }\n\n .tool-output.tone-ok { color: var(--tool-ok); }\n .tool-output.tone-err { color: var(--tool-err); }\n\n /* ── Markdown blocks ──────────────────────────────────────────────────── */\n\n .markdown-body {\n font-family: 'DM Sans', system-ui, sans-serif;\n font-size: 0.9rem;\n line-height: 1.65;\n word-break: break-word;\n }\n\n .markdown-body > *:first-child { margin-top: 0; }\n .markdown-body > *:last-child { margin-bottom: 0; }\n .markdown-body p,\n .markdown-body ul,\n .markdown-body ol,\n .markdown-body blockquote,\n .markdown-body pre,\n .markdown-body table,\n .markdown-body hr {\n margin: 0 0 0.85em;\n }\n\n .markdown-body h1,\n .markdown-body h2,\n .markdown-body h3,\n .markdown-body h4,\n .markdown-body h5,\n .markdown-body h6 {\n margin: 0 0 0.55em;\n line-height: 1.25;\n font-weight: 700;\n letter-spacing: -0.01em;\n }\n\n .markdown-body h1 { font-size: 1.4rem; }\n .markdown-body h2 { font-size: 1.22rem; }\n .markdown-body h3 { font-size: 1.08rem; }\n .markdown-body h4,\n .markdown-body h5,\n .markdown-body h6 { font-size: 0.95rem; }\n\n .markdown-body ul,\n .markdown-body ol {\n padding-left: 1.3em;\n }\n\n .markdown-body li + li {\n margin-top: 0.22em;\n }\n\n .markdown-body blockquote {\n padding-left: 12px;\n border-left: 3px solid rgba(34, 197, 94, 0.35);\n opacity: 0.95;\n }\n\n .markdown-body a {\n color: inherit;\n text-decoration: underline;\n text-underline-offset: 2px;\n }\n\n .markdown-body code {\n font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;\n font-size: 0.82em;\n padding: 0.16em 0.38em;\n border-radius: 6px;\n }\n\n .markdown-body pre {\n overflow-x: auto;\n border-radius: 12px;\n padding: 12px 14px;\n }\n\n .markdown-body pre code {\n display: block;\n padding: 0;\n border-radius: 0;\n background: transparent;\n font-size: 0.82rem;\n line-height: 1.6;\n }\n\n .markdown-body table {\n width: 100%;\n border-collapse: collapse;\n font-size: 0.85rem;\n }\n\n .markdown-body th,\n .markdown-body td {\n padding: 8px 10px;\n border: 1px solid rgba(0, 0, 0, 0.08);\n text-align: left;\n vertical-align: top;\n }\n\n .markdown-body img {\n max-width: 100%;\n border-radius: 12px;\n }\n\n .markdown-user code {\n background: rgba(255,255,255,0.14);\n color: var(--user-text);\n }\n\n .markdown-user pre {\n background: rgba(255,255,255,0.08);\n border: 1px solid rgba(255,255,255,0.08);\n }\n\n .markdown-user table th,\n .markdown-user table td {\n border-color: rgba(255,255,255,0.16);\n }\n\n .markdown-assistant code {\n background: #f4f4f5;\n color: #27272a;\n }\n\n .markdown-assistant pre {\n background: #0f172a;\n color: #e5e7eb;\n }\n\n .markdown-assistant pre code {\n background: transparent;\n color: inherit;\n }\n\n .markdown-assistant table th,\n .markdown-assistant table td {\n border-color: rgba(0, 0, 0, 0.08);\n }\n\n /* ── System events ────────────────────────────────────────────────────── */\n\n .system-event {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n padding: 10px 0;\n color: var(--subtle);\n font-size: 0.775rem;\n }\n\n .event-dot {\n width: 4px;\n height: 4px;\n border-radius: 50%;\n background: var(--subtle);\n flex-shrink: 0;\n opacity: 0.6;\n }\n\n .event-text {\n color: var(--muted);\n }\n\n .system-event-err .event-text {\n color: var(--err-text);\n }\n\n .event-time {\n color: var(--subtle);\n font-style: normal;\n }\n\n /* ── Status page ──────────────────────────────────────────────────────── */\n\n .stack > * + * { margin-top: 14px; }\n\n p { color: var(--muted); font-size: 0.9rem; line-height: 1.5; }\n\n .status {\n padding: 12px 16px;\n border-radius: 10px;\n font-size: 0.9rem;\n }\n\n .status.err {\n background: var(--err-bg);\n color: var(--err-text);\n border: 1px solid rgba(185, 28, 28, 0.12);\n }\n\n /* ── Composer ─────────────────────────────────────────────────────────── */\n\n .composer-card {\n position: fixed;\n left: calc(72px + (100vw - 72px) / 2);\n bottom: calc(16px + env(safe-area-inset-bottom, 0px));\n transform: translateX(-50%);\n width: min(960px, calc(100vw - 96px));\n padding: 10px 12px 10px 14px;\n border: 1px solid var(--border);\n border-radius: 22px;\n background: rgba(250, 248, 244, 0.92);\n box-shadow: 0 12px 36px rgba(0,0,0,0.10), 0 2px 6px rgba(0,0,0,0.04);\n backdrop-filter: blur(14px);\n -webkit-backdrop-filter: blur(14px);\n z-index: 20;\n }\n\n .composer-form {\n display: flex;\n flex-direction: column;\n gap: 6px;\n }\n\n .jump-latest-btn {\n position: fixed;\n left: calc(72px + (100vw - 72px) / 2);\n bottom: calc(env(safe-area-inset-bottom, 0px) + 120px);\n z-index: 25;\n width: 42px;\n height: 42px;\n border: 1px solid var(--border);\n border-radius: 999px;\n background: var(--bg);\n color: var(--text);\n font: 700 1rem/1 'DM Sans', sans-serif;\n box-shadow: 0 10px 30px rgba(0,0,0,0.12);\n cursor: pointer;\n backdrop-filter: blur(10px);\n transform: translateX(-50%);\n outline: none;\n appearance: none;\n -webkit-tap-highlight-color: transparent;\n }\n\n .jump-latest-btn:hover {\n transform: translateX(-50%) translateY(-1px);\n background: #e8e3d9;\n }\n\n .jump-latest-btn:focus,\n .jump-latest-btn:active {\n outline: none;\n }\n\n .jump-latest-btn:focus-visible {\n box-shadow: 0 10px 30px rgba(0,0,0,0.12), 0 0 0 3px rgba(0,0,0,0.08);\n }\n\n .composer-copy { margin-bottom: 12px; color: var(--muted); }\n\n .composer-form textarea {\n width: 100%;\n resize: none;\n overflow-y: auto;\n min-height: 28px;\n max-height: 200px;\n padding: 6px 6px 2px;\n border: 0;\n border-radius: 0;\n font: inherit;\n color: var(--text);\n background: transparent;\n }\n\n .composer-form textarea::placeholder {\n color: rgba(63,63,70,0.55);\n }\n\n .composer-form textarea:focus {\n outline: none;\n border: 0;\n }\n\n .composer-actions {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n margin-top: 0;\n }\n\n .composer-status { color: var(--muted); font-size: 13px; }\n .composer-actions button:disabled { opacity: 0.55; cursor: wait; }\n\n .composer-send-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 32px;\n height: 32px;\n border: none;\n border-radius: 999px;\n background: #d97706;\n color: #ffffff;\n font: 700 1rem/1 'DM Sans', sans-serif;\n cursor: pointer;\n box-shadow: 0 10px 24px rgba(217, 119, 6, 0.26);\n transition: transform 120ms, filter 120ms, box-shadow 120ms, background 120ms;\n }\n\n .composer-send-btn:hover:not(:disabled) {\n transform: translateY(-1px);\n filter: saturate(1.06) brightness(0.98);\n box-shadow: 0 12px 28px rgba(217, 119, 6, 0.32);\n }\n\n .composer-send-btn:focus-visible {\n outline: 2px solid rgba(217, 119, 6, 0.28);\n outline-offset: 3px;\n }\n\n .composer-send-btn:disabled {\n background: #d4d4d8;\n color: rgba(24, 24, 27, 0.45);\n box-shadow: none;\n transform: none;\n filter: none;\n cursor: not-allowed;\n opacity: 1;\n }\n\n /* ── Responsive ───────────────────────────────────────────────────────── */\n\n @media (max-width: 900px) {\n .composer-card {\n left: 50%;\n width: min(960px, calc(100vw - 24px));\n }\n .jump-latest-btn { left: 50%; }\n }\n\n @media (max-width: 600px) {\n body { padding-bottom: calc(130px + env(safe-area-inset-bottom, 0px)); }\n\n .composer-card {\n width: calc(100vw - 16px);\n bottom: calc(8px + env(safe-area-inset-bottom, 0px));\n padding: 8px 10px;\n border-radius: 18px;\n }\n\n .session-side { align-items: flex-start; flex-direction: row; }\n\n .user-bubble,\n .msg-assistant,\n .tool-block { max-width: 100%; }\n\n .asst-avatar { display: none; }\n\n .asst-card { border-radius: 4px 14px 14px 14px; }\n }\n`;\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/session-view/service.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,MAAM,GAAG,QAAQ,CAAC;IAC/C,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,SAAS,GAAG,IAAI,GAAG,KAAK,GAAG,OAAO,CAAC;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,mBAAmB,EAAE,CAAC;CACjC;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,OAAO,EAAE,mBAAmB,EAAE,CAAC;CAChC;AAED,wBAAgB,0BAA0B,CACxC,UAAU,EAAE,MAAM,EAClB,cAAc,EAAE,MAAM,EACtB,UAAU,EAAE,MAAM,GACjB,MAAM,GAAG,IAAI,CAMf;AAED,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,gBAAgB,CAkD1E;AAED,wBAAgB,2BAA2B,CACzC,eAAe,EAAE,MAAM,EACvB,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,GAChC,MAAM,GAAG,IAAI,CA4Bf","sourcesContent":["import { basename, dirname, join, resolve } from \"path\";\nimport { existsSync, readdirSync } from \"fs\";\nimport {\n SessionManager,\n type BranchSummaryEntry as SessionBranchSummaryEntry,\n type CompactionEntry,\n type SessionEntry,\n type SessionMessageEntry,\n} from \"@earendil-works/pi-coding-agent\";\nimport {\n getThreadSessionFile,\n resolveChannelSessionFile,\n tryResolveThreadSession,\n} from \"../sessions/store.js\";\nimport { isPlatformHistorySession } from \"../sessions/metadata.js\";\nimport * as log from \"../log.js\";\n\nexport interface SessionViewItem {\n kind: \"user\" | \"assistant\" | \"tool\" | \"system\";\n title: string;\n body?: string;\n meta?: string;\n tone?: \"default\" | \"ok\" | \"err\" | \"muted\";\n entryId?: string;\n threads?: SessionViewRelation[];\n}\n\nexport interface SessionViewRelation {\n kind: \"parent\" | \"thread\";\n fileName: string;\n sessionId: string;\n title: string;\n updatedAt: string;\n entryCount: number;\n summary?: string;\n anchorEntryId?: string;\n}\n\nexport interface SessionViewModel {\n sessionId: string;\n fileName: string;\n title: string;\n createdAt: string;\n updatedAt: string;\n entryCount: number;\n items: SessionViewItem[];\n parent?: SessionViewRelation;\n threads: SessionViewRelation[];\n}\n\nexport function resolveExistingSessionFile(\n workingDir: string,\n conversationId: string,\n sessionKey: string,\n): string | null {\n const conversationDir = join(workingDir, conversationId);\n if (sessionKey.includes(\":\")) {\n return tryResolveThreadSession(getThreadSessionFile(conversationDir, sessionKey));\n }\n return resolveChannelSessionFile(conversationDir);\n}\n\nexport function loadSessionViewModel(sessionFile: string): SessionViewModel {\n const resolvedFile = resolve(sessionFile);\n const sm = SessionManager.open(resolvedFile);\n const header = sm.getHeader();\n if (!header) throw new Error(`No valid session found: ${sessionFile}`);\n\n const entries = sm.getEntries();\n const updatedAt = entries.at(-1)?.timestamp ?? header.timestamp;\n const title = sm.getSessionName() || `Session ${header.id.slice(0, 8)}`;\n\n const parent = header.parentSession\n ? buildSessionRelation(resolve(header.parentSession), \"parent\")\n : buildInferredThreadParentRelation(resolvedFile);\n const threads = listRelatedSessionFiles(resolvedFile)\n .filter((candidate) => candidate !== resolvedFile)\n .map((candidate) => buildSessionRelation(candidate, \"thread\", resolvedFile))\n .filter((relation): relation is SessionViewRelation => relation !== null)\n .toSorted((a, b) => (a.updatedAt < b.updatedAt ? -1 : a.updatedAt > b.updatedAt ? 1 : 0));\n\n const threadsByEntryId = new Map<string, SessionViewRelation[]>();\n for (const thread of threads) {\n if (!thread.anchorEntryId) continue;\n const bucket = threadsByEntryId.get(thread.anchorEntryId) ?? [];\n bucket.push(thread);\n threadsByEntryId.set(thread.anchorEntryId, bucket);\n }\n\n const items = entries.flatMap((entry) => {\n const item = mapEntryToItem(entry);\n if (!item) return [];\n if (item.entryId) {\n const anchoredThreads = threadsByEntryId.get(item.entryId);\n if (anchoredThreads) {\n item.threads = anchoredThreads;\n }\n }\n return [item];\n });\n\n return {\n sessionId: header.id,\n fileName: basename(resolvedFile),\n title,\n createdAt: header.timestamp,\n updatedAt,\n entryCount: entries.length,\n items,\n parent: parent ?? undefined,\n threads,\n };\n}\n\nexport function resolveRequestedSessionFile(\n baseSessionFile: string,\n requestedFileName?: string | null,\n): string | null {\n const resolvedBase = resolve(baseSessionFile);\n if (!requestedFileName) return resolvedBase;\n\n const trimmed = requestedFileName.trim();\n if (!trimmed) return resolvedBase;\n\n const fileName = basename(trimmed);\n if (fileName !== trimmed || !fileName.endsWith(\".jsonl\")) return null;\n\n const candidate = join(dirname(resolvedBase), fileName);\n if (!existsSync(candidate)) return null;\n\n let sm: SessionManager;\n try {\n sm = SessionManager.open(candidate);\n } catch (err) {\n throw new Error(\n `Session file is corrupted: ${candidate}: ${\n err instanceof Error ? err.message : String(err)\n }`,\n { cause: err },\n );\n }\n if (!sm.getHeader()) {\n throw new Error(`Session file is missing a valid header: ${candidate}`);\n }\n return candidate;\n}\n\nfunction listRelatedSessionFiles(sessionFile: string): string[] {\n const dir = dirname(sessionFile);\n if (!existsSync(dir)) return [];\n\n return readdirSync(dir)\n .filter((name) => name.endsWith(\".jsonl\"))\n .map((fileName) => join(dir, fileName));\n}\n\nfunction buildSessionRelation(\n sessionFile: string,\n kind: \"parent\" | \"thread\",\n expectedParent?: string,\n): SessionViewRelation | null {\n let sm: SessionManager;\n try {\n sm = SessionManager.open(sessionFile);\n } catch (err) {\n log.logWarning(\n `Skipping corrupted session file while building ${kind} relation: ${sessionFile}`,\n err instanceof Error ? err.message : String(err),\n );\n return null;\n }\n const header = sm.getHeader();\n if (!header) {\n log.logWarning(\n `Skipping session file with missing header while building ${kind} relation: ${sessionFile}`,\n );\n return null;\n }\n if (\n kind === \"thread\" &&\n !isChildThreadSession(sessionFile, header.parentSession, expectedParent)\n ) {\n return null;\n }\n\n const entries = sm.getEntries();\n const updatedAt = entries.at(-1)?.timestamp ?? header.timestamp;\n const threadId = kind === \"thread\" ? getFixedThreadSessionId(sessionFile) : null;\n const anchorEntryId =\n kind === \"thread\" && expectedParent\n ? findThreadAnchorEntryId(SessionManager.open(expectedParent).getEntries(), entries, threadId)\n : undefined;\n return {\n kind,\n fileName: basename(sessionFile),\n sessionId: header.id,\n title: sm.getSessionName() || `Session ${header.id.slice(0, 8)}`,\n updatedAt,\n entryCount: entries.length,\n summary: extractSessionSummary(entries),\n anchorEntryId,\n };\n}\n\nfunction buildInferredThreadParentRelation(sessionFile: string): SessionViewRelation | undefined {\n if (!getFixedThreadSessionId(sessionFile)) return undefined;\n\n const parentSession = resolveCurrentChannelSessionForSessionFile(sessionFile);\n if (!parentSession || parentSession === sessionFile) return undefined;\n\n return buildSessionRelation(parentSession, \"parent\") ?? undefined;\n}\n\nfunction isChildThreadSession(\n sessionFile: string,\n parentSession: string | undefined,\n expectedParent: string | undefined,\n): boolean {\n if (!expectedParent) return false;\n\n if (parentSession) {\n const resolvedParent = resolve(parentSession);\n return (\n resolvedParent === expectedParent ||\n (isPlatformHistorySession(expectedParent) && isPlatformHistorySession(resolvedParent))\n );\n }\n\n if (!getFixedThreadSessionId(sessionFile)) return false;\n return resolveCurrentChannelSessionForSessionFile(sessionFile) === expectedParent;\n}\n\nfunction resolveCurrentChannelSessionForSessionFile(sessionFile: string): string | null {\n return resolveChannelSessionFile(dirname(dirname(sessionFile)));\n}\n\nfunction getFixedThreadSessionId(sessionFile: string): string | null {\n const fileName = basename(sessionFile);\n if (!fileName.endsWith(\".jsonl\")) return null;\n if (/^\\d{4}-\\d{2}-\\d{2}T.+_[0-9a-f]{8}\\.jsonl$/i.test(fileName)) return null;\n return fileName.slice(0, -\".jsonl\".length);\n}\n\nfunction findThreadAnchorEntryId(\n parentEntries: SessionEntry[],\n childEntries: SessionEntry[],\n threadId: string | null,\n): string | undefined {\n const threadTimestamp = parseThreadTimestamp(threadId);\n if (threadTimestamp !== undefined) {\n const timestampAnchor = findEntryIdByMessageTimestamp(parentEntries, threadTimestamp);\n if (timestampAnchor) return timestampAnchor;\n }\n let sharedCount = 0;\n while (\n sharedCount < parentEntries.length &&\n sharedCount < childEntries.length &&\n parentEntries[sharedCount]?.id === childEntries[sharedCount]?.id\n ) {\n sharedCount += 1;\n }\n\n if (sharedCount > 0) {\n return parentEntries[sharedCount - 1]?.id;\n }\n\n const contentAnchor = findSharedContentAnchorEntryId(parentEntries, childEntries);\n if (contentAnchor) return contentAnchor;\n\n const childRoot = findComparableUserMessage(childEntries);\n if (!childRoot) return undefined;\n\n return findParentAnchorByRootMessage(parentEntries, childRoot);\n}\n\nfunction parseThreadTimestamp(threadId: string | null): number | undefined {\n if (!threadId) return undefined;\n const timestamp = Number(threadId) * 1000;\n return Number.isFinite(timestamp) ? timestamp : undefined;\n}\n\nfunction findEntryIdByMessageTimestamp(\n entries: SessionEntry[],\n timestamp: number,\n): string | undefined {\n for (const entry of entries) {\n if (entry.type !== \"message\") continue;\n const messageTimestamp = (entry.message as { timestamp?: unknown }).timestamp;\n if (messageTimestamp === timestamp) return entry.id;\n }\n return undefined;\n}\n\nfunction findSharedContentAnchorEntryId(\n parentEntries: SessionEntry[],\n childEntries: SessionEntry[],\n): string | undefined {\n const parentMessages = parentEntries.flatMap((entry) => {\n const comparable = getComparableSessionMessage(entry);\n return comparable ? [comparable] : [];\n });\n const childMessages = childEntries.flatMap((entry) => {\n const comparable = getComparableSessionMessage(entry);\n return comparable ? [comparable] : [];\n });\n if (parentMessages.length === 0 || childMessages.length === 0) return undefined;\n\n let bestParentEnd = -1;\n let bestLength = 0;\n for (let parentStart = 0; parentStart < parentMessages.length; parentStart++) {\n let length = 0;\n while (\n parentStart + length < parentMessages.length &&\n length < childMessages.length &&\n comparableSessionMessagesMatch(parentMessages[parentStart + length], childMessages[length])\n ) {\n length += 1;\n }\n if (length > bestLength) {\n bestLength = length;\n bestParentEnd = parentStart + length - 1;\n }\n }\n\n return bestLength > 0 ? parentMessages[bestParentEnd]?.entryId : undefined;\n}\n\ninterface ComparableSessionMessage {\n entryId: string;\n role: \"user\" | \"assistant\";\n normalizedText: string;\n}\n\nfunction getComparableSessionMessage(entry: SessionEntry): ComparableSessionMessage | null {\n if (entry.type !== \"message\") return null;\n const role = entry.message.role;\n if (role !== \"user\" && role !== \"assistant\") return null;\n\n const body =\n role === \"user\"\n ? contentToText(entry.message.content)\n : assistantContentToText(entry.message.content);\n const normalizedText = normalizeComparableSessionText(body, role);\n if (!normalizedText) return null;\n\n return { entryId: entry.id, role, normalizedText };\n}\n\nfunction comparableSessionMessagesMatch(\n parent: ComparableSessionMessage,\n child: ComparableSessionMessage,\n): boolean {\n return parent.role === child.role && parent.normalizedText === child.normalizedText;\n}\n\nfunction normalizeComparableSessionText(text: string, role: \"user\" | \"assistant\"): string {\n const normalized = role === \"user\" ? normalizeComparableUserText(text) : text.trim();\n return normalized.replace(/\\s+/g, \" \").trim();\n}\n\nfunction findParentAnchorByRootMessage(\n parentEntries: SessionEntry[],\n childRoot: ComparableUserMessage,\n): string | undefined {\n let textMatchId: string | undefined;\n\n for (const entry of parentEntries) {\n const comparable = getComparableUserMessage(entry);\n if (!comparable) continue;\n if (comparable.normalizedText !== childRoot.normalizedText) continue;\n if (\n childRoot.messageTimestamp !== undefined &&\n comparable.messageTimestamp !== undefined &&\n comparable.messageTimestamp === childRoot.messageTimestamp\n ) {\n return entry.id;\n }\n textMatchId ??= entry.id;\n }\n\n return textMatchId;\n}\n\ninterface ComparableUserMessage {\n normalizedText: string;\n messageTimestamp?: number;\n}\n\nfunction findComparableUserMessage(entries: SessionEntry[]): ComparableUserMessage | null {\n for (const entry of entries) {\n const comparable = getComparableUserMessage(entry);\n if (comparable) return comparable;\n }\n return null;\n}\n\nfunction getComparableUserMessage(entry: SessionEntry): ComparableUserMessage | null {\n if (entry.type !== \"message\" || entry.message.role !== \"user\") return null;\n\n const body = contentToText(entry.message.content);\n const normalizedText = normalizeComparableUserText(body);\n if (!normalizedText) return null;\n\n const messageTimestamp =\n typeof entry.message.timestamp === \"number\" ? entry.message.timestamp : undefined;\n return { normalizedText, messageTimestamp };\n}\n\nfunction normalizeComparableUserText(text: string): string {\n const withoutTimestamp = text.replace(\n /^\\[[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}[+-][0-9]{2}:[0-9]{2}\\]\\s+(?=\\[[^\\]]+\\](?:\\s+\\[in-thread:[^\\]]+\\])?:\\s)/,\n \"\",\n );\n return stripSlackAttachmentBlock(withoutTimestamp).trim();\n}\n\nfunction stripSlackAttachmentBlock(text: string): string {\n return text.replace(/\\n*<slack_attachments>\\n[\\s\\S]*?\\n<\\/slack_attachments>\\s*$/g, \"\");\n}\n\nfunction extractSessionSummary(entries: SessionEntry[]): string | undefined {\n for (const entry of entries) {\n if (entry.type !== \"message\") continue;\n const item = mapEntryToItem(entry);\n if (!item?.body) continue;\n return collapseSummary(item.body);\n }\n return undefined;\n}\n\nfunction collapseSummary(text: string): string {\n const singleLine = text.replace(/\\s+/g, \" \").trim();\n return singleLine.length > 96 ? `${singleLine.slice(0, 93)}…` : singleLine;\n}\n\nfunction mapEntryToItem(entry: SessionEntry): SessionViewItem | null {\n switch (entry.type) {\n case \"message\":\n return mapMessageEntry(entry);\n case \"model_change\":\n return {\n kind: \"system\",\n title: \"Model changed\",\n body: `${entry.provider} / ${entry.modelId}`,\n meta: entry.timestamp,\n tone: \"muted\",\n };\n case \"thinking_level_change\":\n return {\n kind: \"system\",\n title: \"Thinking level changed\",\n body: entry.thinkingLevel,\n meta: entry.timestamp,\n tone: \"muted\",\n };\n case \"compaction\":\n return mapCompactionEntry(entry);\n case \"branch_summary\":\n return mapSessionSummaryEntry(entry);\n case \"custom_message\":\n return {\n kind: \"system\",\n title: `Custom message · ${entry.customType}`,\n body: contentToText(entry.content),\n meta: entry.timestamp,\n tone: \"muted\",\n };\n case \"custom\":\n return {\n kind: \"system\",\n title: `Custom data · ${entry.customType}`,\n body: entry.data === undefined ? \"(no data)\" : JSON.stringify(entry.data, null, 2),\n meta: entry.timestamp,\n tone: \"muted\",\n };\n case \"label\":\n return {\n kind: \"system\",\n title: \"Label updated\",\n body: entry.label || \"(cleared)\",\n meta: entry.timestamp,\n tone: \"muted\",\n };\n case \"session_info\":\n return entry.name\n ? {\n kind: \"system\",\n title: \"Session renamed\",\n body: entry.name,\n meta: entry.timestamp,\n tone: \"muted\",\n }\n : null;\n default:\n return null;\n }\n}\n\nfunction mapMessageEntry(entry: SessionMessageEntry): SessionViewItem {\n const message = entry.message as unknown as Record<string, unknown> & {\n role?: string;\n content?: unknown;\n provider?: string;\n model?: string;\n toolName?: string;\n isError?: boolean;\n command?: string;\n output?: string;\n exitCode?: number;\n cancelled?: boolean;\n truncated?: boolean;\n stopReason?: string;\n errorMessage?: string;\n customType?: string;\n summary?: string;\n };\n\n switch (message.role) {\n case \"user\":\n return {\n kind: \"user\",\n title: \"User\",\n body: contentToText(message.content),\n meta: entry.timestamp,\n entryId: entry.id,\n };\n case \"assistant\": {\n const assistantBody =\n assistantContentToText(message.content) ||\n (message.errorMessage ? `_${message.errorMessage}_` : \"\");\n const metaParts = [message.provider, message.model, message.stopReason].filter(Boolean);\n return {\n kind: \"assistant\",\n title: \"Assistant\",\n body: assistantBody,\n meta:\n metaParts.length > 0 ? `${entry.timestamp} · ${metaParts.join(\" · \")}` : entry.timestamp,\n entryId: entry.id,\n };\n }\n case \"toolResult\":\n return {\n kind: \"tool\",\n title: `Tool result · ${String(message.toolName ?? \"unknown\")}`,\n body: contentToText(message.content),\n meta: entry.timestamp,\n tone: message.isError ? \"err\" : \"ok\",\n entryId: entry.id,\n };\n case \"bashExecution\": {\n const command = String(message.command ?? \"\").trim();\n const output = String(message.output ?? \"\").trim();\n const details = [\n typeof message.exitCode === \"number\" ? `[exitCode] ${message.exitCode}` : \"\",\n message.cancelled ? `[cancelled] true` : \"\",\n message.truncated ? `[truncated] true` : \"\",\n ].filter(Boolean);\n const body = [command ? `$ ${command}` : \"\", output, ...details].filter(Boolean).join(\"\\n\\n\");\n return {\n kind: \"tool\",\n title: \"Bash execution\",\n body: body || \"(no output)\",\n meta: entry.timestamp,\n entryId: entry.id,\n };\n }\n case \"custom\":\n return {\n kind: \"system\",\n title: `Custom message · ${String(message.customType ?? \"custom\")}`,\n body: contentToText(message.content),\n meta: entry.timestamp,\n tone: \"muted\",\n entryId: entry.id,\n };\n case \"branchSummary\":\n return {\n kind: \"system\",\n title: \"Session summary\",\n body: String(message.summary ?? \"\"),\n meta: entry.timestamp,\n tone: \"muted\",\n entryId: entry.id,\n };\n case \"compactionSummary\":\n return {\n kind: \"system\",\n title: \"Compaction summary\",\n body: String(message.summary ?? \"\"),\n meta: entry.timestamp,\n tone: \"muted\",\n entryId: entry.id,\n };\n default:\n return {\n kind: \"system\",\n title: `Message · ${String(message.role ?? \"unknown\")}`,\n body: contentToText(message.content),\n meta: entry.timestamp,\n tone: \"muted\",\n entryId: entry.id,\n };\n }\n}\n\nfunction mapCompactionEntry(entry: CompactionEntry): SessionViewItem {\n return {\n kind: \"system\",\n title: \"Context compacted\",\n body: entry.summary,\n meta: `${entry.timestamp} · ${entry.tokensBefore} tokens before compaction`,\n tone: \"muted\",\n };\n}\n\nfunction mapSessionSummaryEntry(entry: SessionBranchSummaryEntry): SessionViewItem {\n return {\n kind: \"system\",\n title: \"Session summary\",\n body: entry.summary,\n meta: entry.timestamp,\n tone: \"muted\",\n };\n}\n\nfunction assistantContentToText(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (!Array.isArray(content)) return \"\";\n\n const lines: string[] = [];\n\n for (const block of content) {\n if (!block || typeof block !== \"object\") continue;\n const value = block as Record<string, unknown>;\n if (value.type === \"text\" && typeof value.text === \"string\") {\n lines.push(value.text);\n continue;\n }\n if (value.type === \"thinking\" && typeof value.thinking === \"string\") {\n lines.push(`[thinking]\\n${value.thinking}`);\n continue;\n }\n if (value.type === \"toolCall\") {\n const name = typeof value.name === \"string\" ? value.name : \"tool\";\n const args = value.arguments === undefined ? \"\" : JSON.stringify(value.arguments, null, 2);\n lines.push([`[toolCall] ${name}`, args].filter(Boolean).join(\"\\n\"));\n continue;\n }\n if (value.type === \"image\") {\n lines.push(`[image ${String(value.mimeType ?? \"unknown\")}]`);\n }\n }\n\n return lines.join(\"\\n\\n\");\n}\n\nfunction contentToText(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (!Array.isArray(content)) return \"\";\n\n const lines: string[] = [];\n for (const block of content) {\n if (!block || typeof block !== \"object\") continue;\n const value = block as Record<string, unknown>;\n if (value.type === \"text\" && typeof value.text === \"string\") {\n lines.push(value.text);\n continue;\n }\n if (value.type === \"thinking\" && typeof value.thinking === \"string\") {\n lines.push(`[thinking]\\n${value.thinking}`);\n continue;\n }\n if (value.type === \"toolCall\") {\n const name = typeof value.name === \"string\" ? value.name : \"tool\";\n const args = value.arguments === undefined ? \"\" : JSON.stringify(value.arguments, null, 2);\n lines.push([`[toolCall] ${name}`, args].filter(Boolean).join(\"\\n\"));\n continue;\n }\n if (value.type === \"image\") {\n lines.push(`[image ${String(value.mimeType ?? \"unknown\")}]`);\n }\n }\n\n return lines.join(\"\\n\\n\");\n}\n"]}