pi-trace-viewer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,276 @@
1
+ /* global marked */
2
+ export function renderEntry(entry, allEntries = []) {
3
+ const timestamp = `<div class="message-timestamp">${formatTime(entry?.timestamp)}</div>`;
4
+ const link = entry?.id ? `<button class="copy-link" data-copy-link="${escapeAttr(entry.id)}" title="Copy link">#</button>` : "";
5
+ if (entry?.type === "message") {
6
+ const message = entry.message || {};
7
+ if (message.role === "user") return `<article id="entry-${escapeAttr(entry.id)}" class="entry user-message">${link}${timestamp}${renderContent(message.content, "user")}</article>`;
8
+ if (message.role === "assistant") return `<article id="entry-${escapeAttr(entry.id)}" class="entry assistant-message">${link}${timestamp}${renderContent(message.content, "assistant")}${message.errorMessage ? `<div class="tool-block error"><div class="tool-content">${escapeHtml(message.errorMessage)}</div></div>` : ""}</article>`;
9
+ if (message.role === "toolResult") {
10
+ return `<article id="entry-${escapeAttr(entry.id)}" class="entry"><details class="tool-block ${message.isError ? "error" : "success"}"><summary><span class="tool-name">${escapeHtml(message.toolName || "tool")}</span> result${message.isError ? " · error" : ""}</summary><div class="tool-content">${renderContent(message.content, "tool")}</div></details></article>`;
11
+ }
12
+ return fallbackEntry(entry, "message");
13
+ }
14
+ if (entry?.type === "custom_message") {
15
+ const open = entry.display ? " open" : "";
16
+ const hidden = entry.display ? "" : " hidden-custom";
17
+ const label = entry.display ? "custom message" : "hidden custom message";
18
+ return `<article id="entry-${escapeAttr(entry.id)}" class="entry"><details class="custom-message${hidden}"${open}><summary><span class="tool-name">${escapeHtml(entry.customType || "custom")}</span> ${label}</summary><div class="summary-content">${renderContent(entry.content, "custom")}${renderDetails(entry.details)}</div></details></article>`;
19
+ }
20
+ if (entry?.type === "custom") {
21
+ return `<article id="entry-${escapeAttr(entry.id)}" class="entry"><details class="custom-message custom-state"><summary><span class="tool-name">${escapeHtml(entry.customType || "custom")}</span> custom state</summary><div class="summary-content"><pre class="json-block">${json(entry.data ?? null)}</pre></div></details></article>`;
22
+ }
23
+ if (entry?.type === "compaction") return `<article id="entry-${escapeAttr(entry.id)}" class="entry"><details class="compaction"><summary>[compaction] Compacted from ${number(entry.tokensBefore)} tokens</summary><div class="summary-content">${renderMarkdownText(entry.summary, "Compaction summary")}</div></details></article>`;
24
+ if (entry?.type === "branch_summary") return `<article id="entry-${escapeAttr(entry.id)}" class="entry"><details class="branch-summary" open><summary>Branch Summary</summary><div class="summary-content">${renderMarkdownText(entry.summary, "Branch summary")}</div></details></article>`;
25
+ if (entry?.type === "model_change") return `<div id="entry-${escapeAttr(entry.id)}" class="entry model-change">${timestamp}Switched to model: ${escapeHtml(`${entry.provider}/${entry.modelId}`)}</div>`;
26
+ if (entry?.type === "thinking_level_change") return `<div id="entry-${escapeAttr(entry.id)}" class="entry setting-change">Thinking level: ${escapeHtml(entry.thinkingLevel)}</div>`;
27
+ if (entry?.type === "session_info") return `<div id="entry-${escapeAttr(entry.id)}" class="entry setting-change">${timestamp}Session name: ${escapeHtml(entry.name || "(cleared)")}</div>`;
28
+ if (entry?.type === "label") return `<div id="entry-${escapeAttr(entry.id)}" class="entry setting-change">${timestamp}Label: ${escapeHtml(entry.label || "(cleared)")}</div>`;
29
+ return fallbackEntry(entry, entry?.type || "unknown");
30
+ }
31
+
32
+ export function renderContent(content, role) {
33
+ if (typeof content === "string") return renderMarkdownText(content, `${role} markdown`);
34
+ if (!Array.isArray(content)) return `<pre class="json-block">${json(content ?? null)}</pre>`;
35
+ let html = "";
36
+ const images = [];
37
+ for (const block of content) {
38
+ if (typeof block === "string") html += renderMarkdownText(block, `${role} markdown`);
39
+ else if (!block || typeof block !== "object") html += `<pre class="json-block">${json(block ?? null)}</pre>`;
40
+ else if (block.type === "text") html += renderMarkdownText(block.text || "", `${role} markdown`);
41
+ else if (block.type === "thinking") html += `<details class="thinking-block"><summary>thinking</summary><div class="thinking-content">${escapeHtml(block.thinking || "")}</div></details>`;
42
+ else if (block.type === "toolCall") html += `<details class="tool-block" open><summary><span class="tool-name">${escapeHtml(block.name || "tool")}</span> call</summary><pre class="json-block">${json(block.arguments ?? {})}</pre></details>`;
43
+ else if (block.type === "image" && block.data) images.push(`<img class="message-image" alt="${escapeAttr(role)} attachment" src="data:${escapeAttr(block.mimeType || "image/png")};base64,${escapeAttr(block.data)}">`);
44
+ else html += `<details class="provider-block"><summary>${escapeHtml(block.type || "content block")}</summary><pre class="json-block">${json(block)}</pre></details>`;
45
+ }
46
+ return html + (images.length ? `<div class="message-images">${images.join("")}</div>` : "");
47
+ }
48
+
49
+ export function renderPiContext(context) {
50
+ const messages = Array.isArray(context?.messages) ? context.messages : [];
51
+ const tools = Array.isArray(context?.tools) ? context.tools : [];
52
+ return `<div class="structured-context">
53
+ <details class="context-section" open><summary>System prompt</summary><div class="context-section-body">${renderMarkdownText(context?.systemPrompt || "", "System prompt source")}</div></details>
54
+ <section class="context-section"><div class="context-section-title">Messages <span>${messages.length}</span></div><div class="context-messages">${messages.map((message, index) => renderContextMessage(message, index)).join("") || `<div class="unavailable">No messages</div>`}</div></section>
55
+ <details class="context-section"><summary>Tools (${tools.length})</summary><div class="context-section-body tool-definition-list">${tools.map((tool) => `<details class="tool-definition"><summary>${escapeHtml(tool?.name || "unnamed tool")}</summary><p>${escapeHtml(tool?.description || "")}</p><pre class="json-block">${json(tool?.parameters || {})}</pre></details>`).join("") || "No tools"}</div></details>
56
+ </div>`;
57
+ }
58
+
59
+ export function renderCompactionContext(context) {
60
+ const messages = Array.isArray(context?.messagesToSummarize) ? context.messagesToSummarize : [];
61
+ const prefix = Array.isArray(context?.turnPrefixMessages) ? context.turnPrefixMessages : [];
62
+ return `<div class="structured-context">
63
+ <div class="context-note"><strong>Compaction preparation</strong>Pi exposes the source messages and cut-point metadata here. The exact generated prompt is visible in Provider Payload when the provider hook was captured.</div>
64
+ <section class="provider-meta">
65
+ <div><span>reason</span><strong>${escapeHtml(context?.reason)}</strong></div><div><span>tokens before</span><strong>${number(context?.tokensBefore)}</strong></div><div><span>split turn</span><strong>${context?.isSplitTurn ? "yes" : "no"}</strong></div><div><span>retry after</span><strong>${context?.willRetry ? "yes" : "no"}</strong></div>
66
+ </section>
67
+ ${context?.customInstructions ? `<details class="context-section"><summary>Custom compaction instructions</summary><div class="context-section-body">${renderMarkdownText(context.customInstructions, "Instruction source")}</div></details>` : ""}
68
+ ${context?.previousSummary ? `<details class="context-section"><summary>Previous summary</summary><div class="context-section-body">${renderMarkdownText(context.previousSummary, "Previous summary source")}</div></details>` : ""}
69
+ <section class="context-section"><div class="context-section-title">Messages to summarize <span>${messages.length}</span></div><div class="context-messages">${messages.map((message, index) => renderContextMessage(message, index)).join("") || `<div class="unavailable">No history messages in this compaction pass.</div>`}</div></section>
70
+ ${prefix.length ? `<section class="context-section"><div class="context-section-title">Split-turn prefix <span>${prefix.length}</span></div><div class="context-messages">${prefix.map((message, index) => renderContextMessage(message, index)).join("")}</div></section>` : ""}
71
+ <details class="context-section"><summary>Cut point and settings</summary><div class="context-section-body"><pre class="json-block">${json({ firstKeptEntryId: context?.firstKeptEntryId, settings: context?.settings, fileOps: context?.fileOps })}</pre></div></details>
72
+ </div>`;
73
+ }
74
+
75
+ export function renderContextMessage(message, index) {
76
+ const role = message?.role || message?.type || "unknown";
77
+ const customType = message?.customType ? ` · ${message.customType}` : "";
78
+ const tool = message?.toolName ? ` · ${message.toolName}` : "";
79
+ const source = message?.content ?? message?.summary ?? message?.text ?? message;
80
+ return `<details class="context-message role-${roleClass(role)}"><summary class="context-message-head"><span>#${index + 1}</span><span class="role-badge">${escapeHtml(role + customType + tool)}</span>${message?.isError ? `<span class="error-label">error</span>` : ""}<span class="context-message-preview">${escapeHtml(truncate(contentText(source) || "(no text)", 130))}</span></summary><div class="context-message-body">${renderContent(source, role)}</div><details class="message-raw-json"><summary>Message JSON</summary><pre class="json-block">${json(message)}</pre></details></details>`;
81
+ }
82
+
83
+ export function renderProviderPayload(payload) {
84
+ if (!payload || typeof payload !== "object") return `<div class="provider-scalar">${escapeHtml(String(payload ?? "null"))}</div>`;
85
+ const input = Array.isArray(payload.input) ? payload.input : Array.isArray(payload.messages) ? payload.messages : typeof payload.prompt === "string" ? [payload.prompt] : [];
86
+ const instructions = payload.instructions ?? payload.system;
87
+ const tools = Array.isArray(payload.tools) ? payload.tools : [];
88
+ const meta = {};
89
+ for (const key of ["model", "temperature", "top_p", "max_tokens", "max_output_tokens", "reasoning", "tool_choice", "parallel_tool_calls", "store", "stream"]) if (payload[key] !== undefined) meta[key] = payload[key];
90
+ return `<div class="structured-context provider-context">
91
+ <section class="provider-meta">${Object.entries(meta).map(([key, value]) => `<div><span>${escapeHtml(key)}</span><strong>${escapeHtml(typeof value === "object" ? safeStringify(value) : value)}</strong></div>`).join("") || `<div><span>format</span><strong>generic payload</strong></div>`}</section>
92
+ ${instructions !== undefined ? `<details class="context-section" open><summary>Instructions / system</summary><div class="context-section-body">${renderProviderContent(instructions, "instructions")}</div></details>` : ""}
93
+ <section class="context-section"><div class="context-section-title">Backend input <span>${input.length}</span></div><div class="context-messages">${input.map((item, index) => renderProviderItem(item, index)).join("") || `<div class="unavailable">No recognized input/messages field. Use Raw JSON below.</div>`}</div></section>
94
+ ${tools.length ? `<details class="context-section"><summary>Backend tools (${tools.length})</summary><div class="context-section-body tool-definition-list">${tools.map((tool) => { const definition = tool?.function || tool || {}; return `<details class="tool-definition"><summary>${escapeHtml(definition.name || tool?.type || "tool")}</summary><p>${escapeHtml(definition.description || "")}</p><pre class="json-block">${json(definition.parameters || definition.input_schema || tool)}</pre></details>`; }).join("")}</div></details>` : ""}
95
+ </div>`;
96
+ }
97
+
98
+ export function renderProviderContent(content, label) {
99
+ if (typeof content === "string") return renderMarkdownText(content, `${label} source`);
100
+ if (!Array.isArray(content)) return `<pre class="json-block">${json(content)}</pre>`;
101
+ return content.map((block) => {
102
+ if (typeof block === "string") return renderMarkdownText(block, `${label} source`);
103
+ const text = block?.text ?? block?.input_text ?? block?.output_text;
104
+ if (typeof text === "string") return renderMarkdownText(text, `${block.type || label} source`);
105
+ if (["function_call", "tool_use"].includes(block?.type)) return `<details class="tool-block" open><summary><span class="tool-name">${escapeHtml(block.name || "tool")}</span> call</summary><pre class="json-block">${json(block.arguments ?? block.input ?? {})}</pre></details>`;
106
+ if (["function_call_output", "tool_result"].includes(block?.type)) return `<details class="tool-block success"><summary>tool result</summary>${renderProviderContent(block.output ?? block.content ?? "", "tool result")}</details>`;
107
+ return `<details class="provider-block"><summary>${escapeHtml(block?.type || "content block")}</summary><pre class="json-block">${json(block)}</pre></details>`;
108
+ }).join("");
109
+ }
110
+
111
+ export function renderProviderItem(item, index) {
112
+ if (typeof item === "string") return `<details class="context-message role-user"><summary class="context-message-head"><span>#${index + 1}</span><span class="role-badge">prompt</span><span class="context-message-preview">${escapeHtml(truncate(item, 130))}</span></summary><div class="context-message-body">${renderMarkdownText(item, "Prompt source")}</div></details>`;
113
+ const role = item?.role || item?.type || "input item";
114
+ const name = item?.name ? ` · ${item.name}` : "";
115
+ if (["function_call", "tool_use"].includes(item?.type)) {
116
+ const args = typeof item.arguments === "string" ? parseJson(item.arguments) : item.arguments ?? item.input ?? {};
117
+ return `<details class="context-message role-${roleClass(role)}"><summary class="context-message-head"><span>#${index + 1}</span><span class="role-badge">${escapeHtml(role + name)}</span><span class="context-message-preview">${escapeHtml(truncate(safeStringify(args), 130))}</span></summary><div class="context-message-body"><pre class="json-block">${json(args)}</pre></div></details>`;
118
+ }
119
+ if (["function_call_output", "tool_result"].includes(item?.type)) {
120
+ const output = item.output ?? item.content ?? "";
121
+ return `<details class="context-message role-${roleClass(role)}"><summary class="context-message-head"><span>#${index + 1}</span><span class="role-badge">${escapeHtml(role + name)}</span><span class="context-message-preview">${escapeHtml(truncate(providerContentText(output), 130))}</span></summary><div class="context-message-body">${renderProviderContent(output, role)}</div></details>`;
122
+ }
123
+ const content = item?.content ?? item?.text ?? item?.arguments ?? item?.output ?? item;
124
+ return `<details class="context-message role-${roleClass(role)}"><summary class="context-message-head"><span>#${index + 1}</span><span class="role-badge">${escapeHtml(role + name)}</span><span class="context-message-preview">${escapeHtml(truncate(providerContentText(content), 130))}</span></summary><div class="context-message-body">${renderProviderContent(content, role)}</div></details>`;
125
+ }
126
+
127
+ export function describeEntry(entry, toolCalls) {
128
+ if (entry?.type === "message") return describeMessageEntry(entry, toolCalls);
129
+ if (entry?.type === "custom_message") {
130
+ const label = entry.display ? "custom: " : "hidden custom: ";
131
+ return { className: entry.display ? "tree-custom" : "tree-custom tree-hidden-custom", lines: [{ className: "line-custom", label, text: `${entry.customType || "custom"} · ${contentText(entry.content) || "(no text)"}` }] };
132
+ }
133
+ if (entry?.type === "custom") return { className: "tree-custom", lines: [{ className: "line-custom", label: "state: ", text: `${entry.customType || "custom"} · ${truncate(safeStringify(entry.data ?? null), 120)}` }] };
134
+ if (entry?.type === "compaction") return { className: "tree-compaction", lines: [{ className: "line-compaction", label: "compact: ", text: `${Math.round((entry.tokensBefore || 0) / 1000)}k tokens` }] };
135
+ if (entry?.type === "branch_summary") return { className: "tree-branch", lines: [{ className: "line-branch", label: "branch: ", text: entry.summary || "summary" }] };
136
+ if (entry?.type === "model_change") return { className: "tree-setting", lines: [{ className: "line-setting", label: "model: ", text: `${entry.provider}/${entry.modelId}` }] };
137
+ if (entry?.type === "thinking_level_change") return { className: "tree-setting", lines: [{ className: "line-setting", label: "thinking: ", text: entry.thinkingLevel || "" }] };
138
+ if (entry?.type === "session_info") return { className: "tree-setting", lines: [{ className: "line-setting", label: "name: ", text: entry.name || "(cleared)" }] };
139
+ if (entry?.type === "label") return { className: "tree-setting", lines: [{ className: "line-setting", label: "label: ", text: entry.label || "(cleared)" }] };
140
+ return { className: "tree-setting", lines: [{ className: "line-setting", label: `${entry?.type || "unknown"}: `, text: truncate(safeStringify(entry ?? null), 160) }] };
141
+ }
142
+
143
+ export function contentText(content) {
144
+ if (typeof content === "string") return content;
145
+ if (!Array.isArray(content)) return typeof content === "object" ? safeStringify(content ?? "") : String(content ?? "");
146
+ return content.map((block) => {
147
+ if (typeof block === "string") return block;
148
+ return block?.text || block?.thinking || (block?.type === "toolCall" ? `${block.name}(${safeStringify(block.arguments)})` : block?.type === "image" ? "[image]" : block?.type || "");
149
+ }).join(" ").replace(/\s+/g, " ").trim();
150
+ }
151
+
152
+ export function providerContentText(content) {
153
+ if (typeof content === "string") return content;
154
+ if (!Array.isArray(content)) return safeStringify(content ?? "");
155
+ return content.map((block) => {
156
+ if (typeof block === "string") return block;
157
+ const direct = block?.text ?? block?.input_text ?? block?.output_text;
158
+ if (direct !== undefined) return String(direct);
159
+ const nested = block?.output ?? block?.content;
160
+ if (typeof nested === "string") return nested;
161
+ if (block?.name) return `${block.name} ${safeStringify(block.arguments ?? block.input ?? {})}`;
162
+ return block?.type || "content block";
163
+ }).join(" ").replace(/\s+/g, " ").trim();
164
+ }
165
+
166
+ export function renderMarkdownText(value, rawLabel = "Raw Markdown") {
167
+ const source = String(value || "");
168
+ return `<div class="markdown-pair"><div class="markdown">${markdown(source)}</div><details class="raw-markdown"><summary>${escapeHtml(rawLabel)}</summary><pre>${escapeHtml(source)}</pre></details></div>`;
169
+ }
170
+
171
+ export function unavailableHtml(message) {
172
+ return `<div class="unavailable"><strong>Unavailable at extension level</strong>${escapeHtml(message)}</div>`;
173
+ }
174
+
175
+ export function json(value) {
176
+ return escapeHtml(safeStringify(value));
177
+ }
178
+
179
+ export function escapeHtml(value) {
180
+ return String(value ?? "").replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[char]);
181
+ }
182
+
183
+ export function escapeAttr(value) {
184
+ return escapeHtml(value);
185
+ }
186
+
187
+ export function number(value) {
188
+ return new Intl.NumberFormat("en-US").format(value || 0);
189
+ }
190
+
191
+ export function truncate(value, length) {
192
+ const text = String(value || "").replace(/\s+/g, " ").trim();
193
+ return text.length > length ? `${text.slice(0, length)}…` : text;
194
+ }
195
+
196
+ export function formatTime(value) {
197
+ return value ? new Date(value).toLocaleString() : "unknown";
198
+ }
199
+
200
+ export function formatClock(value) {
201
+ return value ? new Date(value).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }) : "";
202
+ }
203
+
204
+ function describeMessageEntry(entry, toolCalls) {
205
+ const role = entry.message?.role;
206
+ const content = Array.isArray(entry.message?.content) ? entry.message.content : [];
207
+ if (role === "user") return { className: "tree-user", lines: [{ className: "line-user", label: "user: ", text: contentText(entry.message?.content) || "(empty)" }] };
208
+ if (role === "assistant") {
209
+ const text = content.filter((block) => block?.type === "text").map((block) => block.text).join(" ").replace(/\s+/g, " ").trim();
210
+ const thinking = content.filter((block) => block?.type === "thinking").map((block) => block.thinking).join(" ").replace(/\s+/g, " ").trim();
211
+ const lines = [];
212
+ if (thinking) lines.push({ className: "line-thinking", label: "thinking: ", text: thinking });
213
+ if (text) lines.push({ className: entry.message?.stopReason === "error" ? "line-error" : "line-assistant", label: "assistant: ", text });
214
+ if (!lines.length) lines.push({ className: entry.message?.stopReason === "error" ? "line-error" : "line-assistant", label: "assistant: ", text: entry.message?.errorMessage ? entry.message.errorMessage : "(no text)" });
215
+ for (const block of content.filter((item) => item?.type === "toolCall")) lines.push({ className: "line-tool-call", label: "call: ", text: formatToolCall(block.name || "tool", block.arguments || {}) });
216
+ return { className: entry.message?.stopReason === "error" ? "tree-error" : "tree-assistant", lines };
217
+ }
218
+ const linked = entry.message?.toolCallId ? toolCalls.get(entry.message.toolCallId) : null;
219
+ const toolName = entry.message?.toolName || linked?.name || role || "message";
220
+ const callDescription = linked ? formatToolCall(linked.name, linked.arguments) : `[${toolName}]`;
221
+ const result = contentText(entry.message?.content);
222
+ return { className: entry.message?.isError ? "tree-tool-result tree-error" : "tree-tool-result", lines: [{ className: entry.message?.isError ? "line-tool-error" : "line-tool-result", label: "result: ", text: `${callDescription}${result ? ` · ${result}` : ""}` }] };
223
+ }
224
+
225
+ function fallbackEntry(entry, label) {
226
+ const id = entry?.id ? ` id="entry-${escapeAttr(entry.id)}"` : "";
227
+ return `<article${id} class="entry"><details class="custom-message custom-state"><summary>${escapeHtml(label)} entry</summary><div class="summary-content"><pre class="json-block">${json(entry ?? null)}</pre></div></details></article>`;
228
+ }
229
+
230
+ function renderDetails(details) {
231
+ if (details === undefined) return "";
232
+ return `<details class="message-raw-json"><summary>Details JSON</summary><pre class="json-block">${json(details)}</pre></details>`;
233
+ }
234
+
235
+ function formatToolCall(name, args) {
236
+ const compact = (value, length = 90) => String(value ?? "").replace(/[\n\t]+/g, " ").trim().slice(0, length);
237
+ if (["read", "write", "edit"].includes(name)) return `[${name}: ${compact(args.path || args.file_path)}]`;
238
+ if (name === "bash") return `[bash: ${compact(args.command)}${String(args.command || "").length > 90 ? "…" : ""}]`;
239
+ if (name === "grep") return `[grep: /${compact(args.pattern, 45)}/ in ${compact(args.path || ".", 45)}]`;
240
+ if (name === "find") return `[find: ${compact(args.pattern, 45)} in ${compact(args.path || ".", 45)}]`;
241
+ const serialized = safeStringify(args || {});
242
+ return `[${name}: ${compact(serialized)}${serialized.length > 90 ? "…" : ""}]`;
243
+ }
244
+
245
+ function roleClass(value) {
246
+ return String(value || "unknown").replace(/[^a-zA-Z0-9_-]/g, "-");
247
+ }
248
+
249
+ function markdown(value) {
250
+ const fallback = typeof marked === "undefined" ? undefined : marked;
251
+ const parser = globalThis.marked?.parse ? globalThis.marked : fallback;
252
+ return parser?.parse ? parser.parse(escapeHtml(String(value || ""))) : `<p>${escapeHtml(value)}</p>`;
253
+ }
254
+
255
+ function parseJson(value) {
256
+ try {
257
+ return JSON.parse(value);
258
+ } catch {
259
+ return value;
260
+ }
261
+ }
262
+
263
+ function safeStringify(value) {
264
+ const seen = new WeakSet();
265
+ try {
266
+ return JSON.stringify(value, (_key, item) => {
267
+ if (typeof item === "bigint") return `${item.toString()}n`;
268
+ if (!item || typeof item !== "object") return item;
269
+ if (seen.has(item)) return "[Circular]";
270
+ seen.add(item);
271
+ return item;
272
+ }, 2) ?? "null";
273
+ } catch (error) {
274
+ return JSON.stringify({ error: error instanceof Error ? error.message : String(error) }, null, 2);
275
+ }
276
+ }
package/web/styles.css ADDED
@@ -0,0 +1,318 @@
1
+ :root {
2
+ --accent: #8abeb7;
3
+ --border: #5f87ff;
4
+ --border-accent: #00d7ff;
5
+ --border-muted: #505050;
6
+ --success: #b5bd68;
7
+ --error: #cc6666;
8
+ --warning: #f0c674;
9
+ --muted: #929292;
10
+ --dim: #666;
11
+ --text: #d4d4d4;
12
+ --selected-bg: #3a3a4a;
13
+ --message-user-bg: #30383d;
14
+ --message-assistant-bg: #34352d;
15
+ --message-tool-call-bg: #382f29;
16
+ --message-tool-result-bg: #263534;
17
+ --message-compaction-bg: #3c3728;
18
+ --message-branch-bg: #312d3d;
19
+ --user-bg: var(--message-user-bg);
20
+ --custom-bg: #2d2838;
21
+ --tool-bg: #282832;
22
+ --tool-success-bg: #283228;
23
+ --tool-error-bg: #3c2828;
24
+ --page-bg: #18181e;
25
+ --card-bg: #1e1e24;
26
+ --info-bg: #3c3728;
27
+ --topbar-height: 42px;
28
+ --sidebar-width: 400px;
29
+ --line-height: 18px;
30
+ }
31
+
32
+ * { box-sizing: border-box; }
33
+ html { color-scheme: dark; }
34
+ body {
35
+ margin: 0;
36
+ background: var(--page-bg);
37
+ color: var(--text);
38
+ font: 12px/var(--line-height) ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, monospace;
39
+ }
40
+ button, input, select { font: inherit; }
41
+ button, select { color: inherit; }
42
+ button { cursor: pointer; }
43
+ button:focus-visible, input:focus-visible, select:focus-visible, [tabindex]:focus-visible {
44
+ outline: 2px solid var(--border-accent);
45
+ outline-offset: 1px;
46
+ }
47
+ .hidden { display: none !important; }
48
+ .skip-link { position: fixed; z-index: 200; top: 5px; left: 5px; padding: 7px 10px; background: var(--card-bg); color: var(--text); border: 2px solid var(--border-accent); transform: translateY(-150%); }
49
+ .skip-link:focus { transform: none; }
50
+ .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
51
+
52
+ .topbar {
53
+ position: sticky;
54
+ z-index: 30;
55
+ top: 0;
56
+ height: var(--topbar-height);
57
+ display: flex;
58
+ align-items: center;
59
+ gap: 12px;
60
+ padding: 0 12px;
61
+ border-bottom: 1px solid var(--dim);
62
+ background: #1b1b21;
63
+ }
64
+ .brand { display: flex; align-items: center; gap: 8px; min-width: 118px; color: var(--accent); font-weight: 700; letter-spacing: .08em; }
65
+ .brand-mark { width: 10px; height: 10px; border: 1px solid var(--accent); box-shadow: inset 0 0 0 2px var(--page-bg); background: var(--accent); }
66
+ .mode-switch { display: flex; align-self: stretch; }
67
+ .mode-btn { position: relative; min-width: 92px; border: 0; background: transparent; color: var(--muted); }
68
+ .mode-btn:hover { color: var(--text); background: #24242c; }
69
+ .mode-btn.active { color: var(--text); }
70
+ .mode-btn.active::after { content: ""; position: absolute; inset: auto 12px 0; height: 2px; background: var(--border-accent); }
71
+ .topbar-spacer { flex: 1; }
72
+ .session-picker-label { color: var(--muted); }
73
+ #session-picker { max-width: 320px; min-width: 180px; border: 1px solid var(--border-muted); background: var(--card-bg); padding: 4px 26px 4px 8px; }
74
+ .live-status { display: inline-flex; align-items: center; gap: 6px; color: var(--muted); min-width: 82px; }
75
+ .status-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--warning); }
76
+ .live-status.live { color: var(--success); }
77
+ .live-status.live .status-dot { background: var(--success); box-shadow: 0 0 7px rgba(181,189,104,.45); }
78
+ .live-status.offline { color: var(--error); }
79
+ .live-status.offline .status-dot { background: var(--error); }
80
+ .icon-btn, .sidebar-toggle { width: 30px; height: 30px; display: grid; place-items: center; padding: 6px; border: 1px solid var(--border-muted); background: transparent; }
81
+ .icon-btn:hover, .sidebar-toggle:hover { background: var(--selected-bg); border-color: var(--muted); }
82
+ .icon-btn svg, .sidebar-toggle svg { width: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
83
+
84
+ #app { display: flex; min-height: calc(100vh - var(--topbar-height)); }
85
+ #sidebar {
86
+ position: sticky;
87
+ top: var(--topbar-height);
88
+ width: var(--sidebar-width);
89
+ min-width: var(--sidebar-width);
90
+ height: calc(100vh - var(--topbar-height));
91
+ display: flex;
92
+ flex-direction: column;
93
+ background: var(--card-bg);
94
+ }
95
+ .sidebar-header { padding: 10px; border-bottom: 1px solid var(--border-muted); }
96
+ .sidebar-search { width: 100%; height: 30px; padding: 5px 8px; border: 1px solid var(--border-muted); background: var(--page-bg); color: var(--text); }
97
+ .sidebar-search::placeholder { color: var(--dim); }
98
+ .sidebar-filters { display: flex; gap: 4px; padding-top: 8px; overflow-x: auto; }
99
+ .filter-btn, .call-filter, .detail-tab, .small-btn {
100
+ min-height: 27px;
101
+ padding: 3px 7px;
102
+ white-space: nowrap;
103
+ border: 1px solid var(--border-muted);
104
+ background: transparent;
105
+ color: var(--muted);
106
+ }
107
+ .filter-btn:hover, .call-filter:hover, .detail-tab:hover, .small-btn:hover { color: var(--text); border-color: var(--muted); }
108
+ .filter-btn.active, .call-filter.active, .detail-tab.active { color: var(--text); border-color: var(--border); background: var(--selected-bg); }
109
+ .tree-container { flex: 1; min-height: 0; overflow: auto; padding: 6px 0 18px; scrollbar-gutter: stable; }
110
+ .tree-row {
111
+ display: flex;
112
+ align-items: flex-start;
113
+ gap: 5px;
114
+ width: max-content;
115
+ min-width: 100%;
116
+ min-height: 25px;
117
+ padding: 3px 10px;
118
+ border-left: 2px solid transparent;
119
+ color: var(--muted);
120
+ cursor: pointer;
121
+ }
122
+ .tree-row:hover { background: #292930; color: var(--text); }
123
+ .tree-row.active { background: rgba(58,58,74,.72); border-left-color: var(--border-accent); color: var(--text); }
124
+ .tree-row.on-branch:not(.active) { border-left-color: var(--border-muted); }
125
+ .tree-prefix { flex: none; white-space: pre; color: var(--dim); line-height: 24px; }
126
+ .tree-icon { flex: 0 0 9px; color: var(--dim); text-align: center; line-height: 24px; }
127
+ .tree-lines { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; white-space: nowrap; }
128
+ .tree-line { min-height: 22px; padding: 2px 7px; border-left: 2px solid transparent; border-radius: 2px; }
129
+ .tree-line-label { font-weight: 700; }
130
+ .line-user { background: var(--message-user-bg); border-left-color: #7aa7a2; color: #d8e4e2; }
131
+ .line-user .tree-line-label { color: #91c7c0; }
132
+ .line-assistant { background: var(--message-assistant-bg); border-left-color: #b5bd68; color: var(--text); }
133
+ .line-assistant .tree-line-label { color: #c5cb70; }
134
+ .line-thinking { background: var(--message-assistant-bg); border-left-color: #8f9560; color: var(--muted); font-style: italic; }
135
+ .line-thinking .tree-line-label { color: #aeb56b; }
136
+ .line-tool-call { background: var(--message-tool-call-bg); border-left-color: #de935f; color: #dfc2a9; }
137
+ .line-tool-call .tree-line-label { color: #de935f; }
138
+ .line-tool-result { background: var(--message-tool-result-bg); border-left-color: #5f9f98; color: #a9c9c5; }
139
+ .line-tool-result .tree-line-label { color: #7fbab3; }
140
+ .line-tool-error, .line-error { background: #3c2828; border-left-color: var(--error); color: #e2abab; }
141
+ .line-compaction { background: var(--message-compaction-bg); border-left-color: var(--warning); color: #e0cf9d; }
142
+ .line-branch { background: var(--message-branch-bg); border-left-color: #b294bb; color: #c8b3d0; }
143
+ .line-custom { background: var(--custom-bg); border-left-color: #b294bb; color: #c8b3d0; }
144
+ .line-custom .tree-line-label { color: #d0b0da; }
145
+ .line-setting { background: #292930; border-left-color: var(--dim); color: var(--muted); }
146
+ .call-meta { display: flex; flex-direction: column; align-items: flex-end; gap: 1px; margin-left: auto; flex: none; padding-left: 12px; color: var(--dim); font-size: 10px; }
147
+ .call-status { width: 7px; height: 7px; flex: none; border-radius: 50%; background: var(--warning); }
148
+ .call-status.success { background: var(--success); }
149
+ .call-status.error { border-radius: 0; background: var(--error); }
150
+ .call-tree-row { align-items: flex-start; padding-block: 6px; border-bottom: 1px solid #292930; }
151
+ .call-tree-row.call-compaction { background: rgba(60,55,40,.38); }
152
+ .call-tree-row.call-branch_summary { background: rgba(49,45,61,.4); }
153
+ .call-tree-content { display: flex; flex-direction: column; min-width: 300px; max-width: 620px; white-space: nowrap; }
154
+ .call-tree-title { color: var(--text); font-weight: 700; }
155
+ .call-tree-trigger { color: var(--accent); }
156
+ .call-tree-activity { color: var(--muted); }
157
+ .tree-status { min-height: 28px; padding: 5px 10px; border-top: 1px solid var(--border-muted); color: var(--dim); }
158
+ #sidebar-resizer { position: sticky; top: var(--topbar-height); width: 6px; height: calc(100vh - var(--topbar-height)); cursor: col-resize; border-left: 1px solid var(--dim); background: var(--page-bg); }
159
+ #sidebar-resizer:hover { background: var(--border); }
160
+ #content { min-width: 0; flex: 1; padding: 22px clamp(16px, 4vw, 64px) 80px; }
161
+ .sidebar-toggle, #sidebar-overlay { display: none; }
162
+
163
+ .session-header { margin-bottom: 22px; padding: 14px 16px; border: 1px solid var(--border-muted); background: var(--card-bg); }
164
+ .session-title-row { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
165
+ .session-title { margin: 0; font-size: 15px; color: var(--accent); }
166
+ .session-badge { padding: 1px 6px; border: 1px solid var(--border-muted); color: var(--muted); }
167
+ .session-badge.active { color: var(--success); border-color: var(--success); }
168
+ .info-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 5px 18px; }
169
+ .info-label { color: var(--muted); margin-right: 7px; }
170
+ .info-value { overflow-wrap: anywhere; }
171
+ .trace-warning { color: var(--warning); }
172
+ .header-actions { display: flex; gap: 6px; margin-left: auto; }
173
+ .header-disclosure { margin-top: 10px; border-top: 1px solid var(--border-muted); padding-top: 8px; }
174
+ .header-disclosure summary { color: var(--warning); cursor: pointer; }
175
+ .header-disclosure pre { max-height: 420px; overflow: auto; white-space: pre-wrap; color: var(--muted); }
176
+
177
+ #messages { max-width: 1100px; margin: auto; }
178
+ .entry { position: relative; margin: 0 0 16px; scroll-margin-top: 58px; }
179
+ .message-timestamp { margin-bottom: 3px; color: var(--dim); font-size: 10px; }
180
+ .user-message { margin-left: clamp(20px, 8vw, 100px); padding: 10px 12px; background: var(--message-user-bg); border-left: 2px solid #7aa7a2; }
181
+ .assistant-message { padding: 10px 12px; background: var(--message-assistant-bg); border-left: 2px solid #b5bd68; }
182
+ .entry.highlight { animation: flash 1.2s ease-out; }
183
+ @keyframes flash { 0%, 35% { background: rgba(0,215,255,.12); } 100% { background: transparent; } }
184
+ .markdown { overflow-wrap: anywhere; }
185
+ .markdown > :first-child { margin-top: 0; }
186
+ .markdown > :last-child { margin-bottom: 0; }
187
+ .markdown p, .markdown ul, .markdown ol, .markdown pre, .markdown blockquote { margin: 8px 0; }
188
+ .markdown h1, .markdown h2, .markdown h3, .markdown h4 { margin: 16px 0 7px; color: var(--warning); line-height: 1.3; }
189
+ .markdown h1 { font-size: 17px; } .markdown h2 { font-size: 15px; } .markdown h3 { font-size: 13px; }
190
+ .markdown a { color: #81a2be; }
191
+ .markdown code { color: var(--accent); background: #24242c; padding: 1px 3px; }
192
+ .markdown pre, .json-block { overflow: auto; padding: 10px; border: 1px solid var(--border-muted); background: #141419; color: var(--text); tab-size: 2; }
193
+ .markdown pre code { padding: 0; background: transparent; color: inherit; }
194
+ .markdown blockquote { padding-left: 10px; border-left: 2px solid var(--muted); color: var(--muted); }
195
+ .markdown table { border-collapse: collapse; max-width: 100%; }
196
+ .markdown th, .markdown td { padding: 4px 7px; border: 1px solid var(--border-muted); }
197
+ .markdown-pair + .markdown-pair { margin-top: 8px; }
198
+ .raw-markdown { margin-top: 5px; border-top: 1px dashed #3f3f48; color: var(--dim); }
199
+ .raw-markdown summary { width: max-content; padding: 3px 0; cursor: pointer; font-size: 10px; }
200
+ .raw-markdown pre { max-height: 360px; margin: 3px 0 0; padding: 9px; overflow: auto; white-space: pre-wrap; border: 1px solid var(--border-muted); background: #141419; color: var(--muted); }
201
+ .thinking-block, .tool-block, .compaction, .branch-summary, .custom-message { margin: 8px 0; border: 1px solid var(--border-muted); }
202
+ .thinking-block { background: var(--message-assistant-bg); }
203
+ .tool-block { background: var(--message-tool-call-bg); }
204
+ .thinking-block summary, .tool-block summary, .compaction summary, .branch-summary summary, .custom-message summary { padding: 6px 9px; cursor: pointer; color: var(--muted); }
205
+ .thinking-block[open] summary, .tool-block[open] summary, .compaction[open] summary, .branch-summary[open] summary, .custom-message[open] summary { border-bottom: 1px solid var(--border-muted); color: var(--text); }
206
+ .thinking-content, .tool-content, .summary-content { padding: 9px; white-space: pre-wrap; overflow-wrap: anywhere; }
207
+ .thinking-content { color: var(--muted); font-style: italic; }
208
+ .tool-block.success { background: var(--message-tool-result-bg); }
209
+ .tool-block.error { background: var(--tool-error-bg); border-color: var(--error); }
210
+ .tool-name { color: var(--accent); font-weight: 700; }
211
+ .model-change, .setting-change { margin: 12px 0; padding: 5px 9px; border-left: 2px solid var(--dim); color: var(--muted); }
212
+ .compaction { border-left: 2px solid var(--warning); background: var(--message-compaction-bg); }
213
+ .branch-summary { border-left: 2px solid #b294bb; background: var(--message-branch-bg); }
214
+ .custom-message { border-left: 2px solid #b294bb; background: var(--custom-bg); }
215
+ .custom-message.hidden-custom { border-left-color: var(--dim); background: #26252e; }
216
+ .custom-state { border-left-color: var(--dim); }
217
+ .message-images { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 9px; }
218
+ .message-image { max-width: 320px; max-height: 240px; object-fit: contain; cursor: zoom-in; border: 1px solid var(--border-muted); }
219
+ .copy-link { position: absolute; top: 0; right: 0; opacity: 0; border: 0; background: var(--card-bg); color: var(--muted); }
220
+ .entry:hover .copy-link, .copy-link:focus { opacity: 1; }
221
+
222
+ .call-header { max-width: 1250px; margin: 0 auto 14px; padding: 14px 16px; border: 1px solid var(--border-muted); background: var(--card-bg); }
223
+ .call-header-top { display: flex; align-items: center; gap: 9px; }
224
+ .call-title { margin: 0; font-size: 15px; color: var(--accent); }
225
+ .call-id { color: var(--dim); }
226
+ .status-pill { padding: 1px 6px; border: 1px solid var(--warning); color: var(--warning); }
227
+ .status-pill.success { border-color: var(--success); color: var(--success); }
228
+ .status-pill.error { border-color: var(--error); color: var(--error); }
229
+ .source-pill { padding: 1px 6px; border: 1px solid #8b734d; color: #c9aa72; }
230
+ .call-summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 6px 16px; margin-top: 10px; }
231
+ .detail-tabs { position: sticky; top: var(--topbar-height); z-index: 5; display: flex; gap: 5px; max-width: 1250px; margin: auto; padding: 8px 0; background: var(--page-bg); overflow-x: auto; }
232
+ .detail-panel { max-width: 1250px; margin: auto; }
233
+ .compare-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 12px; }
234
+ .context-view-switch { display: flex; width: max-content; margin: 0 0 8px; border: 1px solid var(--border-muted); background: var(--card-bg); }
235
+ .context-view-btn { min-height: 30px; padding: 4px 10px; border: 0; border-right: 1px solid var(--border-muted); background: transparent; color: var(--muted); }
236
+ .context-view-btn:last-child { border-right: 0; }
237
+ .context-view-btn:hover { color: var(--text); background: #292930; }
238
+ .context-view-btn.active { color: var(--page-bg); background: var(--accent); }
239
+ .context-single-panel { width: 100%; }
240
+ .data-panel { min-width: 0; border: 1px solid var(--border-muted); background: var(--card-bg); }
241
+ .panel-heading { position: sticky; top: 77px; z-index: 3; display: flex; align-items: center; gap: 8px; padding: 7px 9px; border-bottom: 1px solid var(--border-muted); background: var(--card-bg); color: var(--warning); }
242
+ .panel-heading .small-btn { margin-left: auto; min-height: 23px; }
243
+ .panel-subtitle { color: var(--dim); font-size: 10px; }
244
+ .data-panel .json-block { margin: 0; border: 0; max-height: calc(100vh - 155px); }
245
+ .structured-context { display: grid; gap: 10px; padding: 10px; }
246
+ .context-section { min-width: 0; margin: 0; border: 1px solid #3d3d46; background: #1b1b21; }
247
+ .context-section > summary, .context-section-title { padding: 7px 9px; cursor: pointer; color: var(--warning); border-bottom: 1px solid #35353d; }
248
+ .context-section-title { cursor: default; }
249
+ .context-section-title span { color: var(--dim); }
250
+ .context-section-body { padding: 9px; }
251
+ .context-messages { display: grid; gap: 8px; padding: 8px; }
252
+ .context-message { min-width: 0; border: 1px solid #41414a; background: #202027; }
253
+ .context-message.role-user { border-left: 2px solid #7aa7a2; background: var(--message-user-bg); }
254
+ .context-message.role-assistant { border-left: 2px solid #b5bd68; background: var(--message-assistant-bg); }
255
+ .context-message.role-function_call, .context-message.role-tool_use { border-left: 2px solid #de935f; background: var(--message-tool-call-bg); }
256
+ .context-message.role-toolResult, .context-message.role-function_call_output, .context-message.role-tool_result { border-left: 2px solid #5f9f98; background: var(--message-tool-result-bg); }
257
+ .context-message-head { display: flex; align-items: center; gap: 8px; min-width: 0; padding: 5px 7px; cursor: pointer; color: var(--dim); }
258
+ .context-message[open] > .context-message-head { border-bottom: 1px solid #34343c; }
259
+ .role-badge { color: var(--accent); }
260
+ .error-label { color: var(--error); }
261
+ .context-message-preview { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); }
262
+ .context-message-body { padding: 8px; }
263
+ .message-raw-json { margin: 0 8px 8px; color: var(--dim); }
264
+ .message-raw-json summary, .raw-json-disclosure summary { cursor: pointer; padding: 4px 0; font-size: 10px; }
265
+ .structured-context .json-block { max-height: 320px; border: 1px solid var(--border-muted); }
266
+ .tool-definition-list { display: grid; gap: 6px; }
267
+ .tool-definition { padding: 5px 7px; border: 1px solid #3d3d46; background: #202027; }
268
+ .tool-definition summary { cursor: pointer; color: var(--accent); }
269
+ .tool-definition p { color: var(--muted); }
270
+ .provider-meta { display: grid; grid-template-columns: repeat(auto-fit, minmax(145px, 1fr)); gap: 5px; }
271
+ .provider-meta div { display: flex; justify-content: space-between; gap: 8px; padding: 4px 6px; background: #24242c; }
272
+ .provider-meta span { color: var(--dim); }
273
+ .provider-meta strong { overflow-wrap: anywhere; color: var(--text); font-weight: 400; }
274
+ .provider-block { margin: 6px 0; border: 1px solid var(--border-muted); }
275
+ .provider-block summary { padding: 5px 7px; cursor: pointer; color: var(--muted); }
276
+ .context-note { padding: 9px 10px; border: 1px solid #514a36; background: var(--message-compaction-bg); color: var(--muted); }
277
+ .context-note strong { display: block; color: var(--warning); }
278
+ .raw-json-disclosure { margin: 0 10px 10px; padding-top: 5px; border-top: 1px dashed #44444d; color: var(--dim); }
279
+ .raw-json-disclosure > .json-block { margin-top: 4px; border: 1px solid var(--border-muted); }
280
+ .unavailable { padding: 18px; color: var(--muted); }
281
+ .unavailable strong { display: block; margin-bottom: 5px; color: var(--warning); }
282
+ .event-list { display: grid; gap: 8px; }
283
+ .stream-event { border: 1px solid var(--border-muted); background: var(--card-bg); }
284
+ .stream-event-head { display: flex; gap: 10px; padding: 5px 8px; border-bottom: 1px solid #34343c; color: var(--muted); }
285
+ .stream-event-type { color: var(--accent); }
286
+ .stream-event pre { margin: 0; padding: 8px; white-space: pre-wrap; overflow-wrap: anywhere; color: var(--text); }
287
+ .empty-state { max-width: 520px; margin: 18vh auto; text-align: center; color: var(--muted); }
288
+ .empty-state h1 { color: var(--text); font-size: 18px; }
289
+ .empty-glyph { color: var(--accent); font-size: 48px; }
290
+ .image-modal { display: none; position: fixed; z-index: 100; inset: 0; place-items: center; padding: 40px; background: rgba(0,0,0,.86); }
291
+ .image-modal.open { display: grid; }
292
+ .image-modal img { max-width: 95vw; max-height: 90vh; }
293
+ .modal-close { position: fixed; top: 16px; right: 18px; width: 36px; height: 36px; border: 1px solid var(--muted); background: var(--card-bg); font-size: 22px; }
294
+ .toast { position: fixed; z-index: 110; right: 14px; bottom: 14px; max-width: 420px; padding: 8px 11px; border: 1px solid var(--border-muted); background: var(--card-bg); opacity: 0; transform: translateY(8px); pointer-events: none; transition: opacity .15s, transform .15s; }
295
+ .toast.show { opacity: 1; transform: none; }
296
+
297
+ @media (max-width: 850px) {
298
+ .brand { min-width: auto; }
299
+ .brand { font-size: 0; }
300
+ .brand-mark { display: block; }
301
+ .session-picker-label { display: none; }
302
+ #session-picker { min-width: 100px; max-width: 180px; }
303
+ .live-status { min-width: auto; font-size: 0; }
304
+ #sidebar { position: fixed; z-index: 25; left: 0; top: var(--topbar-height); transform: translateX(-100%); transition: transform .18s ease; }
305
+ #sidebar { width: min(var(--sidebar-width), calc(100vw - 28px)); min-width: min(var(--sidebar-width), calc(100vw - 28px)); }
306
+ body.sidebar-open #sidebar { transform: none; }
307
+ #sidebar-resizer { display: none; }
308
+ .sidebar-toggle { display: grid; position: fixed; z-index: 24; top: 50px; left: 8px; background: var(--card-bg); }
309
+ #sidebar-overlay { position: fixed; z-index: 20; inset: var(--topbar-height) 0 0; background: rgba(0,0,0,.55); }
310
+ body.sidebar-open #sidebar-overlay { display: block; }
311
+ #content { padding: 54px 14px 60px; }
312
+ .compare-grid { grid-template-columns: 1fr; }
313
+ .panel-heading { top: 112px; }
314
+ }
315
+
316
+ @media (prefers-reduced-motion: reduce) {
317
+ *, *::before, *::after { scroll-behavior: auto !important; animation-duration: .01ms !important; transition-duration: .01ms !important; }
318
+ }