@geminixiang/mikan 0.3.0-beta.0 → 0.3.1

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 (134) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/adapter.d.ts +5 -0
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/adapter.js.map +1 -1
  5. package/dist/adapters/discord/context.d.ts +0 -1
  6. package/dist/adapters/discord/context.d.ts.map +1 -1
  7. package/dist/adapters/discord/context.js +62 -84
  8. package/dist/adapters/discord/context.js.map +1 -1
  9. package/dist/adapters/shared.d.ts +1 -2
  10. package/dist/adapters/shared.d.ts.map +1 -1
  11. package/dist/adapters/shared.js +3 -2
  12. package/dist/adapters/shared.js.map +1 -1
  13. package/dist/adapters/slack/bot.d.ts +9 -34
  14. package/dist/adapters/slack/bot.d.ts.map +1 -1
  15. package/dist/adapters/slack/bot.js +374 -358
  16. package/dist/adapters/slack/bot.js.map +1 -1
  17. package/dist/adapters/slack/context.d.ts +0 -1
  18. package/dist/adapters/slack/context.d.ts.map +1 -1
  19. package/dist/adapters/slack/context.js +110 -113
  20. package/dist/adapters/slack/context.js.map +1 -1
  21. package/dist/adapters/slack/session.d.ts +0 -3
  22. package/dist/adapters/slack/session.d.ts.map +1 -1
  23. package/dist/adapters/slack/session.js +2 -8
  24. package/dist/adapters/slack/session.js.map +1 -1
  25. package/dist/adapters/slack/thread-manager.d.ts +0 -1
  26. package/dist/adapters/slack/thread-manager.d.ts.map +1 -1
  27. package/dist/adapters/slack/thread-manager.js +1 -4
  28. package/dist/adapters/slack/thread-manager.js.map +1 -1
  29. package/dist/adapters/slack/tools/block-kit.d.ts +16 -0
  30. package/dist/adapters/slack/tools/block-kit.d.ts.map +1 -0
  31. package/dist/adapters/slack/tools/block-kit.js +105 -0
  32. package/dist/adapters/slack/tools/block-kit.js.map +1 -0
  33. package/dist/adapters/telegram/context.d.ts +0 -1
  34. package/dist/adapters/telegram/context.d.ts.map +1 -1
  35. package/dist/adapters/telegram/context.js +44 -54
  36. package/dist/adapters/telegram/context.js.map +1 -1
  37. package/dist/admin/portal.d.ts.map +1 -1
  38. package/dist/admin/portal.js +2 -3
  39. package/dist/admin/portal.js.map +1 -1
  40. package/dist/agent.d.ts +0 -1
  41. package/dist/agent.d.ts.map +1 -1
  42. package/dist/agent.js +47 -80
  43. package/dist/agent.js.map +1 -1
  44. package/dist/commands/admin.d.ts +0 -3
  45. package/dist/commands/admin.d.ts.map +1 -1
  46. package/dist/commands/admin.js +5 -30
  47. package/dist/commands/admin.js.map +1 -1
  48. package/dist/commands/session-view.d.ts.map +1 -1
  49. package/dist/commands/session-view.js +4 -17
  50. package/dist/commands/session-view.js.map +1 -1
  51. package/dist/commands/types.d.ts +3 -2
  52. package/dist/commands/types.d.ts.map +1 -1
  53. package/dist/commands/types.js.map +1 -1
  54. package/dist/commands/utils.d.ts +3 -1
  55. package/dist/commands/utils.d.ts.map +1 -1
  56. package/dist/commands/utils.js +15 -5
  57. package/dist/commands/utils.js.map +1 -1
  58. package/dist/context.d.ts +0 -1
  59. package/dist/context.d.ts.map +1 -1
  60. package/dist/context.js +1 -23
  61. package/dist/context.js.map +1 -1
  62. package/dist/html.d.ts +2 -0
  63. package/dist/html.d.ts.map +1 -0
  64. package/dist/html.js +4 -0
  65. package/dist/html.js.map +1 -0
  66. package/dist/login/index.d.ts +2 -1
  67. package/dist/login/index.d.ts.map +1 -1
  68. package/dist/login/index.js.map +1 -1
  69. package/dist/login/portal.d.ts +1 -1
  70. package/dist/login/portal.d.ts.map +1 -1
  71. package/dist/login/portal.js +2 -3
  72. package/dist/login/portal.js.map +1 -1
  73. package/dist/login/{session.d.ts → store.d.ts} +1 -1
  74. package/dist/login/store.d.ts.map +1 -0
  75. package/dist/login/{session.js → store.js} +1 -1
  76. package/dist/login/store.js.map +1 -0
  77. package/dist/main.d.ts.map +1 -1
  78. package/dist/main.js +1 -1
  79. package/dist/main.js.map +1 -1
  80. package/dist/portal-shell.d.ts +2 -2
  81. package/dist/portal-shell.d.ts.map +1 -1
  82. package/dist/portal-shell.js +11 -16
  83. package/dist/portal-shell.js.map +1 -1
  84. package/dist/sandbox/cloudflare.d.ts +0 -2
  85. package/dist/sandbox/cloudflare.d.ts.map +1 -1
  86. package/dist/sandbox/cloudflare.js +2 -2
  87. package/dist/sandbox/cloudflare.js.map +1 -1
  88. package/dist/sandbox/container.d.ts +0 -3
  89. package/dist/sandbox/container.d.ts.map +1 -1
  90. package/dist/sandbox/container.js +3 -3
  91. package/dist/sandbox/container.js.map +1 -1
  92. package/dist/sandbox/firecracker.d.ts +0 -2
  93. package/dist/sandbox/firecracker.d.ts.map +1 -1
  94. package/dist/sandbox/firecracker.js +2 -2
  95. package/dist/sandbox/firecracker.js.map +1 -1
  96. package/dist/sandbox/host.d.ts +0 -2
  97. package/dist/sandbox/host.d.ts.map +1 -1
  98. package/dist/sandbox/host.js +2 -2
  99. package/dist/sandbox/host.js.map +1 -1
  100. package/dist/sandbox/image.d.ts +0 -2
  101. package/dist/sandbox/image.d.ts.map +1 -1
  102. package/dist/sandbox/image.js +2 -2
  103. package/dist/sandbox/image.js.map +1 -1
  104. package/dist/sandbox/index.d.ts +1 -6
  105. package/dist/sandbox/index.d.ts.map +1 -1
  106. package/dist/sandbox/index.js +0 -5
  107. package/dist/sandbox/index.js.map +1 -1
  108. package/dist/sandbox/path-context.d.ts +0 -1
  109. package/dist/sandbox/path-context.d.ts.map +1 -1
  110. package/dist/sandbox/path-context.js +1 -1
  111. package/dist/sandbox/path-context.js.map +1 -1
  112. package/dist/sentry.d.ts +2 -2
  113. package/dist/sentry.d.ts.map +1 -1
  114. package/dist/sentry.js.map +1 -1
  115. package/dist/session-view/portal.d.ts.map +1 -1
  116. package/dist/session-view/portal.js +2 -8
  117. package/dist/session-view/portal.js.map +1 -1
  118. package/dist/sessions/chat-session-manager.d.ts.map +1 -1
  119. package/dist/sessions/chat-session-manager.js +5 -9
  120. package/dist/sessions/chat-session-manager.js.map +1 -1
  121. package/dist/tools/index.d.ts +2 -0
  122. package/dist/tools/index.d.ts.map +1 -1
  123. package/dist/tools/index.js +4 -0
  124. package/dist/tools/index.js.map +1 -1
  125. package/dist/vault-routing.d.ts +0 -1
  126. package/dist/vault-routing.d.ts.map +1 -1
  127. package/dist/vault-routing.js +1 -4
  128. package/dist/vault-routing.js.map +1 -1
  129. package/dist/vault.d.ts +2 -1
  130. package/dist/vault.d.ts.map +1 -1
  131. package/dist/vault.js.map +1 -1
  132. package/package.json +3 -1
  133. package/dist/login/session.d.ts.map +0 -1
  134. package/dist/login/session.js.map +0 -1
