Size: 45.4 KB Modified: 5/07/2026 11:50 PM
@page "/testing"
@{
    ViewData["Title"] = "Testing - KaiChat";
}

<div class="page-header">
    <a href="/" class="back-link">&larr; Back to Chat</a>
    <h1>Testing</h1>
</div>

<div class="split-page testing-page">
    <nav class="split-nav" aria-label="Testing categories">
        <button type="button" class="active" data-testing-tab="autonomy">Autonomy</button>
        <button type="button" data-testing-tab="personas">Personas</button>
        <button type="button" data-testing-tab="compaction">Compaction</button>
    </nav>

    <div class="split-content">
        <section class="testing-panel active" data-testing-panel="autonomy">
            <section class="timer-panel autonomy-hero">
                <div class="timer-header">
                    <div>
                        <h1>Kai Autonomy</h1>
                        <p class="timer-subtitle">Step runner and state inspection</p>
                    </div>
                    <span id="autonomyStatus" class="timer-status">Stopped</span>
                </div>

                <div class="autonomy-stats">
                    <div>
                        <span>Next</span>
                        <strong id="autonomyNext">-</strong>
                    </div>
                    <div>
                        <span>Last</span>
                        <strong id="autonomyLast">-</strong>
                    </div>
                    <div>
                        <span>Today</span>
                        <strong id="autonomyCostToday">$0.000000</strong>
                    </div>
                    <div>
                        <span>Total</span>
                        <strong id="autonomyCostTotal">$0.000000</strong>
                    </div>
                </div>

                <div class="timer-actions">
                    <button class="btn btn-primary" onclick="forceAutonomyStep()">Run Step</button>
                    <button class="btn" onclick="rebuildAutonomyWorldCopy()">Rebuild World Copy</button>
                    <button class="btn" onclick="loadAutonomy()">Refresh</button>
                </div>

                <div id="autonomyMessage" class="timer-message"></div>
            </section>

            <div class="autonomy-grid testing-grid">
                <section class="timer-panel">
                    <div class="timer-section-header">
                        <h2>Time Advance</h2>
                    </div>
                    <div class="autonomy-settings">
                        <label>
                            <span>Advance minutes</span>
                            <input id="autonomyFakeAdvance" class="form-input" type="number" step="1" value="15">
                        </label>
                        <label>
                            <span>Optional note</span>
                            <input id="autonomyReason" class="form-input" placeholder="Optional">
                        </label>
                        <div class="autonomy-meta">
                            <span>Effective now</span>
                            <strong id="autonomyFakeNow">-</strong>
                        </div>
                        <div class="autonomy-meta">
                            <span>World copy</span>
                            <strong id="autonomySandboxState">-</strong>
                        </div>
                        <div class="autonomy-meta">
                            <span>Conversation</span>
                            <strong id="autonomyConversation">-</strong>
                        </div>
                    </div>
                </section>

                <section class="timer-panel autonomy-wide" id="autonomyCurrentRunPanel" style="display:none">
                    <div class="timer-section-header">
                        <h2>Current Step</h2>
                    </div>
                    <div id="autonomyCurrentRun" class="autonomy-current-run"></div>
                </section>

                <section class="timer-panel autonomy-wide">
                    <div class="timer-section-header">
                        <h2>Intentions</h2>
                    </div>
                    <div id="autonomyIntentions" class="timer-list"></div>
                </section>

                <section class="timer-panel autonomy-wide">
                    <div class="timer-section-header">
                        <h2>Guidance</h2>
                        <div class="timer-actions">
                            <button class="btn" id="autonomyReflectButton" onclick="reflectAutonomyGuidance()">Reflect Now</button>
                            <button class="btn btn-primary" id="autonomyApproveGuidanceButton" onclick="approveAutonomyGuidance()">Approve</button>
                            <button class="btn" id="autonomyRejectGuidanceButton" onclick="rejectAutonomyGuidance()">Reject</button>
                        </div>
                    </div>
                    <div class="autonomy-guidance-grid">
                        <div>
                            <h3>Current</h3>
                            <pre id="autonomyGuidance" class="autonomy-world"></pre>
                        </div>
                        <div>
                            <h3>Pending</h3>
                            <div id="autonomyPendingGuidance" class="autonomy-pending-guidance"></div>
                        </div>
                        <div class="autonomy-wide">
                            <h3>Reflection Log</h3>
                            <pre id="autonomyReflectionLog" class="autonomy-world"></pre>
                        </div>
                    </div>
                </section>

                <section class="timer-panel autonomy-wide">
                    <div class="timer-section-header">
                        <h2>Kai World State</h2>
                    </div>
                    <pre id="autonomyWorld" class="autonomy-world"></pre>
                </section>

                <section class="timer-panel">
                    <div class="timer-section-header">
                        <h2>Continuity Files</h2>
                    </div>
                    <div id="autonomyFiles" class="timer-list"></div>
                </section>

                <section class="timer-panel autonomy-wide">
                    <div class="timer-section-header">
                        <h2>Activity Log</h2>
                    </div>
                    <pre id="autonomyActivityLog" class="autonomy-world"></pre>
                </section>

                <section class="timer-panel autonomy-wide">
                    <div class="timer-section-header">
                        <h2>Recent Steps</h2>
                    </div>
                    <div id="autonomyLogs" class="timer-list"></div>
                </section>
            </div>
        </section>

        <section class="testing-panel" data-testing-panel="personas">
            <section class="timer-panel autonomy-hero">
                <div class="timer-section-header">
                    <h2>Persona Autonomy Dashboard</h2>
                    <p class="timer-subtitle">Autonomy status discovered from each persona's state file.</p>
                </div>

                <div class="autonomy-stats">
                    <div>
                        <span>Visible personas</span>
                        <strong id="autonomyPersonaCount">-</strong>
                    </div>
                    <div>
                        <span>Enabled</span>
                        <strong id="autonomyPersonaEnabledCount">-</strong>
                    </div>
                    <div>
                        <span>Production</span>
                        <strong id="autonomyPersonaProductionCount">-</strong>
                    </div>
                    <div>
                        <span>Updated</span>
                        <strong id="autonomyPersonaUpdatedAt">-</strong>
                    </div>
                </div>

                <div class="timer-settings-row">
                    <label class="timer-check">
                        <input id="autonomyPersonasIncludeDisabled" type="checkbox">
                        <span>Include disabled personas</span>
                    </label>
                    <div class="timer-actions">
                        <button class="btn btn-primary" onclick="loadAutonomyPersonas()">Refresh</button>
                    </div>
                </div>

                <div id="autonomyPersonasMessage" class="timer-message"></div>
            </section>

            <section class="timer-panel autonomy-wide">
                <div id="autonomyPersonas" class="autonomy-persona-grid"></div>
            </section>
        </section>

        <section class="testing-panel" data-testing-panel="compaction">
            <section class="compaction-panel">
                <div class="compaction-controls">
                    <div class="form-group compaction-conversation-field">
                        <label for="compactionConversation">Conversation</label>
                        <select id="compactionConversation" class="form-input"></select>
                    </div>

                    <label class="checkbox-label compaction-checkbox">
                        <input type="checkbox" id="compactionForce" checked />
                        <span>Force plan</span>
                    </label>

                    <div class="compaction-actions">
                        <button type="button" class="btn" id="compactionRefreshBtn">Refresh</button>
                        <button type="button" class="btn btn-primary" id="compactionPreviewBtn">Preview</button>
                        <button type="button" class="btn btn-primary" id="compactionDryRunBtn">Dry Run</button>
                        <button type="button" class="btn btn-primary" id="compactionSummaryBtn">Dry Run + Summary</button>
                    </div>
                </div>

                <div class="compaction-status" id="compactionStatus">Loading conversations...</div>
            </section>

            <section class="compaction-results" id="compactionResults" hidden>
                <div class="compaction-summary-row">
                    <div class="compaction-stat">
                        <span class="compaction-stat-label">Action</span>
                        <strong id="compactionAction">-</strong>
                    </div>
                    <div class="compaction-stat">
                        <span class="compaction-stat-label">API Tokens</span>
                        <strong id="compactionApiTokens">-</strong>
                    </div>
                    <div class="compaction-stat">
                        <span class="compaction-stat-label">After</span>
                        <strong id="compactionAfterTokens">-</strong>
                    </div>
                    <div class="compaction-stat">
                        <span class="compaction-stat-label">Chunks</span>
                        <strong id="compactionChunks">-</strong>
                    </div>
                </div>

                <div class="compaction-detail-grid">
                    <div>
                        <h2>Plan</h2>
                        <dl class="compaction-details">
                            <dt>Reason</dt>
                            <dd id="compactionReason">-</dd>
                            <dt>Messages</dt>
                            <dd id="compactionMessages">-</dd>
                            <dt>Trigger</dt>
                            <dd id="compactionTrigger">-</dd>
                            <dt>Limits</dt>
                            <dd id="compactionLimits">-</dd>
                            <dt>Archive</dt>
                            <dd id="compactionArchive">-</dd>
                        </dl>
                    </div>

                    <div>
                        <h2>Summary Preview</h2>
                        <pre class="compaction-summary-preview" id="compactionSummaryPreview">(none)</pre>
                    </div>
                </div>

                <h2>Chunks</h2>
                <div class="compaction-table-wrap">
                    <table class="compaction-table">
                        <thead>
                            <tr>
                                <th>#</th>
                                <th>Units</th>
                                <th>Tokens</th>
                                <th>First Message</th>
                                <th>Last Message</th>
                            </tr>
                        </thead>
                        <tbody id="compactionChunkRows"></tbody>
                    </table>
                </div>

                <details class="compaction-json">
                    <summary>Raw Result</summary>
                    <pre id="compactionRawJson"></pre>
                </details>
            </section>
        </section>
    </div>
