@bastani/atomic 0.9.40-alpha.1 → 0.9.40
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.
- package/CHANGELOG.md +37 -0
- package/dist/builtin/intercom/CHANGELOG.md +14 -0
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/CHANGELOG.md +31 -0
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/web-access/CHANGELOG.md +12 -0
- package/dist/builtin/web-access/README.md +1 -1
- package/dist/builtin/web-access/index.bundle.mjs +2 -5
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +25 -0
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/src/extension/index.bundle.mjs +11 -2
- package/dist/cli/args.js +1 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/core/export-html/index.d.ts.map +1 -1
- package/dist/core/export-html/index.js +3 -2
- package/dist/core/export-html/index.js.map +1 -1
- package/dist/core/export-html/template.html +1 -0
- package/dist/core/export-html/template.js +37 -20
- package/dist/core/export-html/vendor/marked-cjk-friendly.min.js +6 -0
- package/dist/core/export-html/vendor/marked.min.js +76 -3
- package/docs/providers.md +1 -1
- package/docs/settings.md +1 -1
- package/npm-shrinkwrap.json +35 -35
- package/package.json +5 -5
- package/dist/core/export-html/template-js/data-tree.js +0 -287
- package/dist/core/export-html/template-js/entries-navigation.js +0 -434
- package/dist/core/export-html/template-js/initialization.js +0 -335
- package/dist/core/export-html/template-js/message-tools.js +0 -428
- package/dist/core/export-html/template-js/tree-filter-render.js +0 -419
- package/dist/core/export-html/template-script.d.ts +0 -3
- package/dist/core/export-html/template-script.d.ts.map +0 -1
- package/dist/core/export-html/template-script.js +0 -13
- package/dist/core/export-html/template-script.js.map +0 -1
|
@@ -1,428 +0,0 @@
|
|
|
1
|
-
// ============================================================
|
|
2
|
-
// TREE RENDERING (DOM manipulation)
|
|
3
|
-
// ============================================================
|
|
4
|
-
|
|
5
|
-
let currentLeafId = leafId;
|
|
6
|
-
let currentTargetId = urlTargetId || leafId;
|
|
7
|
-
let treeRendered = false;
|
|
8
|
-
|
|
9
|
-
function renderTree() {
|
|
10
|
-
const tree = buildTree();
|
|
11
|
-
const activePathIds = buildActivePathIds(currentLeafId);
|
|
12
|
-
const flatNodes = flattenTree(tree, activePathIds);
|
|
13
|
-
const filtered = filterNodes(flatNodes, currentLeafId);
|
|
14
|
-
const container = document.getElementById('tree-container');
|
|
15
|
-
|
|
16
|
-
// Full render only on first call or when filter/search changes
|
|
17
|
-
if (!treeRendered) {
|
|
18
|
-
container.innerHTML = '';
|
|
19
|
-
|
|
20
|
-
for (const flatNode of filtered) {
|
|
21
|
-
const entry = flatNode.node.entry;
|
|
22
|
-
const isOnPath = activePathIds.has(entry.id);
|
|
23
|
-
const isTarget = entry.id === currentTargetId;
|
|
24
|
-
|
|
25
|
-
const div = document.createElement('div');
|
|
26
|
-
div.className = 'tree-node';
|
|
27
|
-
if (isOnPath) div.classList.add('in-path');
|
|
28
|
-
if (isTarget) div.classList.add('active');
|
|
29
|
-
div.dataset.id = entry.id;
|
|
30
|
-
|
|
31
|
-
const prefix = buildTreePrefix(flatNode);
|
|
32
|
-
const prefixSpan = document.createElement('span');
|
|
33
|
-
prefixSpan.className = 'tree-prefix';
|
|
34
|
-
prefixSpan.textContent = prefix;
|
|
35
|
-
|
|
36
|
-
const marker = document.createElement('span');
|
|
37
|
-
marker.className = 'tree-marker';
|
|
38
|
-
marker.textContent = isOnPath ? '•' : ' ';
|
|
39
|
-
|
|
40
|
-
const content = document.createElement('span');
|
|
41
|
-
content.className = 'tree-content';
|
|
42
|
-
content.innerHTML = getTreeNodeDisplayHtml(entry, flatNode.node.label);
|
|
43
|
-
|
|
44
|
-
div.appendChild(prefixSpan);
|
|
45
|
-
div.appendChild(marker);
|
|
46
|
-
div.appendChild(content);
|
|
47
|
-
// Navigate to the newest leaf through this node, but scroll to the clicked node
|
|
48
|
-
div.addEventListener('click', () => {
|
|
49
|
-
if (window.getSelection().toString()) return;
|
|
50
|
-
const leafId = findNewestLeaf(entry.id);
|
|
51
|
-
navigateTo(leafId, 'target', entry.id);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
container.appendChild(div);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
treeRendered = true;
|
|
58
|
-
} else {
|
|
59
|
-
// Just update markers and classes
|
|
60
|
-
const nodes = container.querySelectorAll('.tree-node');
|
|
61
|
-
for (const node of nodes) {
|
|
62
|
-
const id = node.dataset.id;
|
|
63
|
-
const isOnPath = activePathIds.has(id);
|
|
64
|
-
const isTarget = id === currentTargetId;
|
|
65
|
-
|
|
66
|
-
node.classList.toggle('in-path', isOnPath);
|
|
67
|
-
node.classList.toggle('active', isTarget);
|
|
68
|
-
|
|
69
|
-
const marker = node.querySelector('.tree-marker');
|
|
70
|
-
if (marker) {
|
|
71
|
-
marker.textContent = isOnPath ? '•' : ' ';
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
document.getElementById('tree-status').textContent = `${filtered.length} / ${flatNodes.length} entries`;
|
|
77
|
-
|
|
78
|
-
// Scroll active node into view after layout
|
|
79
|
-
setTimeout(() => {
|
|
80
|
-
const activeNode = container.querySelector('.tree-node.active');
|
|
81
|
-
if (activeNode) {
|
|
82
|
-
activeNode.scrollIntoView({ block: 'nearest' });
|
|
83
|
-
}
|
|
84
|
-
}, 0);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function forceTreeRerender() {
|
|
88
|
-
treeRendered = false;
|
|
89
|
-
renderTree();
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// ============================================================
|
|
93
|
-
// MESSAGE RENDERING
|
|
94
|
-
// ============================================================
|
|
95
|
-
|
|
96
|
-
function formatTokens(count) {
|
|
97
|
-
if (count < 1000) return count.toString();
|
|
98
|
-
if (count < 10000) return (count / 1000).toFixed(1) + 'k';
|
|
99
|
-
if (count < 1000000) return Math.round(count / 1000) + 'k';
|
|
100
|
-
return (count / 1000000).toFixed(1) + 'M';
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function formatTimestamp(ts) {
|
|
104
|
-
if (!ts) return '';
|
|
105
|
-
const date = new Date(ts);
|
|
106
|
-
return date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function replaceTabs(text) {
|
|
110
|
-
return text.replace(/\t/g, ' ');
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** Safely coerce value to string for display. Returns null if invalid type. */
|
|
114
|
-
function str(value) {
|
|
115
|
-
if (typeof value === 'string') return value;
|
|
116
|
-
if (value == null) return '';
|
|
117
|
-
return null;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Mirrors src/core/tools/render-utils.ts.
|
|
121
|
-
function parseLineNumber(value) {
|
|
122
|
-
if (typeof value === 'string' && value.trim() !== '') value = Number(value);
|
|
123
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function formatReadLineRange(offset, limit) {
|
|
127
|
-
if (offset == null && limit == null) return '';
|
|
128
|
-
const start = offset == null ? 1 : parseLineNumber(offset);
|
|
129
|
-
if (start === undefined) return '';
|
|
130
|
-
if (limit == null) return `:${start}`;
|
|
131
|
-
const count = parseLineNumber(limit);
|
|
132
|
-
if (count === undefined) return '';
|
|
133
|
-
const end = start + count - 1;
|
|
134
|
-
return `:${start}${end ? `-${end}` : ''}`;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function getLanguageFromPath(filePath) {
|
|
138
|
-
const ext = filePath.split('.').pop()?.toLowerCase();
|
|
139
|
-
const extToLang = {
|
|
140
|
-
ts: 'typescript', tsx: 'typescript', js: 'javascript', jsx: 'javascript',
|
|
141
|
-
py: 'python', rb: 'ruby', rs: 'rust', go: 'go', java: 'java',
|
|
142
|
-
c: 'c', cpp: 'cpp', h: 'c', hpp: 'cpp', cs: 'csharp',
|
|
143
|
-
php: 'php', sh: 'bash', bash: 'bash', zsh: 'bash',
|
|
144
|
-
sql: 'sql', html: 'html', css: 'css', scss: 'scss',
|
|
145
|
-
json: 'json', yaml: 'yaml', yml: 'yaml', xml: 'xml',
|
|
146
|
-
md: 'markdown', dockerfile: 'dockerfile'
|
|
147
|
-
};
|
|
148
|
-
return extToLang[ext];
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function findToolResult(toolCallId) {
|
|
152
|
-
for (const entry of entries) {
|
|
153
|
-
if (entry.type === 'message' && entry.message.role === 'toolResult') {
|
|
154
|
-
if (entry.message.toolCallId === toolCallId) {
|
|
155
|
-
return entry.message;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
return null;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function formatExpandableOutput(text, maxLines, lang) {
|
|
163
|
-
text = replaceTabs(text);
|
|
164
|
-
const lines = text.split('\n');
|
|
165
|
-
const displayLines = lines.slice(0, maxLines);
|
|
166
|
-
const remaining = lines.length - maxLines;
|
|
167
|
-
|
|
168
|
-
if (lang) {
|
|
169
|
-
let highlighted;
|
|
170
|
-
try {
|
|
171
|
-
highlighted = hljs.highlight(text, { language: lang }).value;
|
|
172
|
-
} catch {
|
|
173
|
-
highlighted = escapeHtml(text);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (remaining > 0) {
|
|
177
|
-
const previewCode = displayLines.join('\n');
|
|
178
|
-
let previewHighlighted;
|
|
179
|
-
try {
|
|
180
|
-
previewHighlighted = hljs.highlight(previewCode, { language: lang }).value;
|
|
181
|
-
} catch {
|
|
182
|
-
previewHighlighted = escapeHtml(previewCode);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return `<div class="tool-output expandable" onclick="if(window.getSelection().toString())return;this.classList.toggle('expanded')">
|
|
186
|
-
<div class="output-preview"><pre><code class="hljs">${previewHighlighted}</code></pre>
|
|
187
|
-
<div class="expand-hint">... (${remaining} more lines)</div></div>
|
|
188
|
-
<div class="output-full"><pre><code class="hljs">${highlighted}</code></pre></div></div>`;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return `<div class="tool-output"><pre><code class="hljs">${highlighted}</code></pre></div>`;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Plain text output
|
|
195
|
-
if (remaining > 0) {
|
|
196
|
-
let out = '<div class="tool-output expandable" onclick="if(window.getSelection().toString())return;this.classList.toggle(\'expanded\')">';
|
|
197
|
-
out += '<div class="output-preview">';
|
|
198
|
-
for (const line of displayLines) {
|
|
199
|
-
out += `<div>${escapeHtml(replaceTabs(line))}</div>`;
|
|
200
|
-
}
|
|
201
|
-
out += `<div class="expand-hint">... (${remaining} more lines)</div></div>`;
|
|
202
|
-
out += '<div class="output-full">';
|
|
203
|
-
for (const line of lines) {
|
|
204
|
-
out += `<div>${escapeHtml(replaceTabs(line))}</div>`;
|
|
205
|
-
}
|
|
206
|
-
out += '</div></div>';
|
|
207
|
-
return out;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
let out = '<div class="tool-output">';
|
|
211
|
-
for (const line of displayLines) {
|
|
212
|
-
out += `<div>${escapeHtml(replaceTabs(line))}</div>`;
|
|
213
|
-
}
|
|
214
|
-
out += '</div>';
|
|
215
|
-
return out;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function renderToolCall(call) {
|
|
219
|
-
const result = findToolResult(call.id);
|
|
220
|
-
const isError = result?.isError || false;
|
|
221
|
-
const statusClass = result ? (isError ? 'error' : 'success') : 'pending';
|
|
222
|
-
|
|
223
|
-
const getResultText = () => {
|
|
224
|
-
if (!result) return '';
|
|
225
|
-
const textBlocks = result.content.filter(c => c.type === 'text');
|
|
226
|
-
return textBlocks.map(c => c.text).join('\n');
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
const getResultImages = () => {
|
|
230
|
-
if (!result) return [];
|
|
231
|
-
return result.content.filter(c => c.type === 'image');
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
const renderResultImages = () => {
|
|
235
|
-
const images = getResultImages();
|
|
236
|
-
if (images.length === 0) return '';
|
|
237
|
-
return '<div class="tool-images">' +
|
|
238
|
-
images.map(img => `<img src="data:${escapeHtml(img.mimeType || 'image/png')};base64,${escapeHtml(img.data || '')}" class="tool-image" />`).join('') +
|
|
239
|
-
'</div>';
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
let html = `<div class="tool-execution ${statusClass}">`;
|
|
243
|
-
const args = call.arguments || {};
|
|
244
|
-
const name = call.name;
|
|
245
|
-
|
|
246
|
-
const invalidArg = '<span class="tool-error">[invalid arg]</span>';
|
|
247
|
-
|
|
248
|
-
switch (name) {
|
|
249
|
-
case 'bash': {
|
|
250
|
-
const command = str(args.command);
|
|
251
|
-
const cmdDisplay = command === null ? invalidArg : escapeHtml(command || '...');
|
|
252
|
-
html += `<div class="tool-command">$ ${cmdDisplay}</div>`;
|
|
253
|
-
if (result) {
|
|
254
|
-
const output = getResultText().trim();
|
|
255
|
-
if (output) html += formatExpandableOutput(output, 5);
|
|
256
|
-
}
|
|
257
|
-
break;
|
|
258
|
-
}
|
|
259
|
-
case 'read': {
|
|
260
|
-
const filePath = str(args.file_path ?? args.path);
|
|
261
|
-
const readLineRange = formatReadLineRange(args.offset, args.limit);
|
|
262
|
-
let pathHtml = filePath === null ? invalidArg : escapeHtml(shortenPath(filePath || ''));
|
|
263
|
-
if (filePath !== null && readLineRange) {
|
|
264
|
-
pathHtml += `<span class="line-numbers">${escapeHtml(readLineRange)}</span>`;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
html += `<div class="tool-header"><span class="tool-name">read</span> <span class="tool-path">${pathHtml}</span></div>`;
|
|
268
|
-
if (result) {
|
|
269
|
-
html += renderResultImages();
|
|
270
|
-
const output = getResultText();
|
|
271
|
-
const lang = filePath ? getLanguageFromPath(filePath) : null;
|
|
272
|
-
if (output) html += formatExpandableOutput(output, 10, lang);
|
|
273
|
-
}
|
|
274
|
-
break;
|
|
275
|
-
}
|
|
276
|
-
case 'write': {
|
|
277
|
-
const filePath = str(args.file_path ?? args.path);
|
|
278
|
-
const content = str(args.content);
|
|
279
|
-
|
|
280
|
-
html += `<div class="tool-header"><span class="tool-name">write</span> <span class="tool-path">${filePath === null ? invalidArg : escapeHtml(shortenPath(filePath || ''))}</span>`;
|
|
281
|
-
if (content !== null && content) {
|
|
282
|
-
const lines = content.split('\n');
|
|
283
|
-
if (lines.length > 10) html += ` <span class="line-count">(${lines.length} lines)</span>`;
|
|
284
|
-
}
|
|
285
|
-
html += '</div>';
|
|
286
|
-
|
|
287
|
-
if (content === null) {
|
|
288
|
-
html += `<div class="tool-error">[invalid content arg - expected string]</div>`;
|
|
289
|
-
} else if (content) {
|
|
290
|
-
const lang = filePath ? getLanguageFromPath(filePath) : null;
|
|
291
|
-
html += formatExpandableOutput(content, 10, lang);
|
|
292
|
-
}
|
|
293
|
-
if (result) {
|
|
294
|
-
const output = getResultText().trim();
|
|
295
|
-
if (output) html += `<div class="tool-output"><div>${escapeHtml(output)}</div></div>`;
|
|
296
|
-
}
|
|
297
|
-
break;
|
|
298
|
-
}
|
|
299
|
-
case 'edit': {
|
|
300
|
-
const filePath = str(args.file_path ?? args.path);
|
|
301
|
-
html += `<div class="tool-header"><span class="tool-name">edit</span> <span class="tool-path">${filePath === null ? invalidArg : escapeHtml(shortenPath(filePath || ''))}</span></div>`;
|
|
302
|
-
|
|
303
|
-
if (result?.details?.diff) {
|
|
304
|
-
const diffLines = result.details.diff.split('\n');
|
|
305
|
-
html += '<div class="tool-diff">';
|
|
306
|
-
for (const line of diffLines) {
|
|
307
|
-
const cls = line.match(/^\+/) ? 'diff-added' : line.match(/^-/) ? 'diff-removed' : 'diff-context';
|
|
308
|
-
html += `<div class="${cls}">${escapeHtml(replaceTabs(line))}</div>`;
|
|
309
|
-
}
|
|
310
|
-
html += '</div>';
|
|
311
|
-
} else if (result) {
|
|
312
|
-
const output = getResultText().trim();
|
|
313
|
-
if (output) html += `<div class="tool-output"><pre>${escapeHtml(output)}</pre></div>`;
|
|
314
|
-
}
|
|
315
|
-
break;
|
|
316
|
-
}
|
|
317
|
-
case 'ls': {
|
|
318
|
-
const dirPath = str(args.path);
|
|
319
|
-
const limit = args.limit;
|
|
320
|
-
|
|
321
|
-
let pathHtml = dirPath === null ? invalidArg : escapeHtml(shortenPath(dirPath || '.'));
|
|
322
|
-
if (limit !== undefined) {
|
|
323
|
-
pathHtml += ` <span class="line-count">(limit ${escapeHtml(String(limit))})</span>`;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
html += `<div class="tool-header"><span class="tool-name">ls</span> <span class="tool-path">${pathHtml}</span></div>`;
|
|
327
|
-
if (result) {
|
|
328
|
-
const output = getResultText().trim();
|
|
329
|
-
if (output) html += formatExpandableOutput(output, 20);
|
|
330
|
-
}
|
|
331
|
-
break;
|
|
332
|
-
}
|
|
333
|
-
default: {
|
|
334
|
-
// Check for pre-rendered custom tool HTML
|
|
335
|
-
const rendered = renderedTools?.[call.id];
|
|
336
|
-
if (rendered?.callHtml || rendered?.resultHtmlCollapsed || rendered?.resultHtmlExpanded) {
|
|
337
|
-
// Custom tool with pre-rendered HTML from TUI renderer
|
|
338
|
-
if (rendered.callHtml) {
|
|
339
|
-
html += `<div class="tool-header ansi-rendered">${rendered.callHtml}</div>`;
|
|
340
|
-
} else {
|
|
341
|
-
html += `<div class="tool-header"><span class="tool-name">${escapeHtml(name)}</span></div>`;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
if (rendered.resultHtmlCollapsed && rendered.resultHtmlExpanded && rendered.resultHtmlCollapsed !== rendered.resultHtmlExpanded) {
|
|
345
|
-
// Both collapsed and expanded differ - render expandable section
|
|
346
|
-
html += `<div class="tool-output expandable ansi-rendered" onclick="if(window.getSelection().toString())return;this.classList.toggle('expanded')">
|
|
347
|
-
<div class="output-preview">${rendered.resultHtmlCollapsed}</div>
|
|
348
|
-
<div class="output-full">${rendered.resultHtmlExpanded}</div>
|
|
349
|
-
</div>`;
|
|
350
|
-
} else if (rendered.resultHtmlExpanded) {
|
|
351
|
-
// Only expanded exists (or collapsed is identical) - show directly
|
|
352
|
-
html += `<div class="tool-output ansi-rendered">${rendered.resultHtmlExpanded}</div>`;
|
|
353
|
-
} else if (result) {
|
|
354
|
-
// No pre-rendered result HTML - fallback to JSON
|
|
355
|
-
const output = getResultText();
|
|
356
|
-
if (output) html += formatExpandableOutput(output, 10);
|
|
357
|
-
}
|
|
358
|
-
} else {
|
|
359
|
-
// Fallback to JSON display (existing behavior)
|
|
360
|
-
html += `<div class="tool-header"><span class="tool-name">${escapeHtml(name)}</span></div>`;
|
|
361
|
-
html += `<div class="tool-output"><pre>${escapeHtml(JSON.stringify(args, null, 2))}</pre></div>`;
|
|
362
|
-
if (result) {
|
|
363
|
-
const output = getResultText();
|
|
364
|
-
if (output) html += formatExpandableOutput(output, 10);
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
html += '</div>';
|
|
371
|
-
return html;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* Download the session data as a JSONL file.
|
|
376
|
-
* Reconstructs the original format: header line + entry lines.
|
|
377
|
-
*/
|
|
378
|
-
window.downloadSessionJson = function() {
|
|
379
|
-
// Build JSONL content: header first, then all entries
|
|
380
|
-
const lines = [];
|
|
381
|
-
if (header) {
|
|
382
|
-
lines.push(JSON.stringify({ type: 'header', ...header }));
|
|
383
|
-
}
|
|
384
|
-
for (const entry of entries) {
|
|
385
|
-
lines.push(JSON.stringify(entry));
|
|
386
|
-
}
|
|
387
|
-
const jsonlContent = lines.join('\n');
|
|
388
|
-
|
|
389
|
-
// Create download
|
|
390
|
-
const blob = new Blob([jsonlContent], { type: 'application/x-ndjson' });
|
|
391
|
-
const url = URL.createObjectURL(blob);
|
|
392
|
-
const a = document.createElement('a');
|
|
393
|
-
a.href = url;
|
|
394
|
-
a.download = `${header?.id || 'session'}.jsonl`;
|
|
395
|
-
document.body.appendChild(a);
|
|
396
|
-
a.click();
|
|
397
|
-
document.body.removeChild(a);
|
|
398
|
-
URL.revokeObjectURL(url);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* Build a shareable URL for a specific message.
|
|
403
|
-
* URL format: base?gistId&leafId=<leafId>&targetId=<entryId>
|
|
404
|
-
*/
|
|
405
|
-
function buildShareUrl(entryId) {
|
|
406
|
-
// Check for injected base URL (used when loaded in iframe via srcdoc)
|
|
407
|
-
const baseUrlMeta = document.querySelector('meta[name="pi-share-base-url"]');
|
|
408
|
-
const baseUrl = baseUrlMeta ? baseUrlMeta.content : window.location.href.split('?')[0];
|
|
409
|
-
|
|
410
|
-
const url = new URL(window.location.href);
|
|
411
|
-
// Find the gist ID (first query param without value, e.g., ?abc123)
|
|
412
|
-
const gistId = Array.from(url.searchParams.keys()).find(k => !url.searchParams.get(k));
|
|
413
|
-
|
|
414
|
-
// Build the share URL
|
|
415
|
-
const params = new URLSearchParams();
|
|
416
|
-
params.set('leafId', currentLeafId);
|
|
417
|
-
params.set('targetId', entryId);
|
|
418
|
-
|
|
419
|
-
// If we have an injected base URL (iframe context), use it directly
|
|
420
|
-
if (baseUrlMeta) {
|
|
421
|
-
return `${baseUrl}&${params.toString()}`;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// Otherwise build from current location (direct file access)
|
|
425
|
-
url.search = gistId ? `?${gistId}&${params.toString()}` : `?${params.toString()}`;
|
|
426
|
-
return url.toString();
|
|
427
|
-
}
|
|
428
|
-
|