Skip to content

Instantly share code, notes, and snippets.

@dpavlin
Created January 8, 2026 07:30
Show Gist options
  • Select an option

  • Save dpavlin/28eaa39a178bca8ee27ca17d560d9458 to your computer and use it in GitHub Desktop.

Select an option

Save dpavlin/28eaa39a178bca8ee27ca17d560d9458 to your computer and use it in GitHub Desktop.
Gemini CLI chat session viewer
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gemini CLI chat session viewer</title>
<style>
:root {
--bg-color: #ffffff;
--text-color: #212529;
--border-color: #dee2e6;
--user-bg: #e7f5ff;
--model-bg: #f8f9fa;
--tool-bg: #fffde7;
--tool-border: #fdd835;
--thought-bg: #f1f3f5;
--thought-border: #adb5bd;
--header-text: #495057;
--button-bg: #f8f9fa;
--button-hover-bg: #e9ecef;
--code-bg: #f6f8fa;
--code-text: #24292e;
--quote-border: #dfe2e5;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #0d1117;
--text-color: #c9d1d9;
--border-color: #30363d;
--user-bg: #16263a;
--model-bg: #21262d;
--tool-bg: #4b462c;
--tool-border: #fdd835;
--thought-bg: #2d333b;
--thought-border: #444c56;
--header-text: #8b949e;
--button-bg: #21262d;
--button-hover-bg: #30363d;
--code-bg: #161b22;
--code-text: #c9d1d9;
--quote-border: #444c56;
}
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
margin: 0;
padding: 1.5rem;
line-height: 1.6;
}
.container { max-width: 900px; margin: 0 auto; }
h1 { border-bottom: 2px solid var(--border-color); padding-bottom: 0.5rem; margin-bottom: 1.5rem; color: var(--header-text); font-weight: 500; }
.controls {
position: fixed;
top: 10px;
right: 10px;
background: rgba(255, 255, 255, 0.95);
padding: 8px 12px;
border: 1px solid var(--border-color);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 1000;
display: flex;
flex-direction: column;
gap: 8px;
backdrop-filter: blur(5px);
font-size: 0.85rem;
max-width: 250px;
max-height: 90vh;
overflow-y: auto;
}
@media (prefers-color-scheme: dark) {
.controls { background: rgba(33, 38, 45, 0.95); }
}
/* Collapsed State */
.controls.collapsed {
width: 40px;
height: 40px;
padding: 0;
border-radius: 50%;
overflow: hidden;
justify-content: center;
align-items: center;
}
.controls.collapsed .controls-content {
display: none;
}
.controls .controls-icon {
display: none;
cursor: pointer;
width: 40px;
height: 40px;
align-items: center;
justify-content: center;
color: var(--header-text);
}
.controls.collapsed .controls-icon {
display: flex;
}
/* Removed hover expansion to rely on click */
.controls-header-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
padding-bottom: 4px;
border-bottom: 1px solid var(--border-color);
}
.controls-title { font-weight: bold; color: var(--header-text); font-size: 0.9em; }
.minimize-btn {
background: none; border: none; font-size: 1.2em; line-height: 1; cursor: pointer; color: var(--header-text); padding: 0 4px;
}
.minimize-btn:hover { background-color: var(--button-hover-bg); border-radius: 4px; }
.controls-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.controls-section-title { font-weight: bold; margin-bottom: 4px; font-size: 0.75rem; text-transform: uppercase; color: var(--header-text); border-bottom: 1px solid var(--border-color); padding-bottom: 2px; }
.control-group { display: flex; flex-direction: column; gap: 4px; }
.checkbox-label { display: flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; }
.checkbox-label input { accent-color: #007bff; cursor: pointer; }
.btn-small { padding: 4px 8px; font-size: 0.75rem; border: 1px solid var(--border-color); border-radius: 4px; cursor: pointer; background-color: var(--button-bg); color: var(--text-color); }
.btn-small:hover { background-color: var(--button-hover-bg); }
.btn-small:disabled { opacity: 0.5; cursor: not-allowed; }
.turn { margin-bottom: 1.5rem; border-radius: 8px; border: 1px solid var(--border-color); overflow: hidden; position: relative; }
.role { font-weight: bold; text-transform: uppercase; font-size: 0.8em; padding: 0.5rem 1.25rem; color: var(--header-text); border-bottom: 1px solid var(--border-color); }
.user .role { background-color: var(--user-bg); }
.model .role, .gemini .role { background-color: var(--model-bg); }
.info .role { background-color: var(--bg-color); font-style: italic; }
.content-wrapper { padding: 0.25rem 1.25rem 1.25rem; background-color: var(--model-bg); }
.user .content-wrapper { background-color: var(--user-bg); }
.info .content-wrapper { background-color: var(--bg-color); }
.close-turn-btn {
position: absolute;
top: 6px; right: 12px;
font-size: 1.5rem; font-weight: bold; color: var(--header-text); cursor: pointer;
border: none; background: none; line-height: 1; padding: 0.25rem;
}
.close-turn-btn:hover { color: var(--text-color); }
.markdown-container { position: relative; }
.button-group {
position: absolute;
top: 10px; right: 10px;
opacity: 0;
transition: opacity 0.2s ease-in-out;
display: flex;
gap: 5px;
}
.markdown-container:hover .button-group { opacity: 1; }
.button-group button { border: 1px solid var(--border-color); background-color: var(--button-bg); color: var(--text-color); padding: 2px 8px; border-radius: 6px; font-size: 0.8em; cursor: pointer; }
.button-group button:hover { background-color: var(--button-hover-bg); }
/* Compact Details Styles */
details {
border-left: 4px solid var(--tool-border);
background-color: var(--tool-bg);
border-radius: 4px;
margin: 0.5rem 0; /* Reduced margin */
}
details.thoughts { border-left-color: var(--thought-border); background-color: var(--thought-bg); }
summary {
cursor: pointer;
padding: 0.5rem 1rem; /* Reduced padding */
font-weight: bold;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
list-style: none;
outline: none;
font-size: 0.9em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
summary::-webkit-details-marker { display: none; }
summary::before { content: '▶'; margin-right: 0.6em; display: inline-block; transition: transform 0.2s; font-size: 0.8em; }
details[open] > summary::before { transform: rotate(90deg); }
details > .tool-content, details > .thought-content { margin: 0.5rem 1rem 1rem 2rem; }
.user pre { background: var(--bg-color); padding: 1em; border-radius: 5px; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word; }
/* ... Markdown Styles ... */
.markdown-body { font-size: 16px; line-height: 1.5; word-wrap: break-word; }
.markdown-body > *:first-child { margin-top: 0 !important; }
.markdown-body > *:last-child { margin-bottom: 0 !important; }
.markdown-body h1, .markdown-body h2 { border-bottom: 1px solid var(--border-color); padding-bottom: .3em; margin-top: 24px; margin-bottom: 16px; }
.markdown-body h1 { font-size: 2em; } .markdown-body h2 { font-size: 1.5em; } .markdown-body h3 { font-size: 1.25em; }
.markdown-body code { font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; font-size: 85%; padding: .2em .4em; margin: 0; background-color: var(--code-bg); border-radius: 6px; }
.markdown-body pre { font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; font-size: 85%; line-height: 1.45; background-color: var(--code-bg); border-radius: 6px; padding: 16px; overflow: auto; }
.markdown-body pre code { background: none; padding: 0; }
.markdown-body ul, .markdown-body ol { padding-left: 2em; }
.markdown-body blockquote { padding: 0 1em; color: var(--header-text); border-left: .25em solid var(--quote-border); }
.markdown-body table { display: block; width: 100%; overflow: auto; border-spacing: 0; border-collapse: collapse; }
.markdown-body tr { background-color: var(--bg-color); border-top: 1px solid var(--border-color); }
.markdown-body th, .markdown-body td { padding: 6px 13px; border: 1px solid var(--border-color); }
.error { color: #d32f2f; font-weight: bold; padding: 1rem; border: 1px solid #d32f2f; background-color: #ffcdd2; border-radius: 5px; }
.thought-item { margin-bottom: 1rem; border-bottom: 1px solid var(--thought-border); padding-bottom: 0.5rem; }
.thought-item:last-child { border-bottom: none; }
.thought-subject { font-weight: bold; margin-bottom: 0.25rem; }
.thought-description { font-size: 0.9em; }
/* Tool Specific Styles */
.tool-header { display: flex; align-items: center; justify-content: space-between; font-family: monospace; font-size: 0.9em; }
.tool-name { font-weight: bold; color: var(--header-text); }
.terminal-block { background-color: #1e1e1e; color: #d4d4d4; padding: 10px; border-radius: 4px; font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; overflow-x: auto; margin-top: 5px; }
.terminal-command { color: #4ec9b0; margin-bottom: 5px; border-bottom: 1px solid #333; padding-bottom: 5px; }
.terminal-command::before { content: '$ '; color: #6a9955; }
.file-block { border: 1px solid var(--border-color); border-radius: 4px; margin-top: 5px; overflow: hidden; }
.file-header { background-color: var(--button-bg); padding: 5px 10px; font-size: 0.85em; font-weight: bold; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; }
.file-content { padding: 10px; background-color: var(--code-bg); overflow-x: auto; margin: 0; font-family: monospace; font-size: 0.85em; }
.diff-container { display: flex; flex-direction: column; gap: 5px; margin-top: 5px; }
.diff-block { border: 1px solid var(--border-color); border-radius: 4px; overflow: hidden; }
.diff-header { padding: 4px 8px; font-size: 0.8em; font-weight: bold; color: #fff; }
.diff-old .diff-header { background-color: #d73a49; }
.diff-new .diff-header { background-color: #28a745; }
.diff-content { padding: 8px; background-color: var(--code-bg); overflow-x: auto; margin: 0; font-family: monospace; font-size: 0.85em; white-space: pre-wrap; }
.instruction-text { font-style: italic; color: var(--header-text); margin: 5px 0; padding: 5px; background: var(--bg-color); border-left: 3px solid var(--tool-border); }
/* Turn Header Controls */
.turn-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--border-color);
padding: 0.5rem 1.25rem;
background-color: inherit; /* Inherit from .turn subclass */
}
.role-name { font-weight: bold; text-transform: uppercase; font-size: 0.8em; color: var(--header-text); }
.turn-controls { display: flex; align-items: center; gap: 8px; }
.json-btn {
background: none; border: 1px solid var(--border-color); border-radius: 4px;
padding: 2px 6px; font-size: 0.7em; cursor: pointer; color: var(--header-text);
opacity: 0.7; transition: opacity 0.2s;
}
.json-btn:hover { opacity: 1; background-color: rgba(0,0,0,0.05); }
.warning-icon { cursor: help; font-size: 1.2em; line-height: 1; }
.json-view {
display: none;
background-color: var(--code-bg);
padding: 10px;
margin: 0;
border-bottom: 1px solid var(--border-color);
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
font-size: 0.8em;
overflow-x: auto;
white-space: pre-wrap;
color: var(--code-text);
}
.token-info { font-size: 0.75em; color: var(--header-text); margin-left: auto; margin-right: 1rem; opacity: 0.8; font-family: monospace; }
.warning-block {
background-color: #fff3cd;
border-bottom: 1px solid #ffeeba;
color: #856404;
padding: 0.5rem 1.25rem;
font-size: 0.85em;
}
@media (prefers-color-scheme: dark) {
.warning-block {
background-color: #3e3315;
border-bottom: 1px solid #5e4d20;
color: #e8d69b;
}
}
body.hide-warnings .warning-block { display: none; }
</style>
</head>
<body>
<div class="container">
<h1>Gemini CLI chat session viewer</h1>
<div class="controls" id="mainControls">
<div class="controls-icon" id="expandBtn">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
</div>
<div class="controls-content">
<div class="controls-header-bar">
<span class="controls-title">Settings</span>
<button id="minimizeBtn" class="minimize-btn" title="Minimize">−</button>
</div>
<div class="controls-row">
<input type="file" id="fileInput" accept=".json" style="width: 180px;">
<button id="reloadBtn" class="btn-small" disabled>Reload</button>
</div>
<div class="controls-section-title">Expand / Collapse</div>
<div class="control-group">
<label class="checkbox-label"><input type="checkbox" id="expandThoughts"> Thoughts</label>
<label class="checkbox-label"><input type="checkbox" id="expandToolCalls"> Tool Calls</label>
<label class="checkbox-label"><input type="checkbox" id="showWarnings" checked> Warnings</label>
</div>
<div class="controls-section-title">Expand by Tool Type</div> <div class="control-group" id="toolTypeFilters">
<!-- Checkboxes will be populated dynamically -->
<div style="font-style: italic; font-size: 0.8em; color: gray;">Load a file...</div>
</div>
</div>
</div>
<div id="output">
<p>Please select a JSON chat log file to begin.</p>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@2.4.1/dist/purify.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('fileInput');
const outputDiv = document.getElementById('output');
const reloadBtn = document.getElementById('reloadBtn');
const mainControls = document.getElementById('mainControls');
const minimizeBtn = document.getElementById('minimizeBtn');
const expandBtn = document.getElementById('expandBtn');
let userManuallyExpanded = false;
// Scroll handling for controls collapse
window.addEventListener('scroll', () => {
if (userManuallyExpanded) return; // Don't auto-hide if user specifically opened it while scrolled
if (window.scrollY > 50) {
mainControls.classList.add('collapsed');
} else {
mainControls.classList.remove('collapsed');
// Reset manual flag when back at top so it auto-hides again on next scroll
userManuallyExpanded = false;
}
}, { passive: true });
// Manual Minimize
minimizeBtn.addEventListener('click', () => {
mainControls.classList.add('collapsed');
userManuallyExpanded = false;
});
// Manual Expand
expandBtn.addEventListener('click', () => {
mainControls.classList.remove('collapsed');
// If we are scrolled down, mark as manually expanded so it stays open
if (window.scrollY > 50) {
userManuallyExpanded = true;
}
});
// Initial check for scroll position
if (window.scrollY > 50) {
mainControls.classList.add('collapsed');
}
// Toggle Controls
const expandThoughtsCb = document.getElementById('expandThoughts');
const expandToolCallsCb = document.getElementById('expandToolCalls');
const showWarningsCb = document.getElementById('showWarnings');
const toolTypeFiltersDiv = document.getElementById('toolTypeFilters');
let currentFile = null;
let currentChatData = null;
// State for expansion (Default: everything False/Collapsed)
let state = {
thoughts: false,
toolCalls: false,
expandedToolTypes: new Set()
};
// Load persisted data on init
function init() {
// ... (existing init logic) ...
const savedData = localStorage.getItem('gemini_last_session');
// ...
}
// (Keeping init logic implicitly via ... but replacing the listener block below)
// Listeners
expandThoughtsCb.addEventListener('change', (e) => {
state.thoughts = e.target.checked;
applyExpansion();
});
expandToolCallsCb.addEventListener('change', (e) => {
state.toolCalls = e.target.checked;
applyExpansion();
});
showWarningsCb.addEventListener('change', (e) => {
if (e.target.checked) {
document.body.classList.remove('hide-warnings');
} else {
document.body.classList.add('hide-warnings');
}
});
function applyExpansion() {
// Thoughts
document.querySelectorAll('details.thoughts').forEach(el => {
el.open = state.thoughts;
});
// Tool Entries (Calls & Responses)
document.querySelectorAll('.tool-entry').forEach(el => {
const type = el.dataset.toolType;
// Logic: Expand if Master is True OR Specific Type is True
const shouldExpand = state.toolCalls || state.expandedToolTypes.has(type);
if (shouldExpand) {
el.open = true;
} else {
el.open = false;
}
});
}
outputDiv.addEventListener('click', async function(event) {
const btn = event.target;
if (btn.classList.contains('close-turn-btn')) {
btn.closest('.turn').style.display = 'none';
} else if (btn.classList.contains('copy-btn')) {
const originalText = btn.textContent;
let success = false;
try {
if (btn.classList.contains('copy-markdown-btn')) {
const textToCopy = btn.closest('.markdown-container').dataset.rawMarkdown;
await navigator.clipboard.writeText(textToCopy);
success = true;
} else if (btn.classList.contains('copy-html-btn')) {
const body = btn.closest('.markdown-container').querySelector('.markdown-body');
success = copyHtmlToClipboard(body.innerHTML);
}
} catch (err) { console.error('Copy failed:', err); }
btn.textContent = success ? 'Copied!' : 'Failed!';
setTimeout(() => { btn.textContent = originalText; }, 2000);
}
});
function copyHtmlToClipboard(html) {
const listener = (e) => {
e.clipboardData.setData('text/html', html);
e.clipboardData.setData('text/plain', html);
e.preventDefault();
};
document.addEventListener('copy', listener);
document.execCommand('copy');
document.removeEventListener('copy', listener);
return true;
}
function handleFileSelect(event) {
const file = event.target.files[0];
if (!file) return;
currentFile = file;
loadFile(file);
reloadBtn.disabled = false;
}
function loadFile(file) {
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const rawJson = e.target.result;
let chatData = JSON.parse(rawJson);
// Handle new session format with 'messages' array
if (!Array.isArray(chatData) && chatData.messages && Array.isArray(chatData.messages)) {
chatData = chatData.messages.map(m => {
// Preserve all original fields, add normalized ones for renderer
return {
...m,
role: m.type === 'gemini' ? 'model' : m.type,
// Ensure these exist if not present, but prefer original if compatible
content: m.content,
thoughts: m.thoughts,
toolCalls: m.toolCalls,
timestamp: m.timestamp,
parts: m.parts
};
});
}
if (!Array.isArray(chatData)) { throw new Error("Invalid format."); }
currentChatData = chatData;
renderChat();
} catch (err) {
displayError(`Failed to parse JSON file. <br><br>Error: ${err.message}`);
}
};
reader.onerror = () => displayError("Error reading the file.");
reader.readAsText(file);
}
function renderChat() {
outputDiv.innerHTML = '';
const toolTypes = new Set();
currentChatData.forEach((turn) => {
if (turn.toolCalls) turn.toolCalls.forEach(tc => toolTypes.add(tc.name));
if (turn.parts) {
turn.parts.forEach(p => {
if (p.functionCall) toolTypes.add(p.functionCall.name);
});
}
});
// Populate Tool Filters
toolTypeFiltersDiv.innerHTML = '';
state.expandedToolTypes.clear(); // Reset specifics
if (toolTypes.size === 0) {
toolTypeFiltersDiv.innerHTML = '<div style="font-style: italic; font-size: 0.8em; color: gray;">No tools used</div>';
} else {
const toolNameMap = {
'run_shell_command': 'Shell',
'read_file': 'Read File',
'write_file': 'Write File',
'replace': 'Edit File',
'delegate_to_agent': 'Delegate'
};
const sortedTypes = Array.from(toolTypes).sort();
sortedTypes.forEach(type => {
const label = document.createElement('label');
label.className = 'checkbox-label';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = false; // Default: Collapsed
checkbox.dataset.toolType = type;
checkbox.addEventListener('change', (e) => {
if (e.target.checked) {
state.expandedToolTypes.add(type);
} else {
state.expandedToolTypes.delete(type);
}
applyExpansion();
});
const displayName = toolNameMap[type] || type;
label.appendChild(checkbox);
label.appendChild(document.createTextNode(displayName));
toolTypeFiltersDiv.appendChild(label);
});
}
currentChatData.forEach((turn, index) => renderTurn(turn, index));
// Apply initial state (Collapsed)
applyExpansion();
}
function renderTurn(turn, index) {
const turnDiv = document.createElement('div');
const role = (turn.role || turn.type || 'unknown').toLowerCase();
turnDiv.className = `turn ${role}`;
// --- Header Construction ---
const headerDiv = document.createElement('div');
headerDiv.className = 'turn-header';
// Role Label
const roleSpan = document.createElement('span');
roleSpan.className = 'role-name';
roleSpan.textContent = role;
headerDiv.appendChild(roleSpan);
// Token & Model Info
if (turn.model || turn.tokens) {
const tokenInfo = document.createElement('span');
tokenInfo.className = 'token-info';
let infoParts = [];
if (turn.model) infoParts.push(turn.model);
if (turn.tokens) {
if (turn.tokens.input) infoParts.push(`In: ${turn.tokens.input}`);
if (turn.tokens.output) infoParts.push(`Out: ${turn.tokens.output}`);
if (turn.tokens.total) infoParts.push(`Total: ${turn.tokens.total}`);
}
tokenInfo.textContent = infoParts.join(' | ');
headerDiv.appendChild(tokenInfo);
} else {
// Push controls to right if no token info
roleSpan.style.marginRight = 'auto';
}
// Controls Container
const controlsDiv = document.createElement('div');
controlsDiv.className = 'turn-controls';
// JSON Button
const jsonBtn = document.createElement('button');
jsonBtn.className = 'json-btn';
jsonBtn.textContent = '{}';
jsonBtn.title = 'Show original JSON';
jsonBtn.addEventListener('click', () => {
const jsonView = turnDiv.querySelector('.json-view');
jsonView.style.display = jsonView.style.display === 'none' ? 'block' : 'none';
});
controlsDiv.appendChild(jsonBtn);
// Close Button
const closeBtn = document.createElement('button');
closeBtn.className = 'close-turn-btn';
closeBtn.innerHTML = '&times;';
closeBtn.style.position = 'static';
closeBtn.addEventListener('click', () => turnDiv.style.display = 'none');
controlsDiv.appendChild(closeBtn);
headerDiv.appendChild(controlsDiv);
turnDiv.appendChild(headerDiv);
// --- JSON View (Hidden) ---
const jsonPre = document.createElement('pre');
jsonPre.className = 'json-view';
jsonPre.textContent = JSON.stringify(turn, null, 2);
turnDiv.appendChild(jsonPre);
// --- Unparsed Fields Warning ---
// Known/Rendered keys
const renderedKeys = new Set(['role', 'type', 'content', 'thoughts', 'toolCalls', 'parts', 'timestamp', 'id', 'tokens', 'model']);
const unparsedKeys = Object.keys(turn).filter(k => !renderedKeys.has(k));
if (unparsedKeys.length > 0) {
const warningDiv = document.createElement('div');
warningDiv.className = 'warning-block';
warningDiv.innerHTML = `<strong>⚠️ Unparsed Fields:</strong> ${escapeHtml(unparsedKeys.join(', '))}`;
turnDiv.appendChild(warningDiv);
}
const contentWrapper = document.createElement('div');
contentWrapper.className = 'content-wrapper';
// Thoughts
if (turn.thoughts && turn.thoughts.length > 0) {
turnDiv.classList.add('has-thoughts');
const thoughtsDetails = document.createElement('details');
thoughtsDetails.className = 'thoughts';
// Note: 'open' attribute NOT set initially
const summary = document.createElement('summary');
summary.textContent = 'THOUGHTS';
thoughtsDetails.appendChild(summary);
const thoughtContent = document.createElement('div');
thoughtContent.className = 'thought-content';
turn.thoughts.forEach(thought => {
const item = document.createElement('div');
item.className = 'thought-item';
item.innerHTML = `<div class="thought-subject">${escapeHtml(thought.subject)}</div>
<div class="thought-description">${escapeHtml(thought.description)}</div>`;
thoughtContent.appendChild(item);
});
thoughtsDetails.appendChild(thoughtContent);
contentWrapper.appendChild(thoughtsDetails);
}
// Content
const text = turn.content || (turn.parts && turn.parts.find(p => p.text)?.text);
if (text) {
contentWrapper.innerHTML += getTextHtml(text, role);
}
// Tool Calls
if (turn.toolCalls && turn.toolCalls.length > 0) {
turn.toolCalls.forEach(call => {
contentWrapper.innerHTML += getToolCallHtml(call);
});
}
// Legacy
if (turn.parts && turn.parts.length > 0) {
turn.parts.forEach(part => {
if (part.functionCall) {
const call = {
name: part.functionCall.name,
args: part.functionCall.args,
result: []
};
contentWrapper.innerHTML += getToolCallHtml(call);
}
if (part.functionResponse) {
const response = part.functionResponse;
const output = JSON.stringify(response.response, null, 2);
const prettyOutput = `<pre><code>${escapeHtml(output)}</code></pre>`;
contentWrapper.innerHTML += `<details class="tool-response-details tool-entry" data-tool-type="${escapeHtml(response.name)}"><summary>TOOL RESPONSE for ${escapeHtml(response.name)}</summary><div class="tool-content">${prettyOutput}</div></details>`;
}
});
}
turnDiv.appendChild(contentWrapper);
outputDiv.appendChild(turnDiv);
}
function getToolCallHtml(call) {
const name = call.name;
const displayName = call.displayName || name;
const args = call.args;
let summaryText = `TOOL CALL: ${escapeHtml(displayName)}`;
let contentHtml = '';
if (name === 'run_shell_command') {
summaryText = `SHELL: ${escapeHtml(args.command)}`;
} else if (name === 'read_file') {
summaryText = `READ FILE: ${escapeHtml(args.file_path)}`;
} else if (name === 'write_file') {
summaryText = `WRITE FILE: ${escapeHtml(args.file_path)}`;
contentHtml += `<div class="file-block">
<div class="file-header">Writing to: ${escapeHtml(args.file_path)}</div>
<pre class="file-content"><code>${escapeHtml(args.content)}</code></pre>
</div>`;
} else if (name === 'replace') {
summaryText = `EDIT FILE: ${escapeHtml(args.file_path)}`;
contentHtml += `<div class="instruction-text">Instruction: ${escapeHtml(args.instruction)}</div>`;
contentHtml += `<div class="diff-container">
<div class="diff-block diff-old">
<div class="diff-header">Original (Old String)</div>
<pre class="diff-content"><code>${escapeHtml(args.old_string)}</code></pre>
</div>
<div class="diff-block diff-new">
<div class="diff-header">Replacement (New String)</div>
<pre class="diff-content"><code>${escapeHtml(args.new_string)}</code></pre>
</div>
</div>`;
} else {
contentHtml += `<pre><code>${escapeHtml(JSON.stringify(args, null, 2))}</code></pre>`;
}
// Note: removed 'open' attribute
const callHtml = `<details class="tool-call-details tool-entry" data-tool-type="${escapeHtml(name)}"><summary>${summaryText}</summary><div class="tool-content">${contentHtml}</div></details>`;
let responseHtml = '';
if (call.resultDisplay) {
responseHtml += `<div class="terminal-block">
<div class="terminal-command">Result Display</div>
<pre>${escapeHtml(call.resultDisplay)}</pre>
</div>`;
} else if (call.result && Array.isArray(call.result)) {
call.result.forEach(res => {
if (res.functionResponse) {
const output = res.functionResponse.response.output;
if (name === 'run_shell_command') {
responseHtml += `<div class="terminal-block">
<div class="terminal-command">${escapeHtml(args.command)}</div>
<pre>${escapeHtml(output)}</pre>
</div>`;
} else if (name === 'read_file') {
responseHtml += `<div class="file-block">
<div class="file-header">Content of ${escapeHtml(args.file_path)}</div>
<pre class="file-content"><code>${escapeHtml(output)}</code></pre>
</div>`;
} else {
const prettyOutput = (typeof output === 'string') ? output : JSON.stringify(res.functionResponse.response, null, 2);
// Note: removed 'open' attribute
responseHtml += `<details class="tool-response-details tool-entry" data-tool-type="${escapeHtml(name)}"><summary>Response</summary><div class="tool-content"><pre><code>${escapeHtml(prettyOutput)}</code></pre></div></details>`;
}
}
});
}
if (responseHtml) {
if (responseHtml.startsWith('<details')) {
return callHtml + responseHtml;
} else {
// Embedded response
return `<details class="tool-call-details tool-entry" data-tool-type="${escapeHtml(name)}">
<summary>${summaryText}</summary>
<div class="tool-content">
${contentHtml}
<div class="tool-response-details tool-entry" data-tool-type="${escapeHtml(name)}">
${responseHtml}
</div>
</div>
</details>`;
}
}
return callHtml;
}
function getTextHtml(text, role) {
if (role === 'model' || role === 'gemini') {
const rawHtml = marked.parse(text);
return `<div class="markdown-container" data-raw-markdown="${escapeHtml(text)}">
<div class="button-group">
<button class="copy-btn copy-markdown-btn" title="Copy raw Markdown">Copy MD</button>
<button class="copy-btn copy-html-btn" title="Copy rendered HTML">Copy HTML</button>
</div>
<div class="markdown-body">${DOMPurify.sanitize(rawHtml)}</div>
</div>`;
} else {
return `<pre><code>${escapeHtml(text)}</code></pre>`;
}
}
function displayError(message) {
outputDiv.innerHTML = `<div class="error">${message}</div>`;
}
function escapeHtml(unsafe) {
if (typeof unsafe !== 'string') return '';
return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
}
fileInput.addEventListener('change', handleFileSelect);
reloadBtn.addEventListener('click', () => loadFile(currentFile));
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment