@geminixiang/mama 0.2.0-beta.3 → 0.2.0-beta.5

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 (139) hide show
  1. package/README.md +101 -422
  2. package/dist/adapter.d.ts +9 -0
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/adapter.js.map +1 -1
  5. package/dist/adapters/discord/bot.d.ts +1 -0
  6. package/dist/adapters/discord/bot.d.ts.map +1 -1
  7. package/dist/adapters/discord/bot.js +62 -73
  8. package/dist/adapters/discord/bot.js.map +1 -1
  9. package/dist/adapters/discord/context.d.ts.map +1 -1
  10. package/dist/adapters/discord/context.js +9 -2
  11. package/dist/adapters/discord/context.js.map +1 -1
  12. package/dist/adapters/shared.d.ts +48 -0
  13. package/dist/adapters/shared.d.ts.map +1 -1
  14. package/dist/adapters/shared.js +111 -0
  15. package/dist/adapters/shared.js.map +1 -1
  16. package/dist/adapters/slack/bot.d.ts +3 -19
  17. package/dist/adapters/slack/bot.d.ts.map +1 -1
  18. package/dist/adapters/slack/bot.js +58 -188
  19. package/dist/adapters/slack/bot.js.map +1 -1
  20. package/dist/adapters/slack/context.d.ts.map +1 -1
  21. package/dist/adapters/slack/context.js +13 -3
  22. package/dist/adapters/slack/context.js.map +1 -1
  23. package/dist/adapters/telegram/bot.d.ts.map +1 -1
  24. package/dist/adapters/telegram/bot.js +78 -100
  25. package/dist/adapters/telegram/bot.js.map +1 -1
  26. package/dist/adapters/telegram/context.d.ts.map +1 -1
  27. package/dist/adapters/telegram/context.js +9 -2
  28. package/dist/adapters/telegram/context.js.map +1 -1
  29. package/dist/agent.d.ts.map +1 -1
  30. package/dist/agent.js +15 -5
  31. package/dist/agent.js.map +1 -1
  32. package/dist/bindings.d.ts +2 -1
  33. package/dist/bindings.d.ts.map +1 -1
  34. package/dist/bindings.js +3 -2
  35. package/dist/bindings.js.map +1 -1
  36. package/dist/commands/index.d.ts +5 -0
  37. package/dist/commands/index.d.ts.map +1 -0
  38. package/dist/commands/index.js +8 -0
  39. package/dist/commands/index.js.map +1 -0
  40. package/dist/commands/login.d.ts +5 -0
  41. package/dist/commands/login.d.ts.map +1 -0
  42. package/dist/commands/login.js +37 -0
  43. package/dist/commands/login.js.map +1 -0
  44. package/dist/commands/registry.d.ts +7 -0
  45. package/dist/commands/registry.d.ts.map +1 -0
  46. package/dist/commands/registry.js +14 -0
  47. package/dist/commands/registry.js.map +1 -0
  48. package/dist/commands/session-view.d.ts +5 -0
  49. package/dist/commands/session-view.d.ts.map +1 -0
  50. package/dist/commands/session-view.js +38 -0
  51. package/dist/commands/session-view.js.map +1 -0
  52. package/dist/commands/types.d.ts +41 -0
  53. package/dist/commands/types.d.ts.map +1 -0
  54. package/dist/commands/types.js +2 -0
  55. package/dist/commands/types.js.map +1 -0
  56. package/dist/commands/utils.d.ts +5 -0
  57. package/dist/commands/utils.d.ts.map +1 -0
  58. package/dist/commands/utils.js +9 -0
  59. package/dist/commands/utils.js.map +1 -0
  60. package/dist/config.d.ts +4 -4
  61. package/dist/config.d.ts.map +1 -1
  62. package/dist/config.js +37 -42
  63. package/dist/config.js.map +1 -1
  64. package/dist/context.d.ts.map +1 -1
  65. package/dist/context.js +74 -68
  66. package/dist/context.js.map +1 -1
  67. package/dist/execution-resolver.d.ts +6 -3
  68. package/dist/execution-resolver.d.ts.map +1 -1
  69. package/dist/execution-resolver.js +47 -14
  70. package/dist/execution-resolver.js.map +1 -1
  71. package/dist/fs-atomic.d.ts +10 -0
  72. package/dist/fs-atomic.d.ts.map +1 -0
  73. package/dist/fs-atomic.js +45 -0
  74. package/dist/fs-atomic.js.map +1 -0
  75. package/dist/index.d.ts +7 -0
  76. package/dist/index.d.ts.map +1 -0
  77. package/dist/index.js +4 -0
  78. package/dist/index.js.map +1 -0
  79. package/dist/instrument.d.ts.map +1 -1
  80. package/dist/instrument.js +2 -3
  81. package/dist/instrument.js.map +1 -1
  82. package/dist/login/index.d.ts.map +1 -1
  83. package/dist/login/index.js +19 -8
  84. package/dist/login/index.js.map +1 -1
  85. package/dist/login/portal.d.ts.map +1 -1
  86. package/dist/login/portal.js +7 -7
  87. package/dist/login/portal.js.map +1 -1
  88. package/dist/login/session.d.ts +3 -2
  89. package/dist/login/session.d.ts.map +1 -1
  90. package/dist/login/session.js.map +1 -1
  91. package/dist/main.d.ts.map +1 -1
  92. package/dist/main.js +63 -389
  93. package/dist/main.js.map +1 -1
  94. package/dist/provisioner.d.ts +11 -9
  95. package/dist/provisioner.d.ts.map +1 -1
  96. package/dist/provisioner.js +125 -87
  97. package/dist/provisioner.js.map +1 -1
  98. package/dist/runtime/index.d.ts +2 -0
  99. package/dist/runtime/index.d.ts.map +1 -0
  100. package/dist/runtime/index.js +2 -0
  101. package/dist/runtime/index.js.map +1 -0
  102. package/dist/runtime/session-runtime.d.ts +26 -0
  103. package/dist/runtime/session-runtime.d.ts.map +1 -0
  104. package/dist/runtime/session-runtime.js +285 -0
  105. package/dist/runtime/session-runtime.js.map +1 -0
  106. package/dist/sandbox/cloudflare.d.ts +14 -0
  107. package/dist/sandbox/cloudflare.d.ts.map +1 -0
  108. package/dist/sandbox/cloudflare.js +131 -0
  109. package/dist/sandbox/cloudflare.js.map +1 -0
  110. package/dist/sandbox/index.d.ts +6 -4
  111. package/dist/sandbox/index.d.ts.map +1 -1
  112. package/dist/sandbox/index.js +6 -3
  113. package/dist/sandbox/index.js.map +1 -1
  114. package/dist/sandbox/types.d.ts +5 -1
  115. package/dist/sandbox/types.d.ts.map +1 -1
  116. package/dist/sandbox/types.js.map +1 -1
  117. package/dist/session-store.d.ts +5 -1
  118. package/dist/session-store.d.ts.map +1 -1
  119. package/dist/session-store.js +14 -9
  120. package/dist/session-store.js.map +1 -1
  121. package/dist/session-view/portal.d.ts +2 -0
  122. package/dist/session-view/portal.d.ts.map +1 -1
  123. package/dist/session-view/portal.js +45 -7
  124. package/dist/session-view/portal.js.map +1 -1
  125. package/dist/session-view/service.d.ts.map +1 -1
  126. package/dist/session-view/service.js +94 -48
  127. package/dist/session-view/service.js.map +1 -1
  128. package/dist/session-view/store.d.ts +3 -2
  129. package/dist/session-view/store.d.ts.map +1 -1
  130. package/dist/session-view/store.js.map +1 -1
  131. package/dist/vault-routing.d.ts +3 -5
  132. package/dist/vault-routing.d.ts.map +1 -1
  133. package/dist/vault-routing.js +8 -20
  134. package/dist/vault-routing.js.map +1 -1
  135. package/dist/vault.d.ts +7 -5
  136. package/dist/vault.d.ts.map +1 -1
  137. package/dist/vault.js +111 -104
  138. package/dist/vault.js.map +1 -1
  139. package/package.json +7 -9
@@ -17,7 +17,16 @@ export async function handleSessionViewRequest(req, res, url, sessionViewTokenSt
17
17
  return true;
18
18
  }
19
19
  const requestedSession = url.searchParams.get("session");
20
- const targetSessionFile = resolveRequestedSessionFile(entry.sessionFile, requestedSession);
20
+ let targetSessionFile;
21
+ try {
22
+ targetSessionFile = resolveRequestedSessionFile(entry.sessionFile, requestedSession);
23
+ }
24
+ catch (error) {
25
+ log.logWarning(`[${entry.conversationId}] Corrupted session file referenced for ${entry.sessionFile}`, error instanceof Error ? error.message : String(error));
26
+ res.writeHead(500, { "Content-Type": "text/html; charset=utf-8" });
27
+ res.end(renderStatusPage("Session unavailable", "The selected session file appears to be corrupted."));
28
+ return true;
29
+ }
21
30
  if (!targetSessionFile) {
22
31
  res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
23
32
  res.end(renderStatusPage("Session unavailable", "The selected session link is invalid."));
@@ -107,16 +116,32 @@ function renderForkLinks(relations, token) {
107
116
  }
108
117
  export function parseUserBody(raw) {
109
118
  // [timestamp] [username] [in-thread:ts]: content
110
- let m = raw.match(/^\[[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]*)$/);
119
+ let m = raw.match(/^\[([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]*)$/);
111
120
  if (m) {
112
- return { username: m[1], threadTs: m[2] ?? null, content: m[3] };
121
+ const header = [`[${m[1]}]`, `[${m[2]}]`, m[3] ? `[in-thread:${m[3]}]` : ""]
122
+ .filter(Boolean)
123
+ .join(" ");
124
+ return {
125
+ timestamp: m[1],
126
+ username: m[2],
127
+ threadTs: m[3] ?? null,
128
+ header,
129
+ content: m[4],
130
+ };
113
131
  }
114
132
  // [username] [in-thread:ts]: content
115
133
  m = raw.match(/^\[([^\]]+)\](?:\s*\[in-thread:([^\]]+)\])?:\s*([\s\S]*)$/);
116
134
  if (m) {
117
- return { username: m[1], threadTs: m[2] ?? null, content: m[3] };
135
+ const header = [`[${m[1]}]`, m[2] ? `[in-thread:${m[2]}]` : ""].filter(Boolean).join(" ");
136
+ return {
137
+ timestamp: null,
138
+ username: m[1],
139
+ threadTs: m[2] ?? null,
140
+ header,
141
+ content: m[3],
142
+ };
118
143
  }
119
- return { username: null, threadTs: null, content: raw };
144
+ return { timestamp: null, username: null, threadTs: null, header: null, content: raw };
120
145
  }