</div>

@section Scripts {
    <script>
        let autonomyStatus = null;
        let autonomyPersonas = [];
        let autonomyPersonasRefreshTimer = null;

        function activateTestingPanel(name) {
            const hasTab = Array.from(document.querySelectorAll('[data-testing-tab]'))
                .some(function (button) { return button.dataset.testingTab === name; });
            if (!hasTab) {
                name = 'autonomy';
            }
            document.querySelectorAll('[data-testing-tab]').forEach(function (button) {
                button.classList.toggle('active', button.dataset.testingTab === name);
            });
            document.querySelectorAll('[data-testing-panel]').forEach(function (panel) {
                panel.classList.toggle('active', panel.dataset.testingPanel === name);
            });
            if (location.hash !== '#' + name) history.replaceState(null, '', '#' + name);
            if (name === 'personas') {
                loadAutonomyPersonas();
                if (!autonomyPersonasRefreshTimer) {
                    autonomyPersonasRefreshTimer = setInterval(function () {
                        const active = document.querySelector('.testing-panel[data-testing-panel="personas"]')
                            .classList.contains('active');
                        if (active) loadAutonomyPersonas();
                    }, 10000);
                }
            }
            else if (autonomyPersonasRefreshTimer) {
                clearInterval(autonomyPersonasRefreshTimer);
                autonomyPersonasRefreshTimer = null;
            }
        }

        function setAutonomyMessage(message, isError) {
            const el = document.getElementById('autonomyMessage');
            el.textContent = message || '';
            el.classList.toggle('error', !!isError);
        }

        function setAutonomyPersonasMessage(message, isError) {
            const el = document.getElementById('autonomyPersonasMessage');
            if (!el) return;
            el.textContent = message || '';
            el.classList.toggle('error', !!isError);
        }

        function formatAutonomyTime(value) {
            if (!value) return '-';
            const date = new Date(value);
            return Number.isNaN(date.getTime()) ? '-' : date.toLocaleString();
        }

        function formatUsd(value) {
            const num = Number(value || 0);
            return '$' + num.toFixed(6);
        }

        function testingEscapeHtml(value) {
            return String(value ?? '')
                .replace(/&/g, '&amp;')
                .replace(/</g, '&lt;')
                .replace(/>/g, '&gt;')
                .replace(/"/g, '&quot;')
                .replace(/'/g, '&#039;');
        }

        async function autonomyPost(path, body) {
            const response = await fetch(path, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(body || {})
            });
            const data = await response.json().catch(() => null);
            if (!response.ok) {
                throw new Error((data && data.message) || 'Autonomy request failed.');
            }
            return data;
        }

        async function loadAutonomy() {
            try {
                const response = await fetch('/api/autonomy/status');
                const data = await response.json();
                if (!response.ok) throw new Error('Could not load autonomy status.');
                renderAutonomy(data);
            } catch (error) {
                setAutonomyMessage(error.message || String(error), true);
            }
        }

        async function loadAutonomyPersonas() {
            const includeDisabled = document.getElementById('autonomyPersonasIncludeDisabled');
            try {
                const response = await fetch('/api/autonomy/personas?includeDisabled=' + encodeURIComponent((includeDisabled && includeDisabled.checked) ? 'true' : 'false'));
                const data = await response.json();
                if (!response.ok) throw new Error('Could not load persona autonomy status.');
                renderAutonomyPersonas(Array.isArray(data) ? data : []);
            } catch (error) {
                setAutonomyPersonasMessage(error.message || String(error), true);
            }
        }

        function renderAutonomyPersonas(personas) {
            autonomyPersonas = personas || [];
            const list = document.getElementById('autonomyPersonas');
            const enabledCount = autonomyPersonas.filter(item => item.enabled).length;
            const productionCount = autonomyPersonas.filter(item => item.productionMode).length;
            const newest = autonomyPersonas
                .map(item => item.updatedAtUtc)
                .filter(Boolean)
                .map(value => new Date(value).getTime())
                .filter(value => !Number.isNaN(value))
                .sort((a, b) => b - a)[0];

            document.getElementById('autonomyPersonaCount').textContent = String(autonomyPersonas.length);
            document.getElementById('autonomyPersonaEnabledCount').textContent = String(enabledCount);
            document.getElementById('autonomyPersonaProductionCount').textContent = String(productionCount);
            document.getElementById('autonomyPersonaUpdatedAt').textContent = newest ? formatAutonomyTime(new Date(newest).toISOString()) : '-';

            if (!autonomyPersonas.length) {
                list.innerHTML = '<div class="timer-empty">No autonomy states found.</div>';
                return;
            }

            list.innerHTML = autonomyPersonas
                .map(persona => {
                    const statusLabel = persona.enabled ? 'Enabled' : 'Disabled';
                    const statusClass = persona.enabled ? 'enabled' : 'disabled';
                    const productionLabel = persona.productionMode ? 'Production' : 'Testing';
                    const productionClass = persona.productionMode ? 'enabled' : 'testing';
                    const nextDue = formatAutonomyTime(persona.nextDueUtc);
                    const fakeNow = formatAutonomyTime(persona.fakeNowUtc);
                    const updated = formatAutonomyTime(persona.updatedAtUtc);
                    return '<article class="autonomy-persona-card ' + statusClass + '">' +
                        '<div class="autonomy-persona-header">' +
                            '<span class="autonomy-persona-name">' + testingEscapeHtml(persona.name || 'Unknown') + '</span>' +
                            '<span class="timer-pill ' + testingEscapeHtml(statusClass) + '">' + testingEscapeHtml(statusLabel) + '</span>' +
                        '</div>' +
                        '<div class="autonomy-persona-meta">' +
                            '<span><strong>Key:</strong> ' + testingEscapeHtml(persona.personaKey || '-') + '</span>' +
                            '<span><strong>Path:</strong> ' + testingEscapeHtml(persona.statePath || '-') + '</span>' +
                            '<span><strong>Sandbox:</strong> ' + testingEscapeHtml(persona.sandboxRoot || '-') + '</span>' +
                        '</div>' +
                        '<div class="autonomy-persona-meta">' +
                            '<span><strong>Next due:</strong> ' + nextDue + '</span>' +
                            '<span><strong>Fake now:</strong> ' + fakeNow + '</span>' +
                            '<span><strong>Updated:</strong> ' + updated + '</span>' +
                        '</div>' +
                        '<div class="autonomy-persona-meta">' +
                            '<span><strong>Interval:</strong> ' + testingEscapeHtml(persona.intervalMinutes || 15) + 'm</span>' +
                            '<span><strong>Daily budget:</strong> $' + Number(persona.dailyBudgetUsd || 0).toFixed(2) + '</span>' +
                            '<span><strong>Mode:</strong> <span class="timer-pill ' + productionClass + '">' + testingEscapeHtml(productionLabel) + '</span></span>' +
                            '<span><strong>Sandbox ready:</strong> ' + (persona.sandboxReady ? 'Yes' : 'No') + '</span>' +
                            '<span><strong>Cost today:</strong> $' + Number(persona.estimatedCostCacheMissTodayUsd || 0).toFixed(6) + '</span>' +
                        '</div>' +
                    '</article>';
                })
                .join('');
            setAutonomyPersonasMessage('');
        }

        function renderAutonomy(status) {
            autonomyStatus = status || {};
            const settings = autonomyStatus.settings || {};
            const statusEl = document.getElementById('autonomyStatus');
            statusEl.textContent = autonomyStatus.isRunning ? 'Running' : (settings.enabled ? 'Scheduled' : 'Stopped');
            statusEl.classList.toggle('running', !!autonomyStatus.isRunning || !!settings.enabled);

            document.getElementById('autonomyNext').textContent = formatAutonomyTime(autonomyStatus.nextDueUtc);
            document.getElementById('autonomyLast').textContent = formatAutonomyTime(autonomyStatus.lastStepUtc);
            document.getElementById('autonomyFakeNow').textContent = formatAutonomyTime(autonomyStatus.fakeNowUtc);
            document.getElementById('autonomyCostToday').textContent = formatUsd(autonomyStatus.estimatedCostCacheMissTodayUsd);
            document.getElementById('autonomyCostTotal').textContent = formatUsd(autonomyStatus.estimatedCostCacheMissUsd);
            document.getElementById('autonomySandboxState').textContent = autonomyStatus.sandboxReady ? 'Ready' : 'Not ready';
            document.getElementById('autonomyConversation').textContent = autonomyStatus.targetConversationId || '-';
            document.getElementById('autonomyWorld').textContent = autonomyStatus.worldStateMarkdown || '';
            document.getElementById('autonomyActivityLog').textContent = autonomyStatus.activityLogMarkdown || '';
            renderCurrentRun(autonomyStatus.currentRun);
            renderAutonomyIntentions(autonomyStatus.intentions || []);
            renderAutonomyGuidance(autonomyStatus);
            renderAutonomyFiles(autonomyStatus.sandboxFiles || []);
            renderAutonomyLogs(autonomyStatus.logs || []);
        }

        function renderCurrentRun(run) {
            const panel = document.getElementById('autonomyCurrentRunPanel');
            const body = document.getElementById('autonomyCurrentRun');
            if (!run) {
                panel.style.display = 'none';
                body.innerHTML = '';
                return;
            }

            panel.style.display = '';
            const delivery = run.deliveryStatus
                ? '<p><strong>Delivery:</strong> ' + testingEscapeHtml(run.deliveryStatus) +
                    (run.deliveredMessageId ? ' (' + testingEscapeHtml(run.deliveredConversationId || '') + '/' + testingEscapeHtml(run.deliveredMessageId) + ')' : '') +
                    (run.deliveryTriggeredCompaction ? ' Compaction: ' + testingEscapeHtml(run.deliveryArchivePath || 'yes') : '') +
                  '</p>'
                : '';
            body.innerHTML =
                '<div class="autonomy-stats autonomy-current-stats">' +
                    '<div><span>Phase</span><strong>' + testingEscapeHtml(run.phase || '-') + '</strong></div>' +
                    '<div><span>Started</span><strong>' + testingEscapeHtml(formatAutonomyTime(run.startedAtUtc)) + '</strong></div>' +
                    '<div><span>Effective</span><strong>' + testingEscapeHtml(formatAutonomyTime(run.effectiveNowUtc)) + '</strong></div>' +
                    '<div><span>Tokens</span><strong>' + testingEscapeHtml(run.inputTokens || 0) + ' / ' + testingEscapeHtml(run.outputTokens || 0) + '</strong></div>' +
                '</div>' +
                '<p><strong>Status:</strong> ' + testingEscapeHtml(run.message || '') + '</p>' +
                '<p><strong>Intent actions:</strong> ' + testingEscapeHtml(run.appliedActionCount || 0) + ' applied / ' + testingEscapeHtml(run.actionCount || 0) + ' planned</p>' +
                '<p><strong>Local file actions:</strong> ' + testingEscapeHtml(run.appliedFileOperationCount || 0) + ' applied / ' + testingEscapeHtml(run.fileOperationCount || 0) + ' planned</p>' +
                (run.visibleMessage ? '<p><strong>Planned visible message:</strong> ' + testingEscapeHtml(run.visibleMessage) + '</p>' : '') +
                (run.healthAwarenessNote ? '<p><strong>Health awareness:</strong> ' + testingEscapeHtml(run.healthAwarenessNote) + '</p>' : '') +
                (run.guidanceProposalSummary ? '<p><strong>Guidance proposal:</strong> ' + testingEscapeHtml(run.guidanceProposalSummary) + (run.guidanceProposalId ? ' (' + testingEscapeHtml(run.guidanceProposalId) + ')' : '') + '</p>' : '') +
                delivery;
        }

        function renderAutonomyIntentions(intentions) {
            const list = document.getElementById('autonomyIntentions');
            const active = (intentions || []).filter(item => (item.status || 'active') === 'active');
            const closed = (intentions || []).filter(item => (item.status || 'active') !== 'active').slice(0, 10);
            const items = active.concat(closed);
            if (!items.length) {
                list.innerHTML = '<div class="timer-empty">No intentions yet.</div>';
                return;
            }

            list.innerHTML = items.map(item => {
                const due = item.dueAtUtc ? 'Due ' + formatAutonomyTime(item.dueAtUtc) : 'No due time';
                return '<div class="timer-row autonomy-intention ' + testingEscapeHtml(item.status || 'active') + '">' +
                    '<div>' +
                        '<strong>' + testingEscapeHtml(item.title || '(untitled)') + '</strong>' +
                        '<p>' + testingEscapeHtml(item.details || '') + '</p>' +
                        '<span>' + testingEscapeHtml((item.status || 'active') + ' | ' + (item.priority || 'normal') + ' | ' + due) + '</span>' +
                    '</div>' +
                    '<code>' + testingEscapeHtml(item.id || '') + '</code>' +
                '</div>';
            }).join('');
        }

        function renderAutonomyGuidance(status) {
            const pending = status.pendingGuidanceProposal || null;
            document.getElementById('autonomyGuidance').textContent = status.guidanceMarkdown || '';
            document.getElementById('autonomyReflectionLog').textContent = status.reflectionLogMarkdown || '';
            document.getElementById('autonomyApproveGuidanceButton').disabled = !pending || !!status.isRunning;
            document.getElementById('autonomyRejectGuidanceButton').disabled = !pending || !!status.isRunning;
            document.getElementById('autonomyReflectButton').disabled = !!pending || !!status.isRunning;

            const target = document.getElementById('autonomyPendingGuidance');
            if (!pending) {
                target.innerHTML = '<div class="timer-empty">No pending proposal.</div>';
                return;
            }

            target.innerHTML =
                '<div class="autonomy-proposal-meta">' +
                    '<p><strong>ID:</strong> ' + testingEscapeHtml(pending.id || '') + '</p>' +
                    '<p><strong>Created:</strong> ' + testingEscapeHtml(formatAutonomyTime(pending.createdAtUtc)) + '</p>' +
                    '<p><strong>Summary:</strong> ' + testingEscapeHtml(pending.changeSummary || '') + '</p>' +
                    '<p><strong>Reason:</strong> ' + testingEscapeHtml(pending.reason || '') + '</p>' +
                '</div>' +
                '<pre class="autonomy-world">' + testingEscapeHtml(pending.proposedGuidanceMarkdown || '') + '</pre>';
        }

        function renderAutonomyFiles(files) {
            const list = document.getElementById('autonomyFiles');
            list.innerHTML = files.length
                ? files.map(path => '<div class="timer-row"><span class="autonomy-file">' + testingEscapeHtml(path) + '</span></div>').join('')
                : '<div class="timer-empty">No continuity files.</div>';
        }

        function renderAutonomyLogs(logs) {
            const list = document.getElementById('autonomyLogs');
            const hasRendered = list.dataset.rendered === 'true';
            const existingIds = new Set(Array.from(list.querySelectorAll('details.autonomy-log')).map(el => el.dataset.logId || ''));
            const openIds = new Set(Array.from(list.querySelectorAll('details.autonomy-log[open]')).map(el => el.dataset.logId || ''));
            if (!logs.length) {
                list.innerHTML = '<div class="timer-empty">No steps yet.</div>';
                list.dataset.rendered = 'true';
                return;
            }

            list.innerHTML = logs.map(log => {
                const logId = log.id || log.startedAtUtc || '';
                const isNew = !existingIds.has(logId);
                const shouldOpen = openIds.has(logId) || (!log.success && (!hasRendered || isNew));
                const ops = (log.fileOperations || []).map(op => {
                    const state = op.applied ? 'applied' : 'skipped';
                    return '<li><strong>' + testingEscapeHtml(state) + '</strong> ' + testingEscapeHtml(op.action || '') + ' ' + testingEscapeHtml(op.path || '') + ' - ' + testingEscapeHtml(op.message || '') + '</li>';
                }).join('');
                const actions = (log.actionResults || []).map(action => {
                    const state = action.applied ? 'applied' : 'skipped';
                    return '<li><strong>' + testingEscapeHtml(state) + '</strong> ' + testingEscapeHtml(action.kind || '') + ' ' + testingEscapeHtml(action.id || '') + ' - ' + testingEscapeHtml(action.message || '') + '</li>';
                }).join('');
                return '<details class="autonomy-log" data-log-id="' + testingEscapeHtml(logId) + '" ' + (shouldOpen ? 'open' : '') + '>' +
                    '<summary>' +
                        '<span>' + testingEscapeHtml(formatAutonomyTime(log.startedAtUtc)) + '</span>' +
                        '<strong>' + testingEscapeHtml(log.message || log.type || '') + '</strong>' +
                        '<em>' + testingEscapeHtml(formatUsd(log.estimatedCostCacheMissUsd)) + '</em>' +
                    '</summary>' +
                    '<div class="autonomy-log-body">' +
                        '<p><strong>Effective:</strong> ' + testingEscapeHtml(formatAutonomyTime(log.effectiveNowUtc)) + ' | <strong>Elapsed:</strong> ' + testingEscapeHtml(log.elapsedMinutes || 0) + 'm | <strong>Tokens:</strong> ' + testingEscapeHtml(log.inputTokens || 0) + ' in / ' + testingEscapeHtml(log.outputTokens || 0) + ' out</p>' +
                        (log.journalEntry ? '<p><strong>Journal:</strong> ' + testingEscapeHtml(log.journalEntry) + '</p>' : '') +
                        (log.visibleMessage ? '<p><strong>Visible message:</strong> ' + testingEscapeHtml(log.visibleMessage) + '</p>' : '') +
                        (log.deliveryStatus ? '<p><strong>Delivery:</strong> ' + testingEscapeHtml(log.deliveryStatus) + (log.deliveredMessageId ? ' (' + testingEscapeHtml(log.deliveredConversationId || '') + '/' + testingEscapeHtml(log.deliveredMessageId) + ')' : '') + (log.deliveryTriggeredCompaction ? ' Compaction: ' + testingEscapeHtml(log.deliveryArchivePath || 'yes') : '') + '</p>' : '') +
                        (log.kaiContinuityNote ? '<p><strong>Kai continuity:</strong> ' + testingEscapeHtml(log.kaiContinuityNote) + '</p>' : '') +
                        (log.healthAwarenessNote ? '<p><strong>Health awareness:</strong> ' + testingEscapeHtml(log.healthAwarenessNote) + '</p>' : '') +
                        (log.guidanceProposalId ? '<p><strong>Guidance proposal:</strong> ' + testingEscapeHtml(log.guidanceProposalId) + '</p>' : '') +
                        (log.notesForRaymond ? '<p><strong>Legacy note:</strong> ' + testingEscapeHtml(log.notesForRaymond) + '</p>' : '') +
                        (actions ? '<p><strong>Actions:</strong></p><ul>' + actions + '</ul>' : '') +
                        (ops ? '<ul>' + ops + '</ul>' : '') +
                        (log.rawResponsePreview ? '<pre>' + testingEscapeHtml(log.rawResponsePreview) + '</pre>' : '') +
                    '</div>' +
                '</details>';
            }).join('');
            list.dataset.rendered = 'true';
        }

        async function forceAutonomyStep() {
            setAutonomyMessage('Running step...', false);
            try {
                const note = document.getElementById('autonomyReason').value.trim();
                setTimeout(loadAutonomy, 500);
                const result = await autonomyPost('/api/autonomy/step', {
                    fakeAdvanceMinutes: parseInt(document.getElementById('autonomyFakeAdvance').value || '0', 10),
                    reason: note || null
                });
                renderAutonomy(result.status);
                setAutonomyMessage(result.message || 'Step complete.', !result.success);
            } catch (error) {
                setAutonomyMessage(error.message || String(error), true);
                loadAutonomy();
            }
        }

        async function reflectAutonomyGuidance() {
            setAutonomyMessage('Reflecting...', false);
            try {
                setTimeout(loadAutonomy, 500);
                const result = await autonomyPost('/api/autonomy/guidance/reflect', {});
                renderAutonomy(result.status);
                setAutonomyMessage(result.message || 'Reflection complete.', !result.success);
            } catch (error) {
                setAutonomyMessage(error.message || String(error), true);
                loadAutonomy();
            }
        }

        async function approveAutonomyGuidance() {
            const pending = autonomyStatus && autonomyStatus.pendingGuidanceProposal;
            if (!pending) return;
            setAutonomyMessage('', false);
            try {
                const result = await autonomyPost('/api/autonomy/guidance/approve', { proposalId: pending.id });
                renderAutonomy(result.status);
                setAutonomyMessage(result.message || 'Guidance approved.', !result.success);
            } catch (error) {
                setAutonomyMessage(error.message || String(error), true);
                loadAutonomy();
            }
        }

        async function rejectAutonomyGuidance() {
            const pending = autonomyStatus && autonomyStatus.pendingGuidanceProposal;
            if (!pending) return;
            setAutonomyMessage('', false);
            try {
                const result = await autonomyPost('/api/autonomy/guidance/reject', { proposalId: pending.id });
                renderAutonomy(result.status);
                setAutonomyMessage(result.message || 'Guidance rejected.', !result.success);
            } catch (error) {
                setAutonomyMessage(error.message || String(error), true);
                loadAutonomy();
            }
        }

        async function rebuildAutonomyWorldCopy() {
            if (!confirm('Rebuild the autonomy world copy from the current source files? This will also clear the autonomy activity and reflection logs for this sandbox run.')) return;
            setAutonomyMessage('', false);
            try {
                const status = await autonomyPost('/api/autonomy/sandbox/reset', {});
                renderAutonomy(status);
                setAutonomyMessage('World copy rebuilt and logs cleared.', false);
            } catch (error) {
                setAutonomyMessage(error.message || String(error), true);
            }
        }

        function initCompactionTesting() {
            var select = document.getElementById('compactionConversation');
            var status = document.getElementById('compactionStatus');
            var refreshBtn = document.getElementById('compactionRefreshBtn');
            var previewBtn = document.getElementById('compactionPreviewBtn');
            var dryRunBtn = document.getElementById('compactionDryRunBtn');
            var summaryBtn = document.getElementById('compactionSummaryBtn');
            var forceInput = document.getElementById('compactionForce');

            function setBusy(isBusy, text) {
                status.textContent = text || '';
                refreshBtn.disabled = isBusy;
                previewBtn.disabled = isBusy;
                dryRunBtn.disabled = isBusy;
                summaryBtn.disabled = isBusy;
                select.disabled = isBusy;
                forceInput.disabled = isBusy;
            }

            function selectedId() {
                return select.value || '';
            }

            function formatNumber(value) {
                if (value === null || value === undefined) return '-';
                return Number(value).toLocaleString();
            }

            function setText(id, value) {
                document.getElementById(id).textContent = value;
            }

            function renderResult(result, mode) {
                document.getElementById('compactionResults').hidden = false;
                var action = result.compacted ? 'Compacted' : result.shouldCompact ? 'Would compact' : 'Skip';
                if (result.dryRun) action += ' (dry run)';
                setText('compactionAction', action);
                setText('compactionApiTokens', formatNumber(result.estimatedApiTokens));
                setText('compactionAfterTokens', formatNumber(result.estimatedPostCompactionApiTokens));
                setText('compactionChunks', formatNumber(result.chunkCount));
                setText('compactionReason', result.reason || '-');
                setText('compactionMessages',
                    formatNumber(result.messagesToCompact) + ' compacted / '
                    + formatNumber(result.messagesToKeep) + ' kept / '
                    + formatNumber(result.messagesBefore) + ' total');
                setText('compactionTrigger',
                    formatNumber(result.estimatedApiTokens) + ' of '
                    + formatNumber(result.triggerTokens) + ' trigger, '
                    + formatNumber(result.contextTokenLimit) + ' context limit');
                setText('compactionLimits',
                    formatNumber(result.modelInputTokens) + ' input / '
                    + formatNumber(result.modelOutputTokens) + ' output, '
                    + formatNumber(result.chunkTokenLimit) + ' chunk cap, '
                    + formatNumber(result.summaryMaxTokens) + ' summary cap');
                setText('compactionArchive', result.archivePath || '(none written)');
                setText('compactionSummaryPreview', result.summaryPreview || '(none)');
                setText('compactionRawJson', JSON.stringify(result, null, 2));

                var rows = document.getElementById('compactionChunkRows');
                rows.innerHTML = '';
                (result.chunks || []).forEach(function (chunk) {
                    var tr = document.createElement('tr');
                    tr.innerHTML = '<td>' + formatNumber(chunk.index) + '</td>'
                        + '<td>' + formatNumber(chunk.unitCount) + '</td>'
                        + '<td>' + formatNumber(chunk.estimatedTokens) + '</td>'
                        + '<td><code>' + testingEscapeHtml(chunk.firstMessageId || '-') + '</code></td>'
                        + '<td><code>' + testingEscapeHtml(chunk.lastMessageId || '-') + '</code></td>';
                    rows.appendChild(tr);
                });

                if (!rows.children.length) {
                    var tr = document.createElement('tr');
                    tr.innerHTML = '<td colspan="5" class="compaction-empty-cell">No chunks</td>';
                    rows.appendChild(tr);
                }

                status.textContent = mode + ' complete.';
            }

            function loadCompactionConversations() {
                setBusy(true, 'Loading conversations...');
                return fetch('/api/conversations')
                    .then(function (r) {
                        if (!r.ok) throw new Error('Could not load conversations');
                        return r.json();
                    })
                    .then(function (convs) {
                        select.innerHTML = '';
                        convs.forEach(function (conv) {
                            var opt = document.createElement('option');
                            opt.value = conv.id;
                            opt.textContent = (conv.title || 'Chat') + ' (' + conv.messageCount + ' messages)';
                            select.appendChild(opt);
                        });
                        status.textContent = convs.length ? 'Ready.' : 'No conversations found.';
                    })
                    .catch(function (err) {
                        status.textContent = 'Error: ' + (err.message || err);
                    })
                    .finally(function () {
                        setBusy(false, status.textContent);
                    });
            }

            function previewCompaction() {
                var id = selectedId();
                if (!id) return;
                setBusy(true, 'Previewing compaction...');
                fetch('/api/compaction/preview/' + encodeURIComponent(id) + '?force=' + encodeURIComponent(forceInput.checked))
                    .then(function (r) {
                        if (!r.ok) throw new Error('Preview failed');
                        return r.json();
                    })
                    .then(function (result) { renderResult(result, 'Preview'); })
                    .catch(function (err) { status.textContent = 'Error: ' + (err.message || err); })
                    .finally(function () { setBusy(false, status.textContent); });
            }

            function dryRunCompaction(generateSummary) {
                var id = selectedId();
                if (!id) return;
                setBusy(true, generateSummary ? 'Running dry-run summary...' : 'Running dry-run plan...');
                fetch('/api/compaction/test/' + encodeURIComponent(id), {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({
                        force: forceInput.checked,
                        generateSummary: !!generateSummary
                    })
                })
                    .then(function (r) {
                        if (!r.ok) throw new Error('Dry run failed');
                        return r.json();
                    })
                    .then(function (result) { renderResult(result, generateSummary ? 'Summary dry run' : 'Dry run'); })
                    .catch(function (err) { status.textContent = 'Error: ' + (err.message || err); })
                    .finally(function () { setBusy(false, status.textContent); });
            }

            refreshBtn.addEventListener('click', loadCompactionConversations);
            previewBtn.addEventListener('click', previewCompaction);
            dryRunBtn.addEventListener('click', function () { dryRunCompaction(false); });
            summaryBtn.addEventListener('click', function () { dryRunCompaction(true); });

            loadCompactionConversations();
        }

        window.forceAutonomyStep = forceAutonomyStep;
        window.rebuildAutonomyWorldCopy = rebuildAutonomyWorldCopy;
        window.loadAutonomy = loadAutonomy;
        window.loadAutonomyPersonas = loadAutonomyPersonas;
        window.reflectAutonomyGuidance = reflectAutonomyGuidance;
        window.approveAutonomyGuidance = approveAutonomyGuidance;
        window.rejectAutonomyGuidance = rejectAutonomyGuidance;

        document.addEventListener('DOMContentLoaded', function () {
            document.querySelectorAll('[data-testing-tab]').forEach(function (button) {
                button.addEventListener('click', function () {
                    activateTestingPanel(button.dataset.testingTab);
                });
            });
            const personaIncludeDisabled = document.getElementById('autonomyPersonasIncludeDisabled');
            if (personaIncludeDisabled) {
                personaIncludeDisabled.addEventListener('change', loadAutonomyPersonas);
            }
            const requested = (location.hash || '').replace('#', '');
            activateTestingPanel(requested || 'autonomy');
            loadAutonomy();
            initCompactionTesting();
            setInterval(loadAutonomy, 5000);
        });
    </script>
}
Offline