1<!doctype html>
2<html lang="en">
3 <head>
4 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 <title>Eval Explorer</title>
7 <style>
8 :root {
9 /* Light theme (default) */
10 --bg-color: #ffffff;
11 --text-color: #333333;
12 --header-bg: #f8f8f8;
13 --border-color: #eaeaea;
14 --code-bg: #f5f5f5;
15 --link-color: #0066cc;
16 --button-bg: #f5f5f5;
17 --button-border: #ddd;
18 --button-active-bg: #0066cc;
19 --button-active-color: white;
20 --button-active-border: #0055aa;
21 --preview-bg: #f5f5f5;
22 --table-line: #f0f0f0;
23 }
24
25 /* Dark theme */
26 [data-theme="dark"] {
27 --bg-color: #1e1e1e;
28 --text-color: #e0e0e0;
29 --header-bg: #2d2d2d;
30 --border-color: #444444;
31 --code-bg: #2a2a2a;
32 --link-color: #4da6ff;
33 --button-bg: #333333;
34 --button-border: #555555;
35 --button-active-bg: #0066cc;
36 --button-active-color: white;
37 --button-active-border: #0055aa;
38 --preview-bg: #2a2a2a;
39 --table-line: #333333;
40 }
41
42 /* Apply theme variables */
43 body {
44 font-family: monospace;
45 line-height: 1.6;
46 margin: 0;
47 padding: 20px;
48 color: var(--text-color);
49 max-width: 1200px;
50 margin: 0 auto;
51 background-color: var(--bg-color);
52 }
53 h1 {
54 margin-bottom: 20px;
55 border-bottom: 1px solid var(--border-color);
56 padding-bottom: 10px;
57 font-family: monospace;
58 }
59 table {
60 width: 100%;
61 border-collapse: collapse;
62 margin-bottom: 20px;
63 table-layout: fixed; /* Ensure fixed width columns */
64 }
65 th,
66 td {
67 padding: 10px;
68 text-align: left;
69 border-bottom: 1px dotted var(--border-color);
70 vertical-align: top;
71 word-wrap: break-word; /* Ensure long content wraps */
72 overflow-wrap: break-word;
73 }
74 th {
75 background-color: var(--header-bg);
76 font-weight: 600;
77 }
78 .collapsible {
79 cursor: pointer;
80 color: var(--link-color);
81 text-decoration: underline;
82 }
83 .hidden {
84 display: none;
85 }
86 .tool-name {
87 font-weight: bold;
88 }
89 .tool-params {
90 padding-left: 20px;
91 color: #666;
92 }
93 pre {
94 background-color: var(--code-bg);
95 padding: 10px;
96 border-radius: 5px;
97 overflow-x: auto;
98 max-height: 200px;
99 margin: 10px 0;
100 font-family: monospace;
101 width: 100%;
102 box-sizing: border-box;
103 white-space: pre-wrap; /* Ensure text wraps */
104 color: var(--text-color);
105 }
106 code {
107 font-family: monospace;
108 }
109
110 /* Column sizing */
111 .turn-column {
112 width: 3%;
113 max-width: 3%;
114 }
115 .text-column {
116 width: 22%;
117 max-width: 22%;
118 }
119 .tool-column {
120 width: 38%;
121 max-width: 38%;
122 }
123 .result-column {
124 width: 37%;
125 max-width: 37%;
126 overflow-x: auto;
127 }
128
129 /* Content formatting */
130 .text-content {
131 font-family:
132 system-ui,
133 -apple-system,
134 BlinkMacSystemFont,
135 "Segoe UI",
136 Roboto,
137 Oxygen,
138 Ubuntu,
139 Cantarell,
140 "Open Sans",
141 "Helvetica Neue",
142 sans-serif;
143 font-size: 0.7rem;
144 }
145 .action-container .action-preview,
146 .action-container .action-full {
147 margin-bottom: 5px;
148 }
149 .preview-content {
150 white-space: pre-wrap;
151 margin-bottom: 5px;
152 background-color: var(--preview-bg);
153 padding: 10px;
154 border-radius: 5px;
155 font-family: monospace;
156 width: 100%;
157 box-sizing: border-box;
158 overflow-wrap: break-word;
159 color: var(--text-color);
160 }
161 .show-more {
162 color: var(--link-color);
163 cursor: pointer;
164 text-decoration: none;
165 display: block;
166 margin-top: 5px;
167 }
168 .more-inline {
169 color: var(--link-color);
170 cursor: pointer;
171 text-decoration: none;
172 display: inline;
173 margin-left: 5px;
174 }
175
176 /* Compact mode styles */
177 .compact-mode td {
178 padding: 5px; /* Reduced padding in compact mode */
179 }
180
181 .compact-mode .preview-content {
182 padding: 2px;
183 margin-bottom: 2px;
184 }
185
186 .compact-mode pre {
187 padding: 5px;
188 margin: 5px 0;
189 white-space: pre; /* Don't wrap code in compact mode */
190 overflow-x: auto; /* Add horizontal scrollbar */
191 }
192
193 .compact-mode .result-column pre,
194 .compact-mode .result-column .preview-content {
195 max-width: 100%;
196 overflow-x: auto;
197 white-space: pre;
198 }
199
200 /* Make action containers more compact */
201 .compact-mode .action-container {
202 margin-bottom: 2px;
203 }
204
205 /* Reduce space between turns */
206 .compact-mode tr {
207 border-bottom: 1px solid var(--table-line);
208 }
209
210 /* Tool params more compact */
211 .compact-mode .tool-params {
212 padding-left: 10px;
213 margin-top: 2px;
214 }
215
216 hr {
217 margin: 10px 0;
218 border: 0;
219 height: 1px;
220 background-color: var(--border-color);
221 }
222
223 /* View switcher */
224 .view-switcher {
225 display: flex;
226 gap: 10px;
227 margin-bottom: 20px;
228 align-items: center;
229 }
230
231 .view-button {
232 background-color: var(--button-bg);
233 border: 1px solid var(--button-border);
234 border-radius: 4px;
235 padding: 5px 15px;
236 cursor: pointer;
237 font-family: monospace;
238 font-size: 0.9rem;
239 transition: all 0.2s ease;
240 color: var(--text-color);
241 }
242
243 .view-button:hover {
244 background-color: var(--button-border);
245 }
246
247 .view-button.active {
248 background-color: var(--button-active-bg);
249 color: var(--button-active-color);
250 border-color: var(--button-active-border);
251 }
252
253 /* Navigation bar styles */
254 .thread-navigation {
255 display: flex;
256 align-items: center;
257 margin-bottom: 20px;
258 padding: 10px 0;
259 border-bottom: 1px solid var(--border-color);
260 }
261
262 .nav-button {
263 background-color: var(--button-bg);
264 border: 1px solid var(--button-border);
265 border-radius: 4px;
266 padding: 5px 15px;
267 cursor: pointer;
268 font-family: monospace;
269 font-size: 0.9rem;
270 transition: all 0.2s ease;
271 color: var(--text-color);
272 }
273
274 .nav-button:hover:not(:disabled) {
275 background-color: var(--button-border);
276 }
277
278 .nav-button:disabled {
279 opacity: 0.5;
280 cursor: not-allowed;
281 }
282
283 .thread-indicator {
284 margin: 0 15px;
285 font-size: 1rem;
286 flex-grow: 1;
287 text-align: center;
288 }
289
290 #thread-id {
291 font-weight: bold;
292 }
293
294 /* Theme switcher */
295 .theme-switcher {
296 margin-left: auto;
297 display: flex;
298 align-items: center;
299 }
300
301 .theme-button {
302 background-color: var(--button-bg);
303 border: 1px solid var(--button-border);
304 border-radius: 4px;
305 padding: 5px 10px;
306 cursor: pointer;
307 font-size: 0.9rem;
308 transition: all 0.2s ease;
309 color: var(--text-color);
310 display: flex;
311 align-items: center;
312 }
313
314 .theme-button:hover {
315 background-color: var(--button-border);
316 }
317
318 .theme-icon {
319 margin-right: 5px;
320 font-size: 1rem;
321 }
322 </style>
323 </head>
324 <body>
325 <h1 id="current-filename">Thread Explorer</h1>
326 <div class="view-switcher">
327 <button id="full-view" class="view-button active" onclick="switchView('full')">Full View</button>
328 <button id="compact-view" class="view-button" onclick="switchView('compact')">Compact View</button>
329 <button
330 id="export-button"
331 class="view-button"
332 onclick="exportThreadAsJson()"
333 title="Export current thread as JSON"
334 >
335 Export
336 </button>
337 <div class="theme-switcher">
338 <button id="theme-toggle" class="theme-button" onclick="toggleTheme()">
339 <span id="theme-icon" class="theme-icon">☀️</span>
340 <span id="theme-text">Light</span>
341 </button>
342 </div>
343 </div>
344 <div class="thread-navigation">
345 <button
346 id="prev-thread"
347 class="nav-button"
348 onclick="previousThread()"
349 title="Previous thread (Ctrl+←, k, or h)"
350 disabled
351 >
352 ← Previous
353 </button>
354 <div class="thread-indicator">
355 Thread <span id="current-thread-index">1</span> of <span id="total-threads">1</span>:
356 <span id="thread-id">Default Thread</span>
357 </div>
358 <button
359 id="next-thread"
360 class="nav-button"
361 onclick="nextThread()"
362 title="Next thread (Ctrl+→, j, or l)"
363 disabled
364 >
365 Next →
366 </button>
367 </div>
368 <table id="thread-table">
369 <thead>
370 <tr>
371 <th class="turn-column">Turn</th>
372 <th class="text-column">Text</th>
373 <th class="tool-column">Tool</th>
374 <th class="result-column">Result</th>
375 </tr>
376 </thead>
377 <tbody id="thread-body">
378 <!-- Content will be filled dynamically -->
379 </tbody>
380 </table>
381
382 <script>
383 // View mode - 'full' or 'compact'
384 let viewMode = "full";
385
386 // Theme mode - 'light', 'dark', or 'system'
387 let themeMode = localStorage.getItem("theme") || "system";
388
389 // Function to apply theme
390 function applyTheme(theme) {
391 const themeIcon = document.getElementById("theme-icon");
392 const themeText = document.getElementById("theme-text");
393
394 if (theme === "dark") {
395 document.documentElement.setAttribute("data-theme", "dark");
396 themeIcon.textContent = "🌙";
397 themeText.textContent = "Dark";
398 } else {
399 document.documentElement.removeAttribute("data-theme");
400 themeIcon.textContent = "☀️";
401 themeText.textContent = "Light";
402 }
403 }
404
405 // Function to toggle between light and dark themes
406 function toggleTheme() {
407 // If currently system or light, switch to dark
408 if (themeMode === "system") {
409 const systemDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
410 themeMode = systemDark ? "light" : "dark";
411 } else {
412 themeMode = themeMode === "light" ? "dark" : "light";
413 }
414
415 // Save preference
416 localStorage.setItem("theme", themeMode);
417
418 // Apply theme
419 applyTheme(themeMode);
420 }
421
422 // Initialize theme based on system or saved preference
423 function initTheme() {
424 if (themeMode === "system") {
425 // Use system preference
426 const systemDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
427 applyTheme(systemDark ? "dark" : "light");
428
429 // Listen for system theme changes
430 window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
431 if (themeMode === "system") {
432 applyTheme(e.matches ? "dark" : "light");
433 }
434 });
435 } else {
436 // Use saved preference
437 applyTheme(themeMode);
438 }
439 }
440
441 // Function to switch between view modes
442 function switchView(mode) {
443 viewMode = mode;
444
445 // Update button states
446 document.getElementById("full-view").classList.toggle("active", mode === "full");
447 document.getElementById("compact-view").classList.toggle("active", mode === "compact");
448
449 // Add or remove compact-mode class on the body
450 document.body.classList.toggle("compact-mode", mode === "compact");
451
452 // Re-render the thread with the new view mode
453 renderThread();
454 }
455
456 // Function to export the current thread as a JSON file
457 function exportThreadAsJson() {
458 // Clone the thread to avoid modifying the original
459 const threadToExport = JSON.parse(JSON.stringify(thread));
460
461 // Create a Blob with the JSON data
462 const blob = new Blob([JSON.stringify(threadToExport, null, 2)], { type: "application/json" });
463
464 // Create a download link
465 const url = URL.createObjectURL(blob);
466 const a = document.createElement("a");
467 a.href = url;
468
469 // Generate filename based on thread ID or index
470 const filename =
471 threadToExport.thread_id || threadToExport.filename || `thread-${currentThreadIndex + 1}.json`;
472 a.download = filename.endsWith(".json") ? filename : `${filename}.json`;
473
474 // Trigger the download
475 document.body.appendChild(a);
476 a.click();
477
478 // Clean up
479 setTimeout(() => {
480 document.body.removeChild(a);
481 URL.revokeObjectURL(url);
482 }, 0);
483 }
484 // Default dummy thread data for preview purposes
485 let dummyThread = {
486 messages: [
487 {
488 role: "system",
489 content: [{ Text: "System prompt..." }],
490 },
491 {
492 role: "user",
493 content: [{ Text: "Fix the bug: kwargs not passed..." }],
494 },
495 {
496 role: "assistant",
497 content: [
498 { Text: "I'll help you fix that bug." },
499 {
500 ToolUse: {
501 name: "list_directory",
502 input: { path: "fastmcp" },
503 is_input_complete: true,
504 },
505 },
506 ],
507 },
508 {
509 role: "user",
510 content: [
511 {
512 ToolResult: {
513 tool_name: "list_directory",
514 is_error: false,
515 content:
516 "fastmcp/src\nfastmcp/tests\nfastmcp/README.md\nfastmcp/pyproject.toml\nfastmcp/.gitignore\nfastmcp/setup.py\nfastmcp/examples\nfastmcp/LICENSE",
517 },
518 },
519 ],
520 },
521 {
522 role: "assistant",
523 content: [
524 { Text: "Let's examine the code." },
525 {
526 ToolUse: {
527 name: "read_file",
528 input: {
529 path: "fastmcp/main.py",
530 start_line: 253,
531 end_line: 360,
532 },
533 is_input_complete: true,
534 },
535 },
536 ],
537 },
538 {
539 role: "user",
540 content: [
541 {
542 ToolResult: {
543 tool_name: "read_file",
544 is_error: false,
545 content:
546 "def run_application(app, **kwargs):\n return anyio.run(app, **kwargs)\n\nasync def start_server():\n # More code...\n # Multiple lines of code that would be displayed\n # when clicking on the show more link\n app = create_app()\n await run_app(app)\n\ndef main():\n # Initialize everything\n anyio.run(start_server)\n # Even more code here\n # that would be shown when the user\n # expands the content",
547 },
548 },
549 ],
550 },
551 {
552 role: "assistant",
553 content: [
554 { Text: "I found the issue." },
555 {
556 ToolUse: {
557 name: "edit_file",
558 input: {
559 path: "fastmcp/core.py",
560 old_string: "def start_server(app):\n anyio.run(app)",
561 new_string: "def start_server(app, **kwargs):\n anyio.run(app, **kwargs)",
562 display_description: "Fix kwargs passing to anyio.run",
563 },
564 is_input_complete: true,
565 },
566 },
567 ],
568 },
569 {
570 role: "user",
571 content: [
572 {
573 ToolResult: {
574 tool_name: "edit_file",
575 is_error: false,
576 content: "Made edit to fastmcp/core.py",
577 },
578 },
579 ],
580 },
581 {
582 role: "assistant",
583 content: [
584 { Text: "Let's check if there are any errors." },
585 {
586 ToolUse: {
587 name: "diagnostics",
588 input: {},
589 is_input_complete: true,
590 },
591 },
592 ],
593 },
594 {
595 role: "user",
596 content: [
597 {
598 ToolResult: {
599 tool_name: "diagnostics",
600 is_error: false,
601 content: "No errors found",
602 },
603 },
604 ],
605 },
606 ],
607 };
608
609 // The actual thread data will be injected here when opened by eval
610 let threadsData = window.threadsData || { threads: [dummyThread] };
611
612 // Initialize thread variables
613 let threads = threadsData.threads;
614 let currentThreadIndex = 0;
615 let thread = threads[currentThreadIndex];
616
617 // Function to navigate to the previous thread
618 function previousThread() {
619 if (currentThreadIndex > 0) {
620 currentThreadIndex--;
621 switchToThread(currentThreadIndex);
622 }
623 }
624
625 // Function to navigate to the next thread
626 function nextThread() {
627 if (currentThreadIndex < threads.length - 1) {
628 currentThreadIndex++;
629 switchToThread(currentThreadIndex);
630 }
631 }
632
633 // Function to switch to a specific thread by index
634 function switchToThread(index) {
635 if (index >= 0 && index < threads.length) {
636 currentThreadIndex = index;
637 thread = threads[currentThreadIndex];
638 updateNavigationButtons();
639 renderThread();
640 }
641 }
642
643 // Function to update the navigation buttons state
644 function updateNavigationButtons() {
645 document.getElementById("prev-thread").disabled = currentThreadIndex <= 0;
646 document.getElementById("next-thread").disabled = currentThreadIndex >= threads.length - 1;
647 document.getElementById("current-thread-index").textContent = currentThreadIndex + 1;
648 document.getElementById("total-threads").textContent = threads.length;
649 }
650
651 function renderThread() {
652 const tbody = document.getElementById("thread-body");
653 tbody.innerHTML = ""; // Clear existing content
654
655 // Set thread name if available
656 const threadId = thread.thread_id || `Thread ${currentThreadIndex + 1}`;
657 document.getElementById("thread-id").textContent = threadId;
658
659 // Set filename in the header if available
660 const filename = thread.filename || `Thread ${currentThreadIndex + 1}`;
661 document.getElementById("current-filename").textContent = filename;
662
663 // Skip system message
664 const nonSystemMessages = thread.messages.filter((msg) => msg.role !== "system");
665
666 let turnNumber = 0;
667 processMessages(nonSystemMessages, tbody, turnNumber);
668 }
669
670 function processMessages(messages, tbody) {
671 let turnNumber = 0;
672
673 for (let i = 0; i < messages.length; i++) {
674 const msg = messages[i];
675
676 if (isUserQuery(msg)) {
677 // User message starts a new turn
678 turnNumber++;
679 renderUserMessage(msg, turnNumber, tbody);
680 } else if (msg.role === "assistant") {
681 // Each assistant message is one turn
682 turnNumber++;
683
684 // Collect all text content and tool uses for this turn
685 let assistantText = "";
686 let toolUses = [];
687
688 // First, collect all text content
689 for (const content of msg.content) {
690 if (content.hasOwnProperty("Text")) {
691 if (assistantText) {
692 assistantText += "<br><br>" + formatContent(content.Text);
693 } else {
694 assistantText = formatContent(content.Text);
695 }
696 } else if (content.hasOwnProperty("ToolUse")) {
697 toolUses.push(content.ToolUse);
698 }
699 }
700
701 // Create a single row for this turn with text and tools
702 const row = document.createElement("tr");
703 row.id = `assistant-turn-${turnNumber}`;
704
705 // Start with the turn number and assistant text
706 row.innerHTML = `
707 <td class="text-content">${turnNumber}</td>
708 <td class="text-content"><!--Assistant: <br/ -->${assistantText}</td>
709 <td id="tools-${turnNumber}"></td>
710 <td id="results-${turnNumber}"></td>
711 `;
712
713 tbody.appendChild(row);
714
715 // Add all tool calls to the tools cell
716 const toolsCell = document.getElementById(`tools-${turnNumber}`);
717 const resultsCell = document.getElementById(`results-${turnNumber}`);
718
719 // Process all tools and their results
720 for (let j = 0; j < toolUses.length; j++) {
721 const toolUse = toolUses[j];
722 const toolCall = formatToolCall(toolUse.name, toolUse.input);
723
724 // Add the tool call to the tools cell
725 if (j > 0) toolsCell.innerHTML += "<hr>";
726 toolsCell.innerHTML += toolCall;
727
728 // Look for corresponding tool result
729 if (hasMatchingToolResult(messages, i, toolUse.name)) {
730 const resultMsg = messages[i + 1];
731 const toolResult = findToolResult(resultMsg, toolUse.name);
732
733 if (toolResult) {
734 // Add the result to the results cell
735 if (j > 0) resultsCell.innerHTML += "<hr>";
736
737 // Create a container for the result
738 const resultDiv = document.createElement("div");
739 resultDiv.className = "tool-result";
740
741 // Format and display the tool result
742 formatToolResultInline(toolResult.content.Text, resultDiv);
743 resultsCell.appendChild(resultDiv);
744
745 // Skip the result message in the next iteration
746 if (j === toolUses.length - 1) {
747 i++;
748 }
749 }
750 }
751 }
752 } else if (msg.role === "user" && msg.content.some((c) => c.hasOwnProperty("ToolResult"))) {
753 // Skip tool result messages as they are handled with their corresponding tool use
754 continue;
755 }
756 }
757 }
758
759 function isUserQuery(message) {
760 return message.role === "user" && !message.content.some((c) => c.hasOwnProperty("ToolResult"));
761 }
762
763 function renderUserMessage(message, turnNumber, tbody) {
764 const row = document.createElement("tr");
765 row.innerHTML = `
766 <td>${turnNumber}</td>
767 <td class="text-content"><b>[User]:</b><br/> ${formatContent(message.content[0].Text)}</td>
768 <td></td>
769 <td></td>
770 `;
771 tbody.appendChild(row);
772 }
773
774 function hasMatchingToolResult(messages, currentIndex, toolName) {
775 return (
776 currentIndex + 1 < messages.length &&
777 messages[currentIndex + 1].role === "user" &&
778 messages[currentIndex + 1].content.some(
779 (c) => c.hasOwnProperty("ToolResult") && c.ToolResult.tool_name === toolName,
780 )
781 );
782 }
783
784 function findToolResult(resultMessage, toolName) {
785 const toolResultContent = resultMessage.content.find(
786 (c) => c.hasOwnProperty("ToolResult") && c.ToolResult.tool_name === toolName,
787 );
788
789 return toolResultContent ? toolResultContent.ToolResult : null;
790 }
791 function formatToolCall(name, input) {
792 // In compact mode, format tool calls on a single line
793 if (viewMode === "compact") {
794 const params = [];
795 const fullParams = [];
796
797 // Process all parameters
798 for (const [key, value] of Object.entries(input)) {
799 if (value !== null && value !== undefined) {
800 // Store full parameter for expanded view
801 let fullValue = typeof value === "string" ? `"${value}"` : value;
802 fullParams.push([key, fullValue]);
803
804 // Abbreviated value for compact view
805 let displayValue = fullValue;
806 if (typeof value === "string" && value.length > 30) {
807 displayValue = `"${value.substring(0, 30)}..."`;
808 }
809 params.push(`${key}=${displayValue}`);
810 }
811 }
812
813 const paramString = params.join(", ");
814 const fullLine = `<span class="tool-name">${name}</span>(${paramString})`;
815
816 // If the line is too long, add a [more] link
817 if (fullLine.length > 80 || params.length > 1) {
818 // Create a container with the compact and full views
819 const compactView = `<span class="tool-name">${name}</span>(${params[0]}, <span class="more-inline" onclick="toggleActionVisibility(this)">[...]</span>)`;
820
821 // For the full view, use the original untruncated values
822 let result = `<span class="tool-name">${name}</span>(`;
823 const formattedParams = fullParams
824 .map((p) => ` ${p[0]}=${p[1]}`)
825 .join(",<br/>");
826 const fullView = `${result}<br/>${formattedParams}<br/>)`;
827
828 return `<div class="action-container">
829 <div class="action-preview">${compactView}</div>
830 <div class="action-full hidden">${fullView}</div>
831 </div>`;
832 }
833
834 return fullLine;
835 }
836
837 // Regular (full) view formatting with multiple lines
838 let result = `<span class="tool-name">${name}</span>(`;
839 const params = [];
840 for (const [key, value] of Object.entries(input)) {
841 if (value !== null && value !== undefined) {
842 // Format different types of values
843 let formattedValue = typeof value === "string" ? `"${value}"` : value;
844 params.push([key, formattedValue]);
845 }
846 }
847
848 if (params.length === 0) {
849 return `${result})`;
850 } else if (params.length === 1) {
851 // For single parameter, just show the value without the parameter name
852 return `${result}${params[0][1]})`;
853 } else {
854 // Format parameters
855 const formattedParams = params.map((p) => ` ${p[0]}=${p[1]}`).join(",<br/>");
856 return `${result}<br/>${formattedParams}<br/>)`;
857 }
858 }
859
860 function toggleActionVisibility(element, remainingLines) {
861 const container = element.closest(".action-container");
862 const preview = container.querySelector(".action-preview");
863 const full = container.querySelector(".action-full");
864
865 // Once expanded, keep it expanded
866 full.classList.remove("hidden");
867 preview.classList.add("hidden");
868 }
869
870 function formatToolResultInline(content, targetElement) {
871 // Count lines
872 const lines = content.split("\n");
873
874 // In compact mode, show only 1 line with [more] link
875 if (viewMode === "compact" && lines.length > 1) {
876 // Create container
877 const container = document.createElement("div");
878
879 // Preview content
880 const previewDiv = document.createElement("div");
881 previewDiv.className = "preview-content";
882
883 // Add the first line of content plus [more] link
884 const previewContent = lines[0];
885 previewDiv.innerHTML =
886 escapeHtml(previewContent) +
887 ` <span class="more-inline" onclick="toggleResultVisibility(this)">[...]</span>`;
888
889 // Full content (initially hidden)
890 const contentDiv = document.createElement("pre");
891 contentDiv.className = "hidden";
892 contentDiv.innerHTML = escapeHtml(content);
893
894 container.appendChild(previewDiv);
895 container.appendChild(contentDiv);
896 targetElement.appendChild(container);
897 } else {
898 // For full view or short results, display everything
899 const preElement = document.createElement("pre");
900 preElement.textContent = content;
901 targetElement.appendChild(preElement);
902 }
903 }
904
905 function toggleResultVisibility(element, remainingLines) {
906 const container = element.parentElement.parentElement;
907 const preview = container.querySelector(".preview-content");
908 const full = container.querySelector("pre");
909
910 // Once expanded, keep it expanded
911 full.classList.remove("hidden");
912 preview.classList.add("hidden");
913 }
914
915 function formatContent(text) {
916 return escapeHtml(text);
917 }
918
919 function escapeHtml(text) {
920 const div = document.createElement("div");
921 div.textContent = text;
922 return div.innerHTML;
923 }
924
925 // Keyboard navigation handler
926 document.addEventListener("keydown", function (event) {
927 // previous thread
928 if ((event.ctrlKey && event.key === "ArrowLeft") || event.key === "h" || event.key === "k") {
929 if (!document.getElementById("prev-thread").disabled) {
930 previousThread();
931 }
932 }
933 // next thread
934 else if ((event.ctrlKey && event.key === "ArrowRight") || event.key === "j" || event.key === "l") {
935 if (!document.getElementById("next-thread").disabled) {
936 nextThread();
937 }
938 }
939 });
940
941 // Initialize the page
942 document.addEventListener("DOMContentLoaded", function () {
943 initTheme();
944 updateNavigationButtons();
945 renderThread();
946 });
947 </script>
948 </body>
949</html>