121
146
  function renderItem(item, token) {
122
147
  if (item.kind === "system") {
@@ -141,10 +166,12 @@ function renderItem(item, token) {
141
166
  }
142
167
  const time = item.meta ? `<time class="msg-time">${esc(formatDate(item.meta))}</time>` : "";
143
168
  if (item.kind === "user") {
144
- const { username, threadTs, content } = item.body
169
+ const parsed = item.body
145
170
  ? parseUserBody(item.body)
146
- : { username: null, threadTs: null, content: "" };
171
+ : { timestamp: null, username: null, threadTs: null, header: null, content: "" };
172
+ const { username, threadTs, header, content } = parsed;
147
173
  const initial = username ? esc(username.slice(0, 2).toUpperCase()) : "U";
174
+ const rawHeader = header ? `<div class="msg-raw-header">${esc(header)}</div>` : "";
148
175
  const body = content ? `<pre class="msg-body">${esc(content)}</pre>` : "";
149
176
  const threadBadge = threadTs
150
177
  ? `<div class="thread-badge" title="Thread ${esc(threadTs)}">Thread · <code>${esc(threadTs)}</code></div>`
@@ -152,6 +179,7 @@ function renderItem(item, token) {
152
179
  const forks = renderForkLinks(item.forks, token ?? "");
153
180
  return `<div class="msg-row msg-user">
154
181
  <div class="user-bubble">
182
+ ${rawHeader}
155
183
  ${threadBadge}
156
184
  ${body}
157
185
  ${forks}
@@ -506,6 +534,16 @@ const styles = `
506
534
  box-shadow: 0 1px 2px rgba(0,0,0,0.12);
507
535
  }
508
536
 
537
+ .msg-raw-header {
538
+ margin-bottom: 8px;
539
+ color: rgba(250, 250, 250, 0.72);
540
+ font-family: 'JetBrains Mono', ui-monospace, monospace;
541
+ font-size: 0.72rem;
542
+ line-height: 1.5;
543
+ white-space: pre-wrap;
544
+ word-break: break-word;
545
+ }
546
+
509
547
  .thread-badge {
510
548
  display: inline-flex;
511
549
  align-items: center;
@@ -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 * as log from \"../log.js\";\nimport {\n loadSessionViewModel,\n resolveRequestedSessionFile,\n type SessionViewItem,\n type SessionViewRelation,\n} from \"./service.js\";\nimport type { InMemorySessionViewTokenStore } from \"./store.js\";\n\nexport async function handleSessionViewRequest(\n req: IncomingMessage,\n res: ServerResponse,\n url: URL,\n sessionViewTokenStore?: InMemorySessionViewTokenStore,\n): Promise<boolean> {\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 const targetSessionFile = resolveRequestedSessionFile(entry.sessionFile, requestedSession);\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 res.writeHead(200, {\n \"Content-Type\": \"text/html; charset=utf-8\",\n \"Cache-Control\": \"no-store\",\n });\n res.end(renderSessionPage(model, entry.token, entry.expiresAt));\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 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 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 forks: SessionViewRelation[];\n },\n token: string,\n expiresAt: number,\n): string {\n const items =\n model.items.length > 0\n ? model.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 const relatedSections = model.parent\n ? `<section class=\"related-card stack\">\n <p class=\"eyebrow\">Forked from</p>\n ${renderRelationCard(model.parent, token)}\n </section>`\n : \"\";\n\n return renderHtmlDocument(\n `${model.title} · Session Viewer`,\n `<header class=\"hero-card\">\n <div class=\"hero-top\">\n <div class=\"hero-title-group\">\n <span class=\"hero-wordmark\">mama</span>\n <h1 class=\"hero-title\">${esc(model.title)}</h1>\n </div>\n <button class=\"refresh-btn\" onclick=\"window.location.reload()\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" fill=\"none\"><path d=\"M12.5 2.5A6 6 0 1 0 13 7\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/><path d=\"M10 2.5h2.5V5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>\n Refresh\n </button>\n </div>\n <div class=\"stat-row\">\n ${renderSummaryItem(\"ID\", model.sessionId.slice(0, 8))}\n ${renderSummaryItem(\"File\", model.fileName)}\n ${renderSummaryItem(\"Created\", formatDate(model.createdAt))}\n ${renderSummaryItem(\"Updated\", formatDate(model.updatedAt))}\n ${renderSummaryItem(\"Entries\", String(model.entryCount))}\n ${renderSummaryItem(\"Expires\", formatDate(new Date(expiresAt).toISOString()))}\n </div>\n </header>\n\n ${relatedSections}\n\n <main class=\"timeline-shell\">\n <div class=\"timeline-list\">\n ${items}\n </div>\n </main>`,\n );\n}\n\nfunction renderSummaryItem(label: string, value: string): string {\n return `<span class=\"stat-chip\"><span class=\"stat-label\">${esc(label)}</span><strong class=\"stat-value\">${esc(value)}</strong></span>`;\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 renderForkLinks(relations: SessionViewRelation[] | undefined, token: string): string {\n if (!relations || relations.length === 0) return \"\";\n return `<div class=\"fork-links\">${relations\n .map((relation) => {\n const href = `/session?token=${encodeURIComponent(token)}&session=${encodeURIComponent(relation.fileName)}`;\n return `<a class=\"fork-link\" href=\"${href}\" title=\"Open ${esc(relation.title)}\">\n <span class=\"fork-dot\" aria-hidden=\"true\"></span>\n <span class=\"fork-text\">Thread</span>\n </a>`;\n })\n .join(\"\")}</div>`;\n}\n\nexport function parseUserBody(raw: string): {\n username: string | null;\n threadTs: 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 return { username: m[1], threadTs: m[2] ?? null, content: m[3] };\n }\n // [username] [in-thread:ts]: content\n m = raw.match(/^\\[([^\\]]+)\\](?:\\s*\\[in-thread:([^\\]]+)\\])?:\\s*([\\s\\S]*)$/);\n if (m) {\n return { username: m[1], threadTs: m[2] ?? null, content: m[3] };\n }\n return { username: null, threadTs: null, content: raw };\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 { username, threadTs, content } = item.body\n ? parseUserBody(item.body)\n : { username: null, threadTs: null, content: \"\" };\n const initial = username ? esc(username.slice(0, 2).toUpperCase()) : \"U\";\n const body = content ? `<pre class=\"msg-body\">${esc(content)}</pre>` : \"\";\n const threadBadge = threadTs\n ? `<div class=\"thread-badge\" title=\"Thread ${esc(threadTs)}\">Thread · <code>${esc(threadTs)}</code></div>`\n : \"\";\n const forks = renderForkLinks(item.forks, token ?? \"\");\n return `<div class=\"msg-row msg-user\">\n <div class=\"user-bubble\">\n ${threadBadge}\n ${body}\n ${forks}\n ${time}\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 ? `<pre class=\"msg-body\">${esc(item.body)}</pre>` : \"\";\n const forks = renderForkLinks(item.forks, token ?? \"\");\n return `<div class=\"msg-row msg-assistant\">\n <div class=\"msg-avatar asst-avatar\" aria-hidden=\"true\">A</div>\n <div class=\"asst-card\">\n ${body}\n ${forks}\n ${time}\n </div>\n</div>`;\n}\n\nfunction renderStatusPage(title: string, message: string): string {\n return renderHtmlDocument(\n title,\n `<section class=\"card stack\">\n <p class=\"eyebrow\">mama</p>\n <h1>${esc(title)}</h1>\n <div class=\"status err\">${esc(message)}</div>\n </section>`,\n );\n}\n\nfunction renderHtmlDocument(title: string, shellContent: string): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>${esc(title)}</title>\n <style>${styles}</style>\n</head>\n<body>\n <main class=\"shell\">\n ${shellContent}\n </main>\n</body>\n</html>`;\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 styles = `\n @import url('https://fonts.googleapis.com/css2?family=Lora:wght@400;600&family=DM+Sans:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap');\n\n :root {\n --bg: #f0ece3;\n --surface: #ffffff;\n --border: rgba(0, 0, 0, 0.08);\n --text: #18181b;\n --muted: #71717a;\n --subtle: #a1a1aa;\n\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 --ok-bg: #f0fdf4;\n --ok-text: #15803d;\n --err-bg: #fef2f2;\n --err-text: #b91c1c;\n }\n\n *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n\n body {\n min-height: 100vh;\n padding: 40px 20px 80px;\n display: flex;\n flex-direction: column;\n align-items: center;\n background-color: var(--bg);\n background-image:\n radial-gradient(ellipse 80% 40% at 50% -10%, rgba(255,255,255,0.6) 0%, transparent 70%);\n color: var(--text);\n font-family: 'DM Sans', 'Segoe UI', system-ui, sans-serif;\n font-size: 15px;\n line-height: 1.5;\n -webkit-font-smoothing: antialiased;\n }\n\n .shell {\n width: 100%;\n max-width: 780px;\n display: flex;\n flex-direction: column;\n gap: 12px;\n }\n\n /* ── Hero ─────────────────────────────────────────────────────────────── */\n\n .hero-card {\n padding: 28px 32px 24px;\n border: 1px solid var(--border);\n border-radius: 20px;\n background: var(--surface);\n box-shadow: 0 1px 2px rgba(0,0,0,0.04), 0 4px 16px rgba(0,0,0,0.06);\n }\n\n .hero-top {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 16px;\n margin-bottom: 20px;\n }\n\n .hero-wordmark {\n display: block;\n margin-bottom: 6px;\n color: var(--subtle);\n font-size: 0.72rem;\n font-weight: 600;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n }\n\n .hero-title {\n font-family: 'Lora', Georgia, serif;\n font-size: clamp(1.4rem, 2.5vw, 1.75rem);\n font-weight: 600;\n line-height: 1.2;\n letter-spacing: -0.01em;\n color: var(--text);\n text-wrap: balance;\n }\n\n .refresh-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n flex-shrink: 0;\n padding: 7px 14px;\n border: 1px solid var(--border);\n border-radius: 999px;\n background: transparent;\n color: var(--muted);\n font: 500 0.8rem/1 'DM Sans', sans-serif;\n cursor: pointer;\n transition: color 120ms, border-color 120ms, background 120ms;\n white-space: nowrap;\n }\n\n .refresh-btn:hover {\n color: var(--text);\n border-color: rgba(0,0,0,0.2);\n background: rgba(0,0,0,0.03);\n }\n\n .refresh-btn:focus-visible {\n outline: 2px solid var(--text);\n outline-offset: 2px;\n }\n\n .stat-row {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n }\n\n .stat-chip {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n padding: 4px 10px;\n border: 1px solid var(--border);\n border-radius: 999px;\n background: #f4f4f5;\n font-size: 0.775rem;\n line-height: 1;\n }\n\n .stat-label {\n color: var(--muted);\n font-weight: 500;\n }\n\n .stat-value {\n color: var(--text);\n font-weight: 600;\n }\n\n /* ── Timeline shell ───────────────────────────────────────────────────── */\n\n .fork-links {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n margin-top: 10px;\n }\n\n .fork-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 .fork-link:hover {\n transform: translateY(-1px);\n background: #fff1f2;\n border-color: rgba(239, 68, 68, 0.28);\n }\n\n .fork-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 .fork-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: 4px;\n }\n\n /* ── Message rows ─────────────────────────────────────────────────────── */\n\n .msg-row {\n display: flex;\n align-items: flex-end;\n gap: 8px;\n padding: 2px 0;\n }\n\n /* ── User messages ────────────────────────────────────────────────────── */\n\n .msg-user {\n justify-content: flex-end;\n }\n\n .user-bubble {\n max-width: 85%;\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 .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 font-family: 'DM Sans', system-ui, sans-serif;\n font-size: 0.9rem;\n line-height: 1.6;\n white-space: pre-wrap;\n word-break: break-word;\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 }\n\n .asst-card {\n min-width: 0;\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 font-family: 'DM Sans', system-ui, sans-serif;\n font-size: 0.9rem;\n line-height: 1.65;\n white-space: pre-wrap;\n word-break: break-word;\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: 6px 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 /* ── 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 .event-time {\n color: var(--subtle);\n font-style: normal;\n }\n\n /* ── Status page ──────────────────────────────────────────────────────── */\n\n .card {\n padding: 28px 32px;\n border: 1px solid var(--border);\n border-radius: 20px;\n background: var(--surface);\n box-shadow: 0 1px 2px rgba(0,0,0,0.04), 0 4px 16px rgba(0,0,0,0.06);\n }\n\n .stack > * + * { margin-top: 14px; }\n\n .eyebrow {\n color: var(--subtle);\n font-size: 0.72rem;\n font-weight: 600;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n }\n\n h1 {\n font-family: 'Lora', Georgia, serif;\n font-size: clamp(1.4rem, 2.5vw, 1.75rem);\n font-weight: 600;\n letter-spacing: -0.01em;\n line-height: 1.2;\n }\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 /* ── Responsive ───────────────────────────────────────────────────────── */\n\n @media (max-width: 600px) {\n body { padding: 20px 12px 60px; }\n\n .hero-card, .card { padding: 20px; border-radius: 16px; }\n\n .hero-top { flex-direction: column; gap: 12px; }\n\n .refresh-btn { align-self: flex-start; }\n\n .user-bubble { max-width: 88%; }\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 * as log from \"../log.js\";\nimport {\n loadSessionViewModel,\n resolveRequestedSessionFile,\n type SessionViewItem,\n type SessionViewRelation,\n} from \"./service.js\";\nimport type { InMemorySessionViewTokenStore } from \"./store.js\";\n\nexport async function handleSessionViewRequest(\n req: IncomingMessage,\n res: ServerResponse,\n url: URL,\n sessionViewTokenStore?: InMemorySessionViewTokenStore,\n): Promise<boolean> {\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 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 res.writeHead(200, {\n \"Content-Type\": \"text/html; charset=utf-8\",\n \"Cache-Control\": \"no-store\",\n });\n res.end(renderSessionPage(model, entry.token, entry.expiresAt));\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 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 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 forks: SessionViewRelation[];\n },\n token: string,\n expiresAt: number,\n): string {\n const items =\n model.items.length > 0\n ? model.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 const relatedSections = model.parent\n ? `<section class=\"related-card stack\">\n <p class=\"eyebrow\">Forked from</p>\n ${renderRelationCard(model.parent, token)}\n </section>`\n : \"\";\n\n return renderHtmlDocument(\n `${model.title} · Session Viewer`,\n `<header class=\"hero-card\">\n <div class=\"hero-top\">\n <div class=\"hero-title-group\">\n <span class=\"hero-wordmark\">mama</span>\n <h1 class=\"hero-title\">${esc(model.title)}</h1>\n </div>\n <button class=\"refresh-btn\" onclick=\"window.location.reload()\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" fill=\"none\"><path d=\"M12.5 2.5A6 6 0 1 0 13 7\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/><path d=\"M10 2.5h2.5V5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>\n Refresh\n </button>\n </div>\n <div class=\"stat-row\">\n ${renderSummaryItem(\"ID\", model.sessionId.slice(0, 8))}\n ${renderSummaryItem(\"File\", model.fileName)}\n ${renderSummaryItem(\"Created\", formatDate(model.createdAt))}\n ${renderSummaryItem(\"Updated\", formatDate(model.updatedAt))}\n ${renderSummaryItem(\"Entries\", String(model.entryCount))}\n ${renderSummaryItem(\"Expires\", formatDate(new Date(expiresAt).toISOString()))}\n </div>\n </header>\n\n ${relatedSections}\n\n <main class=\"timeline-shell\">\n <div class=\"timeline-list\">\n ${items}\n </div>\n </main>`,\n );\n}\n\nfunction renderSummaryItem(label: string, value: string): string {\n return `<span class=\"stat-chip\"><span class=\"stat-label\">${esc(label)}</span><strong class=\"stat-value\">${esc(value)}</strong></span>`;\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 renderForkLinks(relations: SessionViewRelation[] | undefined, token: string): string {\n if (!relations || relations.length === 0) return \"\";\n return `<div class=\"fork-links\">${relations\n .map((relation) => {\n const href = `/session?token=${encodeURIComponent(token)}&session=${encodeURIComponent(relation.fileName)}`;\n return `<a class=\"fork-link\" href=\"${href}\" title=\"Open ${esc(relation.title)}\">\n <span class=\"fork-dot\" aria-hidden=\"true\"></span>\n <span class=\"fork-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 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 ? `<pre class=\"msg-body\">${esc(content)}</pre>` : \"\";\n const threadBadge = threadTs\n ? `<div class=\"thread-badge\" title=\"Thread ${esc(threadTs)}\">Thread · <code>${esc(threadTs)}</code></div>`\n : \"\";\n const forks = renderForkLinks(item.forks, token ?? \"\");\n return `<div class=\"msg-row msg-user\">\n <div class=\"user-bubble\">\n ${rawHeader}\n ${threadBadge}\n ${body}\n ${forks}\n ${time}\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 ? `<pre class=\"msg-body\">${esc(item.body)}</pre>` : \"\";\n const forks = renderForkLinks(item.forks, token ?? \"\");\n return `<div class=\"msg-row msg-assistant\">\n <div class=\"msg-avatar asst-avatar\" aria-hidden=\"true\">A</div>\n <div class=\"asst-card\">\n ${body}\n ${forks}\n ${time}\n </div>\n</div>`;\n}\n\nfunction renderStatusPage(title: string, message: string): string {\n return renderHtmlDocument(\n title,\n `<section class=\"card stack\">\n <p class=\"eyebrow\">mama</p>\n <h1>${esc(title)}</h1>\n <div class=\"status err\">${esc(message)}</div>\n </section>`,\n );\n}\n\nfunction renderHtmlDocument(title: string, shellContent: string): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>${esc(title)}</title>\n <style>${styles}</style>\n</head>\n<body>\n <main class=\"shell\">\n ${shellContent}\n </main>\n</body>\n</html>`;\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 styles = `\n @import url('https://fonts.googleapis.com/css2?family=Lora:wght@400;600&family=DM+Sans:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap');\n\n :root {\n --bg: #f0ece3;\n --surface: #ffffff;\n --border: rgba(0, 0, 0, 0.08);\n --text: #18181b;\n --muted: #71717a;\n --subtle: #a1a1aa;\n\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 --ok-bg: #f0fdf4;\n --ok-text: #15803d;\n --err-bg: #fef2f2;\n --err-text: #b91c1c;\n }\n\n *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n\n body {\n min-height: 100vh;\n padding: 40px 20px 80px;\n display: flex;\n flex-direction: column;\n align-items: center;\n background-color: var(--bg);\n background-image:\n radial-gradient(ellipse 80% 40% at 50% -10%, rgba(255,255,255,0.6) 0%, transparent 70%);\n color: var(--text);\n font-family: 'DM Sans', 'Segoe UI', system-ui, sans-serif;\n font-size: 15px;\n line-height: 1.5;\n -webkit-font-smoothing: antialiased;\n }\n\n .shell {\n width: 100%;\n max-width: 780px;\n display: flex;\n flex-direction: column;\n gap: 12px;\n }\n\n /* ── Hero ─────────────────────────────────────────────────────────────── */\n\n .hero-card {\n padding: 28px 32px 24px;\n border: 1px solid var(--border);\n border-radius: 20px;\n background: var(--surface);\n box-shadow: 0 1px 2px rgba(0,0,0,0.04), 0 4px 16px rgba(0,0,0,0.06);\n }\n\n .hero-top {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 16px;\n margin-bottom: 20px;\n }\n\n .hero-wordmark {\n display: block;\n margin-bottom: 6px;\n color: var(--subtle);\n font-size: 0.72rem;\n font-weight: 600;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n }\n\n .hero-title {\n font-family: 'Lora', Georgia, serif;\n font-size: clamp(1.4rem, 2.5vw, 1.75rem);\n font-weight: 600;\n line-height: 1.2;\n letter-spacing: -0.01em;\n color: var(--text);\n text-wrap: balance;\n }\n\n .refresh-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n flex-shrink: 0;\n padding: 7px 14px;\n border: 1px solid var(--border);\n border-radius: 999px;\n background: transparent;\n color: var(--muted);\n font: 500 0.8rem/1 'DM Sans', sans-serif;\n cursor: pointer;\n transition: color 120ms, border-color 120ms, background 120ms;\n white-space: nowrap;\n }\n\n .refresh-btn:hover {\n color: var(--text);\n border-color: rgba(0,0,0,0.2);\n background: rgba(0,0,0,0.03);\n }\n\n .refresh-btn:focus-visible {\n outline: 2px solid var(--text);\n outline-offset: 2px;\n }\n\n .stat-row {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n }\n\n .stat-chip {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n padding: 4px 10px;\n border: 1px solid var(--border);\n border-radius: 999px;\n background: #f4f4f5;\n font-size: 0.775rem;\n line-height: 1;\n }\n\n .stat-label {\n color: var(--muted);\n font-weight: 500;\n }\n\n .stat-value {\n color: var(--text);\n font-weight: 600;\n }\n\n /* ── Timeline shell ───────────────────────────────────────────────────── */\n\n .fork-links {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n margin-top: 10px;\n }\n\n .fork-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 .fork-link:hover {\n transform: translateY(-1px);\n background: #fff1f2;\n border-color: rgba(239, 68, 68, 0.28);\n }\n\n .fork-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 .fork-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: 4px;\n }\n\n /* ── Message rows ─────────────────────────────────────────────────────── */\n\n .msg-row {\n display: flex;\n align-items: flex-end;\n gap: 8px;\n padding: 2px 0;\n }\n\n /* ── User messages ────────────────────────────────────────────────────── */\n\n .msg-user {\n justify-content: flex-end;\n }\n\n .user-bubble {\n max-width: 85%;\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 font-family: 'DM Sans', system-ui, sans-serif;\n font-size: 0.9rem;\n line-height: 1.6;\n white-space: pre-wrap;\n word-break: break-word;\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 }\n\n .asst-card {\n min-width: 0;\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 font-family: 'DM Sans', system-ui, sans-serif;\n font-size: 0.9rem;\n line-height: 1.65;\n white-space: pre-wrap;\n word-break: break-word;\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: 6px 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 /* ── 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 .event-time {\n color: var(--subtle);\n font-style: normal;\n }\n\n /* ── Status page ──────────────────────────────────────────────────────── */\n\n .card {\n padding: 28px 32px;\n border: 1px solid var(--border);\n border-radius: 20px;\n background: var(--surface);\n box-shadow: 0 1px 2px rgba(0,0,0,0.04), 0 4px 16px rgba(0,0,0,0.06);\n }\n\n .stack > * + * { margin-top: 14px; }\n\n .eyebrow {\n color: var(--subtle);\n font-size: 0.72rem;\n font-weight: 600;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n }\n\n h1 {\n font-family: 'Lora', Georgia, serif;\n font-size: clamp(1.4rem, 2.5vw, 1.75rem);\n font-weight: 600;\n letter-spacing: -0.01em;\n line-height: 1.2;\n }\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 /* ── Responsive ───────────────────────────────────────────────────────── */\n\n @media (max-width: 600px) {\n body { padding: 20px 12px 60px; }\n\n .hero-card, .card { padding: 20px; border-radius: 16px; }\n\n .hero-top { flex-direction: column; gap: 12px; }\n\n .refresh-btn { align-self: flex-start; }\n\n .user-bubble { max-width: 88%; }\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":"service.d.ts","sourceRoot":"","sources":["../../src/session-view/service.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,MAAM,GAAG,QAAQ,CAAC;IAC/C,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,SAAS,GAAG,IAAI,GAAG,KAAK,GAAG,OAAO,CAAC;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,mBAAmB,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,KAAK,EAAE,mBAAmB,EAAE,CAAC;CAC9B;AAED,wBAAgB,0BAA0B,CACxC,UAAU,EAAE,MAAM,EAClB,cAAc,EAAE,MAAM,EACtB,UAAU,EAAE,MAAM,GACjB,MAAM,GAAG,IAAI,CAMf;AAED,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,gBAAgB,CAkD1E;AAED,wBAAgB,2BAA2B,CACzC,eAAe,EAAE,MAAM,EACvB,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,GAChC,MAAM,GAAG,IAAI,CAkBf","sourcesContent":["import { basename, dirname, join, resolve } from \"path\";\nimport { existsSync, readdirSync } from \"fs\";\nimport {\n SessionManager,\n type BranchSummaryEntry,\n type CompactionEntry,\n type SessionEntry,\n type SessionMessageEntry,\n} from \"@mariozechner/pi-coding-agent\";\nimport {\n getThreadSessionFile,\n resolveChannelSessionFile,\n tryResolveThreadSession,\n} from \"../session-store.js\";\n\nexport interface SessionViewItem {\n kind: \"user\" | \"assistant\" | \"tool\" | \"system\";\n title: string;\n body?: string;\n meta?: string;\n tone?: \"default\" | \"ok\" | \"err\" | \"muted\";\n entryId?: string;\n forks?: SessionViewRelation[];\n}\n\nexport interface SessionViewRelation {\n kind: \"parent\" | \"fork\";\n fileName: string;\n sessionId: string;\n title: string;\n updatedAt: string;\n entryCount: number;\n summary?: string;\n anchorEntryId?: string;\n}\n\nexport interface SessionViewModel {\n sessionId: string;\n fileName: string;\n title: string;\n createdAt: string;\n updatedAt: string;\n entryCount: number;\n items: SessionViewItem[];\n parent?: SessionViewRelation;\n forks: SessionViewRelation[];\n}\n\nexport function resolveExistingSessionFile(\n workingDir: string,\n conversationId: string,\n sessionKey: string,\n): string | null {\n const conversationDir = join(workingDir, conversationId);\n if (sessionKey.includes(\":\")) {\n return tryResolveThreadSession(getThreadSessionFile(conversationDir, sessionKey));\n }\n return resolveChannelSessionFile(conversationDir);\n}\n\nexport function loadSessionViewModel(sessionFile: string): SessionViewModel {\n const resolvedFile = resolve(sessionFile);\n const sm = SessionManager.open(resolvedFile);\n const header = sm.getHeader();\n if (!header) throw new Error(`No valid session found: ${sessionFile}`);\n\n const entries = sm.getEntries();\n const updatedAt = entries.at(-1)?.timestamp ?? header.timestamp;\n const title = sm.getSessionName() || `Session ${header.id.slice(0, 8)}`;\n\n const parent = header.parentSession\n ? buildSessionRelation(resolve(header.parentSession), \"parent\")\n : undefined;\n const forks = listRelatedSessionFiles(resolvedFile)\n .filter((candidate) => candidate !== resolvedFile)\n .map((candidate) => buildSessionRelation(candidate, \"fork\", resolvedFile))\n .filter((relation): relation is SessionViewRelation => relation !== null)\n .sort((a, b) => (a.updatedAt < b.updatedAt ? -1 : a.updatedAt > b.updatedAt ? 1 : 0));\n\n const forksByEntryId = new Map<string, SessionViewRelation[]>();\n for (const fork of forks) {\n if (!fork.anchorEntryId) continue;\n const bucket = forksByEntryId.get(fork.anchorEntryId) ?? [];\n bucket.push(fork);\n forksByEntryId.set(fork.anchorEntryId, bucket);\n }\n\n const items = entries.flatMap((entry) => {\n const item = mapEntryToItem(entry);\n if (!item) return [];\n if (item.entryId) {\n const anchoredForks = forksByEntryId.get(item.entryId);\n if (anchoredForks) {\n item.forks = anchoredForks;\n }\n }\n return [item];\n });\n\n return {\n sessionId: header.id,\n fileName: basename(resolvedFile),\n title,\n createdAt: header.timestamp,\n updatedAt,\n entryCount: entries.length,\n items,\n parent: parent ?? undefined,\n forks,\n };\n}\n\nexport function resolveRequestedSessionFile(\n baseSessionFile: string,\n requestedFileName?: string | null,\n): string | null {\n const resolvedBase = resolve(baseSessionFile);\n if (!requestedFileName) return resolvedBase;\n\n const trimmed = requestedFileName.trim();\n if (!trimmed) return resolvedBase;\n\n const fileName = basename(trimmed);\n if (fileName !== trimmed || !fileName.endsWith(\".jsonl\")) return null;\n\n const candidate = join(dirname(resolvedBase), fileName);\n if (!existsSync(candidate)) return null;\n\n try {\n return SessionManager.open(candidate).getHeader() ? candidate : null;\n } catch {\n return null;\n }\n}\n\nfunction listRelatedSessionFiles(sessionFile: string): string[] {\n const dir = dirname(sessionFile);\n if (!existsSync(dir)) return [];\n\n return readdirSync(dir)\n .filter((name) => name.endsWith(\".jsonl\"))\n .map((fileName) => join(dir, fileName));\n}\n\nfunction buildSessionRelation(\n sessionFile: string,\n kind: \"parent\" | \"fork\",\n expectedParent?: string,\n): SessionViewRelation | null {\n try {\n const sm = SessionManager.open(sessionFile);\n const header = sm.getHeader();\n if (!header) return null;\n if (kind === \"fork\" && resolve(header.parentSession ?? \"\") !== expectedParent) {\n return null;\n }\n\n const entries = sm.getEntries();\n const updatedAt = entries.at(-1)?.timestamp ?? header.timestamp;\n const anchorEntryId =\n kind === \"fork\" && expectedParent\n ? findForkAnchorEntryId(SessionManager.open(expectedParent).getEntries(), entries)\n : undefined;\n return {\n kind,\n fileName: basename(sessionFile),\n sessionId: header.id,\n title: sm.getSessionName() || `Session ${header.id.slice(0, 8)}`,\n updatedAt,\n entryCount: entries.length,\n summary: extractSessionSummary(entries),\n anchorEntryId,\n };\n } catch {\n return null;\n }\n}\n\nfunction findForkAnchorEntryId(\n parentEntries: SessionEntry[],\n childEntries: SessionEntry[],\n): string | undefined {\n let sharedCount = 0;\n while (\n sharedCount < parentEntries.length &&\n sharedCount < childEntries.length &&\n parentEntries[sharedCount]?.id === childEntries[sharedCount]?.id\n ) {\n sharedCount += 1;\n }\n\n for (let i = sharedCount - 1; i >= 0; i--) {\n const entry = parentEntries[i];\n if (entry?.type === \"message\" && entry.message.role === \"user\") {\n return entry.id;\n }\n }\n\n return sharedCount > 0 ? parentEntries[sharedCount - 1]?.id : undefined;\n}\n\nfunction extractSessionSummary(entries: SessionEntry[]): string | undefined {\n for (const entry of entries) {\n if (entry.type !== \"message\") continue;\n const item = mapEntryToItem(entry);\n if (!item?.body) continue;\n return collapseSummary(item.body);\n }\n return undefined;\n}\n\nfunction collapseSummary(text: string): string {\n const singleLine = text.replace(/\\s+/g, \" \").trim();\n return singleLine.length > 96 ? `${singleLine.slice(0, 93)}…` : singleLine;\n}\n\nfunction mapEntryToItem(entry: SessionEntry): SessionViewItem | null {\n switch (entry.type) {\n case \"message\":\n return mapMessageEntry(entry);\n case \"model_change\":\n return {\n kind: \"system\",\n title: \"Model changed\",\n body: `${entry.provider} / ${entry.modelId}`,\n meta: entry.timestamp,\n tone: \"muted\",\n };\n case \"thinking_level_change\":\n return {\n kind: \"system\",\n title: \"Thinking level changed\",\n body: entry.thinkingLevel,\n meta: entry.timestamp,\n tone: \"muted\",\n };\n case \"compaction\":\n return mapCompactionEntry(entry);\n case \"branch_summary\":\n return mapBranchSummaryEntry(entry);\n case \"custom_message\":\n return {\n kind: \"system\",\n title: `Custom message · ${entry.customType}`,\n body: contentToText(entry.content),\n meta: entry.timestamp,\n tone: \"muted\",\n };\n case \"custom\":\n return {\n kind: \"system\",\n title: `Custom data · ${entry.customType}`,\n body: entry.data === undefined ? \"(no data)\" : JSON.stringify(entry.data, null, 2),\n meta: entry.timestamp,\n tone: \"muted\",\n };\n case \"label\":\n return {\n kind: \"system\",\n title: \"Label updated\",\n body: entry.label || \"(cleared)\",\n meta: entry.timestamp,\n tone: \"muted\",\n };\n case \"session_info\":\n return entry.name\n ? {\n kind: \"system\",\n title: \"Session renamed\",\n body: entry.name,\n meta: entry.timestamp,\n tone: \"muted\",\n }\n : null;\n default:\n return null;\n }\n}\n\nfunction mapMessageEntry(entry: SessionMessageEntry): SessionViewItem {\n const message = entry.message as unknown as Record<string, unknown> & {\n role?: string;\n content?: unknown;\n provider?: string;\n model?: string;\n toolName?: string;\n isError?: boolean;\n command?: string;\n output?: string;\n stopReason?: string;\n customType?: string;\n summary?: string;\n };\n\n switch (message.role) {\n case \"user\":\n return {\n kind: \"user\",\n title: \"User\",\n body: contentToText(message.content),\n meta: entry.timestamp,\n entryId: entry.id,\n };\n case \"assistant\": {\n const assistantBody = assistantContentToText(message.content);\n const metaParts = [message.provider, message.model, message.stopReason].filter(Boolean);\n return {\n kind: \"assistant\",\n title: \"Assistant\",\n body: assistantBody,\n meta:\n metaParts.length > 0 ? `${entry.timestamp} · ${metaParts.join(\" · \")}` : entry.timestamp,\n entryId: entry.id,\n };\n }\n case \"toolResult\":\n return {\n kind: \"tool\",\n title: `Tool result · ${String(message.toolName ?? \"unknown\")}`,\n body: contentToText(message.content),\n meta: entry.timestamp,\n tone: message.isError ? \"err\" : \"ok\",\n entryId: entry.id,\n };\n case \"bashExecution\": {\n const command = String(message.command ?? \"\").trim();\n const output = String(message.output ?? \"\").trim();\n const body = [command ? `$ ${command}` : \"\", output].filter(Boolean).join(\"\\n\\n\");\n return {\n kind: \"tool\",\n title: \"Bash execution\",\n body: body || \"(no output)\",\n meta: entry.timestamp,\n entryId: entry.id,\n };\n }\n case \"custom\":\n return {\n kind: \"system\",\n title: `Custom message · ${String(message.customType ?? \"custom\")}`,\n body: contentToText(message.content),\n meta: entry.timestamp,\n tone: \"muted\",\n entryId: entry.id,\n };\n case \"branchSummary\":\n return {\n kind: \"system\",\n title: \"Branch summary\",\n body: String(message.summary ?? \"\"),\n meta: entry.timestamp,\n tone: \"muted\",\n entryId: entry.id,\n };\n case \"compactionSummary\":\n return {\n kind: \"system\",\n title: \"Compaction summary\",\n body: String(message.summary ?? \"\"),\n meta: entry.timestamp,\n tone: \"muted\",\n entryId: entry.id,\n };\n default:\n return {\n kind: \"system\",\n title: `Message · ${String(message.role ?? \"unknown\")}`,\n body: contentToText(message.content),\n meta: entry.timestamp,\n tone: \"muted\",\n entryId: entry.id,\n };\n }\n}\n\nfunction mapCompactionEntry(entry: CompactionEntry): SessionViewItem {\n return {\n kind: \"system\",\n title: \"Context compacted\",\n body: entry.summary,\n meta: `${entry.timestamp} · ${entry.tokensBefore} tokens before compaction`,\n tone: \"muted\",\n };\n}\n\nfunction mapBranchSummaryEntry(entry: BranchSummaryEntry): SessionViewItem {\n return {\n kind: \"system\",\n title: \"Branch summary\",\n body: entry.summary,\n meta: entry.timestamp,\n tone: \"muted\",\n };\n}\n\nfunction assistantContentToText(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (!Array.isArray(content)) return \"\";\n\n const textBlocks: string[] = [];\n const thinkingBlocks: string[] = [];\n const toolCalls: string[] = [];\n const otherBlocks: string[] = [];\n\n for (const block of content) {\n if (!block || typeof block !== \"object\") continue;\n const value = block as Record<string, unknown>;\n if (value.type === \"text\" && typeof value.text === \"string\") {\n textBlocks.push(value.text);\n continue;\n }\n if (value.type === \"thinking\" && typeof value.thinking === \"string\") {\n thinkingBlocks.push(value.thinking);\n continue;\n }\n if (value.type === \"toolCall\") {\n const name = typeof value.name === \"string\" ? value.name : \"tool\";\n const args = value.arguments === undefined ? \"\" : JSON.stringify(value.arguments, null, 2);\n toolCalls.push([name, args].filter(Boolean).join(\"\\n\"));\n continue;\n }\n if (value.type === \"image\") {\n otherBlocks.push(`[image ${String(value.mimeType ?? \"unknown\")}]`);\n }\n }\n\n const sections = [\n textBlocks.join(\"\\n\\n\").trim(),\n thinkingBlocks.length > 0\n ? [`[thinking]`, thinkingBlocks.join(\"\\n\\n\")].filter(Boolean).join(\"\\n\")\n : \"\",\n toolCalls.length > 0 ? [`[tool calls]`, toolCalls.join(\"\\n\\n\")].filter(Boolean).join(\"\\n\") : \"\",\n otherBlocks.join(\"\\n\"),\n ].filter(Boolean);\n\n return sections.join(\"\\n\\n\");\n}\n\nfunction contentToText(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (!Array.isArray(content)) return \"\";\n\n const lines: string[] = [];\n for (const block of content) {\n if (!block || typeof block !== \"object\") continue;\n const value = block as Record<string, unknown>;\n if (value.type === \"text\" && typeof value.text === \"string\") {\n lines.push(value.text);\n continue;\n }\n if (value.type === \"thinking\" && typeof value.thinking === \"string\") {\n lines.push(`[thinking]\\n${value.thinking}`);\n continue;\n }\n if (value.type === \"toolCall\") {\n const name = typeof value.name === \"string\" ? value.name : \"tool\";\n const args = value.arguments === undefined ? \"\" : JSON.stringify(value.arguments, null, 2);\n lines.push([`[toolCall] ${name}`, args].filter(Boolean).join(\"\\n\"));\n continue;\n }\n if (value.type === \"image\") {\n lines.push(`[image ${String(value.mimeType ?? \"unknown\")}]`);\n }\n }\n\n return lines.join(\"\\n\\n\");\n}\n"]}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/session-view/service.ts"],"names":[],"mappings":"AAgBA,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,MAAM,GAAG,QAAQ,CAAC;IAC/C,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,SAAS,GAAG,IAAI,GAAG,KAAK,GAAG,OAAO,CAAC;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,mBAAmB,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,KAAK,EAAE,mBAAmB,EAAE,CAAC;CAC9B;AAED,wBAAgB,0BAA0B,CACxC,UAAU,EAAE,MAAM,EAClB,cAAc,EAAE,MAAM,EACtB,UAAU,EAAE,MAAM,GACjB,MAAM,GAAG,IAAI,CAMf;AAED,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,gBAAgB,CAkD1E;AAED,wBAAgB,2BAA2B,CACzC,eAAe,EAAE,MAAM,EACvB,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,GAChC,MAAM,GAAG,IAAI,CA2Bf","sourcesContent":["import { basename, dirname, join, resolve } from \"path\";\nimport { existsSync, readdirSync } from \"fs\";\nimport {\n SessionManager,\n type BranchSummaryEntry,\n type CompactionEntry,\n type SessionEntry,\n type SessionMessageEntry,\n} from \"@mariozechner/pi-coding-agent\";\nimport {\n getThreadSessionFile,\n resolveChannelSessionFile,\n tryResolveThreadSession,\n} from \"../session-store.js\";\nimport * as log from \"../log.js\";\n\nexport interface SessionViewItem {\n kind: \"user\" | \"assistant\" | \"tool\" | \"system\";\n title: string;\n body?: string;\n meta?: string;\n tone?: \"default\" | \"ok\" | \"err\" | \"muted\";\n entryId?: string;\n forks?: SessionViewRelation[];\n}\n\nexport interface SessionViewRelation {\n kind: \"parent\" | \"fork\";\n fileName: string;\n sessionId: string;\n title: string;\n updatedAt: string;\n entryCount: number;\n summary?: string;\n anchorEntryId?: string;\n}\n\nexport interface SessionViewModel {\n sessionId: string;\n fileName: string;\n title: string;\n createdAt: string;\n updatedAt: string;\n entryCount: number;\n items: SessionViewItem[];\n parent?: SessionViewRelation;\n forks: SessionViewRelation[];\n}\n\nexport function resolveExistingSessionFile(\n workingDir: string,\n conversationId: string,\n sessionKey: string,\n): string | null {\n const conversationDir = join(workingDir, conversationId);\n if (sessionKey.includes(\":\")) {\n return tryResolveThreadSession(getThreadSessionFile(conversationDir, sessionKey));\n }\n return resolveChannelSessionFile(conversationDir);\n}\n\nexport function loadSessionViewModel(sessionFile: string): SessionViewModel {\n const resolvedFile = resolve(sessionFile);\n const sm = SessionManager.open(resolvedFile);\n const header = sm.getHeader();\n if (!header) throw new Error(`No valid session found: ${sessionFile}`);\n\n const entries = sm.getEntries();\n const updatedAt = entries.at(-1)?.timestamp ?? header.timestamp;\n const title = sm.getSessionName() || `Session ${header.id.slice(0, 8)}`;\n\n const parent = header.parentSession\n ? buildSessionRelation(resolve(header.parentSession), \"parent\")\n : undefined;\n const forks = listRelatedSessionFiles(resolvedFile)\n .filter((candidate) => candidate !== resolvedFile)\n .map((candidate) => buildSessionRelation(candidate, \"fork\", resolvedFile))\n .filter((relation): relation is SessionViewRelation => relation !== null)\n .sort((a, b) => (a.updatedAt < b.updatedAt ? -1 : a.updatedAt > b.updatedAt ? 1 : 0));\n\n const forksByEntryId = new Map<string, SessionViewRelation[]>();\n for (const fork of forks) {\n if (!fork.anchorEntryId) continue;\n const bucket = forksByEntryId.get(fork.anchorEntryId) ?? [];\n bucket.push(fork);\n forksByEntryId.set(fork.anchorEntryId, bucket);\n }\n\n const items = entries.flatMap((entry) => {\n const item = mapEntryToItem(entry);\n if (!item) return [];\n if (item.entryId) {\n const anchoredForks = forksByEntryId.get(item.entryId);\n if (anchoredForks) {\n item.forks = anchoredForks;\n }\n }\n return [item];\n });\n\n return {\n sessionId: header.id,\n fileName: basename(resolvedFile),\n title,\n createdAt: header.timestamp,\n updatedAt,\n entryCount: entries.length,\n items,\n parent: parent ?? undefined,\n forks,\n };\n}\n\nexport function resolveRequestedSessionFile(\n baseSessionFile: string,\n requestedFileName?: string | null,\n): string | null {\n const resolvedBase = resolve(baseSessionFile);\n if (!requestedFileName) return resolvedBase;\n\n const trimmed = requestedFileName.trim();\n if (!trimmed) return resolvedBase;\n\n const fileName = basename(trimmed);\n if (fileName !== trimmed || !fileName.endsWith(\".jsonl\")) return null;\n\n const candidate = join(dirname(resolvedBase), fileName);\n if (!existsSync(candidate)) return null;\n\n let sm: SessionManager;\n try {\n sm = SessionManager.open(candidate);\n } catch (err) {\n throw new Error(\n `Session file is corrupted: ${candidate}: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n if (!sm.getHeader()) {\n throw new Error(`Session file is missing a valid header: ${candidate}`);\n }\n return candidate;\n}\n\nfunction listRelatedSessionFiles(sessionFile: string): string[] {\n const dir = dirname(sessionFile);\n if (!existsSync(dir)) return [];\n\n return readdirSync(dir)\n .filter((name) => name.endsWith(\".jsonl\"))\n .map((fileName) => join(dir, fileName));\n}\n\nfunction buildSessionRelation(\n sessionFile: string,\n kind: \"parent\" | \"fork\",\n expectedParent?: string,\n): SessionViewRelation | null {\n let sm: SessionManager;\n try {\n sm = SessionManager.open(sessionFile);\n } catch (err) {\n log.logWarning(\n `Skipping corrupted session file while building ${kind} relation: ${sessionFile}`,\n err instanceof Error ? err.message : String(err),\n );\n return null;\n }\n const header = sm.getHeader();\n if (!header) {\n log.logWarning(\n `Skipping session file with missing header while building ${kind} relation: ${sessionFile}`,\n );\n return null;\n }\n if (kind === \"fork\" && resolve(header.parentSession ?? \"\") !== expectedParent) {\n return null;\n }\n\n const entries = sm.getEntries();\n const updatedAt = entries.at(-1)?.timestamp ?? header.timestamp;\n const anchorEntryId =\n kind === \"fork\" && expectedParent\n ? findForkAnchorEntryId(SessionManager.open(expectedParent).getEntries(), entries)\n : undefined;\n return {\n kind,\n fileName: basename(sessionFile),\n sessionId: header.id,\n title: sm.getSessionName() || `Session ${header.id.slice(0, 8)}`,\n updatedAt,\n entryCount: entries.length,\n summary: extractSessionSummary(entries),\n anchorEntryId,\n };\n}\n\nfunction findForkAnchorEntryId(\n parentEntries: SessionEntry[],\n childEntries: SessionEntry[],\n): string | undefined {\n let sharedCount = 0;\n while (\n sharedCount < parentEntries.length &&\n sharedCount < childEntries.length &&\n parentEntries[sharedCount]?.id === childEntries[sharedCount]?.id\n ) {\n sharedCount += 1;\n }\n\n if (sharedCount > 0) {\n return parentEntries[sharedCount - 1]?.id;\n }\n\n const childRoot = findComparableUserMessage(childEntries);\n if (!childRoot) return undefined;\n\n return findParentAnchorByRootMessage(parentEntries, childRoot);\n}\n\nfunction findParentAnchorByRootMessage(\n parentEntries: SessionEntry[],\n childRoot: ComparableUserMessage,\n): string | undefined {\n let textMatchId: string | undefined;\n\n for (const entry of parentEntries) {\n const comparable = getComparableUserMessage(entry);\n if (!comparable) continue;\n if (comparable.normalizedText !== childRoot.normalizedText) continue;\n if (\n childRoot.messageTimestamp !== undefined &&\n comparable.messageTimestamp !== undefined &&\n comparable.messageTimestamp === childRoot.messageTimestamp\n ) {\n return entry.id;\n }\n textMatchId ??= entry.id;\n }\n\n return textMatchId;\n}\n\ninterface ComparableUserMessage {\n normalizedText: string;\n messageTimestamp?: number;\n}\n\nfunction findComparableUserMessage(entries: SessionEntry[]): ComparableUserMessage | null {\n for (const entry of entries) {\n const comparable = getComparableUserMessage(entry);\n if (comparable) return comparable;\n }\n return null;\n}\n\nfunction getComparableUserMessage(entry: SessionEntry): ComparableUserMessage | null {\n if (entry.type !== \"message\" || entry.message.role !== \"user\") return null;\n\n const body = contentToText(entry.message.content);\n const normalizedText = normalizeComparableUserText(body);\n if (!normalizedText) return null;\n\n const messageTimestamp =\n typeof entry.message.timestamp === \"number\" ? entry.message.timestamp : undefined;\n return { normalizedText, messageTimestamp };\n}\n\nfunction normalizeComparableUserText(text: string): string {\n const withoutTimestamp = text.replace(\n /^\\[[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}[+-][0-9]{2}:[0-9]{2}\\]\\s+(?=\\[[^\\]]+\\](?:\\s+\\[in-thread:[^\\]]+\\])?:\\s)/,\n \"\",\n );\n return stripSlackAttachmentBlock(withoutTimestamp).trim();\n}\n\nfunction stripSlackAttachmentBlock(text: string): string {\n return text.replace(/\\n*<slack_attachments>\\n[\\s\\S]*?\\n<\\/slack_attachments>\\s*$/g, \"\");\n}\n\nfunction extractSessionSummary(entries: SessionEntry[]): string | undefined {\n for (const entry of entries) {\n if (entry.type !== \"message\") continue;\n const item = mapEntryToItem(entry);\n if (!item?.body) continue;\n return collapseSummary(item.body);\n }\n return undefined;\n}\n\nfunction collapseSummary(text: string): string {\n const singleLine = text.replace(/\\s+/g, \" \").trim();\n return singleLine.length > 96 ? `${singleLine.slice(0, 93)}…` : singleLine;\n}\n\nfunction mapEntryToItem(entry: SessionEntry): SessionViewItem | null {\n switch (entry.type) {\n case \"message\":\n return mapMessageEntry(entry);\n case \"model_change\":\n return {\n kind: \"system\",\n title: \"Model changed\",\n body: `${entry.provider} / ${entry.modelId}`,\n meta: entry.timestamp,\n tone: \"muted\",\n };\n case \"thinking_level_change\":\n return {\n kind: \"system\",\n title: \"Thinking level changed\",\n body: entry.thinkingLevel,\n meta: entry.timestamp,\n tone: \"muted\",\n };\n case \"compaction\":\n return mapCompactionEntry(entry);\n case \"branch_summary\":\n return mapBranchSummaryEntry(entry);\n case \"custom_message\":\n return {\n kind: \"system\",\n title: `Custom message · ${entry.customType}`,\n body: contentToText(entry.content),\n meta: entry.timestamp,\n tone: \"muted\",\n };\n case \"custom\":\n return {\n kind: \"system\",\n title: `Custom data · ${entry.customType}`,\n body: entry.data === undefined ? \"(no data)\" : JSON.stringify(entry.data, null, 2),\n meta: entry.timestamp,\n tone: \"muted\",\n };\n case \"label\":\n return {\n kind: \"system\",\n title: \"Label updated\",\n body: entry.label || \"(cleared)\",\n meta: entry.timestamp,\n tone: \"muted\",\n };\n case \"session_info\":\n return entry.name\n ? {\n kind: \"system\",\n title: \"Session renamed\",\n body: entry.name,\n meta: entry.timestamp,\n tone: \"muted\",\n }\n : null;\n default:\n return null;\n }\n}\n\nfunction mapMessageEntry(entry: SessionMessageEntry): SessionViewItem {\n const message = entry.message as unknown as Record<string, unknown> & {\n role?: string;\n content?: unknown;\n provider?: string;\n model?: string;\n toolName?: string;\n isError?: boolean;\n command?: string;\n output?: string;\n exitCode?: number;\n cancelled?: boolean;\n truncated?: boolean;\n stopReason?: string;\n customType?: string;\n summary?: string;\n };\n\n switch (message.role) {\n case \"user\":\n return {\n kind: \"user\",\n title: \"User\",\n body: contentToText(message.content),\n meta: entry.timestamp,\n entryId: entry.id,\n };\n case \"assistant\": {\n const assistantBody = assistantContentToText(message.content);\n const metaParts = [message.provider, message.model, message.stopReason].filter(Boolean);\n return {\n kind: \"assistant\",\n title: \"Assistant\",\n body: assistantBody,\n meta:\n metaParts.length > 0 ? `${entry.timestamp} · ${metaParts.join(\" · \")}` : entry.timestamp,\n entryId: entry.id,\n };\n }\n case \"toolResult\":\n return {\n kind: \"tool\",\n title: `Tool result · ${String(message.toolName ?? \"unknown\")}`,\n body: contentToText(message.content),\n meta: entry.timestamp,\n tone: message.isError ? \"err\" : \"ok\",\n entryId: entry.id,\n };\n case \"bashExecution\": {\n const command = String(message.command ?? \"\").trim();\n const output = String(message.output ?? \"\").trim();\n const details = [\n typeof message.exitCode === \"number\" ? `[exitCode] ${message.exitCode}` : \"\",\n message.cancelled ? `[cancelled] true` : \"\",\n message.truncated ? `[truncated] true` : \"\",\n ].filter(Boolean);\n const body = [command ? `$ ${command}` : \"\", output, ...details].filter(Boolean).join(\"\\n\\n\");\n return {\n kind: \"tool\",\n title: \"Bash execution\",\n body: body || \"(no output)\",\n meta: entry.timestamp,\n entryId: entry.id,\n };\n }\n case \"custom\":\n return {\n kind: \"system\",\n title: `Custom message · ${String(message.customType ?? \"custom\")}`,\n body: contentToText(message.content),\n meta: entry.timestamp,\n tone: \"muted\",\n entryId: entry.id,\n };\n case \"branchSummary\":\n return {\n kind: \"system\",\n title: \"Branch summary\",\n body: String(message.summary ?? \"\"),\n meta: entry.timestamp,\n tone: \"muted\",\n entryId: entry.id,\n };\n case \"compactionSummary\":\n return {\n kind: \"system\",\n title: \"Compaction summary\",\n body: String(message.summary ?? \"\"),\n meta: entry.timestamp,\n tone: \"muted\",\n entryId: entry.id,\n };\n default:\n return {\n kind: \"system\",\n title: `Message · ${String(message.role ?? \"unknown\")}`,\n body: contentToText(message.content),\n meta: entry.timestamp,\n tone: \"muted\",\n entryId: entry.id,\n };\n }\n}\n\nfunction mapCompactionEntry(entry: CompactionEntry): SessionViewItem {\n return {\n kind: \"system\",\n title: \"Context compacted\",\n body: entry.summary,\n meta: `${entry.timestamp} · ${entry.tokensBefore} tokens before compaction`,\n tone: \"muted\",\n };\n}\n\nfunction mapBranchSummaryEntry(entry: BranchSummaryEntry): SessionViewItem {\n return {\n kind: \"system\",\n title: \"Branch summary\",\n body: entry.summary,\n meta: entry.timestamp,\n tone: \"muted\",\n };\n}\n\nfunction assistantContentToText(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (!Array.isArray(content)) return \"\";\n\n const lines: string[] = [];\n\n for (const block of content) {\n if (!block || typeof block !== \"object\") continue;\n const value = block as Record<string, unknown>;\n if (value.type === \"text\" && typeof value.text === \"string\") {\n lines.push(value.text);\n continue;\n }\n if (value.type === \"thinking\" && typeof value.thinking === \"string\") {\n lines.push(`[thinking]\\n${value.thinking}`);\n continue;\n }\n if (value.type === \"toolCall\") {\n const name = typeof value.name === \"string\" ? value.name : \"tool\";\n const args = value.arguments === undefined ? \"\" : JSON.stringify(value.arguments, null, 2);\n lines.push([`[toolCall] ${name}`, args].filter(Boolean).join(\"\\n\"));\n continue;\n }\n if (value.type === \"image\") {\n lines.push(`[image ${String(value.mimeType ?? \"unknown\")}]`);\n }\n }\n\n return lines.join(\"\\n\\n\");\n}\n\nfunction contentToText(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (!Array.isArray(content)) return \"\";\n\n const lines: string[] = [];\n for (const block of content) {\n if (!block || typeof block !== \"object\") continue;\n const value = block as Record<string, unknown>;\n if (value.type === \"text\" && typeof value.text === \"string\") {\n lines.push(value.text);\n continue;\n }\n if (value.type === \"thinking\" && typeof value.thinking === \"string\") {\n lines.push(`[thinking]\\n${value.thinking}`);\n continue;\n }\n if (value.type === \"toolCall\") {\n const name = typeof value.name === \"string\" ? value.name : \"tool\";\n const args = value.arguments === undefined ? \"\" : JSON.stringify(value.arguments, null, 2);\n lines.push([`[toolCall] ${name}`, args].filter(Boolean).join(\"\\n\"));\n continue;\n }\n if (value.type === \"image\") {\n lines.push(`[image ${String(value.mimeType ?? \"unknown\")}]`);\n }\n }\n\n return lines.join(\"\\n\\n\");\n}\n"]}
@@ -2,6 +2,7 @@ import { basename, dirname, join, resolve } from "path";
2
2
  import { existsSync, readdirSync } from "fs";
3
3
  import { SessionManager, } from "@mariozechner/pi-coding-agent";
4
4
  import { getThreadSessionFile, resolveChannelSessionFile, tryResolveThreadSession, } from "../session-store.js";
5
+ import * as log from "../log.js";
5
6
  export function resolveExistingSessionFile(workingDir, conversationId, sessionKey) {
6
7
  const conversationDir = join(workingDir, conversationId);
7
8
  if (sessionKey.includes(":")) {
@@ -71,12 +72,17 @@ export function resolveRequestedSessionFile(baseSessionFile, requestedFileName)
71
72
  const candidate = join(dirname(resolvedBase), fileName);
72
73
  if (!existsSync(candidate))
73
74
  return null;
75
+ let sm;
74
76
  try {
75
- return SessionManager.open(candidate).getHeader() ? candidate : null;
77
+ sm = SessionManager.open(candidate);
76
78
  }
77
- catch {
78
- return null;
79
+ catch (err) {
80
+ throw new Error(`Session file is corrupted: ${candidate}: ${err instanceof Error ? err.message : String(err)}`);
81
+ }
82
+ if (!sm.getHeader()) {
83
+ throw new Error(`Session file is missing a valid header: ${candidate}`);
79
84
  }
85
+ return candidate;
80
86
  }
81
87
  function listRelatedSessionFiles(sessionFile) {
82
88
  const dir = dirname(sessionFile);
@@ -87,33 +93,37 @@ function listRelatedSessionFiles(sessionFile) {
87
93
  .map((fileName) => join(dir, fileName));
88
94
  }
89
95
  function buildSessionRelation(sessionFile, kind, expectedParent) {
96
+ let sm;
90
97
  try {
91
- const sm = SessionManager.open(sessionFile);
92
- const header = sm.getHeader();
93
- if (!header)
94
- return null;
95
- if (kind === "fork" && resolve(header.parentSession ?? "") !== expectedParent) {
96
- return null;
97
- }
98
- const entries = sm.getEntries();
99
- const updatedAt = entries.at(-1)?.timestamp ?? header.timestamp;
100
- const anchorEntryId = kind === "fork" && expectedParent
101
- ? findForkAnchorEntryId(SessionManager.open(expectedParent).getEntries(), entries)
102
- : undefined;
103
- return {
104
- kind,
105
- fileName: basename(sessionFile),
106
- sessionId: header.id,
107
- title: sm.getSessionName() || `Session ${header.id.slice(0, 8)}`,
108
- updatedAt,
109
- entryCount: entries.length,
110
- summary: extractSessionSummary(entries),
111
- anchorEntryId,
112
- };
98
+ sm = SessionManager.open(sessionFile);
99
+ }
100
+ catch (err) {
101
+ log.logWarning(`Skipping corrupted session file while building ${kind} relation: ${sessionFile}`, err instanceof Error ? err.message : String(err));
102
+ return null;
103
+ }
104
+ const header = sm.getHeader();
105
+ if (!header) {
106
+ log.logWarning(`Skipping session file with missing header while building ${kind} relation: ${sessionFile}`);
107
+ return null;
113
108
  }
114
- catch {
109
+ if (kind === "fork" && resolve(header.parentSession ?? "") !== expectedParent) {
115
110
  return null;
116
111
  }
112
+ const entries = sm.getEntries();
113
+ const updatedAt = entries.at(-1)?.timestamp ?? header.timestamp;
114
+ const anchorEntryId = kind === "fork" && expectedParent
115
+ ? findForkAnchorEntryId(SessionManager.open(expectedParent).getEntries(), entries)
116
+ : undefined;
117
+ return {
118
+ kind,
119
+ fileName: basename(sessionFile),
120
+ sessionId: header.id,
121
+ title: sm.getSessionName() || `Session ${header.id.slice(0, 8)}`,
122
+ updatedAt,
123
+ entryCount: entries.length,
124
+ summary: extractSessionSummary(entries),
125
+ anchorEntryId,
126
+ };
117
127
  }
118
128
  function findForkAnchorEntryId(parentEntries, childEntries) {
119
129
  let sharedCount = 0;
@@ -122,13 +132,55 @@ function findForkAnchorEntryId(parentEntries, childEntries) {
122
132
  parentEntries[sharedCount]?.id === childEntries[sharedCount]?.id) {
123
133
  sharedCount += 1;
124
134
  }
125
- for (let i = sharedCount - 1; i >= 0; i--) {
126
- const entry = parentEntries[i];
127
- if (entry?.type === "message" && entry.message.role === "user") {
135
+ if (sharedCount > 0) {
136
+ return parentEntries[sharedCount - 1]?.id;
137
+ }
138
+ const childRoot = findComparableUserMessage(childEntries);
139
+ if (!childRoot)
140
+ return undefined;
141
+ return findParentAnchorByRootMessage(parentEntries, childRoot);
142
+ }
143
+ function findParentAnchorByRootMessage(parentEntries, childRoot) {
144
+ let textMatchId;
145
+ for (const entry of parentEntries) {
146
+ const comparable = getComparableUserMessage(entry);
147
+ if (!comparable)
148
+ continue;
149
+ if (comparable.normalizedText !== childRoot.normalizedText)
150
+ continue;
151
+ if (childRoot.messageTimestamp !== undefined &&
152
+ comparable.messageTimestamp !== undefined &&
153
+ comparable.messageTimestamp === childRoot.messageTimestamp) {
128
154
  return entry.id;
129
155
  }
156
+ textMatchId ??= entry.id;
130
157
  }
131
- return sharedCount > 0 ? parentEntries[sharedCount - 1]?.id : undefined;
158
+ return textMatchId;
159
+ }
160
+ function findComparableUserMessage(entries) {
161
+ for (const entry of entries) {
162
+ const comparable = getComparableUserMessage(entry);
163
+ if (comparable)
164
+ return comparable;
165
+ }
166
+ return null;
167
+ }
168
+ function getComparableUserMessage(entry) {
169
+ if (entry.type !== "message" || entry.message.role !== "user")
170
+ return null;
171
+ const body = contentToText(entry.message.content);
172
+ const normalizedText = normalizeComparableUserText(body);
173
+ if (!normalizedText)
174
+ return null;
175
+ const messageTimestamp = typeof entry.message.timestamp === "number" ? entry.message.timestamp : undefined;
176
+ return { normalizedText, messageTimestamp };
177
+ }
178
+ function normalizeComparableUserText(text) {
179
+ const withoutTimestamp = text.replace(/^\[[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)/, "");
180
+ return stripSlackAttachmentBlock(withoutTimestamp).trim();
181
+ }
182
+ function stripSlackAttachmentBlock(text) {
183
+ return text.replace(/\n*<slack_attachments>\n[\s\S]*?\n<\/slack_attachments>\s*$/g, "");
132
184
  }
133
185
  function extractSessionSummary(entries) {
134
186
  for (const entry of entries) {
@@ -241,7 +293,12 @@ function mapMessageEntry(entry) {
241
293
  case "bashExecution": {
242
294
  const command = String(message.command ?? "").trim();
243
295
  const output = String(message.output ?? "").trim();
244
- const body = [command ? `$ ${command}` : "", output].filter(Boolean).join("\n\n");
296
+ const details = [
297
+ typeof message.exitCode === "number" ? `[exitCode] ${message.exitCode}` : "",
298
+ message.cancelled ? `[cancelled] true` : "",
299
+ message.truncated ? `[truncated] true` : "",
300
+ ].filter(Boolean);
301
+ const body = [command ? `$ ${command}` : "", output, ...details].filter(Boolean).join("\n\n");
245
302
  return {
246
303
  kind: "tool",
247
304
  title: "Bash execution",
@@ -311,41 +368,30 @@ function assistantContentToText(content) {
311
368
  return content;
312
369
  if (!Array.isArray(content))
313
370
  return "";
314
- const textBlocks = [];
315
- const thinkingBlocks = [];
316
- const toolCalls = [];
317
- const otherBlocks = [];
371
+ const lines = [];
318
372
  for (const block of content) {
319
373
  if (!block || typeof block !== "object")
320
374
  continue;
321
375
  const value = block;
322
376
  if (value.type === "text" && typeof value.text === "string") {
323
- textBlocks.push(value.text);
377
+ lines.push(value.text);
324
378
  continue;
325
379
  }
326
380
  if (value.type === "thinking" && typeof value.thinking === "string") {
327
- thinkingBlocks.push(value.thinking);
381
+ lines.push(`[thinking]\n${value.thinking}`);
328
382
  continue;
329
383
  }
330
384
  if (value.type === "toolCall") {
331
385
  const name = typeof value.name === "string" ? value.name : "tool";
332
386
  const args = value.arguments === undefined ? "" : JSON.stringify(value.arguments, null, 2);
333
- toolCalls.push([name, args].filter(Boolean).join("\n"));
387
+ lines.push([`[toolCall] ${name}`, args].filter(Boolean).join("\n"));
334
388
  continue;
335
389
  }
336
390
  if (value.type === "image") {
337
- otherBlocks.push(`[image ${String(value.mimeType ?? "unknown")}]`);
391
+ lines.push(`[image ${String(value.mimeType ?? "unknown")}]`);
338
392
  }
339
393
  }
340
- const sections = [
341
- textBlocks.join("\n\n").trim(),
342
- thinkingBlocks.length > 0
343
- ? [`[thinking]`, thinkingBlocks.join("\n\n")].filter(Boolean).join("\n")
344
- : "",
345
- toolCalls.length > 0 ? [`[tool calls]`, toolCalls.join("\n\n")].filter(Boolean).join("\n") : "",
346
- otherBlocks.join("\n"),
347
- ].filter(Boolean);
348
- return sections.join("\n\n");
394
+ return lines.join("\n\n");
349
395
  }
350
396
  function contentToText(content) {
351
397
  if (typeof content === "string")