black-yt's picture
Prevent AskUser card fade in Space
0dc4dd6
Raw
History Blame Contribute Delete
32.9 kB
(function () {
var canvas, ctx, t = 0;
var TILE = 26, GAP = 1;
var rafId = null;
var cachedRgb = "10,10,10";
var lastDraw = 0;
var FRAME_MS = 1000 / 24;
function rgb() {
var th = document.documentElement.getAttribute("data-theme") || "white";
if (th === "dark") return "220,210,175";
if (th === "yellow") return "120,85,20";
if (th === "blue") return "38,88,155";
return "10,10,10";
}
function frame(ts) {
rafId = requestAnimationFrame(frame);
if (ts - lastDraw < FRAME_MS) return;
lastDraw = ts;
var w = canvas.width, h = canvas.height;
var cols = Math.ceil(w / TILE) + 1;
var rows = Math.ceil(h / TILE) + 1;
var pre = "rgba(" + cachedRgb + ",";
ctx.clearRect(0, 0, w, h);
for (var r = 0; r < rows; r++) {
for (var c = 0; c < cols; c++) {
var wave = 0.6 * Math.sin(c * 0.21 + t * 0.36) * Math.sin(r * 0.17 + t * 0.28)
+ 0.4 * Math.sin(c * 0.11 - r * 0.13 + t * 0.19);
var norm = (wave + 1) * 0.5;
var v = norm * norm * norm;
var a = Math.round((0.004 + v * 0.186) * 100) / 100;
if (a < 0.02) continue;
ctx.fillStyle = pre + a + ")";
ctx.fillRect(c * TILE + GAP, r * TILE + GAP, TILE - GAP, TILE - GAP);
}
}
t += 0.007;
}
var resizeTimer;
function resize() {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(function () {
var newW = window.innerWidth;
var newH = window.innerHeight;
if (newW === canvas.width && Math.abs(newH - canvas.height) <= 90) return;
canvas.width = newW;
canvas.height = newH;
}, 120);
}
function onVisibilityChange() {
if (document.hidden) {
if (rafId) {
cancelAnimationFrame(rafId);
rafId = null;
}
} else if (!rafId) {
rafId = requestAnimationFrame(frame);
}
}
document.addEventListener("DOMContentLoaded", function () {
canvas = document.createElement("canvas");
canvas.style.cssText = "position:fixed;top:0;left:0;width:100%;height:100%;"
+ "z-index:0;pointer-events:none;will-change:transform;"
+ "-webkit-backface-visibility:hidden;backface-visibility:hidden;";
document.body.insertBefore(canvas, document.body.firstChild);
ctx = canvas.getContext("2d");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
cachedRgb = rgb();
window.addEventListener("resize", resize);
document.addEventListener("visibilitychange", onVisibilityChange);
rafId = requestAnimationFrame(frame);
});
new MutationObserver(function () { cachedRgb = rgb(); })
.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });
})();
(function () {
var THEMES = ["white", "yellow", "blue", "dark"];
var LABELS = { white: "Pure White", yellow: "Warm Yellow", blue: "Cool Blue", dark: "Dark" };
function applyTheme(theme) {
if (theme === "white") {
document.documentElement.removeAttribute("data-theme");
} else {
document.documentElement.setAttribute("data-theme", theme);
}
try { localStorage.setItem("rh-ui-theme", theme); } catch (e) {}
document.querySelectorAll(".theme-dot").forEach(function (dot) {
dot.classList.toggle("active", dot.dataset.theme === theme);
});
}
var saved = "white";
try { saved = localStorage.getItem("rh-ui-theme") || "white"; } catch (e) {}
applyTheme(saved);
document.addEventListener("DOMContentLoaded", function () {
var switcher = document.createElement("div");
switcher.id = "theme-switcher";
switcher.setAttribute("aria-label", "Choose colour theme");
THEMES.forEach(function (theme) {
var btn = document.createElement("button");
btn.className = "theme-dot";
btn.dataset.theme = theme;
btn.title = LABELS[theme];
btn.setAttribute("aria-label", LABELS[theme]);
btn.addEventListener("click", function () { applyTheme(theme); });
switcher.appendChild(btn);
});
document.body.appendChild(switcher);
applyTheme(saved);
});
})();
(function () {
var ws;
var running = false;
var interrupting = false;
var pendingAskId = "";
var keepSubmittedMessageOnReset = false;
var autoFollowTimeline = true;
var conversationStarted = false;
var downloadToken = "";
var fileToken = "";
var downloadingWorkspace = false;
var images = [];
var COLLAPSED_STEP_HEIGHT = 220;
var promptInput = document.getElementById("promptInput");
var runBtn = document.getElementById("runBtn");
var newBtn = document.getElementById("newBtn");
var modelSelect = document.getElementById("modelSelect");
var modelDropdown = document.getElementById("modelDropdown");
var modelDropdownButton = document.getElementById("modelDropdownButton");
var modelSelectLabel = document.getElementById("modelSelectLabel");
var modelOptions = document.getElementById("modelOptions");
var attachBtn = document.getElementById("attachBtn");
var imageInput = document.getElementById("imageInput");
var imagePreview = document.getElementById("imagePreview");
var dropZone = document.getElementById("dropZone");
var timeline = document.getElementById("timeline");
var statusPill = document.getElementById("statusPill");
var workspaceStrip = document.getElementById("workspaceStrip");
var workspaceMeta = document.getElementById("workspaceMeta");
var downloadWorkspaceBtn = document.getElementById("downloadWorkspaceBtn");
var defaultPromptPlaceholder = promptInput.getAttribute("placeholder") || "Message ResearchHarness";
var mermaidCounter = 0;
function escapeHtml(value) {
return String(value || "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
function protectMathSegments(text) {
var segments = [];
var protectedText = String(text || "").replace(/(\$\$[\s\S]+?\$\$|\\\[[\s\S]+?\\\]|\\\([\s\S]+?\\\))/g, function (match) {
var token = "@@RH_MATH_" + segments.length + "@@";
segments.push({ token: token, text: match });
return token;
});
return { text: protectedText, segments: segments };
}
function restoreMathSegments(html, segments) {
var restored = String(html || "");
(segments || []).forEach(function (segment) {
restored = restored.split(segment.token).join(escapeHtml(segment.text));
});
return restored;
}
function isRemoteOrInlineImageSrc(src) {
return /^(https?:|data:|blob:|#|\/api\/|\/static\/)/i.test(String(src || "").trim());
}
function rewriteWorkspaceImageSources(html) {
var token = fileToken || downloadToken;
if (!token) return html;
var template = document.createElement("template");
template.innerHTML = html;
template.content.querySelectorAll("img").forEach(function (img) {
var src = img.getAttribute("src") || "";
if (!src || isRemoteOrInlineImageSrc(src)) return;
img.setAttribute("src", "/api/workspace-file?token=" + encodeURIComponent(token) + "&path=" + encodeURIComponent(src));
});
return template.innerHTML;
}
function normalizeMarkdownImageDestinations(text) {
return String(text || "").replace(/!\[([^\]\n]*)\]\(([^)\n]+)\)/g, function (match, alt, target) {
var src = String(target || "").trim();
if (!src || src[0] === "<" || !/\s/.test(src) || isRemoteOrInlineImageSrc(src)) return match;
if (/[<>]/.test(src)) return match;
return "![" + alt + "](<" + src + ">)";
});
}
function unwrapFullMarkdownFence(text) {
var source = String(text || "").trim();
var match = /^(```|~~~)[ \t]*(markdown|md|gfm)[^\n]*\n([\s\S]*?)\n\1[ \t]*$/i.exec(source);
return match ? match[3] : text;
}
function renderMathInMarkdown(container) {
if (!window.renderMathInElement) return;
container.querySelectorAll(".markdown-body").forEach(function (body) {
try {
window.renderMathInElement(body, {
delimiters: [
{ left: "$$", right: "$$", display: true },
{ left: "\\[", right: "\\]", display: true },
{ left: "\\(", right: "\\)", display: false }
],
ignoredTags: ["script", "noscript", "style", "textarea", "pre", "code"],
throwOnError: false
});
} catch (e) {
console.warn("Math rendering failed.", e);
}
});
}
function renderMermaidInMarkdown(container) {
if (!window.mermaid) return;
try {
window.mermaid.initialize({ startOnLoad: false, securityLevel: "strict" });
} catch (e) {
console.warn("Mermaid initialization failed.", e);
return;
}
container.querySelectorAll(".markdown-body pre code").forEach(function (code) {
var className = String(code.className || "").toLowerCase();
if (!className.split(/\s+/).some(function (name) {
return name === "language-mermaid" || name === "lang-mermaid" || name === "language-mmd" || name === "lang-mmd";
})) return;
var pre = code.closest("pre");
if (!pre) return;
var source = code.textContent || "";
var target = document.createElement("div");
var id = "rh-mermaid-" + (++mermaidCounter);
target.className = "mermaid-chart";
window.mermaid.render(id, source).then(function (result) {
target.innerHTML = result.svg || "";
pre.replaceWith(target);
}).catch(function (e) {
console.warn("Mermaid rendering failed.", e);
});
});
}
function renderMarkdown(text) {
if (!window.marked || !window.DOMPurify) {
console.warn("Markdown renderer unavailable; falling back to plain text.");
return "<pre>" + escapeHtml(text) + "</pre>";
}
try {
var normalizedText = normalizeMarkdownImageDestinations(unwrapFullMarkdownFence(text));
var protectedMath = protectMathSegments(normalizedText);
var rawHtml = window.marked.parse(protectedMath.text, { gfm: true, breaks: false, async: false });
rawHtml = rewriteWorkspaceImageSources(rawHtml);
var safeHtml = window.DOMPurify.sanitize(rawHtml, { USE_PROFILES: { html: true } });
safeHtml = restoreMathSegments(safeHtml, protectedMath.segments);
return '<div class="markdown-body">' + safeHtml + "</div>";
} catch (e) {
console.warn("Markdown rendering failed; falling back to plain text.", e);
return "<pre>" + escapeHtml(text) + "</pre>";
}
}
function setStatus(text, kind) {
statusPill.textContent = text;
statusPill.className = "status " + (kind || "idle");
}
function updateDownloadWorkspaceButton() {
if (!downloadWorkspaceBtn) return;
downloadWorkspaceBtn.disabled = !downloadToken || downloadingWorkspace;
downloadWorkspaceBtn.title = downloadingWorkspace
? "Preparing workspace.zip..."
: downloadToken
? "Download files created or handled by the agent in this chat."
: "Run the agent first, then download the generated workspace files.";
}
function setWorkspaceMessage(text, kind) {
if (!workspaceMeta) return;
workspaceMeta.textContent = text;
if (!workspaceStrip) return;
workspaceStrip.classList.toggle("empty", kind === "empty");
workspaceStrip.classList.toggle("error", kind === "error");
workspaceStrip.classList.toggle("busy", kind === "busy");
}
function setDownloadToken(token) {
downloadToken = String(token || "");
updateDownloadWorkspaceButton();
}
function updateWorkspaceHint(hasWorkspace) {
if (!workspaceMeta) return;
setWorkspaceMessage(
hasWorkspace
? "Agent files are ready to download as a zip."
: "Temporary workspace. Download agent files as a zip.",
""
);
}
function filenameFromContentDisposition(headerValue) {
var match = /filename="?([^";]+)"?/i.exec(String(headerValue || ""));
return match ? match[1] : "workspace.zip";
}
function workspaceDownloadMessage(response, fallback) {
return response.json().then(function (payload) {
return payload && payload.detail ? String(payload.detail) : fallback;
}).catch(function () {
return fallback;
});
}
function downloadWorkspaceZip() {
if (!downloadToken || downloadingWorkspace) return;
downloadingWorkspace = true;
updateDownloadWorkspaceButton();
setWorkspaceMessage("Preparing workspace.zip...", "busy");
fetch("/api/workspace.zip?token=" + encodeURIComponent(downloadToken), {
headers: { "Accept": "application/zip, application/json" }
}).then(function (response) {
if (!response.ok) {
return workspaceDownloadMessage(response, "Workspace download is not available.").then(function (message) {
if (response.status === 404 && message.indexOf("no downloadable files") >= 0) {
setWorkspaceMessage("Workspace is empty. Create or handle a file with the agent, then download again.", "empty");
return;
}
setWorkspaceMessage(message, "error");
});
}
var filename = filenameFromContentDisposition(response.headers.get("content-disposition"));
return response.blob().then(function (blob) {
var url = URL.createObjectURL(blob);
var link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
link.remove();
setTimeout(function () { URL.revokeObjectURL(url); }, 1000);
setWorkspaceMessage("Downloading workspace.zip.", "");
});
}).catch(function (error) {
console.warn("Workspace download failed.", error);
setWorkspaceMessage("Workspace download failed. Please try again.", "error");
}).finally(function () {
downloadingWorkspace = false;
updateDownloadWorkspaceButton();
});
}
function closeModelDropdown() {
if (!modelDropdown || !modelDropdownButton) return;
modelDropdown.classList.remove("open");
if (modelOptions) modelOptions.classList.remove("open");
modelDropdownButton.setAttribute("aria-expanded", "false");
}
function positionModelOptions() {
if (!modelDropdownButton || !modelOptions) return;
var rect = modelDropdownButton.getBoundingClientRect();
modelOptions.style.setProperty("--model-options-top", Math.round(rect.bottom + 8) + "px");
modelOptions.style.setProperty("--model-options-right", Math.max(12, Math.round(window.innerWidth - rect.right)) + "px");
}
function setModelValue(value) {
if (!modelSelect || !value) return;
modelSelect.value = value;
if (modelSelectLabel) modelSelectLabel.textContent = value;
if (!modelOptions) return;
modelOptions.querySelectorAll(".model-option").forEach(function (option) {
var selected = option.getAttribute("data-model-value") === value;
option.classList.toggle("selected", selected);
option.setAttribute("aria-selected", selected ? "true" : "false");
});
}
function setModelControlDisabled(disabled) {
if (modelSelect) modelSelect.disabled = disabled;
if (modelDropdownButton) modelDropdownButton.disabled = disabled;
if (disabled) closeModelDropdown();
}
function setupModelDropdown() {
if (!modelDropdown || !modelDropdownButton || !modelOptions || !modelSelect) return;
if (modelOptions.parentElement !== document.body) document.body.appendChild(modelOptions);
setModelValue(modelSelect.value || "gpt-5.5");
modelDropdownButton.addEventListener("click", function () {
if (modelDropdownButton.disabled) return;
var shouldOpen = !modelDropdown.classList.contains("open");
if (shouldOpen) positionModelOptions();
modelDropdown.classList.toggle("open", shouldOpen);
modelOptions.classList.toggle("open", shouldOpen);
modelDropdownButton.setAttribute("aria-expanded", shouldOpen ? "true" : "false");
});
modelOptions.querySelectorAll(".model-option").forEach(function (option) {
option.addEventListener("click", function () {
setModelValue(option.getAttribute("data-model-value"));
closeModelDropdown();
});
});
document.addEventListener("click", function (event) {
if (!modelDropdown.contains(event.target) && !modelOptions.contains(event.target)) closeModelDropdown();
});
document.addEventListener("keydown", function (event) {
if (event.key === "Escape") closeModelDropdown();
});
window.addEventListener("resize", function () {
if (modelDropdown.classList.contains("open")) positionModelOptions();
});
}
function updateComposerMode() {
if (pendingAskId) {
runBtn.disabled = false;
runBtn.classList.remove("is-running");
runBtn.textContent = "Reply";
promptInput.placeholder = defaultPromptPlaceholder;
setModelControlDisabled(true);
return;
}
runBtn.disabled = running && interrupting;
runBtn.classList.toggle("is-running", running);
runBtn.textContent = running ? (interrupting ? "Stopping" : "Stop") : "Run";
promptInput.placeholder = defaultPromptPlaceholder;
setModelControlDisabled(running);
}
function setRunning(active, statusText) {
running = active;
if (!active) interrupting = false;
updateComposerMode();
setStatus(statusText || (active ? "Running" : "Idle"), active ? "running" : "idle");
}
function clearTimeline() {
autoFollowTimeline = true;
timeline.innerHTML = ''
+ '<div class="welcome">'
+ '<h1>What should the agent do?</h1>'
+ '<p>Ask a question, attach images, and watch tool calls stream from an isolated temporary workspace.</p>'
+ '</div>';
}
function ensureTimelineReady() {
var welcome = timeline.querySelector(".welcome");
if (welcome) welcome.remove();
}
function isNearBottom() {
return timeline.scrollHeight - timeline.scrollTop - timeline.clientHeight < 80;
}
function scrollTimeline(force) {
if (!force && !autoFollowTimeline) return;
requestAnimationFrame(function () {
timeline.scrollTop = timeline.scrollHeight;
requestAnimationFrame(function () {
timeline.scrollTop = timeline.scrollHeight;
autoFollowTimeline = isNearBottom();
});
});
}
function syncTimelineFollowMode() {
autoFollowTimeline = isNearBottom();
}
function updateEventToggle(node) {
var toggle = node.querySelector(".event-toggle");
if (!toggle) return;
toggle.setAttribute("aria-expanded", node.classList.contains("collapsed") ? "false" : "true");
}
function eventBody(node) {
return node.querySelector(".event-body");
}
function eventCanCollapse(node) {
return node.classList.contains("can-collapse");
}
function refreshEventCollapseCapability(node) {
var body = eventBody(node);
var toggle = node.querySelector(".event-toggle");
if (!body) return;
var shouldCollapse = !node.classList.contains("event-ask-user")
&& body.scrollHeight > COLLAPSED_STEP_HEIGHT + 8;
node.classList.toggle("can-collapse", shouldCollapse);
if (toggle) toggle.hidden = !shouldCollapse;
if (!shouldCollapse) {
node.classList.remove("collapsed");
body.style.maxHeight = "none";
}
updateEventToggle(node);
}
function setEventExpanded(node, expanded, animate) {
var body = eventBody(node);
if (!body) {
node.classList.toggle("collapsed", !expanded);
updateEventToggle(node);
return;
}
refreshEventCollapseCapability(node);
if (!eventCanCollapse(node)) return;
if (expanded) {
node.classList.remove("collapsed");
body.style.maxHeight = body.scrollHeight + "px";
if (!animate) {
body.style.maxHeight = "none";
} else {
body.addEventListener("transitionend", function onEnd(event) {
if (event.propertyName !== "max-height") return;
body.removeEventListener("transitionend", onEnd);
if (!node.classList.contains("collapsed")) {
body.style.maxHeight = "none";
}
});
}
} else {
if (body.style.maxHeight === "none" || !body.style.maxHeight) {
body.style.maxHeight = body.scrollHeight + "px";
}
body.offsetHeight;
node.classList.add("collapsed");
body.style.maxHeight = COLLAPSED_STEP_HEIGHT + "px";
}
updateEventToggle(node);
}
function toggleEvent(node) {
if (node.classList.contains("latest") || !eventCanCollapse(node)) return;
setEventExpanded(node, node.classList.contains("collapsed"), true);
}
function addEvent(kind, title, bodyHtml, badges) {
var shouldFollow = autoFollowTimeline || isNearBottom();
ensureTimelineReady();
timeline.querySelectorAll(".event.latest").forEach(function (eventNode) {
eventNode.classList.remove("latest");
setEventExpanded(eventNode, false, true);
updateEventToggle(eventNode);
});
var badgeHtml = (badges || []).map(function (badge) {
return '<span class="badge">' + escapeHtml(badge) + "</span>";
}).join("");
var node = document.createElement("article");
node.className = "event event-" + kind + " latest";
node.innerHTML = ''
+ '<div class="event-head">'
+ '<div class="event-title">' + escapeHtml(title) + badgeHtml + '</div>'
+ '<button class="event-toggle" type="button" aria-label="Toggle step details"></button>'
+ '</div>'
+ '<div class="event-body"><div class="event-body-inner">' + bodyHtml + '</div></div>';
node.querySelector(".event-toggle").addEventListener("click", function (event) {
event.stopPropagation();
toggleEvent(node);
});
node.addEventListener("click", function () {
toggleEvent(node);
});
timeline.appendChild(node);
renderMathInMarkdown(node);
renderMermaidInMarkdown(node);
setEventExpanded(node, true, false);
scrollTimeline(shouldFollow);
}
function addMessage(kind, text, attachedImages) {
autoFollowTimeline = true;
ensureTimelineReady();
var node = document.createElement("article");
node.className = "message " + kind;
var imageHtml = "";
(attachedImages || []).forEach(function (image) {
imageHtml += '<img class="message-image" alt="" src="' + image.data_url + '">';
});
node.innerHTML = '<div class="message-body">'
+ (imageHtml ? '<div class="message-images">' + imageHtml + '</div>' : '')
+ '<pre>' + escapeHtml(text) + '</pre>'
+ '</div>';
timeline.appendChild(node);
scrollTimeline(true);
}
function formatJson(value) {
try {
return JSON.stringify(value, null, 2);
} catch (e) {
return String(value);
}
}
function renderTrace(row) {
if (!row || row.capture_type === "llm_call" || row.capture_type === "compaction") return;
var role = row.role || "";
var turn = row.turn_index || 0;
var text = row.text || "";
if (role === "system") return;
if (role === "user" && turn === 0) return;
if (role === "assistant") {
var tools = Array.isArray(row.tool_names) ? row.tool_names : [];
var args = Array.isArray(row.tool_arguments) ? row.tool_arguments : [];
var body = "";
if (text.trim()) {
body += (!tools.length && row.termination === "result")
? renderMarkdown(text)
: "<pre>" + escapeHtml(text) + "</pre>";
}
if (tools.length) {
body += '<div class="tool-grid">';
tools.forEach(function (name, idx) {
body += '<div class="tool-call"><div class="tool-name">' + escapeHtml(name)
+ '</div><pre>' + escapeHtml(formatJson(args[idx] || {})) + '</pre></div>';
});
body += "</div>";
}
if (!body) body = '<pre>(empty assistant output)</pre>';
if (row.error) body += '<pre class="error-text">' + escapeHtml(row.error) + "</pre>";
addEvent("assistant", "Assistant", body, ["round " + turn]);
return;
}
if (role === "tool") {
var toolName = Array.isArray(row.tool_names) && row.tool_names.length ? row.tool_names[0] : "Tool";
var toolBody = "<pre>" + escapeHtml(text) + "</pre>";
if (row.error) toolBody += '<pre class="error-text">' + escapeHtml(row.error) + "</pre>";
addEvent("tool", toolName + " result", toolBody, ["round " + turn]);
return;
}
if (role === "runtime") {
if (!text.trim() && !row.error && !row.termination) return;
var runtimeBody = "<pre>" + escapeHtml(text || row.termination || "") + "</pre>";
if (row.error) runtimeBody += '<pre class="error-text">' + escapeHtml(row.error) + "</pre>";
addEvent("runtime", "Runtime", runtimeBody, turn ? ["round " + turn] : []);
return;
}
if (role === "user") {
addEvent("runtime", "Runtime message", "<pre>" + escapeHtml(text) + "</pre>", ["round " + turn]);
}
}
function connect() {
var protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
ws = new WebSocket(protocol + "//" + window.location.host + "/ws");
ws.onopen = function () {
setStatus("Connected", "idle");
};
ws.onclose = function () {
fileToken = "";
setDownloadToken("");
updateWorkspaceHint(false);
clearAskRequest();
setRunning(false, "Disconnected");
setStatus("Disconnected", "error");
};
ws.onmessage = function (event) {
var message = JSON.parse(event.data);
if (message.type === "ready") {
setStatus("Connected", "idle");
} else if (message.type === "conversation_reset") {
fileToken = "";
setDownloadToken("");
updateWorkspaceHint(false);
if (keepSubmittedMessageOnReset) {
keepSubmittedMessageOnReset = false;
ensureTimelineReady();
} else {
clearTimeline();
}
conversationStarted = false;
clearAskRequest();
} else if (message.type === "uploaded_images") {
addEvent("runtime", "Uploaded images saved", "<pre>" + escapeHtml((message.paths || []).join("\n")) + "</pre>", []);
} else if (message.type === "run_started") {
fileToken = message.download_token || "";
setDownloadToken(message.download_token || "");
updateWorkspaceHint(Boolean(message.download_token));
setRunning(true, "Running");
} else if (message.type === "interrupt_requested") {
interrupting = true;
updateComposerMode();
setStatus("Interrupting", "running");
} else if (message.type === "trace") {
renderTrace(message.row);
} else if (message.type === "ask_user") {
showAskRequest(message);
} else if (message.type === "run_finished") {
conversationStarted = true;
setRunning(false, "Done");
clearAskRequest();
setStatus("Done", "done");
} else if (message.type === "run_error") {
keepSubmittedMessageOnReset = false;
clearAskRequest();
setRunning(false, "Error");
setStatus("Error", "error");
addEvent("runtime", "Error", '<pre class="error-text">' + escapeHtml(message.error || "unknown error") + "</pre>", []);
}
};
}
function showAskRequest(message) {
pendingAskId = message.request_id || "";
var question = message.question || "Question";
var context = message.context || "";
var body = "<pre>" + escapeHtml(question) + "</pre>";
if (context) body += '<pre class="muted-text">' + escapeHtml(context) + "</pre>";
addEvent("ask-user", "Agent question", body, ["AskUser"]);
setStatus("Waiting for input", "running");
updateComposerMode();
promptInput.focus();
}
function clearAskRequest() {
pendingAskId = "";
updateComposerMode();
}
function sendStart() {
if (pendingAskId) {
sendAskUserAnswer();
return;
}
if (!ws || ws.readyState !== WebSocket.OPEN) {
setStatus("Disconnected", "error");
return;
}
if (running) {
sendInterrupt();
return;
}
var prompt = promptInput.value.trim();
if (!prompt) return;
var sentImages = images.slice();
var continueConversation = conversationStarted;
if (!continueConversation) clearTimeline();
addMessage("user", prompt, sentImages);
keepSubmittedMessageOnReset = !continueConversation;
setRunning(true, "Starting");
ws.send(JSON.stringify({
type: "start",
prompt: prompt,
model_name: modelSelect ? modelSelect.value : "",
images: sentImages,
continue_conversation: continueConversation
}));
promptInput.value = "";
promptInput.style.height = "auto";
images = [];
renderImages();
}
function sendInterrupt() {
if (!running || interrupting || !ws || ws.readyState !== WebSocket.OPEN) return;
interrupting = true;
updateComposerMode();
setStatus("Interrupting", "running");
ws.send(JSON.stringify({ type: "interrupt" }));
}
function sendAskUserAnswer() {
if (!pendingAskId || !ws || ws.readyState !== WebSocket.OPEN) return;
var answer = promptInput.value.trim();
if (!answer) return;
var requestId = pendingAskId;
addMessage("user", answer, []);
ws.send(JSON.stringify({ type: "ask_user_answer", request_id: requestId, answer: answer }));
pendingAskId = "";
promptInput.value = "";
promptInput.style.height = "auto";
updateComposerMode();
setStatus("Running", "running");
}
function addImageFiles(fileList) {
Array.from(fileList || []).forEach(function (file) {
if (!file.type || !file.type.startsWith("image/")) return;
var reader = new FileReader();
reader.onload = function () {
images.push({ name: file.name, data_url: String(reader.result || "") });
renderImages();
};
reader.readAsDataURL(file);
});
}
function renderImages() {
imagePreview.innerHTML = "";
images.forEach(function (image, idx) {
var chip = document.createElement("button");
chip.type = "button";
chip.className = "image-chip";
chip.title = "Remove image";
chip.innerHTML = '<img alt="" src="' + image.data_url + '"><span>' + escapeHtml(image.name || "image") + "</span>";
chip.addEventListener("click", function () {
images.splice(idx, 1);
renderImages();
});
imagePreview.appendChild(chip);
});
}
setupModelDropdown();
runBtn.addEventListener("click", sendStart);
timeline.addEventListener("scroll", syncTimelineFollowMode);
timeline.addEventListener("wheel", function (event) {
if (event.deltaY < 0) autoFollowTimeline = false;
}, { passive: true });
timeline.addEventListener("touchmove", function () {
autoFollowTimeline = false;
}, { passive: true });
promptInput.addEventListener("keydown", function (event) {
if (event.isComposing) return;
if (event.key === "Enter" && !event.shiftKey && !event.ctrlKey && !event.metaKey) {
event.preventDefault();
sendStart();
}
});
promptInput.addEventListener("input", function () {
promptInput.style.height = "auto";
promptInput.style.height = Math.min(promptInput.scrollHeight, 180) + "px";
});
newBtn.addEventListener("click", function () {
if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: "new" }));
if (!running) {
promptInput.value = "";
images = [];
renderImages();
clearTimeline();
clearAskRequest();
conversationStarted = false;
setDownloadToken("");
updateWorkspaceHint(false);
setRunning(false, "Idle");
}
});
if (downloadWorkspaceBtn) {
downloadWorkspaceBtn.addEventListener("click", function () {
downloadWorkspaceZip();
});
updateDownloadWorkspaceButton();
}
attachBtn.addEventListener("click", function () {
imageInput.click();
});
imageInput.addEventListener("change", function (event) { addImageFiles(event.target.files); });
["dragenter", "dragover"].forEach(function (name) {
dropZone.addEventListener(name, function (event) {
event.preventDefault();
dropZone.classList.add("dragover");
});
});
["dragleave", "drop"].forEach(function (name) {
dropZone.addEventListener(name, function (event) {
event.preventDefault();
dropZone.classList.remove("dragover");
});
});
dropZone.addEventListener("drop", function (event) {
addImageFiles(event.dataTransfer.files);
});
document.addEventListener("paste", function (event) {
var files = [];
Array.from(event.clipboardData ? event.clipboardData.items : []).forEach(function (item) {
if (item.kind === "file") {
var file = item.getAsFile();
if (file) files.push(file);
}
});
if (files.length) addImageFiles(files);
});
connect();
})();