@@ -1 +1 @@
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 * 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\nfunction esc(value: string): string {\n return value\n .replaceAll(\"&\", \"&amp;\")\n .replaceAll(\"<\", \"&lt;\")\n .replaceAll(\">\", \"&gt;\")\n .replaceAll('\"', \"&quot;\")\n .replaceAll(\"'\", \"&#39;\");\n}\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
+ {"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 +1 @@
1
- {"version":3,"file":"chat-session-manager.d.ts","sourceRoot":"","sources":["../../src/sessions/chat-session-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAqB,MAAM,iCAAiC,CAAC;AAOpF,OAAO,EAUL,KAAK,oBAAoB,EAE1B,MAAM,YAAY,CAAC;AAmBpB,MAAM,WAAW,yBAAyB;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,8BAA8B;IAC7C,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,iGAAiG;IACjG,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,6BAA6B;IAC5C,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,cAAc,CAAC;IAC/B,4FAA4F;IAC5F,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,uBAAuB;IACtC,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,4BAA4B;IAC3C,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,6BAA6B;IAC5C,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,0BAA0B;IACzC,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,OAAO,CAAC;IAChC,eAAe,EAAE,MAAM,OAAO,CAAC;IAC/B,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAMD,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAE9D;AAED,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,6BAA6B,GAAG,OAAO,CAQ1F;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,4BAA4B,GAAG,MAAM,GAAG,IAAI,CAQ1F;AAED,wBAAsB,6BAA6B,CACjD,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,OAAO,CAAC,CAqBlB;AAED,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAS;IAC7C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAa;IAEjC,YAAY,OAAO,GAAE,yBAA8B,EAIlD;IAEK,mBAAmB,CACvB,OAAO,EAAE,8BAA8B,GACtC,OAAO,CAAC,oBAAoB,CAAC,CAqB/B;IAED,kBAAkB,CAAC,OAAO,EAAE,6BAA6B,GAAG,IAAI,CAS/D;IAED,YAAY,CAAC,OAAO,EAAE,uBAAuB,GAAG,MAAM,CAUrD;IAED,qBAAqB,CAAC,OAAO,EAAE,4BAA4B,GAAG,MAAM,GAAG,IAAI,CAE1E;IAED,sBAAsB,CAAC,OAAO,EAAE,6BAA6B,GAAG,OAAO,CAEtE;IAED,OAAO,CAAC,0BAA0B;IAgClC,OAAO,CAAC,yBAAyB;CAoClC","sourcesContent":["import { SessionManager, type SessionEntry } from \"@earendil-works/pi-coding-agent\";\nimport { join } from \"path\";\nimport type { ConversationLogMessage } from \"../context.js\";\nimport { isRecord, parseJsonValue, readTextFileIfExists } from \"../file-guards.js\";\nimport { atomicWritePrivateFile } from \"../fs-atomic.js\";\nimport * as log from \"../log.js\";\nimport { isPlatformHistorySession } from \"./metadata.js\";\nimport {\n createManagedSessionFile,\n createManagedSessionFileAtPath,\n extractSessionSuffix,\n getChannelSessionDir,\n getThreadSessionFile,\n openManagedSession,\n resolveChannelSessionFile,\n tryResolveCurrentSession,\n tryResolveThreadSession,\n type ResolvedSessionScope,\n type ThreadRootMessage,\n} from \"./store.js\";\n\nconst DEFAULT_RECENT_DAYS = 14;\nconst DEFAULT_MAX_TOP_LEVEL_MESSAGES = 200;\nconst CHAT_SYNC_CUSTOM_TYPE = \"mikan.chat_sync\";\n\ninterface ChatSyncState {\n source?: string;\n messageCount?: number;\n lastMessageId?: string;\n}\n\ntype SessionAppendMessage = Parameters<SessionManager[\"appendMessage\"]>[0];\n\ninterface LogRecord {\n message: ConversationLogMessage;\n index: number;\n}\n\nexport interface ChatSessionManagerOptions {\n recentDays?: number;\n maxTopLevelMessages?: number;\n now?: () => Date;\n}\n\nexport interface ResolveChatSessionScopeOptions {\n conversationDir: string;\n sessionKey: string;\n cwd?: string;\n /** The triggering platform message ID. Excluded from bootstrap to avoid duplicate user turns. */\n currentMessageId?: string;\n}\n\nexport interface SyncChatSessionManagerOptions {\n conversationDir: string;\n sessionKey: string;\n sessionManager: SessionManager;\n /** The triggering platform message ID. Excluded from sync to avoid duplicate user turns. */\n currentMessageId?: string;\n}\n\nexport interface ResetChatSessionOptions {\n conversationDir: string;\n sessionKey: string;\n cwd?: string;\n}\n\nexport interface RegisterThreadSessionOptions {\n conversationDir: string;\n sessionKey: string;\n cwd?: string;\n}\n\nexport interface HasMaterializedSessionOptions {\n conversationDir: string;\n sessionKey: string;\n}\n\nexport interface ThreadBootstrapWaitOptions {\n parentSessionKey: string;\n sessionKey: string;\n hasThreadSession: () => boolean;\n isParentRunning: () => boolean;\n sleep?: (ms: number) => Promise<void>;\n pollMs?: number;\n}\n\nfunction defaultSleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nexport function isThreadSessionKey(sessionKey: string): boolean {\n return sessionKey.includes(\":\");\n}\n\nexport function extractThreadId(sessionKey: string): string {\n return extractSessionSuffix(sessionKey);\n}\n\nexport function hasMaterializedChatSession(options: HasMaterializedSessionOptions): boolean {\n if (!isThreadSessionKey(options.sessionKey)) {\n return resolveChannelSessionFile(options.conversationDir) !== null;\n }\n return (\n tryResolveThreadSession(getThreadSessionFile(options.conversationDir, options.sessionKey)) !==\n null\n );\n}\n\nexport function registerThreadSession(options: RegisterThreadSessionOptions): string | null {\n if (!isThreadSessionKey(options.sessionKey)) return null;\n\n const threadFile = getThreadSessionFile(options.conversationDir, options.sessionKey);\n return (\n tryResolveThreadSession(threadFile) ??\n createManagedSessionFileAtPath(threadFile, options.cwd ?? options.conversationDir)\n );\n}\n\nexport async function waitForThreadSessionBootstrap(\n options: ThreadBootstrapWaitOptions,\n): Promise<boolean> {\n const {\n parentSessionKey,\n sessionKey,\n hasThreadSession,\n isParentRunning,\n sleep = defaultSleep,\n pollMs = 100,\n } = options;\n\n if (!isThreadSessionKey(sessionKey)) return false;\n if (sessionKey === parentSessionKey) return false;\n if (hasThreadSession()) return false;\n\n let waited = false;\n while (isParentRunning() && !hasThreadSession()) {\n waited = true;\n await sleep(pollMs);\n }\n\n return waited;\n}\n\nexport class ChatSessionManager {\n private readonly recentDays: number;\n private readonly maxTopLevelMessages: number;\n private readonly now: () => Date;\n\n constructor(options: ChatSessionManagerOptions = {}) {\n this.recentDays = options.recentDays ?? DEFAULT_RECENT_DAYS;\n this.maxTopLevelMessages = options.maxTopLevelMessages ?? DEFAULT_MAX_TOP_LEVEL_MESSAGES;\n this.now = options.now ?? (() => new Date());\n }\n\n async resolveSessionScope(\n options: ResolveChatSessionScopeOptions,\n ): Promise<ResolvedSessionScope> {\n const cwd = options.cwd ?? options.conversationDir;\n const sessionDir = getChannelSessionDir(options.conversationDir);\n\n if (!isThreadSessionKey(options.sessionKey)) {\n const contextFile = this.resolveTopLevelSessionFile({\n conversationDir: options.conversationDir,\n sessionDir,\n cwd,\n currentMessageId: options.currentMessageId,\n });\n return { sessionDir, contextFile, threadRootMessage: null };\n }\n\n return this.resolveThreadSessionScope({\n conversationDir: options.conversationDir,\n sessionDir,\n sessionKey: options.sessionKey,\n cwd,\n currentMessageId: options.currentMessageId,\n });\n }\n\n syncSessionManager(options: SyncChatSessionManagerOptions): void {\n const records = readConversationLog(options.conversationDir);\n syncSessionManagerFromLog(\n options.sessionManager,\n selectExistingSessionSyncMessages(records, {\n sessionKey: isThreadSessionKey(options.sessionKey) ? options.sessionKey : null,\n excludeMessageId: options.currentMessageId,\n }),\n );\n }\n\n resetSession(options: ResetChatSessionOptions): string {\n const cwd = options.cwd ?? options.conversationDir;\n if (isThreadSessionKey(options.sessionKey)) {\n return createManagedSessionFileAtPath(\n getThreadSessionFile(options.conversationDir, options.sessionKey),\n cwd,\n );\n }\n\n return createManagedSessionFile(getChannelSessionDir(options.conversationDir), cwd);\n }\n\n registerThreadSession(options: RegisterThreadSessionOptions): string | null {\n return registerThreadSession(options);\n }\n\n hasMaterializedSession(options: HasMaterializedSessionOptions): boolean {\n return hasMaterializedChatSession(options);\n }\n\n private resolveTopLevelSessionFile(options: {\n conversationDir: string;\n sessionDir: string;\n cwd: string;\n currentMessageId?: string;\n }): string {\n const records = readConversationLog(options.conversationDir);\n const existing = tryResolveCurrentSession(options.sessionDir);\n if (existing && !isPlatformHistorySession(existing)) {\n syncSessionFromLog(\n existing,\n options.sessionDir,\n options.cwd,\n selectExistingSessionSyncMessages(records, {\n sessionKey: null,\n excludeMessageId: options.currentMessageId,\n }),\n );\n return existing;\n }\n\n const sessionFile = createManagedSessionFile(options.sessionDir, options.cwd);\n const bootstrapRecords = selectRecentTopLevelMessages(records, {\n recentDays: this.recentDays,\n maxMessages: this.maxTopLevelMessages,\n now: this.now(),\n excludeMessageId: options.currentMessageId,\n });\n bootstrapSessionFromLog(sessionFile, options.sessionDir, options.cwd, bootstrapRecords);\n return sessionFile;\n }\n\n private resolveThreadSessionScope(options: {\n conversationDir: string;\n sessionDir: string;\n sessionKey: string;\n cwd: string;\n currentMessageId?: string;\n }): ResolvedSessionScope {\n const threadFile = getThreadSessionFile(options.conversationDir, options.sessionKey);\n const threadId = extractThreadId(options.sessionKey);\n const records = readConversationLog(options.conversationDir);\n const threadRootMessage = buildThreadRootSeed(findLogRecordById(records, threadId)?.message);\n const existing = tryResolveThreadSession(threadFile);\n if (existing) {\n syncSessionFromLog(\n existing,\n options.sessionDir,\n options.cwd,\n selectExistingSessionSyncMessages(records, {\n sessionKey: options.sessionKey,\n excludeMessageId: options.currentMessageId,\n }),\n );\n return { sessionDir: options.sessionDir, contextFile: existing, threadRootMessage };\n }\n\n createManagedSessionFileAtPath(threadFile, options.cwd);\n const bootstrapRecords = selectThreadBootstrapMessages(records, threadId, {\n recentDays: this.recentDays,\n maxTopLevelMessages: this.maxTopLevelMessages,\n now: this.now(),\n excludeMessageId: options.currentMessageId,\n });\n bootstrapSessionFromLog(threadFile, options.sessionDir, options.cwd, bootstrapRecords);\n\n return { sessionDir: options.sessionDir, contextFile: threadFile, threadRootMessage };\n }\n}\n\nfunction readConversationLog(conversationDir: string): LogRecord[] {\n const logFile = join(conversationDir, \"log.jsonl\");\n const raw = readTextFileIfExists(logFile);\n if (raw === undefined) return [];\n\n const lines = raw.trim().split(\"\\n\").filter(Boolean);\n const records: LogRecord[] = [];\n for (let i = 0; i < lines.length; i++) {\n try {\n const message = parseJsonValue(\n lines[i],\n (value): value is ConversationLogMessage => isRecord(value),\n (detail) => (detail === \"unexpected JSON shape\" ? \"expected a JSON object\" : detail),\n );\n records.push({ message, index: i });\n } catch (err) {\n log.logWarning(\n `Skipping malformed log entry at ${logFile}:${i + 1}`,\n err instanceof Error ? err.message : String(err),\n );\n }\n }\n return records;\n}\n\nfunction findLogRecordById(records: LogRecord[], messageId: string): LogRecord | undefined {\n for (let i = records.length - 1; i >= 0; i--) {\n if (records[i].message.ts === messageId) return records[i];\n }\n return undefined;\n}\n\nfunction selectRecentTopLevelMessages(\n records: LogRecord[],\n options: {\n recentDays: number;\n maxMessages: number;\n now: Date;\n excludeMessageId?: string;\n },\n): LogRecord[] {\n const sinceMs = options.now.getTime() - options.recentDays * 24 * 60 * 60 * 1000;\n return records\n .filter((record) => isTopLevelHistoryMessage(record.message, sinceMs, options.excludeMessageId))\n .slice(-options.maxMessages);\n}\n\nfunction selectThreadBootstrapMessages(\n records: LogRecord[],\n threadId: string,\n options: {\n recentDays: number;\n maxTopLevelMessages: number;\n now: Date;\n excludeMessageId?: string;\n },\n): LogRecord[] {\n const rootRecord = findLogRecordById(records, threadId);\n const topLevelSource = rootRecord\n ? records.filter((record) => record.index <= rootRecord.index)\n : records;\n const topLevelRecords = selectRecentTopLevelMessages(topLevelSource, {\n recentDays: options.recentDays,\n maxMessages: options.maxTopLevelMessages,\n now: options.now,\n excludeMessageId: options.excludeMessageId,\n });\n const threadRecords = records.filter(\n (record) =>\n isRenderableChatMessage(record.message, options.excludeMessageId) &&\n (record.message.ts === threadId || record.message.threadTs === threadId),\n );\n\n return dedupeAndSortRecords([...topLevelRecords, ...threadRecords]);\n}\n\nfunction isTopLevelHistoryMessage(\n message: ConversationLogMessage,\n sinceMs: number,\n excludeMessageId?: string,\n): boolean {\n if (!isRenderableChatMessage(message, excludeMessageId)) return false;\n if (message.threadTs) return false;\n if (!message.date) return true;\n\n const dateMs = new Date(message.date).getTime();\n return !Number.isFinite(dateMs) || dateMs >= sinceMs;\n}\n\nfunction selectExistingSessionSyncMessages(\n records: LogRecord[],\n options: { sessionKey: string | null; excludeMessageId?: string },\n): LogRecord[] {\n const threadId = options.sessionKey ? extractThreadId(options.sessionKey) : null;\n return dedupeAndSortRecords(\n records.filter((record) => {\n if (!isRenderableChatMessage(record.message, options.excludeMessageId)) return false;\n if (!threadId) return !record.message.threadTs;\n return record.message.ts === threadId || record.message.threadTs === threadId;\n }),\n );\n}\n\nfunction isRenderableChatMessage(\n message: ConversationLogMessage,\n excludeMessageId?: string,\n): boolean {\n if (excludeMessageId && message.ts === excludeMessageId) return false;\n if (isChatCommandMessage(message)) return false;\n return !!message.text?.trim();\n}\n\nfunction isChatCommandMessage(message: ConversationLogMessage): boolean {\n const text = message.text?.trim() ?? \"\";\n return (\n !message.isBot &&\n /^\\/(?:pi-[\\w-]+|login|session|new|stop|model|sandbox|admin|auto-reply)(?:@\\w+)?(?:\\s|$)/i.test(\n text,\n )\n );\n}\n\nfunction dedupeAndSortRecords(records: LogRecord[]): LogRecord[] {\n const byKey = new Map<string, LogRecord>();\n for (const record of records) {\n byKey.set(record.message.ts ?? `line:${record.index}`, record);\n }\n\n return Array.from(byKey.values()).toSorted((a, b) => {\n const aTime = sortTime(a);\n const bTime = sortTime(b);\n if (aTime !== bTime) return aTime - bTime;\n return a.index - b.index;\n });\n}\n\nfunction sortTime(record: LogRecord): number {\n if (record.message.date) {\n const dateMs = new Date(record.message.date).getTime();\n if (Number.isFinite(dateMs)) return dateMs;\n }\n\n if (record.message.ts) {\n const tsMs = Number(record.message.ts) * 1000;\n if (Number.isFinite(tsMs)) return tsMs;\n }\n\n return record.index;\n}\n\nfunction bootstrapSessionFromLog(\n sessionFile: string,\n sessionDir: string,\n cwd: string,\n records: LogRecord[],\n): void {\n if (records.length === 0) return;\n\n const sessionManager = openManagedSession(sessionFile, sessionDir, cwd);\n appendLogRecordsToSession(sessionManager, records);\n sessionManager.appendCustomEntry(CHAT_SYNC_CUSTOM_TYPE, {\n source: \"log.jsonl\",\n messageCount: records.length,\n lastMessageId: records.at(-1)?.message.ts,\n });\n forceRewriteSession(sessionManager, sessionFile);\n}\n\nfunction syncSessionFromLog(\n sessionFile: string,\n sessionDir: string,\n cwd: string,\n records: LogRecord[],\n): void {\n if (records.length === 0) return;\n syncSessionManagerFromLog(openManagedSession(sessionFile, sessionDir, cwd), records);\n}\n\nfunction syncSessionManagerFromLog(sessionManager: SessionManager, records: LogRecord[]): void {\n if (records.length === 0) return;\n\n const existingEntries = sessionManager.getEntries();\n const lastSyncedMessageId = getLatestChatSyncState(existingEntries)?.lastMessageId;\n const startIndex = lastSyncedMessageId\n ? records.findIndex((record) => record.message.ts === lastSyncedMessageId) + 1\n : 0;\n const syncCandidates = records.slice(Math.max(startIndex, 0));\n if (syncCandidates.length === 0) return;\n\n const represented = buildRepresentedMessageCounts(existingEntries);\n const newRecords = syncCandidates.filter(\n (record) => !consumeRepresentedLogMessage(record, represented),\n );\n if (newRecords.length === 0) return;\n\n appendLogRecordsToSession(sessionManager, newRecords);\n sessionManager.appendCustomEntry(CHAT_SYNC_CUSTOM_TYPE, {\n source: \"log.jsonl\",\n messageCount: newRecords.length,\n lastMessageId: syncCandidates.at(-1)?.message.ts,\n });\n}\n\nfunction appendLogRecordsToSession(sessionManager: SessionManager, records: LogRecord[]): void {\n for (const record of records) {\n const message = buildHistorySessionMessage(record.message);\n if (message) sessionManager.appendMessage(message);\n }\n}\n\nfunction forceRewriteSession(sessionManager: SessionManager, sessionFile: string): void {\n const header = sessionManager.getHeader();\n if (!header) return;\n\n const content = [header, ...sessionManager.getEntries()]\n .map((entry) => JSON.stringify(entry))\n .join(\"\\n\");\n atomicWritePrivateFile(sessionFile, `${content}\\n`);\n}\n\nfunction getLatestChatSyncState(entries: SessionEntry[]): ChatSyncState | null {\n for (let i = entries.length - 1; i >= 0; i--) {\n const entry = entries[i];\n if (entry.type !== \"custom\" || entry.customType !== CHAT_SYNC_CUSTOM_TYPE) continue;\n if (!isRecord(entry.data)) return null;\n return {\n source: typeof entry.data.source === \"string\" ? entry.data.source : undefined,\n messageCount:\n typeof entry.data.messageCount === \"number\" ? entry.data.messageCount : undefined,\n lastMessageId:\n typeof entry.data.lastMessageId === \"string\" ? entry.data.lastMessageId : undefined,\n };\n }\n return null;\n}\n\nfunction buildRepresentedMessageCounts(entries: SessionEntry[]): Map<string, number> {\n const counts = new Map<string, number>();\n for (const entry of entries) {\n const comparable = comparableSessionMessage(entry);\n if (!comparable) continue;\n counts.set(comparable, (counts.get(comparable) ?? 0) + 1);\n }\n return counts;\n}\n\nfunction consumeRepresentedLogMessage(record: LogRecord, counts: Map<string, number>): boolean {\n const comparable = comparableLogMessage(record.message);\n if (!comparable) return false;\n\n const count = counts.get(comparable) ?? 0;\n if (count <= 0) return false;\n counts.set(comparable, count - 1);\n return true;\n}\n\nfunction comparableSessionMessage(entry: SessionEntry): string | 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 text = normalizeComparableText(getSessionMessageText(entry));\n if (!text) return null;\n return `${role}:${text}`;\n}\n\nfunction comparableLogMessage(message: ConversationLogMessage): string | null {\n const text = message.text?.trim();\n if (!text) return null;\n return `${message.isBot ? \"assistant\" : \"user\"}:${normalizeComparableText(text)}`;\n}\n\nfunction getSessionMessageText(entry: SessionEntry): string {\n if (entry.type !== \"message\" || !(\"content\" in entry.message)) return \"\";\n const content = entry.message.content;\n if (typeof content === \"string\") return content;\n if (!Array.isArray(content)) return \"\";\n return content\n .map((part) => (part.type === \"text\" && \"text\" in part ? part.text : \"\"))\n .join(\"\\n\");\n}\n\nfunction normalizeComparableText(text: string): string {\n return text\n .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 .trim();\n}\n\nfunction buildHistorySessionMessage(message: ConversationLogMessage): SessionAppendMessage | null {\n const text = message.text?.trim();\n if (!text) return null;\n\n const timestamp = parseMessageTimestamp(message);\n if (!message.isBot) {\n return {\n role: \"user\",\n content: [{ type: \"text\", text: formatHistoryMessage(message) }],\n ...(timestamp !== undefined ? { timestamp } : {}),\n } as SessionAppendMessage;\n }\n\n return {\n role: \"assistant\",\n content: [{ type: \"text\", text }],\n api: \"platform-history\",\n provider: \"platform-history\",\n model: \"platform-history\",\n usage: zeroUsage(),\n stopReason: \"stop\",\n ...(timestamp !== undefined ? { timestamp } : {}),\n } as SessionAppendMessage;\n}\n\nfunction buildThreadRootSeed(\n message: ConversationLogMessage | undefined,\n): ThreadRootMessage | null {\n if (!message) return null;\n return {\n text: message.text,\n userName: message.userName,\n user: message.user,\n loggedAt: message.date ? new Date(message.date).getTime() : undefined,\n isBot: message.isBot,\n };\n}\n\nfunction parseMessageTimestamp(message: ConversationLogMessage): number | undefined {\n if (message.date) {\n const dateMs = new Date(message.date).getTime();\n if (Number.isFinite(dateMs)) return dateMs;\n }\n\n if (message.ts) {\n const tsMs = Number(message.ts) * 1000;\n if (Number.isFinite(tsMs)) return tsMs;\n }\n\n return undefined;\n}\n\nfunction formatHistoryMessage(message: ConversationLogMessage): string {\n const text = message.text?.trim() ?? \"\";\n const userLabel = message.userName || message.user || \"unknown\";\n const timestamp = message.date ? formatLocalTimestamp(new Date(message.date)) : null;\n return timestamp ? `[${timestamp}] [${userLabel}]: ${text}` : `[${userLabel}]: ${text}`;\n}\n\nfunction formatLocalTimestamp(date: Date): string | null {\n const time = date.getTime();\n if (!Number.isFinite(time)) return null;\n\n const offset = -date.getTimezoneOffset();\n const sign = offset >= 0 ? \"+\" : \"-\";\n const abs = Math.abs(offset);\n return (\n `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` +\n `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}` +\n `${sign}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`\n );\n}\n\nfunction pad(n: number): string {\n return n.toString().padStart(2, \"0\");\n}\n\nfunction zeroUsage(): object {\n return {\n input: 0,\n output: 0,\n cacheRead: 0,\n cacheWrite: 0,\n totalTokens: 0,\n cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n };\n}\n"]}
1
+ {"version":3,"file":"chat-session-manager.d.ts","sourceRoot":"","sources":["../../src/sessions/chat-session-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAqB,MAAM,iCAAiC,CAAC;AAOpF,OAAO,EAUL,KAAK,oBAAoB,EAE1B,MAAM,YAAY,CAAC;AAapB,MAAM,WAAW,yBAAyB;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,8BAA8B;IAC7C,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,iGAAiG;IACjG,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,6BAA6B;IAC5C,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,cAAc,CAAC;IAC/B,4FAA4F;IAC5F,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,uBAAuB;IACtC,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,4BAA4B;IAC3C,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,6BAA6B;IAC5C,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,0BAA0B;IACzC,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,OAAO,CAAC;IAChC,eAAe,EAAE,MAAM,OAAO,CAAC;IAC/B,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAMD,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAE9D;AAED,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,6BAA6B,GAAG,OAAO,CAQ1F;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,4BAA4B,GAAG,MAAM,GAAG,IAAI,CAQ1F;AAED,wBAAsB,6BAA6B,CACjD,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,OAAO,CAAC,CAqBlB;AAED,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAS;IAC7C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAa;IAEjC,YAAY,OAAO,GAAE,yBAA8B,EAIlD;IAEK,mBAAmB,CACvB,OAAO,EAAE,8BAA8B,GACtC,OAAO,CAAC,oBAAoB,CAAC,CAqB/B;IAED,kBAAkB,CAAC,OAAO,EAAE,6BAA6B,GAAG,IAAI,CAS/D;IAED,YAAY,CAAC,OAAO,EAAE,uBAAuB,GAAG,MAAM,CAUrD;IAED,qBAAqB,CAAC,OAAO,EAAE,4BAA4B,GAAG,MAAM,GAAG,IAAI,CAE1E;IAED,sBAAsB,CAAC,OAAO,EAAE,6BAA6B,GAAG,OAAO,CAEtE;IAED,OAAO,CAAC,0BAA0B;IAgClC,OAAO,CAAC,yBAAyB;CAoClC","sourcesContent":["import { SessionManager, type SessionEntry } from \"@earendil-works/pi-coding-agent\";\nimport { join } from \"path\";\nimport type { ConversationLogMessage } from \"../context.js\";\nimport { isRecord, parseJsonValue, readTextFileIfExists } from \"../file-guards.js\";\nimport { atomicWritePrivateFile } from \"../fs-atomic.js\";\nimport * as log from \"../log.js\";\nimport { isPlatformHistorySession } from \"./metadata.js\";\nimport {\n createManagedSessionFile,\n createManagedSessionFileAtPath,\n extractSessionSuffix,\n getChannelSessionDir,\n getThreadSessionFile,\n openManagedSession,\n resolveChannelSessionFile,\n tryResolveCurrentSession,\n tryResolveThreadSession,\n type ResolvedSessionScope,\n type ThreadRootMessage,\n} from \"./store.js\";\n\nconst DEFAULT_RECENT_DAYS = 14;\nconst DEFAULT_MAX_TOP_LEVEL_MESSAGES = 200;\nconst CHAT_SYNC_CUSTOM_TYPE = \"mikan.chat_sync\";\n\ntype SessionAppendMessage = Parameters<SessionManager[\"appendMessage\"]>[0];\n\ninterface LogRecord {\n message: ConversationLogMessage;\n index: number;\n}\n\nexport interface ChatSessionManagerOptions {\n recentDays?: number;\n maxTopLevelMessages?: number;\n now?: () => Date;\n}\n\nexport interface ResolveChatSessionScopeOptions {\n conversationDir: string;\n sessionKey: string;\n cwd?: string;\n /** The triggering platform message ID. Excluded from bootstrap to avoid duplicate user turns. */\n currentMessageId?: string;\n}\n\nexport interface SyncChatSessionManagerOptions {\n conversationDir: string;\n sessionKey: string;\n sessionManager: SessionManager;\n /** The triggering platform message ID. Excluded from sync to avoid duplicate user turns. */\n currentMessageId?: string;\n}\n\nexport interface ResetChatSessionOptions {\n conversationDir: string;\n sessionKey: string;\n cwd?: string;\n}\n\nexport interface RegisterThreadSessionOptions {\n conversationDir: string;\n sessionKey: string;\n cwd?: string;\n}\n\nexport interface HasMaterializedSessionOptions {\n conversationDir: string;\n sessionKey: string;\n}\n\nexport interface ThreadBootstrapWaitOptions {\n parentSessionKey: string;\n sessionKey: string;\n hasThreadSession: () => boolean;\n isParentRunning: () => boolean;\n sleep?: (ms: number) => Promise<void>;\n pollMs?: number;\n}\n\nfunction defaultSleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nexport function isThreadSessionKey(sessionKey: string): boolean {\n return sessionKey.includes(\":\");\n}\n\nexport function extractThreadId(sessionKey: string): string {\n return extractSessionSuffix(sessionKey);\n}\n\nexport function hasMaterializedChatSession(options: HasMaterializedSessionOptions): boolean {\n if (!isThreadSessionKey(options.sessionKey)) {\n return resolveChannelSessionFile(options.conversationDir) !== null;\n }\n return (\n tryResolveThreadSession(getThreadSessionFile(options.conversationDir, options.sessionKey)) !==\n null\n );\n}\n\nexport function registerThreadSession(options: RegisterThreadSessionOptions): string | null {\n if (!isThreadSessionKey(options.sessionKey)) return null;\n\n const threadFile = getThreadSessionFile(options.conversationDir, options.sessionKey);\n return (\n tryResolveThreadSession(threadFile) ??\n createManagedSessionFileAtPath(threadFile, options.cwd ?? options.conversationDir)\n );\n}\n\nexport async function waitForThreadSessionBootstrap(\n options: ThreadBootstrapWaitOptions,\n): Promise<boolean> {\n const {\n parentSessionKey,\n sessionKey,\n hasThreadSession,\n isParentRunning,\n sleep = defaultSleep,\n pollMs = 100,\n } = options;\n\n if (!isThreadSessionKey(sessionKey)) return false;\n if (sessionKey === parentSessionKey) return false;\n if (hasThreadSession()) return false;\n\n let waited = false;\n while (isParentRunning() && !hasThreadSession()) {\n waited = true;\n await sleep(pollMs);\n }\n\n return waited;\n}\n\nexport class ChatSessionManager {\n private readonly recentDays: number;\n private readonly maxTopLevelMessages: number;\n private readonly now: () => Date;\n\n constructor(options: ChatSessionManagerOptions = {}) {\n this.recentDays = options.recentDays ?? DEFAULT_RECENT_DAYS;\n this.maxTopLevelMessages = options.maxTopLevelMessages ?? DEFAULT_MAX_TOP_LEVEL_MESSAGES;\n this.now = options.now ?? (() => new Date());\n }\n\n async resolveSessionScope(\n options: ResolveChatSessionScopeOptions,\n ): Promise<ResolvedSessionScope> {\n const cwd = options.cwd ?? options.conversationDir;\n const sessionDir = getChannelSessionDir(options.conversationDir);\n\n if (!isThreadSessionKey(options.sessionKey)) {\n const contextFile = this.resolveTopLevelSessionFile({\n conversationDir: options.conversationDir,\n sessionDir,\n cwd,\n currentMessageId: options.currentMessageId,\n });\n return { sessionDir, contextFile, threadRootMessage: null };\n }\n\n return this.resolveThreadSessionScope({\n conversationDir: options.conversationDir,\n sessionDir,\n sessionKey: options.sessionKey,\n cwd,\n currentMessageId: options.currentMessageId,\n });\n }\n\n syncSessionManager(options: SyncChatSessionManagerOptions): void {\n const records = readConversationLog(options.conversationDir);\n syncSessionManagerFromLog(\n options.sessionManager,\n selectExistingSessionSyncMessages(records, {\n sessionKey: isThreadSessionKey(options.sessionKey) ? options.sessionKey : null,\n excludeMessageId: options.currentMessageId,\n }),\n );\n }\n\n resetSession(options: ResetChatSessionOptions): string {\n const cwd = options.cwd ?? options.conversationDir;\n if (isThreadSessionKey(options.sessionKey)) {\n return createManagedSessionFileAtPath(\n getThreadSessionFile(options.conversationDir, options.sessionKey),\n cwd,\n );\n }\n\n return createManagedSessionFile(getChannelSessionDir(options.conversationDir), cwd);\n }\n\n registerThreadSession(options: RegisterThreadSessionOptions): string | null {\n return registerThreadSession(options);\n }\n\n hasMaterializedSession(options: HasMaterializedSessionOptions): boolean {\n return hasMaterializedChatSession(options);\n }\n\n private resolveTopLevelSessionFile(options: {\n conversationDir: string;\n sessionDir: string;\n cwd: string;\n currentMessageId?: string;\n }): string {\n const records = readConversationLog(options.conversationDir);\n const existing = tryResolveCurrentSession(options.sessionDir);\n if (existing && !isPlatformHistorySession(existing)) {\n syncSessionFromLog(\n existing,\n options.sessionDir,\n options.cwd,\n selectExistingSessionSyncMessages(records, {\n sessionKey: null,\n excludeMessageId: options.currentMessageId,\n }),\n );\n return existing;\n }\n\n const sessionFile = createManagedSessionFile(options.sessionDir, options.cwd);\n const bootstrapRecords = selectRecentTopLevelMessages(records, {\n recentDays: this.recentDays,\n maxMessages: this.maxTopLevelMessages,\n now: this.now(),\n excludeMessageId: options.currentMessageId,\n });\n bootstrapSessionFromLog(sessionFile, options.sessionDir, options.cwd, bootstrapRecords);\n return sessionFile;\n }\n\n private resolveThreadSessionScope(options: {\n conversationDir: string;\n sessionDir: string;\n sessionKey: string;\n cwd: string;\n currentMessageId?: string;\n }): ResolvedSessionScope {\n const threadFile = getThreadSessionFile(options.conversationDir, options.sessionKey);\n const threadId = extractThreadId(options.sessionKey);\n const records = readConversationLog(options.conversationDir);\n const threadRootMessage = buildThreadRootSeed(findLogRecordById(records, threadId)?.message);\n const existing = tryResolveThreadSession(threadFile);\n if (existing) {\n syncSessionFromLog(\n existing,\n options.sessionDir,\n options.cwd,\n selectExistingSessionSyncMessages(records, {\n sessionKey: options.sessionKey,\n excludeMessageId: options.currentMessageId,\n }),\n );\n return { sessionDir: options.sessionDir, contextFile: existing, threadRootMessage };\n }\n\n createManagedSessionFileAtPath(threadFile, options.cwd);\n const bootstrapRecords = selectThreadBootstrapMessages(records, threadId, {\n recentDays: this.recentDays,\n maxTopLevelMessages: this.maxTopLevelMessages,\n now: this.now(),\n excludeMessageId: options.currentMessageId,\n });\n bootstrapSessionFromLog(threadFile, options.sessionDir, options.cwd, bootstrapRecords);\n\n return { sessionDir: options.sessionDir, contextFile: threadFile, threadRootMessage };\n }\n}\n\nfunction readConversationLog(conversationDir: string): LogRecord[] {\n const logFile = join(conversationDir, \"log.jsonl\");\n const raw = readTextFileIfExists(logFile);\n if (raw === undefined) return [];\n\n const lines = raw.trim().split(\"\\n\").filter(Boolean);\n const records: LogRecord[] = [];\n for (let i = 0; i < lines.length; i++) {\n try {\n const message = parseJsonValue(\n lines[i],\n (value): value is ConversationLogMessage => isRecord(value),\n (detail) => (detail === \"unexpected JSON shape\" ? \"expected a JSON object\" : detail),\n );\n records.push({ message, index: i });\n } catch (err) {\n log.logWarning(\n `Skipping malformed log entry at ${logFile}:${i + 1}`,\n err instanceof Error ? err.message : String(err),\n );\n }\n }\n return records;\n}\n\nfunction findLogRecordById(records: LogRecord[], messageId: string): LogRecord | undefined {\n for (let i = records.length - 1; i >= 0; i--) {\n if (records[i].message.ts === messageId) return records[i];\n }\n return undefined;\n}\n\nfunction selectRecentTopLevelMessages(\n records: LogRecord[],\n options: {\n recentDays: number;\n maxMessages: number;\n now: Date;\n excludeMessageId?: string;\n },\n): LogRecord[] {\n const sinceMs = options.now.getTime() - options.recentDays * 24 * 60 * 60 * 1000;\n return records\n .filter((record) => isTopLevelHistoryMessage(record.message, sinceMs, options.excludeMessageId))\n .slice(-options.maxMessages);\n}\n\nfunction selectThreadBootstrapMessages(\n records: LogRecord[],\n threadId: string,\n options: {\n recentDays: number;\n maxTopLevelMessages: number;\n now: Date;\n excludeMessageId?: string;\n },\n): LogRecord[] {\n const rootRecord = findLogRecordById(records, threadId);\n const topLevelSource = rootRecord\n ? records.filter((record) => record.index <= rootRecord.index)\n : records;\n const topLevelRecords = selectRecentTopLevelMessages(topLevelSource, {\n recentDays: options.recentDays,\n maxMessages: options.maxTopLevelMessages,\n now: options.now,\n excludeMessageId: options.excludeMessageId,\n });\n const threadRecords = records.filter(\n (record) =>\n isRenderableChatMessage(record.message, options.excludeMessageId) &&\n (record.message.ts === threadId || record.message.threadTs === threadId),\n );\n\n return dedupeAndSortRecords([...topLevelRecords, ...threadRecords]);\n}\n\nfunction isTopLevelHistoryMessage(\n message: ConversationLogMessage,\n sinceMs: number,\n excludeMessageId?: string,\n): boolean {\n if (!isRenderableChatMessage(message, excludeMessageId)) return false;\n if (message.threadTs) return false;\n if (!message.date) return true;\n\n const dateMs = new Date(message.date).getTime();\n return !Number.isFinite(dateMs) || dateMs >= sinceMs;\n}\n\nfunction selectExistingSessionSyncMessages(\n records: LogRecord[],\n options: { sessionKey: string | null; excludeMessageId?: string },\n): LogRecord[] {\n const threadId = options.sessionKey ? extractThreadId(options.sessionKey) : null;\n return dedupeAndSortRecords(\n records.filter((record) => {\n if (!isRenderableChatMessage(record.message, options.excludeMessageId)) return false;\n if (!threadId) return !record.message.threadTs;\n return record.message.ts === threadId || record.message.threadTs === threadId;\n }),\n );\n}\n\nfunction isRenderableChatMessage(\n message: ConversationLogMessage,\n excludeMessageId?: string,\n): boolean {\n if (excludeMessageId && message.ts === excludeMessageId) return false;\n if (isChatCommandMessage(message)) return false;\n return !!message.text?.trim();\n}\n\nfunction isChatCommandMessage(message: ConversationLogMessage): boolean {\n const text = message.text?.trim() ?? \"\";\n return (\n !message.isBot &&\n /^\\/(?:pi-[\\w-]+|login|session|new|stop|model|sandbox|admin|auto-reply)(?:@\\w+)?(?:\\s|$)/i.test(\n text,\n )\n );\n}\n\nfunction dedupeAndSortRecords(records: LogRecord[]): LogRecord[] {\n const byKey = new Map<string, LogRecord>();\n for (const record of records) {\n byKey.set(record.message.ts ?? `line:${record.index}`, record);\n }\n\n return Array.from(byKey.values()).toSorted((a, b) => {\n const aTime = sortTime(a);\n const bTime = sortTime(b);\n if (aTime !== bTime) return aTime - bTime;\n return a.index - b.index;\n });\n}\n\nfunction sortTime(record: LogRecord): number {\n if (record.message.date) {\n const dateMs = new Date(record.message.date).getTime();\n if (Number.isFinite(dateMs)) return dateMs;\n }\n\n if (record.message.ts) {\n const tsMs = Number(record.message.ts) * 1000;\n if (Number.isFinite(tsMs)) return tsMs;\n }\n\n return record.index;\n}\n\nfunction bootstrapSessionFromLog(\n sessionFile: string,\n sessionDir: string,\n cwd: string,\n records: LogRecord[],\n): void {\n if (records.length === 0) return;\n\n const sessionManager = openManagedSession(sessionFile, sessionDir, cwd);\n appendLogRecordsToSession(sessionManager, records);\n sessionManager.appendCustomEntry(CHAT_SYNC_CUSTOM_TYPE, {\n source: \"log.jsonl\",\n messageCount: records.length,\n lastMessageId: records.at(-1)?.message.ts,\n });\n forceRewriteSession(sessionManager, sessionFile);\n}\n\nfunction syncSessionFromLog(\n sessionFile: string,\n sessionDir: string,\n cwd: string,\n records: LogRecord[],\n): void {\n if (records.length === 0) return;\n syncSessionManagerFromLog(openManagedSession(sessionFile, sessionDir, cwd), records);\n}\n\nfunction syncSessionManagerFromLog(sessionManager: SessionManager, records: LogRecord[]): void {\n if (records.length === 0) return;\n\n const existingEntries = sessionManager.getEntries();\n const lastSyncedMessageId = getLatestChatSyncMessageId(existingEntries);\n const startIndex = lastSyncedMessageId\n ? records.findIndex((record) => record.message.ts === lastSyncedMessageId) + 1\n : 0;\n const syncCandidates = records.slice(Math.max(startIndex, 0));\n if (syncCandidates.length === 0) return;\n\n const represented = buildRepresentedMessageCounts(existingEntries);\n const newRecords = syncCandidates.filter(\n (record) => !consumeRepresentedLogMessage(record, represented),\n );\n if (newRecords.length === 0) return;\n\n appendLogRecordsToSession(sessionManager, newRecords);\n sessionManager.appendCustomEntry(CHAT_SYNC_CUSTOM_TYPE, {\n source: \"log.jsonl\",\n messageCount: newRecords.length,\n lastMessageId: syncCandidates.at(-1)?.message.ts,\n });\n}\n\nfunction appendLogRecordsToSession(sessionManager: SessionManager, records: LogRecord[]): void {\n for (const record of records) {\n const message = buildHistorySessionMessage(record.message);\n if (message) sessionManager.appendMessage(message);\n }\n}\n\nfunction forceRewriteSession(sessionManager: SessionManager, sessionFile: string): void {\n const header = sessionManager.getHeader();\n if (!header) return;\n\n const content = [header, ...sessionManager.getEntries()]\n .map((entry) => JSON.stringify(entry))\n .join(\"\\n\");\n atomicWritePrivateFile(sessionFile, `${content}\\n`);\n}\n\nfunction getLatestChatSyncMessageId(entries: SessionEntry[]): string | undefined {\n for (let i = entries.length - 1; i >= 0; i--) {\n const entry = entries[i];\n if (entry.type !== \"custom\" || entry.customType !== CHAT_SYNC_CUSTOM_TYPE) continue;\n if (!isRecord(entry.data)) return undefined;\n return typeof entry.data.lastMessageId === \"string\" ? entry.data.lastMessageId : undefined;\n }\n return undefined;\n}\n\nfunction buildRepresentedMessageCounts(entries: SessionEntry[]): Map<string, number> {\n const counts = new Map<string, number>();\n for (const entry of entries) {\n const comparable = comparableSessionMessage(entry);\n if (!comparable) continue;\n counts.set(comparable, (counts.get(comparable) ?? 0) + 1);\n }\n return counts;\n}\n\nfunction consumeRepresentedLogMessage(record: LogRecord, counts: Map<string, number>): boolean {\n const comparable = comparableLogMessage(record.message);\n if (!comparable) return false;\n\n const count = counts.get(comparable) ?? 0;\n if (count <= 0) return false;\n counts.set(comparable, count - 1);\n return true;\n}\n\nfunction comparableSessionMessage(entry: SessionEntry): string | 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 text = normalizeComparableText(getSessionMessageText(entry));\n if (!text) return null;\n return `${role}:${text}`;\n}\n\nfunction comparableLogMessage(message: ConversationLogMessage): string | null {\n const text = message.text?.trim();\n if (!text) return null;\n return `${message.isBot ? \"assistant\" : \"user\"}:${normalizeComparableText(text)}`;\n}\n\nfunction getSessionMessageText(entry: SessionEntry): string {\n if (entry.type !== \"message\" || !(\"content\" in entry.message)) return \"\";\n const content = entry.message.content;\n if (typeof content === \"string\") return content;\n if (!Array.isArray(content)) return \"\";\n return content\n .map((part) => (part.type === \"text\" && \"text\" in part ? part.text : \"\"))\n .join(\"\\n\");\n}\n\nfunction normalizeComparableText(text: string): string {\n return text\n .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 .trim();\n}\n\nfunction buildHistorySessionMessage(message: ConversationLogMessage): SessionAppendMessage | null {\n const text = message.text?.trim();\n if (!text) return null;\n\n const timestamp = parseMessageTimestamp(message);\n if (!message.isBot) {\n return {\n role: \"user\",\n content: [{ type: \"text\", text: formatHistoryMessage(message) }],\n ...(timestamp !== undefined ? { timestamp } : {}),\n } as SessionAppendMessage;\n }\n\n return {\n role: \"assistant\",\n content: [{ type: \"text\", text }],\n api: \"platform-history\",\n provider: \"platform-history\",\n model: \"platform-history\",\n usage: zeroUsage(),\n stopReason: \"stop\",\n ...(timestamp !== undefined ? { timestamp } : {}),\n } as SessionAppendMessage;\n}\n\nfunction buildThreadRootSeed(\n message: ConversationLogMessage | undefined,\n): ThreadRootMessage | null {\n if (!message) return null;\n return {\n text: message.text,\n userName: message.userName,\n user: message.user,\n loggedAt: message.date ? new Date(message.date).getTime() : undefined,\n isBot: message.isBot,\n };\n}\n\nfunction parseMessageTimestamp(message: ConversationLogMessage): number | undefined {\n if (message.date) {\n const dateMs = new Date(message.date).getTime();\n if (Number.isFinite(dateMs)) return dateMs;\n }\n\n if (message.ts) {\n const tsMs = Number(message.ts) * 1000;\n if (Number.isFinite(tsMs)) return tsMs;\n }\n\n return undefined;\n}\n\nfunction formatHistoryMessage(message: ConversationLogMessage): string {\n const text = message.text?.trim() ?? \"\";\n const userLabel = message.userName || message.user || \"unknown\";\n const timestamp = message.date ? formatLocalTimestamp(new Date(message.date)) : null;\n return timestamp ? `[${timestamp}] [${userLabel}]: ${text}` : `[${userLabel}]: ${text}`;\n}\n\nfunction formatLocalTimestamp(date: Date): string | null {\n const time = date.getTime();\n if (!Number.isFinite(time)) return null;\n\n const offset = -date.getTimezoneOffset();\n const sign = offset >= 0 ? \"+\" : \"-\";\n const abs = Math.abs(offset);\n return (\n `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` +\n `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}` +\n `${sign}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`\n );\n}\n\nfunction pad(n: number): string {\n return n.toString().padStart(2, \"0\");\n}\n\nfunction zeroUsage(): object {\n return {\n input: 0,\n output: 0,\n cacheRead: 0,\n cacheWrite: 0,\n totalTokens: 0,\n cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n };\n}\n"]}
@@ -260,7 +260,7 @@ function syncSessionManagerFromLog(sessionManager, records) {
260
260
  if (records.length === 0)
261
261
  return;
262
262
  const existingEntries = sessionManager.getEntries();
263
- const lastSyncedMessageId = getLatestChatSyncState(existingEntries)?.lastMessageId;
263
+ const lastSyncedMessageId = getLatestChatSyncMessageId(existingEntries);
264
264
  const startIndex = lastSyncedMessageId
265
265
  ? records.findIndex((record) => record.message.ts === lastSyncedMessageId) + 1
266
266
  : 0;
@@ -294,20 +294,16 @@ function forceRewriteSession(sessionManager, sessionFile) {
294
294
  .join("\n");
295
295
  atomicWritePrivateFile(sessionFile, `${content}\n`);
296
296
  }
297
- function getLatestChatSyncState(entries) {
297
+ function getLatestChatSyncMessageId(entries) {
298
298
  for (let i = entries.length - 1; i >= 0; i--) {
299
299
  const entry = entries[i];
300
300
  if (entry.type !== "custom" || entry.customType !== CHAT_SYNC_CUSTOM_TYPE)
301
301
  continue;
302
302
  if (!isRecord(entry.data))
303
- return null;
304
- return {
305
- source: typeof entry.data.source === "string" ? entry.data.source : undefined,
306
- messageCount: typeof entry.data.messageCount === "number" ? entry.data.messageCount : undefined,
307
- lastMessageId: typeof entry.data.lastMessageId === "string" ? entry.data.lastMessageId : undefined,
308
- };
303
+ return undefined;
304
+ return typeof entry.data.lastMessageId === "string" ? entry.data.lastMessageId : undefined;
309
305
  }
310
- return null;
306
+ return undefined;
311
307
  }
312
308
  function buildRepresentedMessageCounts(entries) {
313
309
  const counts = new Map();