import { ref, triggerRef, watch, computed, nextTick, onUnmounted, defineComponent, mergeProps, useSSRContext, createSlots, withCtx, openBlock, createBlock, createCommentVNode, toDisplayString, Fragment, renderList, createVNode, unref, createTextVNode, resolveDynamicComponent, withModifiers, onUpdated, toRef, onMounted, resolveComponent } from "vue"; import { ssrRenderAttrs, ssrRenderSlot, ssrRenderComponent, ssrRenderAttr, ssrInterpolate, ssrRenderList, ssrRenderVNode, ssrRenderClass, ssrIncludeBooleanAttr, ssrRenderStyle } from "vue/server-renderer"; import { useRouter, useRoute } from "vue-router"; import { u as useChatStore, w as ws, a as agents, g as getApiBase, b as auth, _ as _export_sfc, c as useTtsPlayer, d as useUI, e as agentLogo, s as scrollbarOptions } from "../main.mjs"; import { marked } from "marked"; import { OverlayScrollbarsComponent } from "overlayscrollbars-vue"; import { BoltIcon, ComputerDesktopIcon, CpuChipIcon, GlobeAltIcon, DocumentPlusIcon, WrenchIcon, PencilIcon, BookOpenIcon, ChatBubbleLeftIcon, LinkIcon, Cog6ToothIcon, SpeakerWaveIcon, ExclamationTriangleIcon, ChevronDownIcon, InformationCircleIcon, LockClosedIcon, UserGroupIcon, ChatBubbleBottomCenterTextIcon, PaperClipIcon, MicrophoneIcon, StopIcon, ArrowUpIcon } from "@heroicons/vue/20/solid"; import "@unhead/vue/server"; import "pinia"; import "@heroicons/vue/24/outline"; import "overlayscrollbars"; const VISIBLE_PAGE$1 = 50; const MAX_HUD_NODES = 100; function useSessionHistory(isAgentRunning, visibleCount, agentIdFn) { const store = useChatStore(); const sessionHistoryComplete = ref(false); const lastUsage = ref(null); let loadStartTime = null; let pendingMessages = []; let pendingUsageTotals = null; const lastSystemMsgRef = ref(null); const hudTree = ref([]); const hudVersion = ref(0); const hudPending = /* @__PURE__ */ new Map(); const hudTurns = /* @__PURE__ */ new Map(); const toolCallMap = /* @__PURE__ */ new Map(); function lookupByToolCallId(toolCallId) { if (!toolCallId) return null; const ref2 = toolCallMap.get(toolCallId); if (!ref2) return null; const node = ref2.deref(); if (!node) { toolCallMap.delete(toolCallId); return null; } return node; } const activeTurnCorrId = ref(null); function makeNode(partial) { return { id: partial.id || crypto.randomUUID(), type: partial.type || "received", state: partial.state || "running", label: partial.label || "", children: [], replay: partial.replay ?? false, startedAt: partial.startedAt || Date.now(), ...partial }; } function findNode(nodes, corrId) { for (const n of nodes) { if (n.correlationId === corrId) return n; if (n.children) { const found = findNode(n.children, corrId); if (found) return found; } } return null; } function addHudNode(node, parentCorrelationId) { if (parentCorrelationId && parentCorrelationId !== "history") { const parent = hudTurns.get(parentCorrelationId); if (parent) { parent.children.push(node); triggerRef(hudTree); hudVersion.value++; return; } } hudTree.value.unshift(node); if (hudTree.value.length > MAX_HUD_NODES) hudTree.value.splice(MAX_HUD_NODES); triggerRef(hudTree); hudVersion.value++; } function pushHudEvent(event) { const replay = !!event.replay; const ts = event.ts || Date.now(); const corrId = event.correlationId; const parentId = event.parentId; switch (event.event) { case "turn_start": { if (corrId && (hudPending.has(corrId) || hudTurns.has(corrId))) break; const node = makeNode({ type: "turn", state: "running", label: "🔄 Turn", correlationId: corrId, startedAt: ts, replay }); if (corrId) { hudPending.set(corrId, node); hudTurns.set(corrId, node); } if (!replay && corrId) activeTurnCorrId.value = corrId; addHudNode(node); break; } case "turn_end": { const node = corrId ? hudPending.get(corrId) : null; if (node) { node.state = "done"; node.endedAt = ts; node.durationMs = event.durationMs; if (corrId) { hudPending.delete(corrId); hudTurns.delete(corrId); } triggerRef(hudTree); hudVersion.value++; } if (!replay && corrId && activeTurnCorrId.value === corrId) activeTurnCorrId.value = null; break; } case "think_start": { const node = makeNode({ type: "think", state: "running", label: "Thinking", correlationId: corrId, startedAt: ts, replay }); if (corrId) hudPending.set(corrId, node); addHudNode(node, parentId); break; } case "think_end": { const node = corrId ? hudPending.get(corrId) : null; if (node) { node.state = "done"; node.endedAt = ts; node.durationMs = event.durationMs; if (corrId) hudPending.delete(corrId); triggerRef(hudTree); hudVersion.value++; } else { addHudNode(makeNode({ type: "think", state: "done", label: "Thinking", correlationId: corrId, startedAt: ts, endedAt: ts, durationMs: event.durationMs, replay }), parentId); } break; } case "tool_start": { const tool = event.tool || "unknown"; const args = event.args || {}; const label = buildToolLabel(tool, args); const node = makeNode({ type: "tool", state: "running", label, tool, args, correlationId: corrId, startedAt: ts, replay }); if (corrId) hudPending.set(corrId, node); if (event.toolCallId) toolCallMap.set(event.toolCallId, new WeakRef(node)); addHudNode(node, parentId); lastSystemMsgRef.value = label; break; } case "tool_end": { const tool = event.tool || "unknown"; const result = event.result || {}; let node = lookupByToolCallId(event.toolCallId) ?? (corrId ? hudPending.get(corrId) : null); if (!node && parentId) { const turnNode = findNode(hudTree.value, parentId); if (turnNode?.children) { const match = turnNode.children.find( (n) => n.type === "tool" && n.state === "running" && (n.tool === tool || n.tool === "unknown") ); if (match) { node = match; if (match.correlationId) hudPending.delete(match.correlationId); } } } if (node) { node.state = result.ok === false ? "error" : "done"; node.result = result; node.endedAt = ts; node.durationMs = event.durationMs; node.label = buildToolLabel(tool, node.args || {}, result); if (corrId) hudPending.delete(corrId); if (event.toolCallId) toolCallMap.delete(event.toolCallId); triggerRef(hudTree); hudVersion.value++; } else { const label = buildToolLabel(tool, event.args || {}, result); addHudNode(makeNode({ type: "tool", state: "done", label, tool, result, correlationId: corrId, startedAt: ts, endedAt: ts, durationMs: event.durationMs, replay }), parentId); } break; } case "received": { const node = makeNode({ type: "received", state: "done", subtype: event.subtype, label: event.label || event.subtype || "received", startedAt: ts, endedAt: ts, replay }); addHudNode(node); break; } } } function buildToolLabel(tool, args, result) { const fileTools = ["read", "write", "edit", "append"]; if (fileTools.includes(tool)) { const vp = args.viewerPath || args.path || ""; const filename = vp.split("/").pop() || vp; const area = result?.area || args.area; const areaStr = area ? `:L${area.startLine}–${area.endLine}` : ""; return `${filename}${areaStr}`; } if (tool === "exec") { const cmd = args.command || ""; return `${cmd.slice(0, 60)}${cmd.length > 60 ? "…" : ""}`; } if (tool === "web_fetch") return (args.url || "").slice(0, 60); if (tool === "web_search") return (args.query || "").slice(0, 60); return tool; } function getToolsForTurn(corrId) { if (!corrId) return []; const turn = hudTree.value.find((n) => n.correlationId === corrId) ?? [...hudTurns.values()].find((n) => n.correlationId === corrId); return turn ? turn.children.filter((c) => c.type === "tool") : []; } function hudSnapshot() { const lines = [`HUD tree — ${hudTree.value.length} root node(s) `]; for (const node of hudTree.value) { const dur = node.durationMs != null ? ` [${node.durationMs}ms]` : ""; const repl = node.replay ? " (replay)" : ""; lines.push(` ${node.state === "running" ? "⏳" : node.state === "error" ? "❌" : "✅"} [${node.type}] ${node.label}${dur}${repl}`); lines.push(` id=${node.id.slice(0, 8)} corrId=${(node.correlationId || "—").slice(0, 8)} children=${node.children.length}`); for (const child of node.children) { const cdur = child.durationMs != null ? ` [${child.durationMs}ms]` : ""; lines.push(` ${child.state === "running" ? "⏳" : child.state === "error" ? "❌" : "✅"} [${child.type}] ${child.label}${cdur}`); if (child.args) lines.push(` args: ${JSON.stringify(child.args).slice(0, 80)}`); if (child.result) lines.push(` result: ${JSON.stringify(child.result).slice(0, 80)}`); } } return lines.join("\n"); } function pushSystem(text) { lastSystemMsgRef.value = text; } function flushPendingClear(pendingClearRef) { if (!pendingClearRef.value) return; pendingClearRef.value = false; store.clearMessages(); visibleCount.value = VISIBLE_PAGE$1; sessionHistoryComplete.value = false; loadStartTime = performance.now(); pendingMessages = []; pendingUsageTotals = null; } function revealMessages() { loadStartTime = null; const _pending = pendingMessages; const _usage = pendingUsageTotals; pendingMessages = []; pendingUsageTotals = null; const idx = store.messages.findIndex((m) => m.role === "system" && m.content.includes("Loading session history...")); if (idx !== -1) { store.messages.splice(idx, 1, ..._pending); } else { store.messages.unshift(..._pending); } const revealedCount = _pending.filter((m) => m.role !== "system").length; store.sessionContextHint = revealedCount > 0 ? `${revealedCount} msgs in context` : "fresh context"; if (_usage) lastUsage.value = _usage; } function handleSessionHistory(entries) { if (!entries?.length) return; if (loadStartTime === null) loadStartTime = performance.now(); if (!store.messages.some((m) => m.content?.includes("Loading session history..."))) { store.pushSystem("⏳ Loading session history...", agentIdFn()); } const newMsgs = []; const currentAgentId = agentIdFn(); const currentSessionId = store.localSessionId; let pendingUsage = null; for (const data of entries) { if (data.type === "hud") { pushHudEvent({ ...data, replay: true }); continue; } if (data.event === "tool_start" || data.event === "tool_end" || data.event === "think_start" || data.event === "think_end" || data.event === "turn_start" || data.event === "turn_end" || data.event === "received") { pushHudEvent({ ...data, replay: true }); continue; } if (data.entry_type === "session_context") { newMsgs.push({ role: "session_context", content: data.content || "", agentId: currentAgentId, sessionId: currentSessionId }); } else if (data.entry_type === "user_message") { newMsgs.push({ role: "user", content: data.content || "", agentId: currentAgentId, sessionId: currentSessionId }); } else if (data.entry_type === "assistant_text") { const content = (data.content || "").replace(/^\[\[reply_to[^\]]*\]\]\s*/i, "").trim(); if (!content) continue; const msg = { role: "assistant", content, streaming: false, agentId: currentAgentId, sessionId: currentSessionId, timestamp: data.ts || null }; if (data.truncated) msg.truncated = true; if (pendingUsage) { msg.usage = pendingUsage; pendingUsage = null; } newMsgs.push(msg); } else if (data.entry_type === "usage") { pendingUsage = { input_tokens: data.input_tokens || 0, output_tokens: data.output_tokens || 0, total_tokens: data.total_tokens || 0, cost: Number(data.cost || 0) }; const last = newMsgs[newMsgs.length - 1]; if (last?.role === "assistant") { last.usage = pendingUsage; pendingUsage = null; } } } pendingMessages = newMsgs; const totalUsage = entries.filter((e) => e.entry_type === "usage").reduce((acc, e) => ({ input_tokens: acc.input_tokens + (e.input_tokens || 0), output_tokens: acc.output_tokens + (e.output_tokens || 0), total_tokens: acc.total_tokens + (e.total_tokens || 0), cost: acc.cost + Number(e.cost || 0) }), { input_tokens: 0, output_tokens: 0, total_tokens: 0, cost: 0 }); pendingUsageTotals = totalUsage.total_tokens > 0 ? totalUsage : null; } function handleSessionEntry(data, sentMessages, pushSystemFn) { if (data.type === "hud") { pushHudEvent(data); return; } const isReplay = !sessionHistoryComplete.value; const currentAgentId = agentIdFn(); const currentSessionId = store.localSessionId; switch (data.entry_type) { case "user_message": { const raw = data.content || ""; if (raw.startsWith("A new session was started")) break; if (!isReplay && raw.includes("[voice transcript]:")) break; const hasByMsgId = data.msgId && store.messages.some((m) => m.msgId === data.msgId); if (hasByMsgId) break; if (!isReplay && !sentMessages.has(raw.trim())) { store.messages.push({ role: "user", content: raw, agentId: currentAgentId, sessionId: currentSessionId, msgId: data.msgId }); } else { sentMessages.delete(raw.trim()); } break; } } } function resetHudMaps() { hudPending.clear(); hudTurns.clear(); toolCallMap.clear(); activeTurnCorrId.value = null; hudTree.value = []; hudVersion.value++; } function toolCallMapSnapshot() { return [...toolCallMap.entries()].map(([k, ref2]) => { const node = ref2.deref(); return { toolCallId: k, label: node?.label ?? null, state: node?.state ?? null, stale: !node }; }); } return { sessionHistoryComplete, lastUsage, lastSystemMsgRef, hudTree, hudVersion, activeTurnCorrId, getToolsForTurn, pushHudEvent, hudSnapshot, toolCallMapSnapshot, resetHudMaps, flushPendingClear, revealMessages, handleSessionHistory, handleSessionEntry, pushSystem }; } function useAgentSocket(visibleCount, lastUsage, pendingClearRef, sentMessages, restoreLastSent) { const chatStore = useChatStore(); const { send: wsSend, onMessage: onWsMessage, replayBuffer } = ws; const { updateFromServer, selectedAgent } = agents; const handoverInProgress = () => chatStore.smState === "HANDOVER_PENDING" || chatStore.smState === "HANDOVER_DONE"; const isAgentRunning = () => chatStore.smState === "AGENT_RUNNING"; const history = useSessionHistory(isAgentRunning, visibleCount, () => selectedAgent.value); history.lastUsage = lastUsage; watch(history.activeTurnCorrId, (id) => { chatStore.activeTurnCorrId = id; }); function pushSystem(text) { history.pushSystem(text); } function mount() { const handlers = { auth_ok(data) { updateFromServer(data); }, ready(data) { updateFromServer(data); if (data.session_id) chatStore.sessionKey = data.session_id; else if (data.sessionId) chatStore.sessionKey = data.sessionId; chatStore.applyConnectionState("SYNCED"); chatStore.applyChannelState("READY"); }, // assay session info — store session ID for reconnect session_info(data) { if (data.session_id) chatStore.sessionKey = data.session_id; }, // assay UI controls — store for later rendering controls(_data) { }, // assay artifacts — store for later rendering artifacts(_data) { }, // assay session cleared cleared(_data) { chatStore.messages.splice(0); }, thinking(data) { if (!handoverInProgress()) chatStore.appendThinking(data.content); }, delta(data) { if (!handoverInProgress()) { history.flushPendingClear(pendingClearRef); chatStore.collapseThinking(); chatStore.appendAssistantDelta(data.content, data.agentId); } }, message(data) { if (handoverInProgress()) return; history.flushPendingClear(pendingClearRef); if (data.streaming === false) { chatStore.createCompleteAssistantMessage(data.content, data.agentId, data.usage); } else if (data.final) { chatStore.finalizeAssistantMessage(null, data.usage); } }, truncated_warning(_data) { chatStore.collapseThinking(); if (chatStore.hasActiveStreamingMessage()) chatStore.finalizeAssistantMessage(null, void 0, true); chatStore.truncatedWarning = true; }, done(data) { if (handoverInProgress()) return; chatStore.collapseThinking(); if (data.suppress) { chatStore.suppressAssistantMessage(); } else { const doneContent = data.content || null; if (chatStore.hasActiveStreamingMessage()) { const deltaLen = chatStore.streamingMessageLength(); const useContent = doneContent && deltaLen < doneContent.length ? doneContent : null; chatStore.finalizeAssistantMessage(useContent, data.usage); } else if (doneContent) { chatStore.createCompleteAssistantMessage(doneContent, void 0, data.usage); } } history.lastSystemMsgRef.value = null; }, session_history(data) { if (chatStore.hasActiveStreamingMessage()) chatStore.finalizeAssistantMessage(null); chatStore.collapseThinking(); history.flushPendingClear(pendingClearRef); history.handleSessionHistory(data.entries); }, hud(data) { history.pushHudEvent(data); if (data.event === "turn_start" && !data.replay) { chatStore.activeTurnCorrId = data.correlationId ?? null; chatStore.startNewAssistantMessage(selectedAgent.value); } }, event(data) { if (data.event === "agent") { const stream = data.payload?.stream; if (stream === "tool") { console.log("[HUD agent/tool]", JSON.stringify(data.payload).slice(0, 400)); } } }, tool(data) { if (data.action === "call") { pushSystem(`${data.tool} ${data.args || ""}`); } else if (data.action === "result") { pushSystem(`→ ${data.result || ""}`); } }, session_entry(data) { history.handleSessionEntry(data, sentMessages, pushSystem); }, message_update(data) { if (!data.msgId || !data.patch) return; const patch = { ...data.patch }; if (patch.voiceAudioUrl) { const token = localStorage.getItem("nyx_session") || localStorage.getItem("titan_token") || ""; patch.voiceAudioUrl = `${patch.voiceAudioUrl}?token=${encodeURIComponent(token)}`; } if (patch.transcript) { sentMessages.add(`[voice transcript]: ${patch.transcript}`.trim()); } const patched = chatStore.patchMessage(data.msgId, patch); if (!patched) { console.warn("[message_update] no message found for msgId:", data.msgId); } }, handover_done(data) { chatStore.messages.push({ role: "assistant", content: data.content || "📋 Handover written.", agentId: selectedAgent.value, sessionId: chatStore.localSessionId }); chatStore.messages.push({ role: "system", content: "Start a new session with this handover context?", confirmNew: true, confirmed: false, agentId: selectedAgent.value, sessionId: chatStore.localSessionId }); }, handover_context(_data) { }, // New two-SM protocol: channel_state + connection_state channel_state(data) { if (!data.state) return; if (data.clear_history) { console.log("[clear] channel switch", { state: data.state, msgCount: chatStore.messages.length }); history.resetHudMaps(); chatStore.messages.length = 0; pendingClearRef.value = false; } const prevChannel = chatStore.channelState; chatStore.applyChannelState(data.state); if (data.state === "READY" || data.state === "FRESH") { history.lastSystemMsgRef.value = null; if (chatStore.queuedThought !== null && prevChannel === "AGENT_RUNNING") { const thought = chatStore.queuedThought; chatStore.queuedThought = null; wsSend({ type: "message", content: thought }); } } }, connection_state(data) { if (!data.state) return; chatStore.applyConnectionState(data.state); if (data.state === "LOADING_HISTORY") { history.resetHudMaps(); chatStore.messages.length = 0; pendingClearRef.value = false; } if (data.state === "SYNCED") { history.flushPendingClear(pendingClearRef); history.revealMessages(); } }, // Legacy: still handle session_state for backward compat session_state(data) { if (!data.state) return; if (data.reconnected) ; if (data.reconnected || data.clear_history) { console.log("[clear] immediate flush", { reconnected: data.reconnected, clear_history: data.clear_history, state: data.state, msgCount: chatStore.messages.length }); chatStore.stashMessages(); history.resetHudMaps(); chatStore.messages.length = 0; pendingClearRef.value = false; } chatStore.applySessionState(data.state); if (data.state === "READY" || data.state === "FRESH" || data.state === "IDLE") { history.lastSystemMsgRef.value = null; if (chatStore.queuedThought !== null) { const thought = chatStore.queuedThought; chatStore.queuedThought = null; wsSend({ type: "message", content: thought }); } } }, session_total_tokens(data) { chatStore.sessionTotalTokens = data; }, finance_update(data) { chatStore.finance = data; }, usage(data) { if (!handoverInProgress()) { chatStore.sessionTotalTokens = { input_tokens: data.input_tokens || (chatStore.sessionTotalTokens?.input_tokens || 0), cache_read_tokens: data.cache_read_tokens || (chatStore.sessionTotalTokens?.cache_read_tokens || 0), output_tokens: data.output_tokens || (chatStore.sessionTotalTokens?.output_tokens || 0) }; } }, session_status(data) { if (data.status === "no_session") { pendingClearRef.value = true; history.flushPendingClear(pendingClearRef); chatStore.messages.push({ role: "system", type: "no_session", content: "-- NO SESSION --", agentId: selectedAgent.value, sessionId: chatStore.localSessionId }); chatStore.sessionContextHint = ""; } else if (data.status === "watching") { history.flushPendingClear(pendingClearRef); history.sessionHistoryComplete.value = true; history.revealMessages(); } }, sent(_data) { }, switch_ok(data) { history.sessionHistoryComplete.value = false; if (data.sessionKey) chatStore.sessionKey = data.sessionKey; }, new_ok(_data) { history.sessionHistoryComplete.value = false; }, error(data) { if (data.code === "SESSION_TERMINATED") { chatStore.pushSystem("⚠️ Message not delivered — session was resetting. Please try again.", selectedAgent.value); restoreLastSent?.(); } else if (data.code === "DISCARDED_NOT_IDLE" || data.code === "DISCARDED_NOT_READY") { chatStore.pushSystem("⚠️ Message not delivered — agent was busy. Please try again.", selectedAgent.value); restoreLastSent?.(); } }, stopped(_data) { chatStore.pushSystem("✅ Agent stopped", selectedAgent.value); }, killed(_data) { chatStore.pushSystem("☠️ Agent killed", selectedAgent.value); } }; const unsubscribe = onWsMessage((data) => { const handler = handlers[data.type]; if (handler) handler(data); }); replayBuffer((data) => { const handler = handlers[data.type]; if (handler) handler(data); }); return unsubscribe; } return { mount, lastSystemMsg: history.lastSystemMsgRef, hudTree: history.hudTree, hudVersion: history.hudVersion, getToolsForTurn: history.getToolsForTurn, hudSnapshot: history.hudSnapshot, toolCallMapSnapshot: history.toolCallMapSnapshot, sessionHistoryComplete: history.sessionHistoryComplete, pushSystem, hasActiveStreamingMessage: chatStore.hasActiveStreamingMessage }; } function generateMsgId() { return crypto.randomUUID(); } const renderer = new marked.Renderer(); renderer.link = ({ href, title, text }) => { const titleAttr = title ? ` title="${title}"` : ""; return `${text}`; }; function ansiToHtml(text) { const colorMap = { 30: "#555", 31: "#e06c75", 32: "#98c379", 33: "#e5c07b", 34: "#61afef", 35: "#c678dd", 36: "#56b6c2", 37: "#abb2bf" }; let open = false, bold = false, dim = false; const result = text.replace(/\x1b\[([0-9;]*)m/g, (_match, codes) => { const parts = codes.split(";").map(Number); let out = ""; for (const code of parts) { if (code === 0) { if (open) { out += ""; open = false; } if (bold) { out += ""; bold = false; } if (dim) { out += ""; dim = false; } } else if (code === 1) { if (!bold) { out += ""; bold = true; } } else if (code === 2) { if (!dim) { out += ''; dim = true; } } else if (colorMap[code]) { if (open) { out += ""; } out += ``; open = true; } } return out; }); let tail = ""; if (open) tail += ""; if (bold) tail += ""; if (dim) tail += ""; return result + tail; } const WORKSPACE_PATH_RE = /(workspace\/[^\s"'<>)]+\.(?:pdf|png|jpg|jpeg|gif|csv|json|txt|md|html|zip|mp3|wav|ogg|webm|m4a))/g; const WORKSPACE_PREFIX = ""; const AUDIO_EXTENSIONS = /* @__PURE__ */ new Set(["mp3", "wav", "ogg", "webm", "m4a"]); function linkifyWorkspaceFiles(html) { return html.replace(WORKSPACE_PATH_RE, (match) => { const name = match.split("/").pop() || match; const absPath = match.startsWith("/") ? match : WORKSPACE_PREFIX + match; const ext = name.split(".").pop()?.toLowerCase() || ""; if (AUDIO_EXTENSIONS.has(ext)) { return ``; } return ``; }); } if (typeof window !== "undefined" && !window.__hermesAudioSrc) { window.__hermesAudioSrc = (el) => { if (el.src) return; const filepath = el.dataset.filepath; if (!filepath) return; const token = localStorage.getItem("nyx_session") || localStorage.getItem("titan_token") || ""; const apiBase = getApiBase(); el.src = `${apiBase}/api/files${filepath}?token=${encodeURIComponent(token)}`; }; } if (typeof window !== "undefined" && !window.__hermesDownload) { window.__hermesDownload = async (el) => { const filepath = el.dataset.filepath; const filename = el.dataset.filename || "download"; if (!filepath) return; el.textContent = "⏳ " + filename; try { const token = localStorage.getItem("nyx_session") || localStorage.getItem("titan_token") || ""; const apiBase2 = getApiBase(); const res = await fetch(`${apiBase2}/api/files${filepath}?token=${encodeURIComponent(token)}`); if (!res.ok) throw new Error(`${res.status}`); const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); el.textContent = "✅ " + filename; } catch (err) { el.textContent = "❌ " + filename; console.error("[download]", err); } }; } function parseMd(content) { const raw = content || ""; if (/\x1b\[/.test(raw)) { const escaped = raw.replace(/&/g, "&").replace(//g, ">"); return `
${ansiToHtml(escaped)}`;
}
let html = marked.parse(raw, { renderer, async: false, gfm: true, breaks: true });
html = linkifyWorkspaceFiles(html);
return html;
}
const DRAFT_KEY = "chat_draft";
const HISTORY_KEY = "chat_input_history";
const HISTORY_MAX = 50;
function loadDraft() {
try {
return sessionStorage.getItem(DRAFT_KEY) || "";
} catch {
return "";
}
}
function saveDraft(v) {
try {
if (v) sessionStorage.setItem(DRAFT_KEY, v);
else sessionStorage.removeItem(DRAFT_KEY);
} catch {
}
}
function loadHistory() {
try {
return JSON.parse(sessionStorage.getItem(HISTORY_KEY) || "[]");
} catch {
return [];
}
}
function pushHistory(v) {
try {
const h = loadHistory().filter((x) => x !== v);
h.unshift(v);
sessionStorage.setItem(HISTORY_KEY, JSON.stringify(h.slice(0, HISTORY_MAX)));
} catch {
}
}
function useMessages(wsSendFn) {
const store = useChatStore();
const sending = ref(false);
const input = ref(loadDraft());
const messagesEl = ref(null);
let historyIdx = -1;
function getViewport() {
const el = messagesEl.value;
if (!el) return null;
const root = el.$el || el;
return root.querySelector?.("[data-overlayscrollbars-viewport]") || root;
}
function scrollToBottom() {
nextTick(() => {
nextTick(() => {
requestAnimationFrame(() => {
const vp = getViewport();
if (vp) vp.scrollTop = vp.scrollHeight;
});
});
});
}
function scrollIfAtBottom() {
const vp = getViewport();
if (!vp) return;
if (vp.scrollHeight - vp.scrollTop - vp.clientHeight < 80) scrollToBottom();
}
let draftTimer = null;
function onInputChange() {
if (draftTimer) clearTimeout(draftTimer);
draftTimer = setTimeout(() => saveDraft(input.value), 1e3);
}
function navigateHistory(dir) {
const history = loadHistory();
if (!history.length) return;
if (dir === "up") {
historyIdx = Math.min(historyIdx + 1, history.length - 1);
} else {
historyIdx = Math.max(historyIdx - 1, -1);
}
input.value = historyIdx === -1 ? "" : history[historyIdx];
}
let lastSentContent = "";
function restoreLastSent() {
input.value = lastSentContent;
}
async function send(attachmentPayload) {
const hasText = input.value.trim().length > 0;
const hasAttachments = attachmentPayload && attachmentPayload.length > 0;
if (!hasText && !hasAttachments || sending.value) return;
const content = input.value.trim();
if (hasText) {
lastSentContent = content;
pushHistory(content);
}
historyIdx = -1;
input.value = "";
saveDraft("");
sending.value = true;
const msgId = generateMsgId();
const localAttachments = hasAttachments ? attachmentPayload.map((a) => {
const mime = a.mimeType.split(";")[0];
let dataUrl;
if (a.mimeType.startsWith("audio/")) {
const bytes = Uint8Array.from(atob(a.content), (c) => c.charCodeAt(0));
dataUrl = URL.createObjectURL(new Blob([bytes], { type: mime }));
} else {
dataUrl = `data:${mime};base64,${a.content}`;
}
return { mimeType: a.mimeType, fileName: a.fileName, dataUrl };
}) : void 0;
const hasAudio = hasAttachments && attachmentPayload.some((a) => a.mimeType.startsWith("audio/"));
store.pushMessage({
role: "user",
content,
agentId: null,
msgId,
attachments: localAttachments,
pending: hasAudio
// audio messages are pending until transcript arrives
});
const payload = { type: "message", content, msgId };
if (hasAttachments) payload.attachments = attachmentPayload;
wsSendFn(payload);
sending.value = false;
}
return {
sending,
input,
messagesEl,
parseMd,
scrollToBottom,
scrollIfAtBottom,
send,
onInputChange,
navigateHistory,
restoreLastSent,
startNewAssistantMessage: store.startNewAssistantMessage,
appendAssistantMessage: store.appendAssistantDelta,
finalizeAssistantMessage: store.finalizeAssistantMessage,
resetAssistantMessageState: store.resetLocalSession,
hasActiveStreamingMessage: store.hasActiveStreamingMessage,
streamingMessageVisibleContent: computed(() => store.streamingMessageVisibleContent)
};
}
function useMessageGrouping(messages, visibleCount, selectedAgent, allAgents, sessionKey) {
const VISIBLE_PAGE2 = 50;
const visibleMsgs = computed(() => {
const all = messages.value;
const start = Math.max(0, all.length - visibleCount.value);
return all.slice(start).map((m, i) => ({ ...m, _sourceIndex: start + i }));
});
const hasMore = computed(() => messages.value.length > visibleCount.value);
function loadMore() {
visibleCount.value += VISIBLE_PAGE2;
}
function getFormattedAgentName(agentId) {
if (!agentId) return "Unknown";
const agent = allAgents.value.find((a) => a.id === agentId);
return agent ? agent.name : agentId;
}
function shouldShowHeadline(index, msgsArr) {
if (index === 0) return true;
const current = msgsArr[index];
const prev = msgsArr[index - 1];
if (!current.agentId || !prev.agentId) {
return current.sessionId !== prev.sessionId;
}
return current.agentId !== prev.agentId || current.sessionId !== prev.sessionId;
}
function formatHeadlineText(agentName) {
const key = sessionKey?.value;
return key ? `${agentName} · ${key}` : agentName;
}
function getHeadline(index, msgsArr) {
const current = msgsArr[index];
const targetAgentId = current.agentId || selectedAgent.value;
const agentName = getFormattedAgentName(targetAgentId);
if (index === 0) return { text: formatHeadlineText(agentName), kind: "agent" };
const prev = msgsArr[index - 1];
if (current.agentId !== prev.agentId) return { text: formatHeadlineText(agentName), kind: "agent" };
if (current.sessionId !== prev.sessionId) return { text: "New Session", kind: "new-session" };
return { text: formatHeadlineText(agentName), kind: "agent" };
}
const groupedVisibleMsgs = computed(() => {
const raw = visibleMsgs.value;
const result = [];
let currentGroup = null;
for (let i = 0; i < raw.length; i++) {
const msg = raw[i];
if (shouldShowHeadline(i, raw)) {
if (currentGroup) {
result.push(currentGroup);
currentGroup = null;
}
const { text, kind } = getHeadline(i, raw);
result.push({
role: "system",
type: "headline",
content: text,
headlineKind: kind,
agentId: msg.agentId,
sessionId: msg.sessionId,
position: "header"
// Header appears before agent block
});
}
if (msg.role === "system" && msg.type !== "no_session") {
if (!currentGroup) {
currentGroup = { role: "system_group", messages: [msg], agentId: msg.agentId, sessionId: msg.sessionId };
} else {
currentGroup.messages.push(msg);
}
} else {
if (currentGroup) {
result.push(currentGroup);
currentGroup = null;
}
result.push(msg);
const effectiveAgentId = msg.agentId || selectedAgent.value;
if (effectiveAgentId && i === raw.length - 1) {
const agentName = getFormattedAgentName(effectiveAgentId);
result.push({
role: "system",
type: "headline",
content: formatHeadlineText(agentName),
headlineKind: "agent",
agentId: effectiveAgentId,
sessionId: msg.sessionId,
position: "footer"
});
}
}
}
if (currentGroup) result.push(currentGroup);
return result;
});
return {
visibleMsgs,
groupedVisibleMsgs,
hasMore,
loadMore,
getFormattedAgentName
};
}
function useInputAutogrow(input) {
const inputEl = ref(null);
const isShaking = ref(false);
function autoGrow() {
const el = inputEl.value;
if (!el) return;
el.style.height = "auto";
el.style.height = el.scrollHeight + "px";
el.style.overflowY = el.scrollHeight > 160 ? "auto" : "hidden";
}
function triggerShake() {
isShaking.value = true;
setTimeout(() => {
isShaking.value = false;
}, 400);
}
watch(input, (val) => {
if (!val) nextTick(() => autoGrow());
});
return { inputEl, isShaking, autoGrow, triggerShake };
}
function useAgentDisplay(selectedAgent, defaultAgent, allAgents) {
const chatStore = useChatStore();
const defaultAgentName = computed(() => {
const agent = allAgents.value.find((a) => a.id === defaultAgent.value);
return agent ? agent.name : defaultAgent.value;
});
const agentDisplayName = computed(() => {
const agent = allAgents.value.find((a) => a.id === selectedAgent.value);
return (agent ? agent.name : selectedAgent.value).toUpperCase();
});
const isAgentRunning = computed(() => chatStore.smState === "AGENT_RUNNING");
const agentStatusDone = computed(() => chatStore.channelState === "READY" || chatStore.channelState === "FRESH");
const agentStatus = computed(() => {
switch (chatStore.smState) {
case "CONNECTING":
return "⚙️ Connecting…";
case "AGENT_RUNNING":
return "⚙️ Working…";
case "HANDOVER_PENDING":
return "📝 Writing handover…";
case "HANDOVER_DONE":
return "✅ Handover ready";
case "SWITCHING":
return "🔀 Switching…";
default:
return null;
}
});
return { defaultAgentName, agentDisplayName, isAgentRunning, agentStatusDone, agentStatus };
}
const ACCEPTED_TYPES = [
"image/jpeg",
"image/png",
"image/gif",
"image/webp",
"application/pdf",
"audio/webm",
"audio/mp4",
"audio/ogg",
"audio/mpeg",
"audio/wav",
"audio/x-m4a"
];
const MAX_BYTES = 10 * 1024 * 1024;
function readAsBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const result = reader.result;
const idx = result.indexOf(",");
resolve(idx >= 0 ? result.slice(idx + 1) : result);
};
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(file);
});
}
function useAttachments() {
const attachments = ref([]);
async function addFiles(files) {
for (const file of Array.from(files)) {
const baseType = file.type.split(";")[0];
if (!ACCEPTED_TYPES.includes(baseType) && !ACCEPTED_TYPES.includes(file.type)) {
console.warn(`[attachments] skipped ${file.name}: unsupported type ${file.type}`);
continue;
}
if (file.size > MAX_BYTES) {
console.warn(`[attachments] skipped ${file.name}: exceeds 5MB (${(file.size / 1024 / 1024).toFixed(1)}MB)`);
continue;
}
const base64 = await readAsBase64(file);
const preview = URL.createObjectURL(file);
attachments.value.push({ file, preview, base64, mimeType: file.type, fileName: file.name });
}
}
function removeAttachment(index) {
const att = attachments.value[index];
if (att) URL.revokeObjectURL(att.preview);
attachments.value.splice(index, 1);
}
function clearAttachments() {
for (const att of attachments.value) URL.revokeObjectURL(att.preview);
attachments.value = [];
}
function toPayload() {
return attachments.value.map((a) => ({
type: a.mimeType.startsWith("image/") ? "image" : a.mimeType.startsWith("audio/") ? "audio" : "document",
mimeType: a.mimeType,
content: a.base64,
fileName: a.fileName
}));
}
function hasAttachments() {
return attachments.value.length > 0;
}
return { attachments, addFiles, removeAttachment, clearAttachments, toPayload, hasAttachments };
}
function useAudioRecorder() {
const isRecording = ref(false);
const duration = ref(0);
const audioLevel = ref(0);
const micDenied = ref(false);
let mediaRecorder = null;
let stream = null;
let audioCtx = null;
let analyser = null;
let levelBuf = null;
let chunks = [];
let timer = null;
let startTime = 0;
function cleanup() {
if (timer) {
clearInterval(timer);
timer = null;
}
if (audioCtx) {
audioCtx.close().catch(() => {
});
audioCtx = null;
analyser = null;
levelBuf = null;
}
if (stream) {
stream.getTracks().forEach((t) => t.stop());
stream = null;
}
mediaRecorder = null;
chunks = [];
duration.value = 0;
audioLevel.value = 0;
isRecording.value = false;
}
function updateLevel() {
if (!analyser || !levelBuf) return;
analyser.getByteTimeDomainData(levelBuf);
let sum = 0;
for (let i = 0; i < levelBuf.length; i++) {
const v = (levelBuf[i] - 128) / 128;
sum += v * v;
}
audioLevel.value = Math.min(1, Math.sqrt(sum / levelBuf.length) * 3);
}
async function startRecording() {
if (isRecording.value) return;
try {
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
} catch (err) {
console.warn("[audio] mic access denied:", err);
micDenied.value = true;
setTimeout(() => {
micDenied.value = false;
}, 5e3);
return;
}
try {
audioCtx = new AudioContext();
const source = audioCtx.createMediaStreamSource(stream);
analyser = audioCtx.createAnalyser();
analyser.fftSize = 256;
source.connect(analyser);
levelBuf = new Uint8Array(analyser.fftSize);
} catch (err) {
console.warn("[audio] analyser setup failed:", err);
}
chunks = [];
const mimeType = MediaRecorder.isTypeSupported("audio/webm;codecs=opus") ? "audio/webm;codecs=opus" : MediaRecorder.isTypeSupported("audio/webm") ? "audio/webm" : "";
mediaRecorder = new MediaRecorder(stream, mimeType ? { mimeType } : void 0);
mediaRecorder.ondataavailable = (e) => {
if (e.data.size > 0) chunks.push(e.data);
};
mediaRecorder.start(250);
isRecording.value = true;
startTime = Date.now();
timer = setInterval(() => {
duration.value = Math.floor((Date.now() - startTime) / 1e3);
updateLevel();
}, 80);
}
function stopRecording() {
return new Promise((resolve) => {
if (!mediaRecorder || mediaRecorder.state === "inactive") {
cleanup();
resolve(null);
return;
}
mediaRecorder.onstop = () => {
const mimeType = mediaRecorder?.mimeType || "audio/webm";
const ext = mimeType.includes("mp4") ? "mp4" : mimeType.includes("ogg") ? "ogg" : "webm";
const blob = new Blob(chunks, { type: mimeType });
const file = new File([blob], `recording-${Date.now()}.${ext}`, { type: mimeType });
cleanup();
resolve(file);
};
mediaRecorder.stop();
});
}
function cancelRecording() {
if (mediaRecorder && mediaRecorder.state !== "inactive") {
mediaRecorder.onstop = () => {
};
mediaRecorder.stop();
}
cleanup();
}
function formatDuration(secs) {
const m = Math.floor(secs / 60);
const s = secs % 60;
return `${m}:${s.toString().padStart(2, "0")}`;
}
onUnmounted(cleanup);
return { isRecording, duration, audioLevel, micDenied, startRecording, stopRecording, cancelRecording, formatDuration };
}
const _sfc_main$6 = /* @__PURE__ */ defineComponent({
__name: "MessageFrame",
__ssrInlineRender: true,
props: {
role: {},
copyContent: {}
},
setup(__props) {
return (_ctx, _push, _parent, _attrs) => {
_push(`| ${ssrInterpolate(col)} | `); }); _push(`
|---|
| ${ssrInterpolate(cell)} | `); }); _push(`
Select an agent
`); ssrRenderList(agentSegments.value, (seg) => { _push(``); _push(ssrRenderComponent(unref(LockClosedIcon), { class: "w-5 h-5 inline" }, null, _parent)); _push(` Not logged in
`); _push(ssrRenderComponent(_component_RouterLink, { to: "/login" }, { default: withCtx((_, _push2, _parent2, _scopeId) => { if (_push2) { _push2(`Sign in →`); } else { return [ createTextVNode("Sign in →") ]; } }), _: 1 }, _parent)); _push(`