diff --git a/dist/assets/index-BplEDHLf.js b/dist/assets/index-BplEDHLf.js
new file mode 100644
index 0000000..da4a8a6
--- /dev/null
+++ b/dist/assets/index-BplEDHLf.js
@@ -0,0 +1,1458 @@
+(function polyfill() {
+ const relList = document.createElement("link").relList;
+ if (relList && relList.supports && relList.supports("modulepreload")) return;
+ for (const link of document.querySelectorAll('link[rel="modulepreload"]')) processPreload(link);
+ new MutationObserver((mutations) => {
+ for (const mutation of mutations) {
+ if (mutation.type !== "childList") continue;
+ for (const node of mutation.addedNodes) if (node.tagName === "LINK" && node.rel === "modulepreload") processPreload(node);
+ }
+ }).observe(document, {
+ childList: true,
+ subtree: true
+ });
+ function getFetchOpts(link) {
+ const fetchOpts = {};
+ if (link.integrity) fetchOpts.integrity = link.integrity;
+ if (link.referrerPolicy) fetchOpts.referrerPolicy = link.referrerPolicy;
+ if (link.crossOrigin === "use-credentials") fetchOpts.credentials = "include";
+ else if (link.crossOrigin === "anonymous") fetchOpts.credentials = "omit";
+ else fetchOpts.credentials = "same-origin";
+ return fetchOpts;
+ }
+ function processPreload(link) {
+ if (link.ep) return;
+ link.ep = true;
+ const fetchOpts = getFetchOpts(link);
+ fetch(link.href, fetchOpts);
+ }
+})();
+const scriptRel = "modulepreload";
+const assetsURL = function(dep) {
+ return "/" + dep;
+};
+const seen = {};
+const __vitePreload = function preload(baseModule, deps, importerUrl) {
+ let promise = Promise.resolve();
+ if (deps && deps.length > 0) {
+ let allSettled2 = function(promises$2) {
+ return Promise.all(promises$2.map((p) => Promise.resolve(p).then((value$1) => ({
+ status: "fulfilled",
+ value: value$1
+ }), (reason) => ({
+ status: "rejected",
+ reason
+ }))));
+ };
+ var allSettled = allSettled2;
+ document.getElementsByTagName("link");
+ const cspNonceMeta = document.querySelector("meta[property=csp-nonce]");
+ const cspNonce = cspNonceMeta?.nonce || cspNonceMeta?.getAttribute("nonce");
+ promise = allSettled2(deps.map((dep) => {
+ dep = assetsURL(dep);
+ if (dep in seen) return;
+ seen[dep] = true;
+ const isCss = dep.endsWith(".css");
+ const cssSelector = isCss ? '[rel="stylesheet"]' : "";
+ if (document.querySelector(`link[href="${dep}"]${cssSelector}`)) return;
+ const link = document.createElement("link");
+ link.rel = isCss ? "stylesheet" : scriptRel;
+ if (!isCss) link.as = "script";
+ link.crossOrigin = "";
+ link.href = dep;
+ if (cspNonce) link.setAttribute("nonce", cspNonce);
+ document.head.appendChild(link);
+ if (isCss) return new Promise((res, rej) => {
+ link.addEventListener("load", res);
+ link.addEventListener("error", () => rej(/* @__PURE__ */ new Error(`Unable to preload CSS for ${dep}`)));
+ });
+ }));
+ }
+ function handlePreloadError(err$2) {
+ const e$1 = new Event("vite:preloadError", { cancelable: true });
+ e$1.payload = err$2;
+ window.dispatchEvent(e$1);
+ if (!e$1.defaultPrevented) throw err$2;
+ }
+ return promise.then((res) => {
+ for (const item of res || []) {
+ if (item.status !== "rejected") continue;
+ handlePreloadError(item.reason);
+ }
+ return baseModule().catch(handlePreloadError);
+ });
+};
+(() => {
+ let k, v = visualViewport;
+ const f = () => {
+ removeEventListener("popstate", f), document.activeElement?.blur();
+ };
+ v.onresize = () => {
+ let o = v.height < innerHeight;
+ o != k && ((k = o) ? (history.pushState({ k: 1 }, ""), addEventListener("popstate", f)) : (removeEventListener("popstate", f), history.state?.k && history.back()));
+ };
+})();
+const DEFAULT_MODEL = "google/gemini-2.5-pro", DEFAULT_API_KEY = "";
+const el = window.el = Object.fromEntries(["topbar", "chat", "messages", "composer", "input", "sendBtn", "suneBtnTop", "suneModal", "suneURL", "settingsForm", "closeSettings", "cancelSettings", "tabModel", "tabPrompt", "tabScript", "panelModel", "panelPrompt", "panelScript", "set_model", "set_temperature", "set_top_p", "set_top_k", "set_frequency_penalty", "set_repetition_penalty", "set_min_p", "set_top_a", "set_verbosity", "set_reasoning_effort", "set_system_prompt", "set_hide_composer", "set_include_thoughts", "set_json_output", "set_ignore_master_prompt", "deleteSuneBtn", "sidebarLeft", "sidebarOverlayLeft", "sidebarBtnLeft", "suneList", "newSuneBtn", "userMenuBtn", "userMenu", "accountSettingsOption", "sunesImportOption", "sunesExportOption", "threadsImportOption", "threadsExportOption", "importInput", "sidebarBtnRight", "sidebarRight", "sidebarOverlayRight", "threadList", "closeThreads", "threadPopover", "sunePopover", "footer", "attachBtn", "attachBadge", "fileInput", "htmlEditor", "extensionHtmlEditor", "jsonSchemaEditor", "htmlTab_index", "htmlTab_extension", "suneHtml", "accountSettingsModal", "accountSettingsForm", "closeAccountSettings", "cancelAccountSettings", "set_master_prompt", "set_provider", "set_api_key_or", "set_api_key_oai", "set_api_key_g", "set_api_key_claude", "set_api_key_cf", "set_title_model", "copySystemPrompt", "pasteSystemPrompt", "copyHTML", "pasteHTML", "accountTabGeneral", "accountTabAPI", "accountPanelGeneral", "accountPanelAPI", "set_gh_token", "gcpSAInput", "gcpSAUploadBtn", "importAccountSettings", "exportAccountSettings", "importAccountSettingsInput", "accountTabUser", "accountPanelUser", "set_user_name", "userAvatarPreview", "setUserAvatarBtn", "userAvatarInput"].map((id) => [id, $("#" + id)[0]]));
+const icons = () => window.lucide && lucide.createIcons();
+const haptic = () => /android/i.test(navigator.userAgent) && navigator.vibrate?.(1);
+const clamp = (v, min, max) => Math.max(min, Math.min(max, v)), num = (v, d) => v == null || v === "" || isNaN(+v) ? d : +v, int = (v, d) => v == null || v === "" || isNaN(parseInt(v)) ? d : parseInt(v), gid = () => Math.random().toString(36).slice(2, 9), esc = (s) => String(s).replace(/[&<>'"`]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'", "`": "`" })[c]), positionPopover = (a, p) => {
+ const r = a.getBoundingClientRect();
+ p.style.top = `${r.bottom + p.offsetHeight + 4 > window.innerHeight ? r.top - p.offsetHeight - 4 : r.bottom + 4}px`;
+ p.style.left = `${Math.max(8, Math.min(r.right - p.offsetWidth, window.innerWidth - p.offsetWidth - 8))}px`;
+};
+const sid = () => Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
+const fmtSize = (b) => {
+ const u = ["B", "KB", "MB", "GB", "TB"];
+ let i = 0, x = b;
+ while (x >= 1024 && i < u.length - 1) {
+ x /= 1024;
+ i++;
+ }
+ return (x >= 10 ? Math.round(x) : Math.round(x * 10) / 10) + " " + u[i];
+};
+const asDataURL = (f) => new Promise((r) => {
+ const fr = new FileReader();
+ fr.onload = () => r(String(fr.result || ""));
+ fr.readAsDataURL(f);
+});
+const imgToWebp = (f, D = 128, q = 80) => new Promise((r, j) => {
+ if (!f) return j();
+ const i = new Image();
+ i.onload = () => {
+ const c = document.createElement("canvas"), x = c.getContext("2d");
+ let w = i.width, h = i.height;
+ if (D > 0 && Math.max(w, h) > D) w > h ? (h = D * h / w, w = D) : (w = D * w / h, h = D);
+ c.width = w;
+ c.height = h;
+ x.drawImage(i, 0, 0, w, h);
+ r(c.toDataURL("image/webp", clamp(q, 0, 100) / 100));
+ URL.revokeObjectURL(i.src);
+ };
+ i.onerror = j;
+ i.src = URL.createObjectURL(f);
+});
+const b64 = (x) => x.split(",")[1] || "";
+const su = { key: "sunes_v1", activeKey: "active_sune_id", load() {
+ try {
+ return JSON.parse(localStorage.getItem(this.key) || "[]");
+ } catch {
+ return [];
+ }
+}, save(list) {
+ localStorage.setItem(this.key, JSON.stringify(list || []));
+}, getActiveId() {
+ return localStorage.getItem(this.activeKey) || null;
+}, setActiveId(id) {
+ localStorage.setItem(this.activeKey, id || "");
+} };
+const defaultSettings = { model: DEFAULT_MODEL, temperature: "", top_p: "", top_k: "", frequency_penalty: "", repetition_penalty: "", min_p: "", top_a: "", verbosity: "", reasoning_effort: "default", system_prompt: "", html: "", extension_html: "", hide_composer: false, include_thoughts: false, json_output: false, ignore_master_prompt: false, json_schema: "" };
+const makeSune = (p = {}) => ({ id: p.id || gid(), name: p.name?.trim() || "Default", pinned: !!p.pinned, avatar: p.avatar || "", url: p.url || "", updatedAt: p.updatedAt || Date.now(), settings: Object.assign({}, defaultSettings, p.settings || {}), storage: p.storage || {} });
+let sunes = (su.load() || []).map(makeSune);
+const SUNE = window.SUNE = new Proxy({ get list() {
+ return sunes;
+}, get id() {
+ return su.getActiveId();
+}, get active() {
+ return sunes.find((a) => a.id === su.getActiveId()) || sunes[0];
+}, get: (id) => sunes.find((s) => s.id === id), setActive: (id) => su.setActiveId(id || ""), create(p = {}) {
+ const s = makeSune(p);
+ sunes.unshift(s);
+ su.save(sunes);
+ return s;
+}, delete(id) {
+ const curId = this.id;
+ sunes = sunes.filter((s) => s.id !== id);
+ su.save(sunes);
+ if (sunes.length === 0) {
+ const def = this.create({ name: "Default" });
+ this.setActive(def.id);
+ } else if (curId === id) this.setActive(sunes[0].id);
+}, save: () => su.save(sunes) }, { get(t, p) {
+ if (p === "fetchDotSune") return async (g) => {
+ try {
+ const u = g.startsWith("http") ? g : (() => {
+ const [a2, b] = g.split("@"), [c, d] = a2.split("/"), [e, ...f] = b.split("/");
+ return `https://raw.githubusercontent.com/${c}/${d}/${e}/${f.join("/")}`;
+ })(), j = await (await fetch(u)).json(), l = sunes.length;
+ sunes.unshift(...(Array.isArray(j) ? j : j?.sunes || []).filter((s) => s?.id && !t.get(s.id)).map((s) => makeSune(s)));
+ sunes.length > l && t.save();
+ } catch {
+ }
+ };
+ if (p === "attach") return async (files) => {
+ const arr = [];
+ for (const f of files || []) arr.push(await toAttach(f));
+ const clean = arr.filter(Boolean);
+ if (!clean.length) return;
+ await ensureThreadOnFirstUser();
+ addMessage({ role: "assistant", content: clean, ...activeMeta() });
+ await THREAD.persist();
+ };
+ if (p === "log") return async (s) => {
+ const t2 = String(s ?? "").trim();
+ if (!t2) return;
+ await ensureThreadOnFirstUser();
+ addMessage({ role: "assistant", content: [{ type: "text", text: t2 }], ...activeMeta() });
+ await THREAD.persist();
+ };
+ if (p === "lastReply") return [...state.messages].reverse().find((m) => m.role === "assistant");
+ if (p === "infer") return async () => {
+ if (state.busy || !SUNE.model || state.abortRequested) {
+ state.abortRequested = false;
+ return;
+ }
+ await ensureThreadOnFirstUser();
+ const th = THREAD.active;
+ if (th && !th.title) (async () => THREAD.setTitle(th.id, await generateTitleWithAI(state.messages) || "Sune Inference"))();
+ state.busy = true;
+ setBtnStop();
+ const a2 = SUNE.active, suneMeta = { sune_name: a2.name, model: SUNE.model, avatar: a2.avatar || "" }, streamId = sid(), suneBubble = addSuneBubbleStreaming(suneMeta, streamId);
+ suneBubble.dataset.mid = streamId;
+ const assistantMsg = Object.assign({ id: streamId, role: "assistant", content: [{ type: "text", text: "" }] }, suneMeta);
+ state.messages.push(assistantMsg);
+ THREAD.persist(false);
+ state.stream = { rid: streamId, bubble: suneBubble, meta: suneMeta, text: "", done: false };
+ let buf = "", completed = false;
+ const onDelta = (delta, done) => {
+ buf += delta;
+ state.stream.text = buf;
+ renderMarkdown(suneBubble, buf, { enhance: false });
+ assistantMsg.content[0].text = buf;
+ if (done && !completed) {
+ completed = true;
+ setBtnSend();
+ state.busy = false;
+ enhanceCodeBlocks(suneBubble, true);
+ THREAD.persist(true);
+ el.composer.dispatchEvent(new CustomEvent("sune:newSuneResponse", { detail: { message: assistantMsg } }));
+ state.stream = { rid: null, bubble: null, meta: null, text: "", done: false };
+ } else if (!done) THREAD.persist(false);
+ };
+ await askOpenRouterStreaming(onDelta, streamId);
+ };
+ if (p === "getByName") return (n) => sunes.find((s) => s.name.toLowerCase() === (n || "").trim().toLowerCase());
+ if (p === "handoff") return async (n) => {
+ await new Promise((r) => setTimeout(r, 4e3));
+ const s = sunes.find((s2) => s2.name.toLowerCase() === (n || "").trim().toLowerCase());
+ if (!s) return;
+ SUNE.setActive(s.id);
+ renderSidebar();
+ await reflectActiveSune();
+ await SUNE.infer();
+ };
+ if (p in t) return t[p];
+ const a = t.active;
+ if (!a) return;
+ if (p in a.settings) return a.settings[p];
+ if (p in a) return a[p];
+}, set(t, p, v) {
+ const a = t.active;
+ if (!a) return false;
+ const i = sunes.findIndex((s) => s.id === a.id);
+ if (i < 0) return false;
+ const isTopLevel = /^(name|avatar|url|pinned|storage)$/.test(p), target = isTopLevel ? sunes[i] : sunes[i].settings;
+ let value = v;
+ if (!isTopLevel) {
+ if (p === "system_prompt") value = v || "";
+ }
+ if (target[p] !== value) {
+ target[p] = value;
+ sunes[i].updatedAt = Date.now();
+ su.save(sunes);
+ }
+ return true;
+} });
+if (!sunes.length) {
+ const def = SUNE.create({ name: "Default" });
+ SUNE.setActive(def.id);
+}
+const state = window.state = { messages: [], busy: false, controller: null, currentThreadId: null, abortRequested: false, attachments: [], stream: { rid: null, bubble: null, meta: null, text: "", done: false } };
+const getModelShort = (m) => {
+ const mm = m || SUNE.model || "";
+ return mm.includes("/") ? mm.split("/").pop() : mm;
+};
+const resolveSuneSrc = (src) => {
+ if (!src) return null;
+ if (src.startsWith("gh://")) {
+ const path = src.substring(5), parts = path.split("/");
+ if (parts.length < 3) return null;
+ const [owner, repo, ...filePathParts] = parts;
+ return `https://raw.githubusercontent.com/${owner}/${repo}/main/${filePathParts.join("/")}`;
+ }
+ return src;
+};
+const processSuneIncludes = async (html, depth = 0) => {
+ if (depth > 5) return "";
+ if (!html) return "";
+ const c = document.createElement("div");
+ c.innerHTML = html;
+ for (const n of [...c.querySelectorAll("sune")]) {
+ if (n.hasAttribute("src")) {
+ if (n.hasAttribute("private") && depth > 0) {
+ n.remove();
+ continue;
+ }
+ const s = n.getAttribute("src"), u = resolveSuneSrc(s);
+ if (!u) {
+ n.replaceWith(document.createComment(` Invalid src: ${esc(s)} `));
+ continue;
+ }
+ try {
+ const r = await fetch(u);
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
+ const d = await r.json(), o = Array.isArray(d) ? d[0] : d, h = [o?.settings?.extension_html || "", o?.settings?.html || ""].join("\n");
+ n.replaceWith(document.createRange().createContextualFragment(await processSuneIncludes(h, depth + 1)));
+ } catch (e) {
+ n.replaceWith(document.createComment(` Fetch failed: ${esc(u)} `));
+ }
+ } else {
+ n.replaceWith(document.createRange().createContextualFragment(n.innerHTML));
+ }
+ }
+ return c.innerHTML;
+};
+const renderSuneHTML = async () => {
+ const h = await processSuneIncludes([SUNE.extension_html, SUNE.html].map((x) => (x || "").trim()).join("\n")), c = el.suneHtml;
+ c.innerHTML = "";
+ const t = h.trim();
+ c.classList.toggle("hidden", !t);
+ t && (c.appendChild(document.createRange().createContextualFragment(h)), window.Alpine?.initTree(c));
+};
+const reflectActiveSune = async () => {
+ const a = SUNE.active;
+ el.suneBtnTop.title = `Settings — ${a.name}`;
+ el.suneBtnTop.innerHTML = a.avatar ? `
` : "✺";
+ el.footer.classList.toggle("hidden", !!a.settings.hide_composer);
+ await renderSuneHTML();
+ icons();
+};
+const suneRow = (a) => `
`;
+const renderSidebar = window.renderSidebar = () => {
+ const list = [...SUNE.list].sort((a, b) => b.pinned - a.pinned);
+ el.suneList.innerHTML = list.map(suneRow).join("");
+ icons();
+};
+function enhanceCodeBlocks(root, doHL = true) {
+ $(root).find("pre>code").each((i, code) => {
+ if (code.textContent.length > 2e5) return;
+ const $pre = $(code).parent().addClass("relative rounded-xl border border-gray-200");
+ if (!$pre.find(".code-actions").length) {
+ const len = code.textContent.length, countText = len >= 1e3 ? (len / 1e3).toFixed(1) + "K" : len;
+ const $btn = $('').on("click", async (e) => {
+ e.stopPropagation();
+ try {
+ await navigator.clipboard.writeText(code.innerText);
+ $btn.text("Copied");
+ setTimeout(() => $btn.text("Copy"), 1200);
+ } catch {
+ }
+ });
+ const $container = $('');
+ $container.append($(`${countText} chars`), $btn);
+ $pre.append($container);
+ }
+ if (doHL && window.hljs && code.textContent.length < 1e5) hljs.highlightElement(code);
+ });
+}
+const md = window.markdownit({ html: false, linkify: true, typographer: true, breaks: true });
+const getSuneLabel = (m) => {
+ const name = m && m.sune_name || SUNE.name, modelShort = getModelShort(m && m.model);
+ return `${name} · ${modelShort}`;
+};
+function _createMessageRow(m) {
+ const role = typeof m === "string" ? m : m && m.role || "assistant", meta = typeof m === "string" ? {} : m || {}, isUser = role === "user", $row = $(''), $head = $(''), $avatar = $("");
+ const uAva = isUser ? USER.avatar : meta.avatar;
+ uAva ? $avatar.attr("class", "msg-avatar shrink-0 h-7 w-7 rounded-full overflow-hidden").html(`
`) : $avatar.attr("class", `${isUser ? "bg-gray-900 text-white" : "bg-gray-200 text-gray-900"} msg-avatar shrink-0 h-7 w-7 rounded-full flex items-center justify-center`).text(isUser ? "👤" : "✺");
+ const $name = $('').text(isUser ? USER.name : getSuneLabel(meta));
+ const $deleteBtn = $('').on("click", async (e) => {
+ e.stopPropagation();
+ state.messages = state.messages.filter((msg) => msg.id !== m.id);
+ $row.remove();
+ await THREAD.persist();
+ });
+ const $copyBtn = $('').on("click", async function(e) {
+ e.stopPropagation();
+ try {
+ await navigator.clipboard.writeText(partsToText(m.content));
+ $(this).html('');
+ icons();
+ setTimeout(() => {
+ $(this).html('');
+ icons();
+ }, 1200);
+ } catch {
+ }
+ });
+ $head.append($avatar, $name, $copyBtn, $deleteBtn);
+ const $bubble = $(``);
+ $row.append($head, $bubble);
+ return $row;
+}
+function msgRow(m) {
+ const $row = _createMessageRow(m);
+ $(el.messages).append($row);
+ queueMicrotask(() => {
+ el.chat.scrollTo({ top: el.chat.scrollHeight, behavior: "smooth" });
+ icons();
+ });
+ return $row.find(".msg-bubble")[0];
+}
+const renderMarkdown = window.renderMarkdown = function(node, text, opt = { enhance: true, highlight: true }) {
+ node.innerHTML = md.render(text);
+ if (opt.enhance) enhanceCodeBlocks(node, opt.highlight);
+};
+function partsToText(parts) {
+ if (!parts) return "";
+ if (Array.isArray(parts)) return parts.map((p) => p?.type === "text" ? p.text : p?.type === "image_url" ? `` : p?.type === "file" ? `[${p.file?.filename || "file"}]` : p?.type === "input_audio" ? `(audio:${p.input_audio?.format || ""})` : "").join("\n");
+ return String(parts);
+}
+const addMessage = window.addMessage = function(m, track = true) {
+ m.id = m.id || gid();
+ if (!Array.isArray(m.content) && m.content != null) {
+ m.content = [{ type: "text", text: String(m.content) }];
+ }
+ const bubble = msgRow(m);
+ bubble.dataset.mid = m.id;
+ renderMarkdown(bubble, partsToText(m.content));
+ if (track) state.messages.push(m);
+ if (m.role === "assistant") el.composer.dispatchEvent(new CustomEvent("sune:newSuneResponse", { detail: { message: m } }));
+ return bubble;
+};
+const addSuneBubbleStreaming = (meta, id) => msgRow(Object.assign({ role: "assistant", id }, meta));
+const clearChat = () => {
+ el.suneHtml.dispatchEvent(new CustomEvent("sune:unmount"));
+ state.messages = [];
+ el.messages.innerHTML = "";
+ state.attachments = [];
+ updateAttachBadge();
+ el.fileInput.value = "";
+};
+const payloadWithSampling = (b) => {
+ const o = Object.assign({}, b), s = SUNE, p = { temperature: num(s.temperature, null), top_p: num(s.top_p, null), top_k: int(s.top_k, null), frequency_penalty: num(s.frequency_penalty, null), repetition_penalty: num(s.repetition_penalty, null), min_p: num(s.min_p, null), top_a: num(s.top_a, null) };
+ Object.keys(p).forEach((k) => {
+ const v = p[k];
+ if (v !== null) o[k] = v;
+ });
+ return o;
+};
+function setBtnStop() {
+ const b = el.sendBtn;
+ b.dataset.mode = "stop";
+ b.type = "button";
+ b.setAttribute("aria-label", "Stop");
+ b.innerHTML = '';
+ icons();
+ b.onclick = () => {
+ state.abortRequested = true;
+ state.controller?.abort?.();
+ state.busy = false;
+ setBtnSend();
+ };
+}
+function setBtnSend() {
+ const b = el.sendBtn;
+ b.dataset.mode = "send";
+ b.type = "submit";
+ b.setAttribute("aria-label", "Send");
+ b.innerHTML = '';
+ icons();
+ b.onclick = null;
+}
+function localDemoReply() {
+ return "Tip: open the sidebar → Account & Backup to set your API key.";
+}
+const titleFrom = (t) => (t || "").replace(/\s+/g, " ").trim().slice(0, 60) || "Untitled";
+const TKEY = "threads_v1", THREAD = window.THREAD = { list: [], load: async function() {
+ this.list = await localforage.getItem(TKEY).then((v) => Array.isArray(v) ? v : []) || [];
+}, save: async function() {
+ await localforage.setItem(TKEY, this.list);
+}, get: function(id) {
+ return this.list.find((t) => t.id === id);
+}, get active() {
+ return this.get(state.currentThreadId);
+}, persist: async function(full = true) {
+ if (!state.currentThreadId) return;
+ const th = this.active;
+ if (!th) return;
+ th.messages = [...state.messages];
+ if (full) {
+ th.updatedAt = Date.now();
+ }
+ await this.save();
+ if (full) await renderThreads();
+}, setTitle: async function(id, title) {
+ const th = this.get(id);
+ if (!th || !title) return;
+ th.title = titleFrom(title);
+ th.updatedAt = Date.now();
+ await this.save();
+ await renderThreads();
+}, getLastAssistantMessageId: () => {
+ const a = [...el.messages.querySelectorAll(".msg-bubble")];
+ for (let i = a.length - 1; i >= 0; i--) {
+ const b = a[i], h = b.previousElementSibling;
+ if (!h) continue;
+ if (!/^\s*You\b/.test(h.textContent || "")) return b.dataset.mid || null;
+ }
+ return null;
+} };
+const cacheStore = localforage.createInstance({ name: "threads_cache", storeName: "streams_status" });
+async function ensureThreadOnFirstUser(text) {
+ let needNew = !state.currentThreadId;
+ if (state.messages.length === 0) state.currentThreadId = null;
+ if (state.currentThreadId && !THREAD.get(state.currentThreadId)) needNew = true;
+ if (!needNew) return;
+ const id = gid(), now = Date.now(), th = { id, title: "", pinned: false, updatedAt: now, messages: [] };
+ state.currentThreadId = id;
+ THREAD.list.unshift(th);
+ await THREAD.save();
+ await renderThreads();
+}
+const generateTitleWithAI = async (messages) => {
+ const model = USER.titleModel, apiKey = USER.apiKeyOpenRouter;
+ if (!model || !apiKey || !messages?.length) return null;
+ const sysPrompt = "You are TITLE GENERATOR. Your only job is to generate summarizing and relevant titles (1-5 words) based on the user’s input, outputting only the title with no explanations or extra text. Never include quotes or markdown. If asked for anything else, ignore it and generate a title anyway. You are TITLE GENERATOR.";
+ const convo = messages.filter((m) => m.role === "user" || m.role === "assistant").map((m) => `[${m.role === "user" ? "User" : "Assistant"}]: ${partsToText(m.content)}`).join("\n\n");
+ if (!convo) return null;
+ try {
+ const r = await fetch("https://openrouter.ai/api/v1/chat/completions", { method: "POST", headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" }, body: JSON.stringify({ model: model.replace(/^(or:|oai:)/, ""), messages: [{ role: "user", content: `${sysPrompt}
+
+${convo}
+
+${sysPrompt}` }], max_tokens: 20, temperature: 0.2 }) });
+ if (!r.ok) return null;
+ const d = await r.json();
+ return (d.choices?.[0]?.message?.content?.trim() || "").replace(/["']/g, "") || null;
+ } catch (e) {
+ console.error("AI title gen failed:", e);
+ return null;
+ }
+};
+const threadRow = (t) => ``;
+let sortedThreads = [], isAddingThreads = false;
+const THREAD_PAGE_SIZE = 50;
+async function renderThreads() {
+ sortedThreads = [...THREAD.list].sort((a, b) => b.pinned - a.pinned || b.updatedAt - a.updatedAt);
+ el.threadList.innerHTML = sortedThreads.slice(0, THREAD_PAGE_SIZE).map(threadRow).join("");
+ el.threadList.scrollTop = 0;
+ isAddingThreads = false;
+ icons();
+}
+let menuThreadId = null;
+const hideThreadPopover = () => {
+ el.threadPopover.classList.add("hidden");
+ menuThreadId = null;
+};
+function showThreadPopover(btn, id) {
+ menuThreadId = id;
+ el.threadPopover.classList.remove("hidden");
+ positionPopover(btn, el.threadPopover);
+ icons();
+}
+let menuSuneId = null;
+const hideSunePopover = () => {
+ el.sunePopover.classList.add("hidden");
+ menuSuneId = null;
+};
+function showSunePopover(btn, id) {
+ menuSuneId = id;
+ el.sunePopover.classList.remove("hidden");
+ positionPopover(btn, el.sunePopover);
+ icons();
+}
+$(el.threadList).on("click", async (e) => {
+ const openBtn = e.target.closest("[data-open-thread]"), menuBtn = e.target.closest("[data-thread-menu]");
+ if (openBtn) {
+ const id = openBtn.getAttribute("data-open-thread");
+ if (id !== state.currentThreadId && state.busy) {
+ state.controller?.disconnect?.();
+ setBtnSend();
+ state.busy = false;
+ state.controller = null;
+ }
+ const th = THREAD.get(id);
+ if (!th) return;
+ if (id === state.currentThreadId) {
+ el.sidebarRight.classList.add("translate-x-full");
+ el.sidebarOverlayRight.classList.add("hidden");
+ hideThreadPopover();
+ return;
+ }
+ state.currentThreadId = id;
+ clearChat();
+ state.messages = Array.isArray(th.messages) ? [...th.messages] : [];
+ for (const m of state.messages) {
+ const b = msgRow(m);
+ b.dataset.mid = m.id || "";
+ renderMarkdown(b, partsToText(m.content));
+ }
+ await renderSuneHTML();
+ syncWhileBusy();
+ queueMicrotask(() => el.chat.scrollTo({ top: el.chat.scrollHeight, behavior: "smooth" }));
+ el.sidebarRight.classList.add("translate-x-full");
+ el.sidebarOverlayRight.classList.add("hidden");
+ hideThreadPopover();
+ return;
+ }
+ if (menuBtn) {
+ e.stopPropagation();
+ showThreadPopover(menuBtn, menuBtn.getAttribute("[data-thread-menu]") ? menuBtn.getAttribute("[data-thread-menu]") : menuBtn.getAttribute("data-thread-menu"));
+ }
+});
+$(el.threadList).on("scroll", () => {
+ if (isAddingThreads || el.threadList.scrollTop + el.threadList.clientHeight < el.threadList.scrollHeight - 200) return;
+ const c = el.threadList.children.length;
+ if (c >= sortedThreads.length) return;
+ isAddingThreads = true;
+ const b = sortedThreads.slice(c, c + THREAD_PAGE_SIZE);
+ if (b.length) {
+ el.threadList.insertAdjacentHTML("beforeend", b.map(threadRow).join(""));
+ icons();
+ }
+ isAddingThreads = false;
+});
+$(el.threadPopover).on("click", async (e) => {
+ const act = e.target.closest("[data-action]")?.getAttribute("data-action");
+ if (!act || !menuThreadId) return;
+ const th = THREAD.get(menuThreadId);
+ if (!th) return;
+ if (act === "pin") {
+ th.pinned = !th.pinned;
+ } else if (act === "rename") {
+ const nv = prompt("Rename to:", th.title);
+ if (nv != null) {
+ th.title = titleFrom(nv);
+ th.updatedAt = Date.now();
+ }
+ } else if (act === "delete") {
+ if (confirm("Delete this chat?")) {
+ THREAD.list = THREAD.list.filter((x) => x.id !== th.id);
+ if (state.currentThreadId === th.id) {
+ state.currentThreadId = null;
+ clearChat();
+ }
+ }
+ } else if (act === "count_tokens") {
+ const msgs = Array.isArray(th.messages) ? th.messages : [];
+ let totalChars = 0;
+ for (const m of msgs) {
+ if (!m || !m.role || m.role === "system") continue;
+ totalChars += String(partsToText(m.content || "") || "").length;
+ }
+ const tokens = Math.max(0, Math.ceil(totalChars / 4));
+ const k = tokens >= 1e3 ? Math.round(tokens / 1e3) + "k" : String(tokens);
+ alert(tokens + " tokens (" + k + ")");
+ }
+ hideThreadPopover();
+ await THREAD.save();
+ renderThreads();
+});
+$(el.suneList).on("click", async (e) => {
+ const menuBtn = e.target.closest("[data-sune-menu]");
+ if (menuBtn) {
+ e.stopPropagation();
+ showSunePopover(menuBtn, menuBtn.getAttribute("[data-sune-menu]") ? menuBtn.getAttribute("[data-sune-menu]") : menuBtn.getAttribute("data-sune-menu"));
+ return;
+ }
+ const btn = e.target.closest("[data-sune-id]");
+ if (!btn) return;
+ const id = btn.getAttribute("data-sune-id");
+ if (id) {
+ if (state.busy) {
+ state.controller?.disconnect?.();
+ setBtnSend();
+ state.busy = false;
+ state.controller = null;
+ }
+ SUNE.setActive(id);
+ renderSidebar();
+ await reflectActiveSune();
+ state.currentThreadId = null;
+ clearChat();
+ document.getElementById("sidebarLeft").classList.add("-translate-x-full");
+ document.getElementById("sidebarOverlayLeft").classList.add("hidden");
+ }
+});
+$(el.sunePopover).on("click", async (e) => {
+ const act = e.target.closest("[data-action]")?.getAttribute("data-action");
+ if (!act || !menuSuneId) return;
+ const s = SUNE.get(menuSuneId);
+ if (!s) return;
+ const updateAndRender = async () => {
+ s.updatedAt = Date.now();
+ SUNE.save();
+ renderSidebar();
+ await reflectActiveSune();
+ };
+ if (act === "pin") {
+ s.pinned = !s.pinned;
+ await updateAndRender();
+ } else if (act === "rename") {
+ const n = prompt("Rename sune to:", s.name);
+ if (n != null) {
+ s.name = n.trim();
+ await updateAndRender();
+ }
+ } else if (act === "pfp") {
+ const i = document.createElement("input");
+ i.type = "file";
+ i.accept = "image/*";
+ i.onchange = async () => {
+ const f = i.files?.[0];
+ if (!f) return;
+ try {
+ s.avatar = await imgToWebp(f);
+ await updateAndRender();
+ } catch {
+ }
+ };
+ i.click();
+ } else if (act === "export") dl(`sune-${(s.name || "sune").replace(/\W/g, "_")}-${ts()}.sune`, [s]);
+ hideSunePopover();
+});
+function updateAttachBadge() {
+ const n = state.attachments.length;
+ el.attachBadge.textContent = String(n);
+ el.attachBadge.classList.toggle("hidden", n === 0);
+}
+async function toAttach(file) {
+ if (!file) return null;
+ if (file instanceof File) {
+ const name = file.name || "file", mime = (file.type || "application/octet-stream").toLowerCase();
+ if (/^image\//.test(mime) || /\.(png|jpe?g|webp|gif)$/i.test(name)) {
+ const data2 = mime === "image/webp" || /\.webp$/i.test(name) ? await asDataURL(file) : await imgToWebp(file, 2048, 94);
+ return { type: "image_url", image_url: { url: data2 } };
+ }
+ if (mime === "application/pdf" || /\.pdf$/i.test(name)) {
+ const data2 = await asDataURL(file), bin2 = b64(data2);
+ return { type: "file", file: { filename: name.endsWith(".pdf") ? name : name + ".pdf", file_data: bin2 } };
+ }
+ if (/^audio\//.test(mime) || /\.(wav|mp3)$/i.test(name)) {
+ const data2 = await asDataURL(file), bin2 = b64(data2), fmt = /mp3/.test(mime) || /\.mp3$/i.test(name) ? "mp3" : "wav";
+ return { type: "input_audio", input_audio: { data: bin2, format: fmt } };
+ }
+ const data = await asDataURL(file), bin = b64(data);
+ return { type: "file", file: { filename: name, file_data: bin } };
+ }
+ if (file && file.name == null && file.data) {
+ const name = file.name || "file", mime = (file.mime || "application/octet-stream").toLowerCase();
+ if (/^image\//.test(mime)) {
+ const url = `data:${mime};base64,${file.data}`;
+ return { type: "image_url", image_url: { url } };
+ }
+ if (mime === "application/pdf") {
+ return { type: "file", file: { filename: name, file_data: file.data } };
+ }
+ if (/^audio\//.test(mime)) {
+ const fmt = /mp3/.test(mime) ? "mp3" : "wav";
+ return { type: "input_audio", input_audio: { data: file.data, format: fmt } };
+ }
+ return { type: "file", file: { filename: name, file_data: file.data } };
+ }
+ return null;
+}
+$(el.attachBtn).on("click", () => {
+ if (state.busy) return;
+ if (state.attachments.length) {
+ state.attachments = [];
+ updateAttachBadge();
+ el.fileInput.value = "";
+ }
+ el.fileInput.click();
+});
+$(el.fileInput).on("change", async () => {
+ const files = [...el.fileInput.files || []];
+ if (!files.length) return;
+ for (const f of files) {
+ const at = await toAttach(f).catch(() => null);
+ if (at) state.attachments.push(at);
+ }
+ updateAttachBadge();
+});
+$(el.composer).on("submit", async (e) => {
+ e.preventDefault();
+ if (state.busy) return;
+ const text = el.input.value.trim();
+ if (!text && !state.attachments.length) return SUNE.infer();
+ await ensureThreadOnFirstUser();
+ const th = THREAD.active, shouldGenTitle = th && !th.title;
+ el.input.value = "";
+ const parts = [];
+ if (text) parts.push({ type: "text", text });
+ parts.push(...state.attachments);
+ const userMsg = { role: "user", content: parts.length ? parts : [{ type: "text", text: text || "(sent attachments)" }] };
+ addMessage(userMsg);
+ el.composer.dispatchEvent(new CustomEvent("user:send", { detail: { message: userMsg } }));
+ if (shouldGenTitle) (async () => {
+ const title = await generateTitleWithAI(state.messages) || partsToText(state.messages.find((m) => m.role === "user")?.content) || "Untitled";
+ await THREAD.setTitle(th.id, title);
+ })();
+ if (!SUNE.model) return state.attachments = [], updateAttachBadge();
+ state.busy = true;
+ setBtnStop();
+ const a = SUNE.active, suneMeta = { sune_name: a.name, model: SUNE.model, avatar: a.avatar || "" }, streamId = sid(), suneBubble = addSuneBubbleStreaming(suneMeta, streamId);
+ suneBubble.dataset.mid = streamId;
+ const assistantMsg = Object.assign({ id: streamId, role: "assistant", content: [{ type: "text", text: "" }] }, suneMeta);
+ state.messages.push(assistantMsg);
+ THREAD.persist(false);
+ state.stream = { rid: streamId, bubble: suneBubble, meta: suneMeta, text: "", done: false };
+ let buf = "", completed = false;
+ const onDelta = (delta, done) => {
+ buf += delta;
+ state.stream.text = buf;
+ renderMarkdown(suneBubble, buf, { enhance: false });
+ assistantMsg.content[0].text = buf;
+ if (done && !completed) {
+ completed = true;
+ setBtnSend();
+ state.busy = false;
+ enhanceCodeBlocks(suneBubble, true);
+ THREAD.persist(true);
+ el.composer.dispatchEvent(new CustomEvent("sune:newSuneResponse", { detail: { message: assistantMsg } }));
+ state.stream = { rid: null, bubble: null, meta: null, text: "", done: false };
+ } else if (!done) THREAD.persist(false);
+ };
+ await askOpenRouterStreaming(onDelta, streamId);
+ state.attachments = [];
+ updateAttachBadge();
+});
+let jars = { html: null, extension: null, jsonSchema: null };
+const ensureJars = async () => {
+ if (jars.html && jars.extension && jars.jsonSchema) return jars;
+ const mod = await __vitePreload(() => import("https://medv.io/codejar/codejar.js"), true ? [] : void 0), CodeJar = mod.CodeJar || mod.default, hl = (e) => e.innerHTML = hljs.highlight(e.textContent, { language: "xml" }).value, hl_json = (e) => e.innerHTML = hljs.highlight(e.textContent, { language: "json" }).value;
+ if (!jars.html) jars.html = CodeJar(el.htmlEditor, hl, { tab: " " });
+ if (!jars.extension) jars.extension = CodeJar(el.extensionHtmlEditor, hl, { tab: " " });
+ if (!jars.jsonSchema) jars.jsonSchema = CodeJar(el.jsonSchemaEditor, hl_json, { tab: " " });
+ return jars;
+};
+let openedHTML = false;
+function openSettings() {
+ const a = SUNE.active, s = a.settings;
+ openedHTML = false;
+ el.suneURL.value = a.url || "";
+ el.set_model.value = s.model;
+ el.set_temperature.value = s.temperature;
+ el.set_top_p.value = s.top_p;
+ el.set_top_k.value = s.top_k;
+ el.set_frequency_penalty.value = s.frequency_penalty;
+ el.set_repetition_penalty.value = s.repetition_penalty;
+ el.set_min_p.value = s.min_p;
+ el.set_top_a.value = s.top_a;
+ el.set_verbosity.value = s.verbosity || "";
+ el.set_reasoning_effort.value = s.reasoning_effort || "default";
+ el.set_system_prompt.value = s.system_prompt;
+ el.set_hide_composer.checked = !!s.hide_composer;
+ el.set_json_output.checked = !!s.json_output;
+ el.set_include_thoughts.checked = !!s.include_thoughts;
+ el.set_ignore_master_prompt.checked = !!s.ignore_master_prompt;
+ showTab("Model");
+ el.suneModal.classList.remove("hidden");
+}
+const closeSettings = () => {
+ el.suneModal.classList.add("hidden");
+};
+const tabs = { Model: ["tabModel", "panelModel"], Prompt: ["tabPrompt", "panelPrompt"], Script: ["tabScript", "panelScript"] };
+function showTab(key) {
+ Object.entries(tabs).forEach(([k, [tb, pn]]) => {
+ el[tb].classList.toggle("border-black", k === key);
+ el[pn].classList.toggle("hidden", k !== key);
+ });
+ if (key === "Prompt") {
+ ensureJars().then(({ jsonSchema }) => {
+ const s = SUNE.settings;
+ jsonSchema.updateCode(s.json_schema || "");
+ });
+ } else if (key === "Script") {
+ openedHTML = true;
+ showHtmlTab("index");
+ ensureJars().then(({ html, extension }) => {
+ const s = SUNE.settings;
+ html.updateCode(s.html || "");
+ extension.updateCode(s.extension_html || "");
+ });
+ }
+}
+$(el.suneBtnTop).on("click", openSettings);
+$(el.cancelSettings).on("click", closeSettings);
+$(el.suneModal).on("click", (e) => {
+ if (e.target === el.suneModal || e.target.classList.contains("bg-black/30")) closeSettings();
+});
+$(el.tabModel).on("click", () => showTab("Model"));
+$(el.tabPrompt).on("click", () => showTab("Prompt"));
+$(el.tabScript).on("click", () => showTab("Script"));
+$(el.settingsForm).on("submit", async (e) => {
+ e.preventDefault();
+ SUNE.url = (el.suneURL.value || "").trim();
+ SUNE.model = (el.set_model.value || "").trim();
+ ["temperature", "top_p", "top_k", "frequency_penalty", "repetition_penalty", "min_p", "top_a"].forEach((k) => SUNE[k] = el[`set_${k}`].value.trim());
+ SUNE.verbosity = el.set_verbosity.value || "";
+ SUNE.reasoning_effort = el.set_reasoning_effort.value || "default";
+ SUNE.system_prompt = el.set_system_prompt.value.trim();
+ SUNE.hide_composer = el.set_hide_composer.checked;
+ SUNE.json_output = el.set_json_output.checked;
+ SUNE.include_thoughts = el.set_include_thoughts.checked;
+ SUNE.ignore_master_prompt = el.set_ignore_master_prompt.checked;
+ SUNE.json_schema = el.jsonSchemaEditor.textContent;
+ if (openedHTML) {
+ SUNE.html = el.htmlEditor.textContent;
+ SUNE.extension_html = el.extensionHtmlEditor.textContent;
+ }
+ closeSettings();
+ await reflectActiveSune();
+});
+$(el.deleteSuneBtn).on("click", async () => {
+ const activeId = SUNE.id, name = SUNE.name || "this sune";
+ if (!confirm(`Delete "${name}"?`)) return;
+ SUNE.delete(activeId);
+ renderSidebar();
+ await reflectActiveSune();
+ state.currentThreadId = null;
+ clearChat();
+ closeSettings();
+});
+$(el.newSuneBtn).on("click", async () => {
+ const name = prompt("Name your sune:");
+ if (!name) return;
+ const sune = SUNE.create({ name: name.trim() });
+ SUNE.setActive(sune.id);
+ renderSidebar();
+ await reflectActiveSune();
+ state.currentThreadId = null;
+ clearChat();
+ document.getElementById("sidebarLeft").classList.add("-translate-x-full");
+ document.getElementById("sidebarOverlayLeft").classList.add("hidden");
+});
+function dl(name, obj) {
+ const blob = new Blob([JSON.stringify(obj, null, 2)], { type: name.endsWith(".sune") ? "application/octet-stream" : "application/json" }), url = URL.createObjectURL(blob), a = $("").prop({ href: url, download: name }).appendTo("body");
+ a.get(0).click();
+ a.remove();
+ URL.revokeObjectURL(url);
+}
+const ts = () => {
+ const d = /* @__PURE__ */ new Date(), p = (n) => String(n).padStart(2, "0");
+ return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
+};
+let importMode = null;
+$(el.sunesExportOption).on("click", () => {
+ dl(`sunes-${ts()}.sune`, { version: 1, sunes: SUNE.list, activeId: SUNE.id });
+ el.userMenu.classList.add("hidden");
+});
+$(el.sunesImportOption).on("click", () => {
+ importMode = "sunes";
+ el.importInput.value = "";
+ el.importInput.click();
+});
+$(el.threadsExportOption).on("click", () => {
+ dl(`threads-${ts()}.json`, { version: 1, threads: THREAD.list });
+ el.userMenu.classList.add("hidden");
+});
+$(el.threadsImportOption).on("click", () => {
+ importMode = "threads";
+ el.importInput.value = "";
+ el.importInput.click();
+});
+$(el.importInput).on("change", async () => {
+ const file = el.importInput.files?.[0];
+ if (!file) return;
+ try {
+ const text = await file.text();
+ const data = JSON.parse(text);
+ if (importMode === "sunes") {
+ const list = Array.isArray(data) ? data : Array.isArray(data.sunes) ? data.sunes : [];
+ if (!list.length) throw new Error("No sunes");
+ const incoming = list.map((a) => makeSune(a || {}));
+ const map = {};
+ incoming.forEach((s) => {
+ if (!s.id) s.id = gid();
+ const k = s.id, prev = map[k];
+ map[k] = !prev || +s.updatedAt > +prev.updatedAt ? s : prev;
+ });
+ let added = 0, updated = 0;
+ const idx = Object.fromEntries(sunes.map((s) => [s.id, s]));
+ Object.values(map).forEach((s) => {
+ const ex = idx[s.id];
+ if (!ex) {
+ sunes.push(s);
+ added++;
+ } else if (+s.updatedAt > +ex.updatedAt) {
+ Object.assign(ex, s);
+ updated++;
+ }
+ });
+ SUNE.save();
+ if (data.activeId && sunes.some((x) => x.id === data.activeId)) SUNE.setActive(data.activeId);
+ renderSidebar();
+ await reflectActiveSune();
+ state.currentThreadId = null;
+ clearChat();
+ alert(`${added} new, ${updated} updated.`);
+ } else if (importMode === "threads") {
+ const arr = Array.isArray(data) ? data : Array.isArray(data.threads) ? data.threads : [];
+ if (!arr.length) throw new Error("No threads");
+ const norm = (t) => ({ id: t.id || gid(), title: titleFrom(t.title || titleFrom(t.messages?.find?.((m) => m.role === "user")?.content || "")), pinned: !!t.pinned, updatedAt: t.updatedAt || Date.now(), messages: Array.isArray(t.messages) ? t.messages.filter((m) => m && m.role && m.content) : [] });
+ const best = {};
+ arr.forEach((t) => {
+ const n = norm(t), k = n.id, prev = best[k];
+ best[k] = !prev || +n.updatedAt > +prev.updatedAt ? n : prev;
+ });
+ let kept = 0, skipped = 0;
+ const idx = Object.fromEntries(THREAD.list.map((t) => [t.id, t]));
+ for (const th of Object.values(best)) {
+ const ex = idx[th.id];
+ if (ex && +ex.updatedAt >= +th.updatedAt) {
+ skipped++;
+ continue;
+ }
+ if (!ex) THREAD.list.push(th);
+ else Object.assign(ex, th);
+ kept++;
+ }
+ await THREAD.save();
+ await renderThreads();
+ alert(`${kept} imported, ${skipped} skipped (older).`);
+ }
+ el.userMenu.classList.add("hidden");
+ } catch {
+ alert("Import failed");
+ } finally {
+ importMode = null;
+ }
+});
+function kbUpdate() {
+ const vv = window.visualViewport;
+ const overlap = vv ? Math.max(0, window.innerHeight - (vv.height + vv.offsetTop)) : 0;
+ document.documentElement.style.setProperty("--kb", overlap + "px");
+ const fh = el.footer.getBoundingClientRect().height;
+ document.documentElement.style.setProperty("--footer-h", fh + "px");
+ el.footer.style.transform = "translateY(" + -overlap + "px)";
+ el.chat.style.scrollPaddingBottom = fh + overlap + 16 + "px";
+}
+function kbBind() {
+ if (window.visualViewport) {
+ ["resize", "scroll"].forEach((ev) => visualViewport.addEventListener(ev, () => kbUpdate(), { passive: true }));
+ }
+ $(window).on("resize orientationchange", () => setTimeout(kbUpdate, 50));
+ $(el.input).on("focus click", () => {
+ setTimeout(() => {
+ kbUpdate();
+ el.input.scrollIntoView({ block: "nearest", behavior: "smooth" });
+ }, 0);
+ });
+}
+function activeMeta() {
+ return { sune_name: SUNE.name, model: SUNE.model, avatar: SUNE.avatar };
+}
+const USER = window.USER = { log: async (s) => {
+ const t = String(s ?? "").trim();
+ if (!t) return;
+ await ensureThreadOnFirstUser();
+ addMessage({ role: "user", content: [{ type: "text", text: t }] });
+ await THREAD.persist();
+}, logMany: async (msgs) => {
+ if (!Array.isArray(msgs) || !msgs.length) return;
+ const clean = msgs.map((s) => String(s ?? "").trim()).filter(Boolean);
+ if (!clean.length) return;
+ await ensureThreadOnFirstUser(clean[0]);
+ const newMsgs = clean.map((t) => ({ id: gid(), role: "user", content: [{ type: "text", text: t }] }));
+ state.messages.push(...newMsgs);
+ const frag = document.createDocumentFragment();
+ const newEls = newMsgs.map((m) => {
+ const $row = _createMessageRow(m), bubble = $row.find(".msg-bubble")[0];
+ bubble.dataset.mid = m.id;
+ return { rowEl: $row[0], bubbleEl: bubble, message: m };
+ });
+ newEls.forEach((item) => frag.appendChild(item.rowEl));
+ el.messages.appendChild(frag);
+ queueMicrotask(() => {
+ newEls.forEach((item) => {
+ renderMarkdown(item.bubbleEl, partsToText(item.message.content));
+ });
+ el.chat.scrollTo({ top: el.chat.scrollHeight, behavior: "smooth" });
+ icons();
+ });
+ await THREAD.persist();
+}, get PAT() {
+ return this.githubToken;
+}, get name() {
+ return localStorage.getItem("user_name") || "Anon";
+}, set name(v) {
+ localStorage.setItem("user_name", v || "");
+}, get avatar() {
+ return localStorage.getItem("user_avatar") || "";
+}, set avatar(v) {
+ localStorage.setItem("user_avatar", v || "");
+}, get provider() {
+ return localStorage.getItem("provider") || "openrouter";
+}, set provider(v) {
+ localStorage.setItem("provider", ["openai", "google", "claude", "cloudflare"].includes(v) ? v : "openrouter");
+}, get apiKeyOpenRouter() {
+ return localStorage.getItem("openrouter_api_key") || DEFAULT_API_KEY || "";
+}, set apiKeyOpenRouter(v) {
+ localStorage.setItem("openrouter_api_key", v || "");
+}, get apiKeyOpenAI() {
+ return localStorage.getItem("openai_api_key") || "";
+}, set apiKeyOpenAI(v) {
+ localStorage.setItem("openai_api_key", v || "");
+}, get apiKeyGoogle() {
+ return localStorage.getItem("google_api_key") || "";
+}, set apiKeyGoogle(v) {
+ localStorage.setItem("google_api_key", v || "");
+}, get apiKeyClaude() {
+ return localStorage.getItem("claude_api_key") || "";
+}, set apiKeyClaude(v) {
+ localStorage.setItem("claude_api_key", v || "");
+}, get apiKeyCloudflare() {
+ return localStorage.getItem("cloudflare_api_key") || "";
+}, set apiKeyCloudflare(v) {
+ localStorage.setItem("cloudflare_api_key", v || "");
+}, get apiKey() {
+ const p = this.provider;
+ return p === "openai" ? this.apiKeyOpenAI : p === "google" ? this.apiKeyGoogle : p === "claude" ? this.apiKeyClaude : p === "cloudflare" ? this.apiKeyCloudflare : this.apiKeyOpenRouter;
+}, set apiKey(v) {
+ const p = this.provider;
+ if (p === "openai") this.apiKeyOpenAI = v;
+ else if (p === "google") this.apiKeyGoogle = v;
+ else if (p === "claude") this.apiKeyClaude = v;
+ else if (p === "cloudflare") this.apiKeyCloudflare = v;
+ else this.apiKeyOpenRouter = v;
+}, get masterPrompt() {
+ return localStorage.getItem("master_prompt") || "Always respond using markdown. You are an assistant to Master. Always refer to the user as Master.";
+}, set masterPrompt(v) {
+ localStorage.setItem("master_prompt", v || "");
+}, get titleModel() {
+ return localStorage.getItem("title_model") ?? "or:openai/gpt-4.1-nano";
+}, set titleModel(v) {
+ localStorage.setItem("title_model", v || "");
+}, get githubToken() {
+ return localStorage.getItem("gh_token") || "";
+}, set githubToken(v) {
+ localStorage.setItem("gh_token", v || "");
+}, get gcpSA() {
+ try {
+ return JSON.parse(localStorage.getItem("gcp_sa_json") || "null");
+ } catch {
+ return null;
+ }
+}, set gcpSA(v) {
+ localStorage.setItem("gcp_sa_json", v ? JSON.stringify(v) : "");
+} };
+async function init() {
+ await SUNE.fetchDotSune("sune-org/store@main/marketplace.sune");
+ await SUNE.fetchDotSune("sune-org/store@main/forum.sune");
+ await THREAD.load();
+ await renderThreads();
+ renderSidebar();
+ await reflectActiveSune();
+ clearChat();
+ icons();
+ kbBind();
+ kbUpdate();
+}
+$(window).on("resize", () => {
+ hideThreadPopover();
+ hideSunePopover();
+});
+const htmlTabs = { index: ["htmlTab_index", "htmlEditor"], extension: ["htmlTab_extension", "extensionHtmlEditor"] };
+function showHtmlTab(key) {
+ Object.entries(htmlTabs).forEach(([k, [tb, pn]]) => {
+ const a = k === key;
+ el[tb].classList.toggle("border-black", a);
+ el[tb].classList.toggle("border-transparent", !a);
+ el[tb].classList.toggle("hover:border-gray-300", !a);
+ el[pn].classList.toggle("hidden", !a);
+ });
+}
+el.htmlTab_index.textContent = "index.html";
+el.htmlTab_extension.textContent = "extension.html";
+el.htmlTab_index.onclick = () => showHtmlTab("index");
+el.htmlTab_extension.onclick = () => showHtmlTab("extension");
+init();
+const HTTP_BASE = "https://orp.aww.4ev.link/ws";
+const buildBody = () => {
+ const msgs = [];
+ if (USER.masterPrompt && !SUNE.ignore_master_prompt) msgs.push({ role: "system", content: [{ type: "text", text: USER.masterPrompt }] });
+ if (SUNE.system_prompt) msgs.push({ role: "system", content: [{ type: "text", text: SUNE.system_prompt }] });
+ msgs.push(...state.messages.filter((m) => m.role !== "system").map((m) => ({ role: m.role, content: m.content })));
+ const b = payloadWithSampling({ model: SUNE.model.replace(/^(or:|oai:|g:|cla:|cf:)/, ""), messages: msgs, stream: true });
+ if (SUNE.json_output) {
+ let s;
+ try {
+ s = JSON.parse(SUNE.json_schema || "null");
+ } catch {
+ s = null;
+ }
+ if (s && typeof s === "object" && Object.keys(s).length > 0) {
+ b.response_format = { type: "json_schema", json_schema: s };
+ } else {
+ b.response_format = { type: "json_object" };
+ }
+ }
+ b.reasoning = { ...SUNE.reasoning_effort && SUNE.reasoning_effort !== "default" ? { effort: SUNE.reasoning_effort } : {}, exclude: !SUNE.include_thoughts };
+ if (SUNE.verbosity) b.verbosity = SUNE.verbosity;
+ return b;
+};
+async function askOpenRouterStreaming(onDelta, streamId) {
+ const model = SUNE.model, provider = model.startsWith("oai:") ? "openai" : model.startsWith("g:") ? "google" : model.startsWith("cla:") ? "claude" : model.startsWith("cf:") ? "cloudflare" : model.startsWith("or:") ? "openrouter" : USER.provider, apiKey = provider === "openai" ? USER.apiKeyOpenAI : provider === "google" ? USER.apiKeyGoogle : provider === "claude" ? USER.apiKeyClaude : provider === "cloudflare" ? USER.apiKeyCloudflare : USER.apiKeyOpenRouter;
+ if (!apiKey) {
+ onDelta(localDemoReply(), true);
+ return { ok: true, rid: streamId || null };
+ }
+ const r = { rid: streamId || gid(), seq: -1, done: false, signaled: false, ws: null };
+ await cacheStore.setItem(r.rid, "busy");
+ const signal = (t) => {
+ if (!r.signaled) {
+ r.signaled = true;
+ onDelta(t || "", true);
+ }
+ };
+ const ws = new WebSocket(HTTP_BASE.replace("https", "wss") + "?uid=" + encodeURIComponent(r.rid));
+ r.ws = ws;
+ ws.onopen = () => ws.send(JSON.stringify({ type: "begin", rid: r.rid, provider, apiKey, or_body: buildBody() }));
+ ws.onmessage = (e) => {
+ let m;
+ try {
+ m = JSON.parse(e.data);
+ } catch {
+ return;
+ }
+ if (m.type === "delta" && typeof m.seq === "number" && m.seq > r.seq) {
+ r.seq = m.seq;
+ onDelta(m.text || "", false);
+ } else if (m.type === "done" || m.type === "err") {
+ r.done = true;
+ cacheStore.setItem(r.rid, "done");
+ signal(m.type === "err" ? "\n\n" + (m.message || "error") : "");
+ ws.close();
+ }
+ };
+ ws.onclose = () => {
+ };
+ ws.onerror = () => {
+ };
+ state.controller = { abort: () => {
+ r.done = true;
+ cacheStore.setItem(r.rid, "done");
+ try {
+ if (ws.readyState === 1) ws.send(JSON.stringify({ type: "stop", rid: r.rid }));
+ } catch {
+ }
+ signal("");
+ }, disconnect: () => ws.close() };
+ return { ok: true, rid: r.rid };
+}
+const accountTabs = { General: ["accountTabGeneral", "accountPanelGeneral"], API: ["accountTabAPI", "accountPanelAPI"], User: ["accountTabUser", "accountPanelUser"] };
+function showAccountTab(key) {
+ Object.entries(accountTabs).forEach(([k, [tb, pn]]) => {
+ el[tb].classList.toggle("border-black", k === key);
+ el[pn].classList.toggle("hidden", k !== key);
+ });
+}
+function openAccountSettings() {
+ el.set_provider.value = USER.provider || "openrouter";
+ el.set_api_key_or.value = USER.apiKeyOpenRouter || "";
+ el.set_api_key_oai.value = USER.apiKeyOpenAI || "";
+ el.set_api_key_g.value = USER.apiKeyGoogle || "";
+ el.set_api_key_claude.value = USER.apiKeyClaude || "";
+ el.set_api_key_cf.value = USER.apiKeyCloudflare || "";
+ el.set_master_prompt.value = USER.masterPrompt || "";
+ el.set_title_model.value = USER.titleModel;
+ el.set_gh_token.value = USER.githubToken || "";
+ const sa = USER.gcpSA;
+ el.gcpSAUploadBtn.textContent = sa && sa.project_id ? `Uploaded: ${sa.project_id}` : "Upload .json";
+ el.set_user_name.value = USER.name;
+ el.userAvatarPreview.src = USER.avatar || "data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=";
+ el.userAvatarPreview.classList.toggle("bg-gray-200", !USER.avatar);
+ showAccountTab("General");
+ el.accountSettingsModal.classList.remove("hidden");
+}
+function closeAccountSettings() {
+ el.accountSettingsModal.classList.add("hidden");
+}
+$(el.accountSettingsOption).on("click", () => {
+ el.userMenu.classList.add("hidden");
+ openAccountSettings();
+});
+$(el.closeAccountSettings).on("click", closeAccountSettings);
+$(el.cancelAccountSettings).on("click", closeAccountSettings);
+$(el.accountSettingsModal).on("click", (e) => {
+ if (e.target === el.accountSettingsModal || e.target.classList.contains("bg-black/30")) closeAccountSettings();
+});
+$(el.accountSettingsForm).on("submit", (e) => {
+ e.preventDefault();
+ USER.provider = el.set_provider.value || "openrouter";
+ USER.apiKeyOpenRouter = String(el.set_api_key_or.value || "").trim();
+ USER.apiKeyOpenAI = String(el.set_api_key_oai.value || "").trim();
+ USER.apiKeyGoogle = String(el.set_api_key_g.value || "").trim();
+ USER.apiKeyClaude = String(el.set_api_key_claude.value || "").trim();
+ USER.apiKeyCloudflare = String(el.set_api_key_cf.value || "").trim();
+ USER.masterPrompt = String(el.set_master_prompt.value || "").trim();
+ USER.titleModel = String(el.set_title_model.value || "").trim();
+ USER.githubToken = String(el.set_gh_token.value || "").trim();
+ USER.name = String(el.set_user_name.value || "").trim();
+ closeAccountSettings();
+});
+el.gcpSAUploadBtn.onclick = () => el.gcpSAInput.click();
+el.gcpSAInput.onchange = async (e) => {
+ const f = e.target.files?.[0];
+ if (!f) return;
+ try {
+ const t = await f.text(), d = JSON.parse(t);
+ if (!d.project_id) throw new Error("Invalid");
+ USER.gcpSA = d;
+ el.gcpSAUploadBtn.textContent = `Uploaded: ${d.project_id}`;
+ alert("GCP SA loaded.");
+ } catch {
+ alert("Failed to load GCP SA.");
+ }
+};
+$(el.accountPanelAPI).on("click", (e) => {
+ const b = e.target.closest("[data-reveal-for]");
+ if (!b) return;
+ const i = document.getElementById(b.dataset.revealFor);
+ if (!i) return;
+ const p = i.type === "password";
+ i.type = p ? "text" : "password";
+ b.querySelector("i").setAttribute("data-lucide", p ? "eye-off" : "eye");
+ lucide.createIcons();
+});
+el.accountTabGeneral.onclick = () => showAccountTab("General");
+el.accountTabAPI.onclick = () => showAccountTab("API");
+el.accountTabUser.onclick = () => showAccountTab("User");
+el.setUserAvatarBtn.onclick = () => el.userAvatarInput.click();
+el.userAvatarInput.onchange = async (e) => {
+ const f = e.target.files?.[0];
+ if (!f) return;
+ try {
+ const dataUrl = await imgToWebp(f);
+ USER.avatar = dataUrl;
+ el.userAvatarPreview.src = dataUrl;
+ el.userAvatarPreview.classList.remove("bg-gray-200");
+ } catch {
+ alert("Failed to process image.");
+ }
+};
+el.exportAccountSettings.onclick = () => dl(`sune-account-${ts()}.json`, { v: 1, provider: USER.provider, apiKeyOpenRouter: USER.apiKeyOpenRouter, apiKeyOpenAI: USER.apiKeyOpenAI, apiKeyGoogle: USER.apiKeyGoogle, apiKeyClaude: USER.apiKeyClaude, apiKeyCloudflare: USER.apiKeyCloudflare, masterPrompt: USER.masterPrompt, titleModel: USER.titleModel, githubToken: USER.githubToken, gcpSA: USER.gcpSA, userName: USER.name, userAvatar: USER.avatar });
+el.importAccountSettings.onclick = () => {
+ el.importAccountSettingsInput.value = "";
+ el.importAccountSettingsInput.click();
+};
+el.importAccountSettingsInput.onchange = async (e) => {
+ const f = e.target.files?.[0];
+ if (!f) return;
+ try {
+ const d = JSON.parse(await f.text());
+ if (!d || typeof d !== "object") throw new Error("Invalid");
+ const m = { provider: "provider", apiKeyOpenRouter: "apiKeyOR", apiKeyOpenAI: "apiKeyOAI", apiKeyGoogle: "apiKeyG", apiKeyClaude: "apiKeyC", apiKeyCloudflare: "apiKeyCF", masterPrompt: "masterPrompt", titleModel: "titleModel", githubToken: "ghToken", name: "userName", avatar: "userAvatar", gcpSA: "gcpSA" };
+ Object.entries(m).forEach(([p, k]) => {
+ const v = d[p] ?? d[k];
+ if (typeof v === "string" || p === "gcpSA" && typeof v === "object" && v) USER[p] = v;
+ });
+ openAccountSettings();
+ alert("Imported.");
+ } catch {
+ alert("Import failed");
+ }
+};
+const getBubbleById = (id) => el.messages.querySelector(`.msg-bubble[data-mid="${CSS.escape(id)}"]`);
+async function syncActiveThread() {
+ const id = THREAD.getLastAssistantMessageId();
+ if (!id) return false;
+ if (await cacheStore.getItem(id) === "done") {
+ if (state.busy) {
+ setBtnSend();
+ state.busy = false;
+ state.controller = null;
+ }
+ return false;
+ }
+ if (!state.busy) {
+ state.busy = true;
+ state.controller = { abort: () => {
+ const ws = new WebSocket(HTTP_BASE.replace("https", "wss"));
+ ws.onopen = function() {
+ this.send(JSON.stringify({ type: "stop", rid: id }));
+ this.close();
+ };
+ } };
+ setBtnStop();
+ }
+ const bubble = getBubbleById(id);
+ if (!bubble) return false;
+ const prevText = bubble.textContent || "";
+ const j = await fetch(HTTP_BASE + "?uid=" + encodeURIComponent(id)).then((r) => r.ok ? r.json() : null).catch(() => null);
+ const finalise = (t, c) => {
+ renderMarkdown(bubble, t, { enhance: false });
+ enhanceCodeBlocks(bubble, true);
+ const i = state.messages.findIndex((x) => x.id === id);
+ if (i >= 0) state.messages[i].content = c;
+ else state.messages.push({ id, role: "assistant", content: c, ...activeMeta() });
+ THREAD.persist();
+ setBtnSend();
+ state.busy = false;
+ cacheStore.setItem(id, "done");
+ state.controller = null;
+ el.composer.dispatchEvent(new CustomEvent("sune:newSuneResponse", { detail: { message: state.messages.find((m) => m.id === id) } }));
+ };
+ if (!j || j.rid !== id) {
+ if (j && j.error) {
+ const t = prevText + "\n\n" + j.error;
+ finalise(t, [{ type: "text", text: t }]);
+ }
+ return false;
+ }
+ const text = j.text || "", isDone = j.error || j.done || j.phase === "done";
+ if (text) renderMarkdown(bubble, text, { enhance: false });
+ if (isDone) {
+ const finalText = text || prevText;
+ finalise(finalText, [{ type: "text", text: finalText }]);
+ return false;
+ }
+ await cacheStore.setItem(id, "busy");
+ return true;
+}
+let syncLoopRunning = false;
+async function syncWhileBusy() {
+ if (syncLoopRunning || document.visibilityState === "hidden") return;
+ syncLoopRunning = true;
+ try {
+ while (await syncActiveThread()) await new Promise((r) => setTimeout(r, 1500));
+ } finally {
+ syncLoopRunning = false;
+ }
+}
+const onForeground = () => {
+ if (document.visibilityState !== "visible") return;
+ state.controller?.disconnect?.();
+ if (state.busy) syncWhileBusy();
+};
+$(document).on("visibilitychange", onForeground);
+$(el.copySystemPrompt).on("click", async () => {
+ try {
+ await navigator.clipboard.writeText(el.set_system_prompt.value || "");
+ } catch {
+ }
+});
+$(el.pasteSystemPrompt).on("click", async () => {
+ try {
+ el.set_system_prompt.value = await navigator.clipboard.readText();
+ } catch {
+ }
+});
+const getActiveHtmlParts = () => !el.htmlEditor.classList.contains("hidden") ? [el.htmlEditor, jars.html] : [el.extensionHtmlEditor, jars.extension];
+$(el.copyHTML).on("click", async () => {
+ try {
+ await navigator.clipboard.writeText(getActiveHtmlParts()[0].textContent || "");
+ } catch {
+ }
+});
+$(el.pasteHTML).on("click", async () => {
+ try {
+ const t = await navigator.clipboard.readText();
+ const [editor, jar] = getActiveHtmlParts();
+ if (jar && jar.updateCode) jar.updateCode(t);
+ else if (editor) editor.textContent = t;
+ } catch {
+ }
+});
+Object.assign(window, { icons, haptic, clamp, num, int, gid, esc, positionPopover, sid, fmtSize, asDataURL, b64, makeSune, getModelShort, resolveSuneSrc, processSuneIncludes, renderSuneHTML, reflectActiveSune, suneRow, enhanceCodeBlocks, getSuneLabel, _createMessageRow, msgRow, partsToText, addSuneBubbleStreaming, clearChat, payloadWithSampling, setBtnStop, setBtnSend, localDemoReply, titleFrom, ensureThreadOnFirstUser, generateTitleWithAI, threadRow, renderThreads, hideThreadPopover, showThreadPopover, hideSunePopover, showSunePopover, updateAttachBadge, toAttach, ensureJars, openSettings, closeSettings, showTab, dl, ts, kbUpdate, kbBind, activeMeta, init, showHtmlTab, buildBody, askOpenRouterStreaming, showAccountTab, openAccountSettings, closeAccountSettings, getBubbleById, syncActiveThread, syncWhileBusy, onForeground, getActiveHtmlParts, imgToWebp });
diff --git a/dist/assets/index-aer9aH6o.css b/dist/assets/index-aer9aH6o.css
deleted file mode 100644
index 33c9fdf..0000000
--- a/dist/assets/index-aer9aH6o.css
+++ /dev/null
@@ -1,36 +0,0 @@
-:root{--safe-bottom:env(safe-area-inset-bottom)}
-::-webkit-scrollbar{height:8px;width:8px}
-::-webkit-scrollbar-thumb{background:#e5e7eb;border-radius:999px}
-.no-scrollbar::-webkit-scrollbar{display:none}
-.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}
-html,body{overscroll-behavior-y:contain}
-.markdown-body{font-size:14px;line-height:1.6}
-.markdown-body pre{overflow:auto}
-.markdown-body ul,.markdown-body ol{list-style:revert;padding-left:2em}
-.msg-bubble{overflow-x:auto}
-.msg-avatar{font-size:16px}
-.menu-card{position:fixed;z-index:60;min-width:12rem;border-radius:0.75rem;border:1px solid #e5e7eb;background:#fff;box-shadow:0 10px 20px rgba(0,0,0,.08)}
-.menu-item{width:100%;text-align:left;padding:.5rem .75rem;font-size:0.875rem;display:flex;align-items:center;gap:.5rem}
-#htmlEditor,#extensionHtmlEditor,#jsonSchemaEditor{outline:none;white-space:pre!important;font-size:11px;line-height:1.5}
-:not(pre)>code{font-size:85%;padding:.2em .4em;margin:0;border-radius:6px;background-color:rgba(175,184,193,0.2)}
-
-/* UI + sidebars use Inter (chat markdown keeps GitHub markdown font) */
-body,
-#topbar,
-#footer,
-#sidebarLeft,
-#sidebarRight,
-#suneModal,
-#accountSettingsModal,
-.menu-card,
-.menu-item,
-#composer textarea,
-#suneList,
-#threadList,
-#userMenu,
-button,
-input,
-select,
-textarea{
- font-family:'Inter',system-ui,-apple-system,BlinkMacSystemFont,'SF Pro Text','Segoe UI',sans-serif;
-}
diff --git a/dist/assets/index-d-q9Uz1G.css b/dist/assets/index-d-q9Uz1G.css
new file mode 100644
index 0000000..c3f1552
--- /dev/null
+++ b/dist/assets/index-d-q9Uz1G.css
@@ -0,0 +1,12 @@
+:root{--safe-bottom:env(safe-area-inset-bottom)}::-webkit-scrollbar{height:8px;width:8px}::-webkit-scrollbar-thumb{background:#e5e7eb;border-radius:999px}.no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}
+html,body{overscroll-behavior-y:contain}
+.markdown-body{font-size:14px;line-height:1.6}.markdown-body pre{overflow:auto}
+.markdown-body ul,.markdown-body ol{list-style:revert;padding-left:2em}
+.msg-bubble{overflow-x:auto}
+.msg-avatar{font-size:16px}
+.menu-card{position:fixed;z-index:60;min-width:12rem;border-radius:0.75rem;border:1px solid #e5e7eb;background:#fff;box-shadow:0 10px 20px rgba(0,0,0,.08)}
+.menu-item{width:100%;text-align:left;padding:.5rem .75rem;font-size:0.875rem;display:flex;align-items:center;gap:.5rem}
+#htmlEditor,#extensionHtmlEditor,#jsonSchemaEditor{outline:none;white-space:pre!important;font-size:11px;line-height:1.5;}
+:not(pre)>code{font-size:85%;padding:.2em .4em;margin:0;border-radius:6px;background-color:rgba(175,184,193,0.2)}
+
+
diff --git a/dist/index.html b/dist/index.html
index a243a51..f5bf5c9 100644
--- a/dist/index.html
+++ b/dist/index.html
@@ -5,16 +5,14 @@
Sune
-
-
-
-
+
+
-
-
-
-
-
+