Agent.html / index.html
Nymbo's picture
Update index.html
f67267e verified
Raw
History Blame Contribute Delete
152 kB
<!DOCTYPE html>
<!--
content/apps/Agent.html
Runs a standalone configurable AI chatbot with streaming, MCP tools, attachments, agent traces, saved chats, and sandboxed HTML artifacts.
-->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="dark">
<title>Agent</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preconnect" href="https://cdn.jsdelivr.net">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11.11.1/styles/github-dark.min.css">
<script src="https://cdn.jsdelivr.net/npm/lucide@0.468.0/dist/umd/lucide.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked@15.0.12/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.2.6/dist/purify.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
<style>
* { box-sizing: border-box; }
:root {
color-scheme: dark;
--bg: #0b0d10; --bg-2: #0f1216; --surface: #14181d; --surface-2: #191e24;
--surface-3: #20262e; --text: #f3f5f7; --text-2: #c9d0d8; --muted: #8b949f;
--faint: #626c78; --line: rgba(196, 208, 222, .13); --line-2: rgba(196, 208, 222, .23);
--accent: #78d6c6; --accent-2: #9be8dc; --accent-soft: rgba(120, 214, 198, .11);
--blue: #7cb7ff; --green: #6bd69b; --amber: #eac36a; --red: #ef8181; --purple: #bba0f7;
--sidebar: 278px; --radius: 10px; --shadow: 0 18px 48px rgba(0,0,0,.34);
--sans: "Inter", system-ui, sans-serif; --mono: "IBM Plex Mono", ui-monospace, monospace;
}
html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; background: var(--bg); color: var(--text); }
body { font: 14px/1.5 var(--sans); -webkit-font-smoothing: antialiased; }
button, input, textarea, select { font: inherit; }
button { cursor: pointer; }
button:disabled, input:disabled, textarea:disabled, select:disabled { cursor: not-allowed; opacity: .5; }
a { color: var(--accent); }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
::-webkit-scrollbar { width: 9px; height: 9px; }
::-webkit-scrollbar-thumb { background: rgba(178,191,207,.19); border: 2px solid transparent; border-radius: 10px; background-clip: padding-box; }
::-webkit-scrollbar-thumb:hover { background: rgba(178,191,207,.32); border: 2px solid transparent; background-clip: padding-box; }
.hidden { display: none !important; }
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; }
.app { height: 100dvh; display: grid; grid-template-columns: var(--sidebar) minmax(0,1fr); overflow: hidden; }
.sidebar { min-width: 0; display: flex; flex-direction: column; border-right: 1px solid var(--line); background: #0e1115; transition: width .18s ease, transform .18s ease; z-index: 30; }
.side-head { padding: 10px; border-bottom: 1px solid var(--line); }
.side-head-actions { display:flex; align-items:center; gap:6px; }
.side-head .new-chat { flex:1; width: 100%; min-height: 31px; height: 31px; font-size: .73rem; }
.side-head .new-chat svg { width: 14px; height: 14px; }
.chat-list { min-height: 0; flex: 1; overflow: auto; padding: 0 7px 10px; }
.chat-row { position: relative; display: grid; grid-template-columns: minmax(0,1fr) 22px; align-items: center; gap: 1px; border-radius: 7px; }
.chat-row:hover { background: var(--surface); }
.chat-row.active { background: var(--surface-2); }
.chat-open { min-width: 0; display: flex; align-items: center; gap: 7px; height: 32px; padding: 0 7px; border: 0; background: transparent; color: var(--text-2); text-align: left; font-size: .72rem; }
.chat-open svg { width: 13px; height: 13px; flex: 0 0 auto; }
.chat-open span { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.chat-delete { opacity: 0; width: 20px; height: 20px; padding: 0; display: grid; place-items: center; border: 0; border-radius: 5px; background: transparent; color: var(--muted); }
.chat-delete svg { width: 12px; height: 12px; }
.chat-row:hover .chat-delete, .chat-row:focus-within .chat-delete { opacity: 1; }
.chat-delete:hover { color: var(--red); background: rgba(239,129,129,.1); }
.empty-chats { padding: 24px 12px; color: var(--faint); text-align: center; font-size: .78rem; }
.side-foot { padding: 8px; border-top: 1px solid var(--line); display: grid; gap: 5px; }
.side-foot .btn { min-height: 31px; justify-content: flex-start; padding-inline: 9px; font-size: .72rem; }
.side-foot .btn svg { width: 14px; height: 14px; }
.workspace { min-width: 0; min-height: 0; display: grid; grid-template-rows: minmax(0,1fr); background: var(--bg); }
.model-loading { width: 13px; height: 13px; border: 2px solid var(--line-2); border-top-color: var(--accent); border-radius: 50%; animation: spin .7s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.main { min-height: 0; position: relative; display: grid; grid-template-rows: minmax(0,1fr) auto; }
.messages { min-height: 0; overflow-y: auto; overflow-anchor: auto; }
.messages-inner { width: min(860px, calc(100% - 24px)); margin: 0 auto; padding: 18px 0 13px; }
.welcome { min-height: calc(100dvh - 160px); display: grid; place-items: center; padding: 20px 0; }
.welcome-card { width: min(680px,100%); text-align: center; }
.welcome-icon { width: 44px; height: 44px; margin: 0 auto 13px; display: grid; place-items: center; border-radius: 12px; color: var(--accent); background: var(--accent-soft); border: 1px solid rgba(120,214,198,.25); box-shadow: 0 0 36px rgba(120,214,198,.08); }
.welcome h1 { margin: 0 0 5px; font-size: clamp(1.3rem, 3vw, 1.7rem); letter-spacing: -.03em; }
.prompt-grid { margin-top: 15px; display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 7px; text-align: left; }
.prompt-card { min-height: 60px; padding: 9px 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--surface); color: var(--text-2); }
.prompt-card:hover { border-color: var(--line-2); background: var(--surface-2); }
.prompt-card strong { display: block; margin-bottom: 2px; color: var(--text); font-size: .75rem; }
.prompt-card span { color: var(--muted); font-size: .68rem; }
.message { position: relative; display: flex; flex-direction: column; margin: 0 0 16px; animation: rise .16s ease; }
@keyframes rise { from { opacity: 0; transform: translateY(4px); } }
.message.user { align-items: flex-end; }
.message.assistant { align-items: flex-start; }
.message-timeline { width: 100%; display: flex; flex-direction: column; align-items: flex-start; gap: 5px; }
.message-label { margin: 0 4px 3px; color: var(--muted); font-size: .62rem; font-weight: 600; letter-spacing: .03em; }
.bubble { max-width: min(94%, 730px); border: 1px solid var(--line); border-radius: 12px; padding: 9px 11px; background: var(--surface); box-shadow: 0 2px 9px rgba(0,0,0,.12); overflow-wrap: anywhere; }
.user .bubble { max-width: min(84%, 650px); color: #08110f; background: var(--accent); border-color: transparent; border-top-right-radius: 5px; }
.assistant .bubble { border-top-left-radius: 5px; }
.bubble.error { border-color: rgba(239,129,129,.35); background: rgba(239,129,129,.06); }
.message-actions { display: flex; gap: 1px; min-height: 23px; margin: 3px 2px 0; opacity: 0; transition: opacity .14s; }
.message:hover .message-actions, .message:focus-within .message-actions { opacity: 1; }
.user .message-actions { justify-content: flex-end; }
.message-actions button { width: 23px; height: 23px; padding: 0; border: 0; border-radius: 5px; background: transparent; color: var(--muted); }
.message-actions button svg { width: 12px; height: 12px; }
.message-actions button:hover { color: var(--text); background: var(--surface-2); }
.model-badge { padding: 0 6px; align-self: center; color: var(--faint); font-size: .65rem; }
.markdown { min-width: 0; font-size: .82rem; line-height: 1.6; }
.markdown > :first-child { margin-top: 0; } .markdown > :last-child { margin-bottom: 0; }
.markdown p, .markdown ul, .markdown ol, .markdown blockquote, .markdown pre, .markdown table { margin: .65em 0; }
.markdown h1, .markdown h2, .markdown h3 { margin: 1em 0 .45em; line-height: 1.25; }
.markdown h1 { font-size: 1.32rem; } .markdown h2 { font-size: 1.14rem; } .markdown h3 { font-size: 1rem; }
.markdown ul, .markdown ol { padding-left: 1.4rem; }
.markdown blockquote { border-left: 3px solid var(--line-2); padding-left: 12px; color: var(--muted); }
.markdown :not(pre) > code { padding: 2px 5px; border: 1px solid var(--line); border-radius: 5px; background: rgba(0,0,0,.22); color: var(--accent-2); font: .88em var(--mono); }
.user .markdown :not(pre) > code { color: #07100e; border-color: rgba(0,0,0,.15); }
.code-wrap { margin: .8em 0; border: 1px solid var(--line); border-radius: 9px; overflow: hidden; background: #0d1014; }
.code-head { height: 30px; display: flex; align-items: center; gap: 5px; padding: 0 6px 0 9px; border-bottom: 1px solid var(--line); background: #11151a; }
.code-head span { min-width: 0; flex: 1; color: var(--muted); font: .67rem var(--mono); text-transform: uppercase; }
.code-head button { height: 22px; padding: 0 7px; border: 1px solid var(--line); border-radius: 5px; background: var(--surface); color: var(--text-2); font-size: .64rem; }
.code-head button:hover { border-color: var(--line-2); color: var(--text); }
.code-wrap pre { margin: 0; padding: 10px; overflow: auto; background: transparent !important; }
.code-wrap code { font: .78rem/1.55 var(--mono); background: transparent !important; }
.markdown table { width: 100%; border-collapse: collapse; display: block; overflow-x: auto; }
.markdown th, .markdown td { padding: 7px 9px; border: 1px solid var(--line); text-align: left; }
.markdown th { background: var(--surface-2); }
.markdown img { max-width: 100%; border-radius: 8px; }
.trace { width: min(94%,730px); margin-bottom: 5px; border: 1px solid var(--line); border-radius: 8px; background: rgba(20,24,29,.65); overflow: hidden; }
.trace-row + .trace-row { border-top: 1px solid var(--line); }
.trace-row summary { min-height: 31px; display: flex; align-items: center; gap: 7px; padding: 5px 8px; color: var(--text-2); cursor: pointer; list-style: none; font-size: .7rem; }
.trace-row summary svg { width: 13px; height: 13px; }
.trace-row summary::-webkit-details-marker { display: none; }
.trace-row summary .trace-title { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.trace-row .chev { transition: transform .15s; color: var(--faint); }
.trace-row[open] .chev { transform: rotate(180deg); }
.trace-detail { padding: 0 8px 8px 28px; }
.trace-detail pre { max-height: 240px; margin: 0; padding: 7px; overflow: auto; border: 1px solid var(--line); border-radius: 6px; background: #0c0f12; color: var(--text-2); white-space: pre-wrap; font: .66rem/1.5 var(--mono); }
.trace-tool { color: var(--amber); } .trace-done { color: var(--green); } .trace-error { color: var(--red); } .trace-thought { color: var(--purple); }
.attachment-list { display: flex; gap: 5px; flex-wrap: wrap; margin-bottom: 6px; }
.attachment { max-width: 230px; display: grid; grid-template-columns: 26px minmax(0,1fr) auto; align-items: center; gap: 6px; padding: 5px; border: 1px solid var(--line); border-radius: 7px; background: rgba(0,0,0,.14); }
.attachment img { width: 26px; height: 26px; object-fit: cover; border-radius: 4px; }
.attachment-icon { width: 26px; height: 26px; display: grid; place-items: center; border-radius: 4px; background: var(--surface-3); color: var(--blue); }
.attachment-copy { min-width: 0; }
.attachment-copy strong, .attachment-copy span { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.attachment-copy strong { font-size: .7rem; } .attachment-copy span { color: var(--muted); font-size: .62rem; }
.attachment-remove { width: 19px; height: 19px; padding: 0; border: 0; border-radius: 4px; background: transparent; color: var(--muted); }
.attachment-remove svg { width: 11px; height: 11px; }
.attachment-remove:hover { color: var(--red); background: rgba(239,129,129,.1); }
.status-line { width: min(860px,calc(100% - 24px)); min-height: 23px; margin: 0 auto 3px; display: flex; align-items: center; gap: 6px; color: var(--muted); font-size: .68rem; }
.status-line .pulse { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 0 0 rgba(120,214,198,.4); animation: pulse 1.25s infinite; }
@keyframes pulse { 70% { box-shadow: 0 0 0 7px rgba(120,214,198,0); } 100% { box-shadow: 0 0 0 0 rgba(120,214,198,0); } }
.composer-shell { position: relative; padding: 0 11px 10px; background: linear-gradient(transparent, var(--bg) 22%); }
.composer { width: min(860px,100%); margin: 0 auto; border: 1px solid var(--line-2); border-radius: 11px; background: var(--surface); box-shadow: 0 10px 28px rgba(0,0,0,.24); overflow: hidden; }
.pending-attachments { padding: 7px 8px 0; }
.composer textarea { width: 100%; min-height: 46px; max-height: 170px; display: block; resize: none; padding: 10px 11px 5px; border: 0; outline: 0; background: transparent; color: var(--text); font-size: .82rem; line-height: 1.45; }
.composer textarea::placeholder { color: var(--faint); }
.composer-foot { min-height: 36px; display: flex; align-items: center; gap: 3px; padding: 3px 5px; }
.composer-spacer { flex: 1; }
.reasoning-btn { width: auto; max-width: 125px; height: 27px; padding: 0 7px; display: inline-flex; align-items: center; gap: 4px; border: 0; border-radius: 6px; background: transparent; color: var(--muted); font-size: .68rem; }
.reasoning-btn:hover:not(:disabled) { background: var(--surface-2); color: var(--text-2); }
.reasoning-btn span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.reasoning-btn svg { width: 13px; height: 13px; flex: 0 0 auto; }
.model-picker-btn { width: auto; max-width: 210px; height: 27px; padding: 0 7px; display: inline-flex; align-items: center; gap: 4px; border: 0; border-radius: 6px; background: transparent; color: var(--accent); font-size: .68rem; }
.model-picker-btn:hover { background: var(--accent-soft); }
.model-picker-btn span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.model-picker-btn svg { width: 13px; height: 13px; flex: 0 0 auto; }
.send-btn { color: #07110f !important; background: var(--accent) !important; border-color: transparent !important; }
.send-btn:hover { background: var(--accent-2) !important; }
.stop-btn { color: var(--red) !important; background: rgba(239,129,129,.1) !important; }
.command-panel { position:absolute; left:50%; bottom:calc(100% + 4px); z-index:45; width:min(860px,calc(100% - 22px)); max-height:min(360px,48dvh); transform:translateX(-50%); padding:5px; border:1px solid var(--line-2); border-radius:12px; background:rgba(27,29,32,.98); box-shadow:0 18px 48px rgba(0,0,0,.38); overflow:auto; }
.command-row { width:100%; min-width:0; display:grid; grid-template-columns:24px minmax(0,1fr); align-items:center; gap:5px; min-height:35px; padding:5px 7px; border:0; border-radius:8px; background:transparent; color:var(--text-2); text-align:left; }
.command-row:hover, .command-row.selected { background:rgba(255,255,255,.08); color:var(--text); }
.command-row > svg { width:15px; height:15px; color:var(--muted); }
.command-copy { min-width:0; display:flex; align-items:baseline; gap:7px; overflow:hidden; }
.command-copy strong { flex:0 0 auto; font-size:.72rem; font-weight:500; }
.command-copy span { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:var(--muted); font-size:.68rem; }
.command-empty { padding:18px 10px; color:var(--muted); text-align:center; font-size:.72rem; }
.composer-popover { position:absolute; bottom:calc(100% + 4px); z-index:46; padding:7px; border:1px solid var(--line-2); border-radius:10px; background:rgba(27,29,32,.98); box-shadow:0 16px 42px rgba(0,0,0,.36); }
.model-popover { width:min(340px,calc(100% - 22px)); }
.reasoning-popover { width:min(290px,calc(100% - 22px)); }
.tools-popover { width:min(440px,calc(100% - 22px)); }
.config-popover { width:min(570px,calc(100% - 22px)); }
.data-controls-popover { width:min(420px,calc(100% - 22px)); }
.panel-popover { max-height:min(70dvh,640px); padding:0; overflow:hidden; grid-template-rows:auto minmax(0,1fr) auto; }
.panel-popover:not(.hidden) { display:grid; }
.panel-popover .dialog-body { min-height:0; overflow:auto; }
.composer-popover .model-search-wrap { margin-bottom:4px; border:1px solid var(--line); border-radius:6px; background:rgba(255,255,255,.025); }
.composer-popover .model-search-wrap:focus-within { border-color:rgba(120,214,198,.45); box-shadow:inset 0 0 0 1px rgba(120,214,198,.08); }
.composer-popover .model-search-wrap input { min-height:27px; height:27px; padding-left:27px; padding-right:28px; border:0; border-radius:6px; background:transparent; font-size:.68rem; }
.composer-popover .model-search-wrap input:focus-visible { outline:none; }
.composer-popover .model-search-wrap svg { left:8px; width:12px; height:12px; }
.composer-popover .model-search-wrap .model-loading { position:absolute; left:auto; right:8px; top:7px; width:11px; height:11px; }
.composer-popover .model-list { min-height:80px; max-height:min(310px,42dvh); }
.composer-popover .reasoning-options { display:grid; grid-template-columns:1fr; }
.btn, .icon-btn, select, input[type="text"], input[type="password"], input[type="number"], textarea.field-input {
border: 1px solid var(--line); border-radius: 8px; background: var(--surface-2); color: var(--text-2);
}
.btn { min-height: 30px; display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 0 9px; font-weight: 600; font-size: .71rem; }
.btn:hover, .icon-btn:hover { border-color: var(--line-2); background: var(--surface-3); color: var(--text); }
.btn.primary { color: #07110f; border-color: transparent; background: var(--accent); }
.btn.danger { color: var(--red); background: rgba(239,129,129,.08); }
.btn.ghost { background: transparent; }
.icon-btn { width: 30px; height: 30px; padding: 0; display: grid; place-items: center; }
.icon-btn svg, .btn svg, .attachment-icon svg, .welcome-icon svg { width: 14px; height: 14px; }
select, input[type="text"], input[type="url"], input[type="password"], input[type="number"] { min-height: 31px; padding: 0 8px; font-size: .75rem; }
.dialog-head { min-height: 46px; display: flex; align-items: center; gap: 8px; padding: 8px 9px 8px 13px; border-bottom: 1px solid var(--line); }
.dialog-head > div { min-width: 0; flex: 1; }
.dialog-head h2 { margin: 0; font-size: .86rem; } .dialog-head p { margin: 1px 0 0; color: var(--muted); font-size: .65rem; }
.dialog-body { min-height:0; max-height:none; padding: 12px; overflow:auto; }
.config-popover .dialog-body, .data-controls-popover .dialog-body { padding-block: 10px; }
.dialog-foot { min-height: 46px; display: flex; justify-content: flex-end; align-items: center; gap: 6px; padding: 7px 9px; border-top: 1px solid var(--line); }
.settings-section + .settings-section { margin-top: 15px; padding-top: 13px; border-top: 1px solid var(--line); }
.settings-section h3 { margin: 0 0 3px; font-size: .78rem; } .settings-section > p { margin: 0 0 11px; color: var(--muted); font-size: .7rem; }
.mcp-settings-head { display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:7px; }
.mcp-settings-head h3 { margin:0; }
.mcp-server-list { display:grid; gap:6px; }
.mcp-server-row { display:grid; grid-template-columns:22px minmax(100px,.38fr) minmax(180px,1fr) 30px; align-items:center; gap:6px; }
.mcp-server-row input[type="checkbox"] { accent-color:var(--accent); justify-self:center; }
.mcp-server-row input[type="text"], .mcp-server-row input[type="url"] { min-width:0; }
.mcp-server-row .icon-btn { width:30px; height:30px; }
.provider-settings-head { display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:9px; }
.provider-settings-head h3 { margin-bottom:2px; }
.provider-settings-head p { margin:0; color:var(--muted); font-size:.66rem; }
.provider-picker { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:7px; margin-bottom:10px; }
.provider-card { min-width:0; display:grid; grid-template-columns:30px minmax(0,1fr) auto; align-items:center; gap:8px; padding:9px; border:1px solid var(--line); border-radius:8px; background:var(--surface-2); color:var(--text-2); text-align:left; }
.provider-card:hover { border-color:var(--line-2); background:var(--surface-3); }
.provider-card.active { border-color:rgba(120,214,198,.45); background:var(--accent-soft); color:var(--text); box-shadow:inset 0 0 0 1px rgba(120,214,198,.08); }
.provider-card-icon { width:30px; height:30px; display:grid; place-items:center; border-radius:7px; background:rgba(255,255,255,.045); color:var(--accent); }
.provider-card-icon svg { width:15px; height:15px; }
.provider-card-copy { min-width:0; display:grid; gap:2px; }
.provider-card-copy strong, .provider-card-copy span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.provider-card-copy strong { font-size:.72rem; }
.provider-card-copy span { color:var(--muted); font-size:.63rem; }
.provider-card-check { width:17px; height:17px; color:var(--accent); opacity:0; }
.provider-card.active .provider-card-check { opacity:1; }
.provider-editor-head { display:flex; align-items:center; justify-content:space-between; gap:8px; margin:2px 0 9px; padding-top:10px; border-top:1px solid var(--line); }
.provider-editor-title { min-width:0; display:flex; align-items:center; gap:7px; }
.provider-editor-title strong { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:.75rem; }
.provider-badge { padding:2px 6px; border-radius:999px; background:var(--surface-3); color:var(--muted); font-size:.58rem; text-transform:uppercase; letter-spacing:.04em; }
.field { display: grid; gap: 4px; margin-bottom: 8px; }
.field label, .field-row label { color: var(--text-2); font-size: .7rem; font-weight: 600; }
.field small { color: var(--faint); font-size: .65rem; }
.field input, .field select, .field textarea { width: 100%; }
.field textarea { min-height: 82px; padding: 7px; resize: vertical; font-size: .75rem; }
.field-row { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 10px; }
.range-row { display: grid; grid-template-columns: minmax(0,1fr) 58px; align-items: center; gap: 7px; }
input[type="range"] { width: 100%; accent-color: var(--accent); }
.check-row { display: flex; align-items: flex-start; gap: 8px; color: var(--text-2); font-size: .72rem; }
.check-row input { margin-top: 3px; accent-color: var(--accent); }
.security-note, .server-note { display: flex; gap: 8px; padding: 9px 10px; border-radius: 8px; color: var(--muted); font-size: .68rem; }
.security-note { background: rgba(107,214,155,.06); border: 1px solid rgba(107,214,155,.15); }
.server-note { background: rgba(124,183,255,.06); border: 1px solid rgba(124,183,255,.15); }
.tool-grid { margin-top:8px; border:1px solid var(--line); border-radius:7px; overflow:hidden; background:var(--surface-2); }
.tool-server-group + .tool-server-group { border-top:1px solid var(--line-2); }
.tool-server-head { min-height:34px; display:grid; grid-template-columns:16px minmax(0,1fr); align-items:center; gap:8px; padding:6px 9px; background:rgba(255,255,255,.035); cursor:pointer; }
.tool-server-head:hover { background:rgba(255,255,255,.055); }
.tool-server-head input { margin:0; accent-color:var(--accent); }
.tool-server-head span { min-width:0; display:flex; align-items:baseline; justify-content:space-between; gap:8px; }
.tool-server-head strong { overflow:hidden; color:var(--text); font-size:.7rem; text-overflow:ellipsis; white-space:nowrap; }
.tool-server-head small { flex:0 0 auto; color:var(--muted); font-size:.61rem; }
.tool-card { display:grid; grid-template-columns:16px 17px minmax(0,1fr); align-items:start; gap:8px; min-height:43px; padding:7px 9px; border:0; border-bottom:1px solid var(--line); background:transparent; cursor:pointer; }
.tool-card:last-child { border-bottom:0; }
.tool-card:hover { background:var(--surface-3); }
.tool-card.disabled { opacity: .55; }
.tool-card input { margin:2px 0 0; accent-color:var(--accent); }
.tool-card strong, .tool-card span { display:block; }
.tool-card > span:nth-child(2) { color:var(--muted); }
.tool-card > span:last-child { min-width:0; }
.tool-card strong { color:var(--text-2); font-size:.72rem; line-height:1.25; }
.tool-card > span:last-child > span { overflow:hidden; color:var(--muted); font-size:.63rem; line-height:1.35; text-overflow:ellipsis; white-space:nowrap; }
.tool-card svg { width:15px; height:15px; margin-top:1px; }
.data-actions { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 8px; }
.data-actions .btn { min-width: 0; padding-inline: 8px; }
.model-search-wrap { position: relative; margin-bottom: 7px; }
.model-search-wrap svg { position: absolute; left: 10px; top: 50%; width: 14px; height: 14px; color: var(--muted); transform: translateY(-50%); pointer-events: none; }
.model-search-wrap input { width: 100%; padding-left: 32px; }
.model-list { min-height: 100px; max-height: min(56dvh,400px); overflow-y: auto; display: grid; align-content: start; gap: 2px; }
.model-option { width: 100%; min-width: 0; min-height: 33px; padding: 5px 7px; display: flex; align-items: center; gap: 7px; border: 0; border-radius: 6px; background: transparent; color: var(--text-2); text-align: left; font-size: .74rem; }
.model-option:hover, .model-option.selected { background: var(--surface-2); color: var(--text); }
.model-option.selected { color: var(--accent); }
.model-option span { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.model-option small { color: var(--faint); font: .62rem var(--mono); }
.model-empty { padding: 34px 12px; color: var(--muted); text-align: center; font-size: .75rem; }
.reasoning-options { display: flex; flex-wrap: wrap; gap: 5px; }
.reasoning-option { min-height: 30px; padding: 0 9px; border: 1px solid var(--line); border-radius: 7px; background: var(--surface-2); color: var(--text-2); font-size: .69rem; text-transform: capitalize; }
.reasoning-option:hover { border-color: var(--line-2); background: var(--surface-3); }
.reasoning-option.selected { border-color: rgba(120,214,198,.45); background: var(--accent-soft); color: var(--accent); }
.reasoning-budget { margin-top: 11px; padding-top: 10px; border-top: 1px solid var(--line); display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: end; gap: 6px; }
.reasoning-budget .field { margin: 0; }
.reasoning-unavailable { padding: 24px 10px; color: var(--muted); text-align: center; }
.artifact { position: fixed; inset: 0 0 0 auto; width: min(780px,92vw); display: grid; grid-template-rows: 48px minmax(0,1fr); transform: translateX(102%); transition: transform .2s ease; background: var(--surface); border-left: 1px solid var(--line-2); box-shadow: var(--shadow); z-index: 60; }
.artifact.open { transform: translateX(0); }
.artifact-head { display: flex; align-items: center; gap: 7px; padding: 7px 9px; border-bottom: 1px solid var(--line); }
.artifact-head > div { min-width: 0; flex: 1; } .artifact-head strong, .artifact-head span { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.artifact-head span { color: var(--muted); font-size: .67rem; }
.artifact iframe { width: 100%; height: 100%; border: 0; background: white; }
.artifact-backdrop { position: fixed; inset: 0; z-index: 55; background: rgba(0,0,0,.45); }
.drop-overlay { position: fixed; inset: 12px; z-index: 90; display: grid; place-items: center; border: 2px dashed var(--accent); border-radius: 15px; background: rgba(11,13,16,.9); color: var(--text); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); pointer-events: none; }
.drop-overlay div { text-align: center; } .drop-overlay strong { display: block; margin-top: 8px; font-size: 1rem; }
.toast-stack { position: fixed; right: 14px; bottom: 14px; z-index: 100; display: grid; gap: 7px; pointer-events: none; }
.toast { max-width: min(340px,calc(100vw - 24px)); padding: 7px 9px; border: 1px solid var(--line-2); border-radius: 7px; background: var(--surface-2); color: var(--text-2); box-shadow: var(--shadow); animation: toast-in .18s ease; font-size: .68rem; }
.toast.error { border-color: rgba(239,129,129,.35); } @keyframes toast-in { from { opacity: 0; transform: translateY(5px); } }
.mobile-only { display: none; }
@media (max-width: 760px) {
.app { display: block; }
.sidebar { position: fixed; inset: 0 auto 0 0; width: min(300px,88vw); transform: translateX(-102%); box-shadow: var(--shadow); }
.sidebar.open { transform: translateX(0); }
.sidebar-backdrop { position: fixed; inset: 0; z-index: 25; background: rgba(0,0,0,.5); }
.workspace { height: 100dvh; }
.mobile-only { display: grid; }
.top-title span { display: none; }
.messages-inner { width: calc(100% - 18px); padding-top: 16px; }
.bubble { max-width: 97%; } .user .bubble { max-width: 91%; }
.message-actions { opacity: 1; }
.prompt-grid { grid-template-columns: 1fr; }
.welcome { min-height: 0; padding: 28px 0 18px; }
.composer-shell { padding-inline: 8px; }
input:not([type="range"]):not([type="checkbox"]):not([type="file"]), textarea, select { font-size: 16px !important; }
.composer-foot { min-height: 50px; gap: 4px; padding-block: 4px; }
.composer-foot .icon-btn { width: 42px; height: 42px; }
.composer-foot .icon-btn svg { width: 17px; height: 17px; }
.field-row, .provider-picker, .tool-grid { grid-template-columns: 1fr; }
.mcp-server-row { grid-template-columns:22px minmax(0,1fr) 30px; }
.mcp-server-row input[type="url"] { grid-column:2 / 4; }
.data-actions { grid-template-columns: 1fr; }
.reasoning-btn, .model-picker-btn { height: 42px; max-width: 96px; padding-inline: 8px; font-size: .72rem; }
.reasoning-btn svg, .model-picker-btn svg { width: 15px; height: 15px; }
}
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: .01ms !important; transition-duration: .01ms !important; } }
@media (max-width: 360px) { .reasoning-btn, .model-picker-btn { max-width: 60px; } }
</style>
</head>
<body>
<div class="app">
<aside class="sidebar" id="sidebar" aria-label="Chats">
<div class="side-head">
<div class="side-head-actions">
<button class="btn new-chat" id="newChatBtn"><i data-lucide="plus"></i> New chat</button>
<button class="icon-btn mobile-only" id="closeSidebarBtn" aria-label="Close sidebar"><i data-lucide="x"></i></button>
</div>
</div>
<div class="chat-list" id="chatList"></div>
</aside>
<div class="sidebar-backdrop hidden" id="sidebarBackdrop"></div>
<section class="workspace">
<main class="main" id="main">
<div class="messages" id="messages" aria-live="polite">
<div class="messages-inner" id="messagesInner"></div>
</div>
<div>
<div class="status-line hidden" id="statusLine"><span class="pulse"></span><span id="statusText">Thinking…</span></div>
<div class="composer-shell">
<div class="command-panel hidden" id="commandPanel" role="listbox" aria-label="Commands"><div id="commandList"></div></div>
<div class="composer-popover model-popover hidden" id="modelPopover" role="dialog" aria-label="Choose a model">
<label class="sr-only" for="modelSearchInput">Search models</label>
<div class="model-search-wrap"><i data-lucide="search"></i><input id="modelSearchInput" type="text" placeholder="Search models…" autocomplete="off"><div class="model-loading hidden" id="modelLoading" aria-label="Loading models"></div></div>
<div class="model-list" id="modelList" role="listbox" aria-label="Models"></div>
</div>
<div class="composer-popover reasoning-popover hidden" id="reasoningPopover" role="dialog" aria-label="Reasoning settings">
<div id="reasoningControls"></div>
</div>
<div class="composer-popover panel-popover tools-popover hidden" id="toolsPopover" role="dialog" aria-label="MCP tools">
<div class="dialog-head"><div><h2>MCP tools</h2></div><div class="model-loading hidden" id="toolLoading" aria-label="Loading tools"></div><button class="icon-btn" type="button" data-close-popover aria-label="Close"><i data-lucide="x"></i></button></div>
<div class="dialog-body">
<div class="tool-grid" id="toolGrid"></div>
</div>
<div class="dialog-foot"><button class="btn" type="button" id="refreshToolsBtn"><i data-lucide="refresh-cw"></i> Refresh</button><button class="btn" type="button" id="disableToolsBtn">Disable all</button><button class="btn primary" type="button" id="saveToolsBtn">Done</button></div>
</div>
<div class="composer-popover panel-popover config-popover hidden" id="providersPopover" role="dialog" aria-label="Providers">
<div class="dialog-head"><div><h2>Providers</h2></div><button class="icon-btn" type="button" data-close-popover aria-label="Close"><i data-lucide="x"></i></button></div>
<div class="dialog-body">
<section class="settings-section">
<div class="provider-settings-head"><div><h3>Inference providers</h3><p>Choose where models run.</p></div><button class="btn" type="button" id="addInferenceProviderBtn"><i data-lucide="plus"></i> Add custom</button></div>
<div class="provider-picker" id="inferenceProviderSelect" aria-label="Inference providers"></div>
<div class="provider-editor-head"><div class="provider-editor-title"><strong id="providerEditorTitle">Provider</strong><span class="provider-badge" id="providerEditorBadge">Built-in</span></div><button class="icon-btn danger" type="button" id="removeInferenceProviderBtn" aria-label="Remove custom provider" title="Remove custom provider"><i data-lucide="trash-2"></i></button></div>
<div class="field-row"><div class="field"><label for="inferenceProviderNameInput">Provider name</label><input id="inferenceProviderNameInput" type="text" placeholder="Provider name"></div><div class="field"><label for="inferenceDefaultModelInput">Default model</label><input id="inferenceDefaultModelInput" type="text" placeholder="model-id"></div></div>
<div class="field"><label for="inferenceChatUrlInput">Chat completions URL</label><input id="inferenceChatUrlInput" type="url" placeholder="https://api.example.com/v1/chat/completions"></div>
<div class="field"><label for="inferenceModelsUrlInput">Models URL (optional)</label><input id="inferenceModelsUrlInput" type="url" placeholder="https://api.example.com/v1/models"></div>
<div class="field"><label for="apiKeyInput">API key</label><input id="apiKeyInput" type="password" placeholder="Optional for local providers" autocomplete="off"></div>
<label class="check-row"><input type="checkbox" id="rememberKeyInput"><span>Remember API key</span></label>
</section>
</div>
<div class="dialog-foot"><button class="btn primary" type="button" id="saveProvidersBtn">Save providers</button></div>
</div>
<div class="composer-popover panel-popover config-popover hidden" id="parametersPopover" role="dialog" aria-label="Parameters">
<div class="dialog-head"><div><h2>Parameters</h2></div><button class="icon-btn" type="button" data-close-popover aria-label="Close"><i data-lucide="x"></i></button></div>
<div class="dialog-body">
<section class="settings-section"><h3>System prompt</h3><div class="field"><label class="sr-only" for="systemPromptInput">System prompt</label><textarea class="field-input" id="systemPromptInput"></textarea></div></section>
<section class="settings-section"><h3>Generation</h3><div class="field-row">
<div class="field"><label for="temperatureInput">Temperature</label><div class="range-row"><input id="temperatureRange" type="range" min="0" max="2" step="0.05"><input id="temperatureInput" type="number" min="0" max="2" step="0.05"></div></div>
<div class="field"><label for="topPInput">Top P</label><div class="range-row"><input id="topPRange" type="range" min="0" max="1" step="0.01"><input id="topPInput" type="number" min="0" max="1" step="0.01"></div></div>
<div class="field"><label for="topKInput">Top K</label><div class="range-row"><input id="topKRange" type="range" min="0" max="100" step="1"><input id="topKInput" type="number" min="0" max="100" step="1"></div></div>
<div class="field"><label for="repetitionPenaltyInput">Repetition Penalty</label><div class="range-row"><input id="repetitionPenaltyRange" type="range" min="0" max="2" step="0.1"><input id="repetitionPenaltyInput" type="number" min="0" max="2" step="0.1"></div></div>
<div class="field"><label for="maxTokensInput">Max output tokens</label><input id="maxTokensInput" type="number" min="64" max="32768" step="64"></div>
<div class="field"><label for="seedInput">Seed (0 = random)</label><input id="seedInput" type="number" min="0" step="1"></div>
</div></section>
</div>
<div class="dialog-foot"><button class="btn" type="button" id="resetParametersBtn">Reset defaults</button></div>
</div>
<div class="composer-popover panel-popover config-popover hidden" id="mcpConfigPopover" role="dialog" aria-label="MCP configuration">
<div class="dialog-head"><div><h2>MCP Config</h2></div><button class="icon-btn" type="button" data-close-popover aria-label="Close"><i data-lucide="x"></i></button></div>
<div class="dialog-body"><section class="settings-section"><div class="mcp-settings-head"><h3>Servers</h3><button class="btn" type="button" id="addMcpServerBtn"><i data-lucide="plus"></i> Add server</button></div><div class="mcp-server-list" id="mcpServerList"></div></section></div>
</div>
<div class="composer-popover panel-popover data-controls-popover hidden" id="dataControlsPopover" role="dialog" aria-label="Data controls">
<div class="dialog-head"><div><h2>Data Controls</h2></div><button class="icon-btn" type="button" data-close-popover aria-label="Close"><i data-lucide="x"></i></button></div>
<div class="dialog-body"><section class="settings-section"><div class="data-actions"><button class="btn" type="button" id="exportChatSettingsBtn"><i data-lucide="download"></i> Export current chat</button><button class="btn" type="button" id="exportAllBtn"><i data-lucide="archive"></i> Export all data</button><button class="btn" type="button" id="importAllBtn"><i data-lucide="upload"></i> Import data</button><button class="btn danger" type="button" id="clearDataBtn"><i data-lucide="trash-2"></i> Clear data</button></div><input class="sr-only" id="importInput" type="file" accept="application/json,.json"></section></div>
</div>
<form class="composer" id="composer">
<div class="pending-attachments hidden" id="pendingAttachments"></div>
<label class="sr-only" for="promptInput">Message Agent</label>
<textarea id="promptInput" rows="1" placeholder="Message Agent…" autocomplete="off"></textarea>
<div class="composer-foot">
<input class="sr-only" id="fileInput" type="file" multiple aria-label="Attach files" accept="text/*,.txt,.md,.mdx,.json,.jsonl,.csv,.ts,.tsx,.js,.jsx,.py,.html,.css,.xml,.yaml,.yml,.toml,.ini,.sql,.sh,.ps1,.lua,image/png,image/jpeg,image/webp,image/gif">
<button class="icon-btn mobile-only" type="button" id="openSidebarBtn" aria-label="Open chats" title="Open chats"><i data-lucide="panel-left"></i></button>
<button class="icon-btn" type="button" id="commandBtn" aria-label="Open commands" title="Commands"><i data-lucide="plus"></i></button>
<button class="model-picker-btn" type="button" id="modelPickerBtn" aria-label="Choose model" title="Choose model"><span id="modelButtonLabel">Model</span><i data-lucide="chevron-down"></i></button>
<button class="reasoning-btn" type="button" id="reasoningBtn" aria-label="Reasoning settings" title="Reasoning settings"><span id="reasoningLabel">Thinking: auto</span><i data-lucide="chevron-down"></i></button>
<span class="composer-spacer"></span>
<button class="icon-btn send-btn" id="sendBtn" type="submit" aria-label="Send message" title="Send"><i data-lucide="arrow-up"></i></button>
<button class="icon-btn stop-btn hidden" id="stopBtn" type="button" aria-label="Stop generating" title="Stop"><i data-lucide="square"></i></button>
</div>
</form>
</div>
</div>
</main>
</section>
</div>
<div class="artifact-backdrop hidden" id="artifactBackdrop"></div>
<aside class="artifact" id="artifactPanel" aria-label="HTML artifact preview">
<div class="artifact-head"><i data-lucide="monitor-play"></i><div><strong id="artifactTitle">HTML Preview</strong><span>Sandboxed generated artifact</span></div><button class="btn" id="downloadArtifactBtn"><i data-lucide="download"></i> Download</button><button class="icon-btn" id="closeArtifactBtn" aria-label="Close preview"><i data-lucide="x"></i></button></div>
<iframe id="artifactFrame" title="Generated HTML artifact" sandbox="allow-scripts allow-forms allow-modals allow-popups allow-downloads"></iframe>
</aside>
<div class="drop-overlay hidden" id="dropOverlay"><div><i data-lucide="files"></i><strong>Drop files to attach</strong><span>Text, code, JSON, or supported images</span></div></div>
<div class="toast-stack" id="toastStack" aria-live="polite"></div>
<script>
(() => {
"use strict";
const $ = (selector, root = document) => root.querySelector(selector);
const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];
const STORAGE = { chats: "agent:chats:v1", settings: "agent:settings:v1", key: "agent:openrouter-key:v1", providerKeys: "agent:inference-keys:v1", models: "agent:models:v1" };
const CHAT_DB = { name: "agent-chat-storage", version: 3, chats: "chats", secrets: "secrets", preferences: "preferences" };
const MAX_CHATS = 100, MAX_ATTACHMENTS = 6, MAX_TEXT_BYTES = 512 * 1024, MAX_IMAGE_BYTES = 2 * 1024 * 1024, MAX_TOOL_ITERATIONS = 5;
const DEFAULT_MCP_SERVERS = [{ id:"nymbo-tools", name:"Nymbo-Tools", url:"https://nymbo-tools.hf.space/gradio_api/mcp/", enabled:true }];
const MCP_PROTOCOL_VERSION = "2025-03-26";
const MCP_DISCOVERY_TIMEOUT_MS = 10000, MCP_TOOL_TIMEOUT_MS = 90000;
const DEFAULT_MODEL = "openai/gpt-oss-120b:free";
const DEFAULT_INFERENCE_PROVIDERS = [
{ id:"openrouter", name:"OpenRouter", description:"Cloud models · API key required", chatUrl:"https://openrouter.ai/api/v1/chat/completions", modelsUrl:"https://openrouter.ai/api/v1/models", defaultModel:DEFAULT_MODEL, rememberKey:false, openRouter:true, builtIn:true },
{ id:"lmstudio", name:"LM Studio", description:"Local models · localhost:1234", chatUrl:"http://localhost:1234/v1/chat/completions", modelsUrl:"http://localhost:1234/api/v1/models", defaultModel:"local-model", rememberKey:false, openRouter:false, builtIn:true }
];
const DEFAULT_PROMPT = "You are Agent, a capable, thoughtful assistant. Give direct, accurate answers; use the available tools when they materially help; explain uncertainty; and never claim a tool ran unless a tool result is present. When creating a browser artifact, return one complete HTML document in an html code fence.";
const DEFAULTS = { activeProviderId:"openrouter", inferenceProviders:DEFAULT_INFERENCE_PROVIDERS, model:DEFAULT_MODEL, systemPrompt: DEFAULT_PROMPT, temperature: .6, topP: .95, topK: 40, repetitionPenalty: 1.1, maxTokens: 8192, seed: 0, reasoningSelections: {}, mcpServers: DEFAULT_MCP_SERVERS, disabledMcpTools: [] };
const FALLBACK_MODELS = [
{ id: "openai/gpt-oss-120b:free", name: "OpenAI: gpt-oss-120b (free)", modalities: ["text"], supportedParameters: ["reasoning", "tools"], reasoning: { mandatory:true, supported_efforts:["low","medium","high"], default_effort:"medium" } },
{ id: "openai/gpt-oss-20b:free", name: "OpenAI: gpt-oss-20b (free)", modalities: ["text"], supportedParameters: ["reasoning", "tools"], reasoning: { mandatory:true, supported_efforts:["low","medium","high"], default_effort:"medium" } }
];
const TEXT_EXT = new Set(["txt","md","mdx","markdown","json","jsonl","csv","ts","tsx","js","jsx","mjs","cjs","py","html","htm","css","scss","xml","yaml","yml","toml","ini","sql","sh","bash","ps1","bat","cmd","java","cs","cpp","c","h","hpp","go","rs","rb","php","swift","kt","r","lua","vue","svelte"]);
const IMAGE_TYPES = new Set(["image/png","image/jpeg","image/webp","image/gif"]);
const IMAGE_EXTENSIONS = {png:"image/png",jpg:"image/jpeg",jpeg:"image/jpeg",webp:"image/webp",gif:"image/gif"};
const VISION_HINTS = ["gpt-4o","gpt-4.1","gpt-5","gemini","claude-3","claude-opus","claude-sonnet","claude-haiku","pixtral","qwen-vl","qwen2-vl","qwen2.5-vl","llama-3.2-vision","llava","vision","mimo-v"];
const state = { chats: [], activeId: null, settings: { ...DEFAULTS }, providerKeys:{}, providerDrafts:[], providerKeyDrafts:{}, providerDraftId:"openrouter", models: FALLBACK_MODELS, modelLoadToken:0, attachments: [], generating: false, abort: null, status: "", artifactHtml: "", dragDepth: 0, chatDbAvailable: true, persistTimer: null, modelQuery: "", streamFrame: null, traceOpen:new Set(), followOutput:true, commandOpen:false, commandIndex:0, commandFromSlash:false, commandPromptBase:"", commandMatches:[], mcpTools: [], mcpConnections: {}, mcpLoading: false };
let chatDbPromise = null;
const reasoningBuffers = new WeakMap();
const els = {
sidebar: $("#sidebar"), sidebarBackdrop: $("#sidebarBackdrop"), chatList: $("#chatList"), modelPickerBtn: $("#modelPickerBtn"), modelButtonLabel: $("#modelButtonLabel"), modelPopover: $("#modelPopover"), modelSearch: $("#modelSearchInput"), modelList: $("#modelList"), modelLoading: $("#modelLoading"), reasoningBtn: $("#reasoningBtn"), reasoningLabel: $("#reasoningLabel"), reasoningPopover: $("#reasoningPopover"), reasoningControls: $("#reasoningControls"), toolsPopover: $("#toolsPopover"), providersPopover: $("#providersPopover"), parametersPopover: $("#parametersPopover"), mcpConfigPopover: $("#mcpConfigPopover"), dataControlsPopover: $("#dataControlsPopover"), messages: $("#messages"), messagesInner: $("#messagesInner"), composer: $("#composer"), prompt: $("#promptInput"), fileInput: $("#fileInput"), pending: $("#pendingAttachments"), statusLine: $("#statusLine"), statusText: $("#statusText"), send: $("#sendBtn"), stop: $("#stopBtn"), providerSelect: $("#inferenceProviderSelect"), mcpServerList: $("#mcpServerList"), commandPanel: $("#commandPanel"), toolGrid: $("#toolGrid"), toolLoading: $("#toolLoading"), commandList: $("#commandList"), dropOverlay: $("#dropOverlay"), toastStack: $("#toastStack"), artifact: $("#artifactPanel"), artifactBackdrop: $("#artifactBackdrop"), artifactFrame: $("#artifactFrame"), artifactTitle: $("#artifactTitle")
};
function uid(prefix = "id") { return `${prefix}-${crypto.randomUUID ? crypto.randomUUID() : Date.now() + "-" + Math.random().toString(16).slice(2)}`; }
function clone(value) { return JSON.parse(JSON.stringify(value)); }
function escapeHtml(value) { const div = document.createElement("div"); div.textContent = String(value ?? ""); return div.innerHTML; }
function formatBytes(bytes) { return bytes < 1024 ? `${bytes} B` : bytes < 1048576 ? `${(bytes/1024).toFixed(1)} KB` : `${(bytes/1048576).toFixed(1)} MB`; }
function toast(message, type = "info") { const node = document.createElement("div"); node.className = `toast ${type}`; node.textContent = message; els.toastStack.append(node); setTimeout(() => node.remove(), 3500); }
function safeParse(value, fallback) { if (value == null || value === "") return fallback; try { return JSON.parse(value) ?? fallback; } catch { return fallback; } }
function saveJson(key, value, notify = true) { try { const raw=JSON.stringify(value);if([STORAGE.settings,STORAGE.models].includes(key))void writePreference(key,raw).catch(error=>{console.warn(`Could not save ${key} to IndexedDB; using the legacy fallback.`,error);try{localStorage.setItem(key,raw);}catch{if(notify)toast("Could not save this preference.","error");}});else localStorage.setItem(key,raw);return true; } catch (error) { if(notify) toast("Could not save this preference.", "error"); return false; } }
function download(name, content, type = "text/plain") { const url = URL.createObjectURL(new Blob([content], { type: `${type};charset=utf-8` })); const a = document.createElement("a"); a.href = url; a.download = name; a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); }
async function copyText(text) { try { await navigator.clipboard.writeText(text); toast("Copied to clipboard."); } catch { const area = document.createElement("textarea"); area.value = text; document.body.append(area); area.select(); document.execCommand("copy"); area.remove(); toast("Copied to clipboard."); } }
function icon(name, cls = "") { return `<i data-lucide="${name}"${cls ? ` class="${cls}"` : ""}></i>`; }
function refreshIcons(root = document) { if (window.lucide) lucide.createIcons({ nodes: [root] }); }
function normalizeMcpServers(value) {
const source=Array.isArray(value)?value:DEFAULT_MCP_SERVERS, seen=new Set();
return source.slice(0,12).map((entry,index)=>{const rawId=String(entry?.id || "").trim(),baseId=(rawId || `mcp-${index+1}`).replace(/[^a-zA-Z0-9_-]/g,"-").slice(0,48) || `mcp-${index+1}`;let id=baseId,n=2;while(seen.has(id))id=`${baseId}-${n++}`;seen.add(id);return {id,name:String(entry?.name || `MCP Server ${index+1}`).trim().slice(0,80) || `MCP Server ${index+1}`,url:String(entry?.url || "").trim(),enabled:entry?.enabled!==false};}).filter(server=>server.url);
}
function validateMcpServerUrl(value) { let url;try{url=new URL(String(value).trim());}catch{throw new Error("Enter a valid MCP server URL.");}if(!["http:","https:"].includes(url.protocol))throw new Error("MCP servers must use HTTP or HTTPS.");return url.href; }
function normalizeInferenceProviders(value,saved={}) {
const supplied=Array.isArray(value)&&value.length?value:[],source=[...supplied];
DEFAULT_INFERENCE_PROVIDERS.forEach(defaultProvider=>{if(!source.some(entry=>entry?.id===defaultProvider.id))source.push(defaultProvider);});
const seen=new Set();
return source.slice(0,10).map((entry,index)=>{const rawId=String(entry?.id || "").trim(),baseId=(rawId || `provider-${index+1}`).replace(/[^a-zA-Z0-9_-]/g,"-").slice(0,48) || `provider-${index+1}`;let id=baseId,n=2;while(seen.has(id))id=`${baseId}-${n++}`;seen.add(id);const builtIn=DEFAULT_INFERENCE_PROVIDERS.find(provider=>provider.id===id),isOpenRouter=id==="openrouter" || entry?.openRouter===true,legacyLmStudioModels="http://localhost:1234/v1/models",modelsUrl=id==="lmstudio"&&entry?.modelsUrl===legacyLmStudioModels?builtIn.modelsUrl:entry?.modelsUrl || builtIn?.modelsUrl || "";return {id,name:String(entry?.name || builtIn?.name || `Custom provider ${index+1}`).trim().slice(0,80) || `Custom provider ${index+1}`,description:String(entry?.description || builtIn?.description || "OpenAI-compatible endpoint").trim().slice(0,100),chatUrl:String(entry?.chatUrl || builtIn?.chatUrl || (isOpenRouter?saved.endpoint:"") || "").trim(),modelsUrl:String(modelsUrl).trim(),defaultModel:String(entry?.defaultModel || builtIn?.defaultModel || (isOpenRouter?saved.model:"") || DEFAULT_MODEL).trim(),rememberKey:entry?.rememberKey===true || (isOpenRouter&&saved.rememberKey===true),openRouter:isOpenRouter,builtIn:Boolean(builtIn)};});
}
function validateInferenceUrl(value,label,optional=false) { const raw=String(value || "").trim();if(optional&&!raw)return "";let url;try{url=new URL(raw);}catch{throw new Error(`Enter a valid ${label}.`);}if(!["http:","https:"].includes(url.protocol))throw new Error(`${label} must use HTTP or HTTPS.`);return url.href; }
function inferenceProvider(providerId) { return state.settings.inferenceProviders.find(provider=>provider.id===providerId) || state.settings.inferenceProviders[0]; }
function activeProvider(chat=activeChat()) { return inferenceProvider(chat?.providerId || state.settings.activeProviderId); }
function providerKey(providerId) { return state.providerKeys[providerId] || ""; }
function providerReasoningKey(modelId,provider=activeProvider()) { return `${provider?.id || "provider"}::${modelId}`; }
function openChatDb() {
if (!window.indexedDB) return Promise.reject(new Error("IndexedDB is unavailable."));
if (chatDbPromise) return chatDbPromise;
chatDbPromise = new Promise((resolve,reject) => {
const request = indexedDB.open(CHAT_DB.name, CHAT_DB.version);
request.onupgradeneeded = () => { const db=request.result; if(!db.objectStoreNames.contains(CHAT_DB.chats)) db.createObjectStore(CHAT_DB.chats,{keyPath:"id"}); if(!db.objectStoreNames.contains(CHAT_DB.secrets)) db.createObjectStore(CHAT_DB.secrets); if(!db.objectStoreNames.contains(CHAT_DB.preferences)) db.createObjectStore(CHAT_DB.preferences); };
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error || new Error("Could not open chat storage."));
request.onblocked = () => reject(new Error("Chat storage upgrade was blocked by another Agent tab."));
});
return chatDbPromise;
}
async function readChatsFromDb() {
const db=await openChatDb();
return new Promise((resolve,reject) => { const request=db.transaction(CHAT_DB.chats,"readonly").objectStore(CHAT_DB.chats).getAll(); request.onsuccess=()=>resolve(request.result || []); request.onerror=()=>reject(request.error); });
}
async function readSecret(key,fallback) {
try { const db=await openChatDb(); return await new Promise((resolve,reject)=>{const request=db.transaction(CHAT_DB.secrets,"readonly").objectStore(CHAT_DB.secrets).get(key);request.onsuccess=()=>resolve(request.result ?? fallback);request.onerror=()=>reject(request.error);}); } catch { return fallback; }
}
async function writeSecret(key,value) {
const db=await openChatDb();
return new Promise((resolve,reject)=>{const tx=db.transaction(CHAT_DB.secrets,"readwrite");tx.objectStore(CHAT_DB.secrets).put(value,key);tx.oncomplete=resolve;tx.onerror=()=>reject(tx.error);});
}
async function removeSecret(key) {
try { const db=await openChatDb(); await new Promise((resolve,reject)=>{const tx=db.transaction(CHAT_DB.secrets,"readwrite");tx.objectStore(CHAT_DB.secrets).delete(key);tx.oncomplete=resolve;tx.onerror=()=>reject(tx.error);}); } catch {}
}
async function readPreference(key,fallback=null) {
try { const db=await openChatDb(); return await new Promise((resolve,reject)=>{const request=db.transaction(CHAT_DB.preferences,"readonly").objectStore(CHAT_DB.preferences).get(key);request.onsuccess=()=>resolve(request.result ?? fallback);request.onerror=()=>reject(request.error);}); } catch { return fallback; }
}
async function writePreference(key,value) {
const db=await openChatDb();
return new Promise((resolve,reject)=>{const tx=db.transaction(CHAT_DB.preferences,"readwrite");tx.objectStore(CHAT_DB.preferences).put(value,key);tx.oncomplete=resolve;tx.onerror=()=>reject(tx.error);});
}
async function migratePreference(key,validate) {
const legacy=localStorage.getItem(key);if(legacy===null)return;
try { if(!validate(legacy))return;const existing=await readPreference(key,null);if(existing===null)await writePreference(key,legacy);if(await readPreference(key,null)===legacy)localStorage.removeItem(key);else console.warn(`IndexedDB migration conflict for ${key}; legacy value retained.`); } catch(error){console.warn(`IndexedDB migration skipped for ${key}.`,error);}
}
async function persistChatsNow() {
const chats=state.chats.slice(0,MAX_CHATS);
if (!state.chatDbAvailable) return persistLightweightFallback(chats);
try {
const db=await openChatDb();
await new Promise((resolve,reject) => { const tx=db.transaction(CHAT_DB.chats,"readwrite"), store=tx.objectStore(CHAT_DB.chats); store.clear(); chats.forEach(chat=>store.put(chat)); tx.oncomplete=resolve; tx.onerror=()=>reject(tx.error); tx.onabort=()=>reject(tx.error || new Error("Chat storage transaction was aborted.")); });
localStorage.removeItem(STORAGE.chats);
return true;
} catch(error) {
state.chatDbAvailable=false;
console.warn("IndexedDB chat persistence is unavailable.",error);
persistLightweightFallback(chats);
toast("Full chat storage is unavailable. Messages will be saved without attachment contents in this browser session.","error");
return false;
}
}
function persistLightweightFallback(chats) {
const lightweight=chats.map(chat=>({...chat,messages:chat.messages.map(message=>({...message,attachments:message.attachments?.map(({dataUrl,text,...metadata})=>metadata)}))}));
return saveJson(STORAGE.chats,lightweight);
}
function scheduleChatPersistence() { clearTimeout(state.persistTimer); state.persistTimer=setTimeout(()=>{state.persistTimer=null; void persistChatsNow();},80); }
async function clearAgentDb() { try { const db=await openChatDb(); await new Promise((resolve,reject)=>{const tx=db.transaction([CHAT_DB.chats,CHAT_DB.secrets,CHAT_DB.preferences],"readwrite");tx.objectStore(CHAT_DB.chats).clear();tx.objectStore(CHAT_DB.secrets).clear();tx.objectStore(CHAT_DB.preferences).clear();tx.oncomplete=resolve;tx.onerror=()=>reject(tx.error);}); } catch {} }
async function loadState() {
await migratePreference(STORAGE.settings,raw=>{const value=JSON.parse(raw);return value&&typeof value==="object";});
await migratePreference(STORAGE.models,raw=>{const value=JSON.parse(raw);return value&&typeof value==="object"&&Array.isArray(value.models);});
const savedSettings = safeParse(await readPreference(STORAGE.settings,localStorage.getItem(STORAGE.settings) || ""), {});
state.settings = { ...DEFAULTS, ...savedSettings };
state.settings.inferenceProviders=normalizeInferenceProviders(state.settings.inferenceProviders,savedSettings);
if(!state.settings.inferenceProviders.some(provider=>provider.id===state.settings.activeProviderId))state.settings.activeProviderId=state.settings.inferenceProviders[0].id;
state.settings.mcpServers=normalizeMcpServers(state.settings.mcpServers);
if (!Array.isArray(state.settings.disabledMcpTools)) state.settings.disabledMcpTools = [];
delete state.settings.enabledTools;
if (!state.settings.reasoningSelections || typeof state.settings.reasoningSelections !== "object") state.settings.reasoningSelections={};
const legacyChats = safeParse(localStorage.getItem(STORAGE.chats), []).filter(validChat).slice(0, MAX_CHATS);
try {
const storedChats=(await readChatsFromDb()).filter(validChat).sort((a,b)=>b.updatedAt-a.updatedAt).slice(0,MAX_CHATS);
state.chats=storedChats.length ? storedChats : legacyChats;
if (legacyChats.length && !storedChats.length) await persistChatsNow();
else if (storedChats.length) localStorage.removeItem(STORAGE.chats);
} catch(error) {
state.chatDbAvailable=false;
state.chats=legacyChats;
console.warn("Using lightweight chat persistence because IndexedDB is unavailable.",error);
}
const sessionKeys = safeParse(sessionStorage.getItem(STORAGE.providerKeys),{});
const localKey = localStorage.getItem(STORAGE.key) || "";
const sessionKey = sessionStorage.getItem(STORAGE.key) || "";
const indexedKeys = await readSecret(STORAGE.providerKeys,{});
const indexedKey = await readSecret(STORAGE.key,"");
state.providerKeys={...(indexedKeys&&typeof indexedKeys==="object"?indexedKeys:{}),...(sessionKeys&&typeof sessionKeys==="object"?sessionKeys:{})};
const legacyKey=sessionKey || indexedKey || localKey;if(legacyKey&&!state.providerKeys.openrouter)state.providerKeys.openrouter=legacyKey;
if(indexedKey||localKey){const openrouter=state.settings.inferenceProviders.find(provider=>provider.id==="openrouter");if(openrouter)openrouter.rememberKey=true;try{await writeSecret(STORAGE.providerKeys,{...indexedKeys,openrouter:legacyKey});localStorage.removeItem(STORAGE.key);sessionStorage.removeItem(STORAGE.key);await removeSecret(STORAGE.key);}catch{}}
if (!state.chats.length) state.chats.push(createChat());
state.chats.forEach(chat=>{if(!chat.providerId)chat.providerId="openrouter";});
state.activeId = state.chats[0].id;
const cached = safeParse(await readPreference(STORAGE.models,localStorage.getItem(STORAGE.models) || ""), null);
const provider=activeProvider();if(cached && cached.providerId===provider.id && cached.signature===provider.modelsUrl && Date.now() - cached.savedAt < 86400000 && Array.isArray(cached.models)) state.models = cached.models;else state.models=provider.openRouter?FALLBACK_MODELS:[{id:provider.defaultModel,name:provider.defaultModel}];
}
function validChat(chat) { return chat && typeof chat.id === "string" && Array.isArray(chat.messages); }
function createChat(model,providerId=state.settings.activeProviderId) { const provider=inferenceProvider(providerId),now=Date.now(); return { id: uid("chat"), title: "New Chat", providerId:provider.id, model:model || provider.defaultModel, messages: [], notes: [], createdAt: now, updatedAt: now }; }
function activeChat() { return state.chats.find(chat => chat.id === state.activeId) || state.chats[0]; }
function saveChats() { state.chats.sort((a,b) => b.updatedAt - a.updatedAt); scheduleChatPersistence(); }
function touchChat(chat) { chat.updatedAt = Date.now(); if (chat.title === "New Chat") { const first = chat.messages.find(m => m.role === "user"); if (first) { const basis = (first.content || first.attachments?.[0]?.name || "Attached files").trim(); chat.title = basis.length > 50 ? basis.slice(0,47) + "…" : basis; } } saveChats(); }
function renderApp() { renderSidebar(); renderModels(); renderMessages(); renderPending(); updateControls(); }
function renderSidebar() {
const chats = [...state.chats].sort((a,b) => b.updatedAt - a.updatedAt);
els.chatList.innerHTML = chats.length ? chats.map(chat => `<div class="chat-row ${chat.id === state.activeId ? "active" : ""}" data-chat-id="${chat.id}"><button class="chat-open" data-action="open-chat">${icon("message-square")}<span>${escapeHtml(chat.title)}</span></button><button class="chat-delete" data-action="delete-chat" aria-label="Delete ${escapeHtml(chat.title)}">${icon("trash-2")}</button></div>`).join("") : `<div class="empty-chats">No chats yet</div>`;
refreshIcons(els.chatList);
}
function renderModels() {
const provider=activeProvider(), current = activeChat()?.model || provider.defaultModel || DEFAULT_MODEL;
const models = state.models.some(m => m.id === current) ? state.models : [{ id: current, name: current }, ...state.models];
const selected=models.find(model=>model.id===current);
els.modelButtonLabel.textContent=selected?.name || shortModel(current) || "Model";
els.modelPickerBtn.title=`${provider.name} · ${selected?.name || current}`;
const query=state.modelQuery.trim().toLowerCase();
const filtered=query ? models.map(model=>{const name=(model.name || "").toLowerCase(),id=model.id.toLowerCase(),tokens=`${name} ${id}`.split(/[^a-z0-9.]+/);const score=tokens.includes(query)?0:name.startsWith(query)||id.startsWith(query)?1:name.includes(query)||id.includes(query)?2:99;return {model,score};}).filter(entry=>entry.score<99).sort((a,b)=>a.score-b.score||(a.model.name || a.model.id).localeCompare(b.model.name || b.model.id)).map(entry=>entry.model) : models;
els.modelList.innerHTML=filtered.length ? filtered.map(model=>`<button class="model-option ${model.id===current?"selected":""}" type="button" role="option" aria-selected="${model.id===current}" data-model-id="${escapeHtml(model.id)}"><span>${escapeHtml(model.name || model.id)}</span>${modelSupportsImages(model.id)?"<small>VISION</small>":""}${model.id.endsWith(":free")?"<small>FREE</small>":""}${model.id===current?icon("check"):""}</button>`).join("") : `<div class="model-empty">No models match “${escapeHtml(state.modelQuery)}”.</div>`;
refreshIcons(els.modelList);
renderReasoning();
}
function selectedModelDetails() { const id=activeChat()?.model || activeProvider()?.defaultModel || DEFAULT_MODEL; return state.models.find(model=>model.id===id) || FALLBACK_MODELS.find(model=>model.id===id) || {id}; }
function getReasoningCapabilities(model) {
if(!model) return null;
const raw=model.reasoning && typeof model.reasoning==="object" ? model.reasoning : model.capabilities?.reasoning && typeof model.capabilities.reasoning==="object" ? model.capabilities.reasoning : {};
const advertised=Boolean(Object.keys(raw).length || model.supportedParameters?.includes("reasoning") || model.id?.includes("gpt-oss"));
if(!advertised) return null;
const allowed=["minimal","low","medium","high","xhigh","max"];
const options=Array.isArray(raw.allowed_options) ? raw.allowed_options : Array.isArray(raw.allowedOptions) ? raw.allowedOptions : [];
let efforts=Array.isArray(raw.supported_efforts) ? raw.supported_efforts : Array.isArray(raw.supportedEfforts) ? raw.supportedEfforts : options;
efforts=efforts.filter(effort=>allowed.includes(effort));
if(!efforts.length) efforts=options.includes("on")?["minimal","low","medium","high","xhigh"]:["low","medium","high"];
efforts.sort((a,b)=>allowed.indexOf(a)-allowed.indexOf(b));
const defaultOption=raw.default_effort || raw.defaultEffort || raw.default;
return { efforts, mandatory:raw.mandatory===true || (options.length>0&&!options.includes("off")), defaultEnabled:raw.default_enabled ?? raw.defaultEnabled ?? (defaultOption==="off"?false:defaultOption==="on"?true:undefined), defaultEffort:allowed.includes(defaultOption)?defaultOption:undefined, supportsMaxTokens:raw.supports_max_tokens===true || raw.supportsMaxTokens===true };
}
function getReasoningSelection(model=selectedModelDetails()) {
const capabilities=getReasoningCapabilities(model); if(!capabilities) return null;
let selection=state.settings.reasoningSelections?.[providerReasoningKey(model.id)] || state.settings.reasoningSelections?.[model.id];
if(!selection) { const legacy=state.settings.reasoning; selection=legacy==="off"?{mode:"off"}:["minimal","low","medium","high","xhigh","max"].includes(legacy)?{mode:"effort",effort:legacy}:null; }
if(!selection) { if(!capabilities.mandatory && capabilities.defaultEnabled===false) selection={mode:"off"}; else if(capabilities.defaultEffort && capabilities.efforts.includes(capabilities.defaultEffort)) selection={mode:"effort",effort:capabilities.defaultEffort}; else selection={mode:"auto"}; }
selection={...selection};
if(selection.mode==="off" && capabilities.mandatory) selection=capabilities.defaultEffort?{mode:"effort",effort:capabilities.defaultEffort}:{mode:"auto"};
if(selection.mode==="effort" && !capabilities.efforts.includes(selection.effort)) selection={mode:"effort",effort:capabilities.defaultEffort && capabilities.efforts.includes(capabilities.defaultEffort)?capabilities.defaultEffort:capabilities.efforts[0]};
if(selection.mode==="max_tokens" && !capabilities.supportsMaxTokens) selection={mode:"auto"};
if(selection.mode==="max_tokens") selection.maxTokens=Math.max(1,Math.round(Number(selection.maxTokens)||2000));
return selection;
}
function reasoningLabel(selection) { if(!selection)return "Thinking: unavailable"; if(selection.mode==="off")return "Thinking: off"; if(selection.mode==="effort")return `Thinking: ${selection.effort}`; if(selection.mode==="max_tokens")return `Thinking: ${selection.maxTokens} tokens`; return "Thinking: auto"; }
function renderReasoning() {
const model=selectedModelDetails(), capabilities=getReasoningCapabilities(model), selection=getReasoningSelection(model);
els.reasoningLabel.textContent=reasoningLabel(selection); els.reasoningBtn.title=capabilities?`Reasoning settings for ${model.name || model.id}`:"Reasoning is unavailable for this model"; els.reasoningBtn.disabled=state.generating || !capabilities;
if(!capabilities) { els.reasoningControls.innerHTML=`<div class="reasoning-unavailable">Reasoning controls are unavailable for this model.</div>`;return; }
const option=(mode,label,extra="")=>`<button class="reasoning-option ${selection.mode===mode && (!extra || selection.effort===extra)?"selected":""}" type="button" data-reasoning-mode="${mode}" ${extra?`data-reasoning-effort="${extra}"`:""}>${label}</button>`;
els.reasoningControls.innerHTML=`<div class="reasoning-options">${option("auto","Auto")}${capabilities.mandatory?"":option("off","Off")}${capabilities.efforts.map(effort=>option("effort",effort,effort)).join("")}</div>${capabilities.supportsMaxTokens?`<div class="reasoning-budget"><div class="field"><label for="reasoningBudgetInput">Token budget</label><input id="reasoningBudgetInput" type="number" min="1" step="128" value="${selection.maxTokens || 2000}"></div>${option("max_tokens","Use budget")}</div>`:""}`;
}
function positionComposerPopover(popover,button) { const shell=popover.parentElement,shellRect=shell.getBoundingClientRect(),buttonRect=button.getBoundingClientRect(),width=popover.offsetWidth,margin=11;popover.style.left=`${Math.max(margin,Math.min(shellRect.width-width-margin,buttonRect.left-shellRect.left))}px`; }
function positionCenteredComposerPopover(popover) { const shellRect=popover.parentElement.getBoundingClientRect(),width=popover.offsetWidth;popover.style.left=`${Math.max(11,(shellRect.width-width)/2)}px`; }
function dismissComposerPopover(popover) { if(!popover||popover.classList.contains("hidden"))return true;if(popover===els.parametersPopover)persistParameters();if(popover===els.mcpConfigPopover&&!persistMcpConfig())return false;popover.classList.add("hidden");return true; }
function closeComposerPopovers(except=null) { let closed=true;[els.modelPopover,els.reasoningPopover,els.toolsPopover,els.providersPopover,els.parametersPopover,els.mcpConfigPopover,els.dataControlsPopover].forEach(popover=>{if(popover!==except&&!dismissComposerPopover(popover))closed=false;});return closed; }
function openReasoning() { if(!getReasoningCapabilities(selectedModelDetails()))return;if(!els.reasoningPopover.classList.contains("hidden")){els.reasoningPopover.classList.add("hidden");return;}closeCommands(true);if(!closeComposerPopovers(els.reasoningPopover))return;renderReasoning();els.reasoningPopover.classList.remove("hidden");positionComposerPopover(els.reasoningPopover,els.reasoningBtn); }
function setReasoningSelection(selection) { const model=selectedModelDetails(); state.settings.reasoningSelections={...state.settings.reasoningSelections,[providerReasoningKey(model.id)]:selection}; saveJson(STORAGE.settings,state.settings); renderReasoning(); els.reasoningPopover.classList.add("hidden");els.prompt.focus(); }
function renderWelcome() {
return `<section class="welcome"><div class="welcome-card"><div class="welcome-icon">${icon("bot")}</div><h1>How can I help?</h1><div class="prompt-grid"><button class="prompt-card" data-prompt="Explain the key ideas in the attached file and identify anything that needs follow-up."><strong>Review a file</strong><span>Attach text, code, JSON, or an image.</span></button><button class="prompt-card" data-prompt="Create a polished, self-contained HTML to-do app with a dark interface. Return the complete file in an html code fence."><strong>Build an artifact</strong><span>Create and run a complete HTML app.</span></button><button class="prompt-card" data-prompt="Help me reason through a difficult decision. Start by identifying the key tradeoffs and missing information."><strong>Think through a decision</strong><span>Structure options and tradeoffs.</span></button><button class="prompt-card" data-prompt="Search the web for the latest browser AI news and summarize the key developments with sources."><strong>Use MCP tools</strong><span>Search, fetch, calculate, research, and more.</span></button></div></div></section>`;
}
function renderMessages() {
const chat = activeChat();
if (!chat || !chat.messages.length) els.messagesInner.innerHTML = renderWelcome();
else els.messagesInner.innerHTML = chat.messages.map((message,index) => renderMessage(message,index)).join("");
refreshIcons(els.messagesInner);
enhanceMarkdown(els.messagesInner);
}
function renderMessage(message,index) {
const attachments = message.attachments?.length ? renderAttachments(message.attachments, false) : "";
const assistantLabel=message.modelName || state.models.find(model=>model.id===message.model)?.name || shortModel(message.model) || "Model";
if(message.role==="assistant"&&message.segments?.length) { const timeline=renderAssistantTimeline(message);return `<article class="message assistant" data-message-index="${index}" data-message-id="${escapeHtml(message.id || `message-${index}`)}"><div class="message-label">${escapeHtml(assistantLabel)}</div><div class="message-timeline">${timeline}</div><div class="message-actions"><button data-action="copy-message" aria-label="Copy message" title="Copy">${icon("copy")}</button><button data-action="retry-message" aria-label="Retry response" title="Retry from here">${icon("rotate-ccw")}</button><span class="model-badge">${escapeHtml(shortModel(message.model))}</span></div></article>`; }
const trace = message.trace?.length ? renderTrace(message.trace,message.id || `message-${index}`) : "";
const body = message.content ? `<div class="markdown">${renderMarkdown(message.content)}</div>` : state.generating && index === activeChat().messages.length - 1 ? `<span style="color:var(--muted)">Preparing response…</span>` : "";
const isError = message.error ? " error" : "";
return `<article class="message ${message.role}" data-message-index="${index}" data-message-id="${escapeHtml(message.id || `message-${index}`)}"><div class="message-label">${message.role === "user" ? "You" : escapeHtml(assistantLabel)}</div>${trace}${attachments || body ? `<div class="bubble${isError}">${attachments}${body}</div>` : ""}<div class="message-actions"><button data-action="copy-message" aria-label="Copy message" title="Copy">${icon("copy")}</button>${message.role === "user" ? `<button data-action="edit-message" aria-label="Edit message" title="Edit from here">${icon("pencil")}</button>` : `<button data-action="retry-message" aria-label="Retry response" title="Retry from here">${icon("rotate-ccw")}</button>`}<span class="model-badge">${message.role === "assistant" ? escapeHtml(shortModel(message.model)) : ""}</span></div></article>`;
}
function scheduleStreamingRender(message) {
if(state.streamFrame) return;
state.streamFrame=requestAnimationFrame(()=>{state.streamFrame=null;renderStreamingMessage(message);});
}
function renderStreamingMessage(message) {
const followOutput=state.followOutput;
const article=els.messagesInner.querySelector(`[data-message-id="${CSS.escape(message.id)}"]`);
if(!article) { renderMessages(); return; }
if(message.segments?.length) { let timeline=article.querySelector(":scope > .message-timeline");if(!timeline){timeline=document.createElement("div");timeline.className="message-timeline";article.querySelector(":scope > .trace")?.remove();article.querySelector(":scope > .bubble")?.remove();article.insertBefore(timeline,article.querySelector(".message-actions"));}syncAssistantTimeline(timeline,message);const modelBadge=article.querySelector(".model-badge");if(modelBadge)modelBadge.textContent=shortModel(message.model);if(followOutput)requestAnimationFrame(followStreamOutput);return; }
const index=Number(article.dataset.messageIndex), attachments=message.attachments?.length ? renderAttachments(message.attachments,false) : "";
const body=message.content ? `<div class="markdown">${renderMarkdown(message.content)}</div>` : `<span style="color:var(--muted)">Preparing response…</span>`;
let bubble=article.querySelector(":scope > .bubble");
if(!bubble) { bubble=document.createElement("div"); bubble.className="bubble"; article.insertBefore(bubble,article.querySelector(".message-actions")); }
bubble.classList.toggle("error",Boolean(message.error)); bubble.innerHTML=attachments+body; enhanceMarkdown(bubble);
const traceHtml=message.trace?.length ? renderTrace(message.trace,message.id) : "", existingTrace=article.querySelector(":scope > .trace");
if(traceHtml) { const holder=document.createElement("div");holder.innerHTML=traceHtml;const nextTrace=holder.firstElementChild;existingTrace?existingTrace.replaceWith(nextTrace):article.insertBefore(nextTrace,bubble); }
else existingTrace?.remove();
const modelBadge=article.querySelector(".model-badge");if(modelBadge)modelBadge.textContent=shortModel(message.model);
refreshIcons(article); if(followOutput)requestAnimationFrame(followStreamOutput);
}
function renderAssistantTimeline(message) {
return message.segments.map(segment=>renderAssistantSegment(segment,message)).join("") || `<span data-stream-placeholder style="color:var(--muted)">Preparing response…</span>`;
}
function renderAssistantSegment(segment,message) {
const segmentId=escapeHtml(segment.uiKey || segment.id || `${segment.kind}:${segment.traceId || "segment"}`);
if(segment.kind==="text")return segment.content?`<div class="bubble${message.error?" error":""}" data-segment-id="${segmentId}" data-segment-kind="text"><div class="markdown">${renderMarkdown(segment.content)}</div></div>`:"";
if(segment.kind==="trace"){const entry=message.trace?.find(item=>item.id===segment.traceId);return entry?renderTrace([entry],message.id).replace('<div class="trace">',`<div class="trace" data-segment-id="${segmentId}" data-segment-kind="trace">`):"";}
return "";
}
function syncAssistantTimeline(timeline,message) {
timeline.querySelector("[data-stream-placeholder]")?.remove();
const activeIds=new Set();
let insertionPoint=timeline.firstElementChild;
message.segments.forEach(segment=>{
const segmentId=segment.uiKey || segment.id || `${segment.kind}:${segment.traceId || "segment"}`;
activeIds.add(segmentId);
let node=[...timeline.children].find(child=>child.dataset.segmentId===segmentId);
if(!node){const holder=document.createElement("div");holder.innerHTML=renderAssistantSegment(segment,message);node=holder.firstElementChild;if(!node)return;timeline.appendChild(node);enhanceMarkdown(node);refreshIcons(node);}
else if(segment.kind==="text"){
node.classList.toggle("error",Boolean(message.error));
if(node.dataset.streamContent!==segment.content){const markdown=node.querySelector(":scope > .markdown");if(markdown){markdown.innerHTML=renderMarkdown(segment.content);enhanceMarkdown(markdown);}node.dataset.streamContent=segment.content;}
} else if(segment.kind==="trace") {
const entry=message.trace?.find(item=>item.id===segment.traceId);if(entry)syncTraceRow(node,entry,message.id);
}
if(insertionPoint){if(node!==insertionPoint)timeline.insertBefore(node,insertionPoint);}
else if(node!==timeline.lastElementChild)timeline.appendChild(node);
insertionPoint=node.nextElementSibling;
});
[...timeline.children].forEach(node=>{if(node.dataset.segmentId&&!activeIds.has(node.dataset.segmentId))node.remove();});
}
function syncTraceRow(trace,entry,messageId) {
const details=trace.querySelector(":scope > .trace-row");if(!details)return;
const running=entry.status==="running",failed=entry.status==="error",cls=entry.kind==="thought"?"trace-thought":failed?"trace-error":running?"trace-tool":"trace-done",ico=entry.kind==="thought"?"brain-circuit":failed?"circle-x":running?"wrench":"circle-check";
const detail=entry.detail || [entry.input?`Input\n${formatValue(entry.input)}`:"",entry.output?`Output\n${formatValue(entry.output)}`:"",entry.error?`Error\n${entry.error}`:""].filter(Boolean).join("\n\n");
const traceId=String(entry.id || entry.toolName || "step"),disclosureKey=`${messageId}:${traceId}`,previousKey=details.dataset.traceKey;details.dataset.traceKey=disclosureKey;if(previousKey&&previousKey!==disclosureKey){state.traceOpen.delete(previousKey);if(details.open)state.traceOpen.add(disclosureKey);}
const status=details.querySelector("summary > span:first-child");if(status&&status.dataset.icon!==ico){status.className=cls;status.dataset.icon=ico;status.innerHTML=icon(ico);refreshIcons(status);}else if(status)status.className=cls;
const title=details.querySelector(".trace-title");if(title&&title.textContent!==(entry.title || entry.toolName || "Agent step"))title.textContent=entry.title || entry.toolName || "Agent step";
const summary=details.querySelector("summary");let chevron=summary?.querySelector(".chev");if(detail&&!chevron&&summary){summary.insertAdjacentHTML("beforeend",icon("chevron-down","chev"));refreshIcons(summary);}
if(!detail&&chevron)chevron.remove();
let detailNode=details.querySelector(":scope > .trace-detail");if(detail){if(!detailNode){detailNode=document.createElement("div");detailNode.className="trace-detail";detailNode.innerHTML="<pre></pre>";details.appendChild(detailNode);}const pre=detailNode.querySelector("pre");if(pre&&pre.textContent!==detail)pre.textContent=detail;}else detailNode?.remove();
}
function renderMarkdown(text) {
if (!window.marked || !window.DOMPurify) return `<p>${escapeHtml(text).replace(/\n/g,"<br>")}</p>`;
marked.setOptions({ gfm: true, breaks: true });
return DOMPurify.sanitize(marked.parse(text), { USE_PROFILES: { html: true } });
}
function enhanceMarkdown(root) {
$$(".markdown a", root).forEach(a => { a.target = "_blank"; a.rel = "noopener noreferrer"; });
$$(".markdown pre", root).forEach(pre => {
if (pre.closest(".code-wrap")) return;
const code = $("code", pre); if (!code) return;
try { if (window.hljs) hljs.highlightElement(code); } catch {}
const languageClass = [...code.classList].find(c => c.startsWith("language-")) || "";
const language = languageClass.replace("language-","") || "code";
const html = code.textContent || "";
const isArtifact = (language === "html" || html.trim().startsWith("<!DOCTYPE html>")) && /<!DOCTYPE html>/i.test(html);
const wrap = document.createElement("div"); wrap.className = "code-wrap";
const head = document.createElement("div"); head.className = "code-head"; head.innerHTML = `<span>${escapeHtml(isArtifact ? "HTML artifact" : language)}</span><button data-code-copy>Copy</button>${isArtifact ? "<button data-run-html>Run</button>" : ""}`;
pre.replaceWith(wrap); wrap.append(head, pre); wrap.dataset.code = html;
});
}
function renderTrace(entries,messageId) {
const display=entries;
return `<div class="trace">${display.map(entry => { const running = entry.status === "running"; const failed = entry.status === "error"; const cls = entry.kind === "thought" ? "trace-thought" : failed ? "trace-error" : running ? "trace-tool" : "trace-done"; const ico = entry.kind === "thought" ? "brain-circuit" : failed ? "circle-x" : running ? "wrench" : "circle-check"; const detail = entry.detail || [entry.input ? `Input\n${formatValue(entry.input)}` : "", entry.output ? `Output\n${formatValue(entry.output)}` : "", entry.error ? `Error\n${entry.error}` : ""].filter(Boolean).join("\n\n"),traceId=String(entry.id || entry.toolName || "step"),disclosureKey=`${messageId}:${traceId}`; return `<details class="trace-row" data-trace-key="${escapeHtml(disclosureKey)}" ${state.traceOpen.has(disclosureKey)?"open":""}><summary><span class="${cls}">${icon(ico)}</span><span class="trace-title">${escapeHtml(entry.title || entry.toolName || "Agent step")}</span>${detail ? icon("chevron-down","chev") : ""}</summary>${detail ? `<div class="trace-detail"><pre>${escapeHtml(detail)}</pre></div>` : ""}</details>`; }).join("")}</div>`;
}
function formatValue(value) { if (typeof value === "string") return value; try { return JSON.stringify(value,null,2); } catch { return String(value); } }
function renderAttachments(attachments, removable) {
return `<div class="attachment-list">${attachments.map(file => `<div class="attachment" data-attachment-id="${file.id}">${file.kind === "image" && file.dataUrl ? `<img src="${file.dataUrl}" alt="">` : `<span class="attachment-icon">${icon(file.kind === "image" ? "image" : "file-code-2")}</span>`}<span class="attachment-copy"><strong>${escapeHtml(file.name)}</strong><span>${file.kind === "image" ? "Image" : "Text"} · ${formatBytes(file.size)}</span></span>${removable ? `<button class="attachment-remove" data-action="remove-attachment" aria-label="Remove ${escapeHtml(file.name)}">${icon("x")}</button>` : ""}</div>`).join("")}</div>`;
}
function renderPending() { els.pending.classList.toggle("hidden", !state.attachments.length); els.pending.innerHTML = state.attachments.length ? renderAttachments(state.attachments,true) : ""; refreshIcons(els.pending); }
function updateControls() {
const canSend = !state.generating && (els.prompt.value.trim() || state.attachments.length) && !state.attachments.some(a => a.error);
els.send.disabled = !canSend; els.send.classList.toggle("hidden", state.generating); els.stop.classList.toggle("hidden", !state.generating);
els.statusLine.classList.toggle("hidden", !state.generating); els.statusText.textContent = state.status || "Thinking…";
els.modelPickerBtn.disabled = state.generating; els.reasoningBtn.disabled = state.generating || !getReasoningCapabilities(selectedModelDetails());
}
function isNearBottom() { return els.messages.scrollHeight-els.messages.scrollTop-els.messages.clientHeight<72; }
function followStreamOutput() { if(state.followOutput)els.messages.scrollTop=els.messages.scrollHeight; }
function scrollToBottom() { state.followOutput=true;els.messages.scrollTop=els.messages.scrollHeight; }
function shortModel(model = "") { return model.split("/").pop()?.replace(":free","") || model; }
async function addFiles(fileList) {
const files = [...fileList].slice(0, Math.max(0, MAX_ATTACHMENTS - state.attachments.length));
if ([...fileList].length > files.length) toast(`Up to ${MAX_ATTACHMENTS} attachments per turn.`, "error");
for (const file of files) {
const ext = file.name.toLowerCase().split(".").pop() || "";
const imageMimeType=IMAGE_TYPES.has(file.type)?file.type:IMAGE_EXTENSIONS[ext],kind=imageMimeType?"image":(file.type.startsWith("text/") || TEXT_EXT.has(ext))?"text":null;
if (!kind) { toast(`${file.name}: unsupported file type.`, "error"); continue; }
const limit = kind === "image" ? MAX_IMAGE_BYTES : MAX_TEXT_BYTES;
if (file.size > limit) { toast(`${file.name}: exceeds the ${formatBytes(limit)} limit.`, "error"); continue; }
try { const attachment={id:uid("file"),name:file.name,mimeType:imageMimeType || file.type || "text/plain",size:file.size,kind};if(kind==="image"){attachment.dataUrl=await readFile(file,"data");if(!attachment.dataUrl.startsWith(`data:${attachment.mimeType};`))attachment.dataUrl=attachment.dataUrl.replace(/^data:[^;]*;/,`data:${attachment.mimeType};`);}else attachment.text=await readFile(file,"text");state.attachments.push(attachment); } catch { toast(`${file.name}: could not read the file.`, "error"); }
}
renderPending(); updateControls();
}
function readFile(file, mode) { return new Promise((resolve,reject) => { const reader = new FileReader(); reader.onload = () => resolve(String(reader.result || "")); reader.onerror = reject; mode === "data" ? reader.readAsDataURL(file) : reader.readAsText(file); }); }
function modelSupportsImages(modelId) { const model=state.models.find(m=>m.id===modelId);if(model?.capabilities?.vision===true)return true;if(model?.capabilities?.vision===false)return false;if(model?.modalities?.length)return model.modalities.map(value=>String(value).toLowerCase()).includes("image");return VISION_HINTS.some(h=>modelId.toLowerCase().includes(h)); }
function providerSupportsImageType(provider,mimeType) { const type=String(mimeType || "").toLowerCase();if(provider?.id==="lmstudio")return ["image/png","image/jpeg","image/webp"].includes(type);return IMAGE_TYPES.has(type); }
function attachmentText(attachments = []) { const files = attachments.filter(a => a.kind === "text" && a.text); if (!files.length) return ""; return ["The user attached the following text/code files. Treat them as user-provided context.", ...files.map((a,i) => `\n### Attachment ${i+1}: ${a.name}\n\n\`\`\`${a.name.split(".").pop() || "text"}\n${a.text}\n\`\`\``)].join("\n"); }
function providerContent(message,modelId,provider=activeProvider(),requireImageSupport=true) {
const context = attachmentText(message.attachments); const text = [message.content?.trim(),context].filter(Boolean).join("\n\n");
const images = (message.attachments || []).filter(a => a.kind === "image" && a.dataUrl);
if (!images.length) return text;
if (!modelSupportsImages(modelId)) {
if (requireImageSupport) throw new Error("The selected model does not support image input. Choose a vision-capable model or remove the image.");
const imageSummary=images.map(image=>`- ${image.name} (${image.mimeType || "image"}, ${formatBytes(image.size)})`).join("\n");
return [text,"A prior user message included image attachments that are unavailable to this text-only model.",imageSummary].filter(Boolean).join("\n\n");
}
const unsupported=images.filter(image=>!providerSupportsImageType(provider,image.mimeType));
if(unsupported.length) { if(requireImageSupport)throw new Error(`${provider?.name || "This provider"} does not support ${unsupported.map(image=>image.name).join(", ")} in this image format. Use PNG, JPEG, or WebP.`);const supported=images.filter(image=>providerSupportsImageType(provider,image.mimeType));if(!supported.length)return [text,"A prior user message included image attachments in a format unavailable to this provider."].filter(Boolean).join("\n\n");return [{type:"text",text:text || "Please review the attached image."},...supported.map(image=>({type:"image_url",image_url:{url:image.dataUrl,detail:"auto"}}))]; }
return [{ type: "text", text: text || "Please review the attached image." }, ...images.map(a => ({ type: "image_url", image_url: { url: a.dataUrl, detail: "auto" } }))];
}
class McpStreamableHttpClient {
constructor(url) { this.url=url.endsWith("/")?url:`${url}/`; this.requestId=0; this.sessionId=""; this.initialized=false; this.serverInfo=null; this.initializePromise=null; }
async connect(signal) {
if(this.initialized)return this.serverInfo;
if(this.initializePromise)return this.initializePromise;
this.initializePromise=(async()=>{
const result=await this.request("initialize",{protocolVersion:MCP_PROTOCOL_VERSION,capabilities:{},clientInfo:{name:"agent-micro-app",version:"1.0.0"}},{signal,skipConnect:true,timeoutMs:MCP_DISCOVERY_TIMEOUT_MS});
this.serverInfo=result.serverInfo || null;
await this.notify("notifications/initialized",{},signal);
this.initialized=true;
return this.serverInfo;
})();
try{return await this.initializePromise;}finally{this.initializePromise=null;}
}
async request(method,params={},options={}) {
if(!options.skipConnect)await this.connect(options.signal);
const id=++this.requestId;
const response=await this.post({jsonrpc:"2.0",id,method,params},options.signal,options.timeoutMs || MCP_TOOL_TIMEOUT_MS);
if(response?.error)throw new Error(response.error.message || `MCP error ${response.error.code || ""}`.trim());
if(!response || response.id!==id)throw new Error("The MCP server returned an invalid response.");
return response.result;
}
async notify(method,params={},signal) { await this.post({jsonrpc:"2.0",method,params},signal,MCP_DISCOVERY_TIMEOUT_MS,true); }
async post(payload,outerSignal,timeoutMs,notification=false) {
const controller=new AbortController();
const onAbort=()=>controller.abort(outerSignal?.reason);
if(outerSignal?.aborted)onAbort();else outerSignal?.addEventListener("abort",onAbort,{once:true});
const timeout=setTimeout(()=>controller.abort(new DOMException("MCP request timed out.","TimeoutError")),timeoutMs);
const headers={"Content-Type":"application/json","Accept":"application/json, text/event-stream","MCP-Protocol-Version":MCP_PROTOCOL_VERSION};
if(this.sessionId)headers["MCP-Session-Id"]=this.sessionId;
try {
const response=await fetch(this.url,{method:"POST",headers,body:JSON.stringify(payload),signal:controller.signal});
const session=response.headers.get("MCP-Session-Id"); if(session)this.sessionId=session;
if(!response.ok)throw new Error(`MCP ${response.status}: ${(await response.text()).slice(0,500) || response.statusText}`);
if(notification || response.status===202)return null;
const raw=await response.text();
return parseMcpResponse(raw,response.headers.get("Content-Type") || "");
} catch(error) {
if(controller.signal.aborted && !outerSignal?.aborted)throw new Error(`MCP server did not respond within ${Math.round(timeoutMs/1000)} seconds.`);
throw error;
} finally { clearTimeout(timeout); outerSignal?.removeEventListener("abort",onAbort); }
}
async listTools(signal) { const result=await this.request("tools/list",{}, {signal,timeoutMs:MCP_DISCOVERY_TIMEOUT_MS}); return Array.isArray(result?.tools)?result.tools:[]; }
async callTool(name,args,signal) { return this.request("tools/call",{name,arguments:args || {}},{signal,timeoutMs:MCP_TOOL_TIMEOUT_MS}); }
reset() { this.sessionId=""; this.initialized=false; this.serverInfo=null; this.initializePromise=null; }
}
function parseMcpResponse(raw,contentType) {
if(contentType.includes("application/json"))return safeParse(raw,null);
const messages=[]; let data=[];
for(const line of raw.split(/\r?\n/)) { if(line.startsWith("data:"))data.push(line.slice(5).trim()); else if(!line.trim() && data.length){const parsed=safeParse(data.join("\n"),null);if(parsed)messages.push(parsed);data=[];} }
if(data.length){const parsed=safeParse(data.join("\n"),null);if(parsed)messages.push(parsed);}
if(!messages.length)throw new Error("The MCP server returned an unreadable response.");
return messages.find(message=>message?.result || message?.error) || messages[0];
}
const mcpClients=new Map();
function getMcpClient(server) { const current=mcpClients.get(server.id);if(current?.url===server.url)return current.client;const client=new McpStreamableHttpClient(server.url);mcpClients.set(server.id,{url:server.url,client});return client; }
function resetMcpClients() { mcpClients.clear();state.mcpTools=[];state.mcpConnections={}; }
function mcpToolIcon(name) { const value=String(name).toLowerCase(); if(value.includes("search"))return "search";if(value.includes("fetch"))return "globe";if(value.includes("file"))return "folder";if(value.includes("code")||value.includes("terminal")||value.includes("shell"))return "square-terminal";if(value.includes("image"))return "image";if(value.includes("video"))return "video";if(value.includes("speech"))return "audio-lines";if(value.includes("research"))return "telescope";if(value.includes("memory"))return "brain";if(value.includes("skill"))return "blocks";return "wrench"; }
function toolKey(serverId,name) { return `${serverId}::${name}`; }
function safeToolName(value) { return String(value || "tool").replace(/[^a-zA-Z0-9_-]/g,"_").replace(/^_+|_+$/g,"").slice(0,64) || "tool"; }
function assignExposedToolNames(tools) { const counts={};for(const tool of tools){const base=safeToolName(tool.name);counts[base]=(counts[base]||0)+1;}const used=new Set();return tools.map(tool=>{const base=safeToolName(tool.name),prefix=safeToolName(tool.serverName).slice(0,20),seed=counts[base]>1?`${prefix}__${base}`:base;let exposedName=seed.slice(0,64),n=2;while(used.has(exposedName)){const suffix=`_${n++}`;exposedName=`${seed.slice(0,64-suffix.length)}${suffix}`;}used.add(exposedName);return {...tool,exposedName};}); }
function isMcpToolDisabled(tool) { return state.settings.disabledMcpTools.includes(tool.key) || (tool.serverId==="nymbo-tools" && state.settings.disabledMcpTools.includes(tool.name)); }
function enabledMcpTools() { return state.mcpTools.filter(tool=>!isMcpToolDisabled(tool)); }
function enabledToolSchemas() { return enabledMcpTools().map(tool=>({type:"function",function:{name:tool.exposedName,description:`${tool.description || ""}${tool.description?"\n":""}MCP server: ${tool.serverName}`,parameters:tool.inputSchema && typeof tool.inputSchema==="object"?tool.inputSchema:{type:"object",properties:{}}}})); }
async function loadMcpTools(force=false,signal) {
const servers=state.settings.mcpServers.filter(server=>server.enabled);
if(!force && servers.length && servers.every(server=>state.mcpConnections[server.id]?.status==="connected"))return state.mcpTools;
state.mcpLoading=true;renderMcpTools();
try {
const previous=state.mcpTools;
const groups=await Promise.all(servers.map(async server=>{
const client=getMcpClient(server);if(force)client.reset();
try { const tools=await client.listTools(signal);state.mcpConnections[server.id]={status:"connected",count:tools.length,serverInfo:client.serverInfo};return tools.map(tool=>({...tool,key:toolKey(server.id,tool.name),serverId:server.id,serverName:server.name})); }
catch(error) { const message=cleanError(error.message);state.mcpConnections[server.id]={status:"error",count:0,error:message};return previous.filter(tool=>tool.serverId===server.id); }
}));
state.mcpTools=assignExposedToolNames(groups.flat());
const failures=servers.filter(server=>state.mcpConnections[server.id]?.status==="error");
if(failures.length===servers.length && servers.length && !state.mcpTools.length)throw new Error(failures.map(server=>`${server.name}: ${state.mcpConnections[server.id].error}`).join(" · "));
return state.mcpTools;
} finally { state.mcpLoading=false;renderMcpTools(); }
}
function formatMcpToolResult(result) {
const parts=[];
if(Array.isArray(result?.content))for(const entry of result.content){if(typeof entry?.text==="string")parts.push(entry.text);else if(entry!=null)parts.push(JSON.stringify(entry));}
if(result?.structuredContent!==undefined)parts.push(JSON.stringify(result.structuredContent));
const output=parts.filter(Boolean).join("\n");
if(result?.isError)throw new Error(output || "The MCP tool reported an error.");
return output || "Tool returned no structured response.";
}
async function executeTool(name,args,signal) {
const tool=state.mcpTools.find(entry=>entry.exposedName===name);
if(!tool)throw new Error(`Unknown MCP tool: ${name}`);
if(isMcpToolDisabled(tool))throw new Error(`${humanize(tool.name)} is disabled.`);
const server=state.settings.mcpServers.find(entry=>entry.id===tool.serverId);
if(!server?.enabled)throw new Error(`${tool.serverName} is disabled.`);
return formatMcpToolResult(await getMcpClient(server).callTool(tool.name,args,signal));
}
async function sendMessage(content = els.prompt.value, suppliedAttachments = null) {
if (state.generating) return;
const text = String(content || "").trim(); const attachments = suppliedAttachments ? clone(suppliedAttachments) : clone(state.attachments);
if (!text && !attachments.length) return;
const chat = activeChat(),provider=activeProvider(chat),key=providerKey(provider.id);if(provider.openRouter&&!key.trim()){openProviders();toast(`Add a ${provider.name} API key first.`,"error");return;}const model = chat.model || provider.defaultModel;
try { providerContent({content:text,attachments},model,provider); } catch(error) { toast(error.message,"error"); return; }
const userMessage = { id:uid("msg"), role:"user", content:text, attachments, createdAt:Date.now() };
const modelName=state.models.find(entry=>entry.id===model)?.name || model;
const assistant = { id:uid("msg"), role:"assistant", content:"", model, modelName, providerId:provider.id, trace:[], segments:[], createdAt:Date.now() };
const prior = chat.messages.map(m => ({ role:m.role, content:providerContent(m,model,provider,false) }));
chat.messages.push(userMessage,assistant); state.attachments=[]; els.prompt.value=""; autoSizePrompt(); touchChat(chat); setGenerating(true,"Thinking…"); renderApp(); scrollToBottom();
const controller = new AbortController(); state.abort = controller;
try {
state.status="Just a sec…";updateControls();
try { await loadMcpTools(false,controller.signal); }
catch(error) { console.warn("MCP tool discovery failed; continuing without tools.",error); }
const conversation = [{ role:"system", content:state.settings.systemPrompt }, ...prior, { role:"user", content:providerContent(userMessage,model,provider) }];
let iterations=0;
while (iterations <= MAX_TOOL_ITERATIONS) {
const result = await InferenceAdapter.complete({ messages:conversation, model, provider, assistant, signal:controller.signal });
if (!result.toolCalls.length) break;
if (iterations++ >= MAX_TOOL_ITERATIONS) throw new Error("The agent reached the tool-call limit for one turn.");
conversation.push({ role:"assistant", content:result.content || "", tool_calls:result.toolCalls.map(call=>({id:call.id,type:call.type || "function",function:call.function})) });
for (const call of result.toolCalls) {
let args={}; try { args=JSON.parse(call.function.arguments || "{}"); } catch { args={ raw:call.function.arguments }; }
const entry = upsertToolTrace(assistant,call.traceId || call.id,call.function.name,{ status:"running", input:args, title:`Using ${humanize(call.function.name)}…` });
state.status=`Using ${humanize(call.function.name)}…`; scheduleStreamingRender(assistant); updateControls();
try { const output=await executeTool(call.function.name,args,controller.signal); Object.assign(entry,{status:"completed",output,title:`Used ${humanize(call.function.name)}`}); conversation.push({role:"tool",tool_call_id:call.id,name:call.function.name,content:output}); }
catch(error) { Object.assign(entry,{status:"error",error:error.message,title:`${humanize(call.function.name)} failed`}); conversation.push({role:"tool",tool_call_id:call.id,name:call.function.name,content:`Error: ${error.message}`}); }
scheduleStreamingRender(assistant);
}
state.status="Reasoning over tool results…"; updateControls();
}
if (!assistant.content.trim() && !assistant.trace.length) appendAssistantText(assistant,"empty","The model returned an empty response.");
} catch(error) {
if (error.name === "AbortError") { if (!assistant.content) appendAssistantText(assistant,"stopped","Generation stopped."); }
else { assistant.error=true;appendAssistantText(assistant,"error",`Error: ${cleanError(error.message)}`);toast(cleanError(error.message),"error"); }
} finally { const followOutput=state.followOutput;touchChat(chat);setGenerating(false);renderStreamingMessage(assistant);renderSidebar();renderPending();updateControls();if(followOutput)requestAnimationFrame(followStreamOutput); }
}
function setGenerating(value,status="") { state.generating=value; state.status=status; if (!value) { state.abort=null; state.status=""; } updateControls(); }
function cleanError(message) { let value=String(message || "Request failed.").replace(/sk-or-v1-[A-Za-z0-9_-]+/g,"[redacted]");for(const key of Object.values(state.providerKeys))if(key&&key.length>=8)value=value.split(key).join("[redacted]");return value.slice(0,1000); }
function humanize(value) { return String(value || "tool").replace(/^Tools_/,"").replace(/_/g," ").replace(/\b\w/g,c=>c.toUpperCase()); }
function ensureSegment(message,segment) { message.segments ||= [];let existing=message.segments.find(item=>item.id===segment.id);if(!existing){existing={...segment,uiKey:segment.uiKey || segment.id};message.segments.push(existing);}else existing.uiKey ||= existing.id;return existing; }
function appendAssistantText(message,segmentId,text) { if(!text)return;const segment=ensureSegment(message,{id:segmentId,kind:"text",content:""});segment.content+=text;message.content+=text; }
function upsertToolTrace(message,id,name,patch) { let entry=message.trace.find(e=>e.id===id); if (!entry) { entry={id,kind:"tool",toolName:name}; message.trace.push(entry);ensureSegment(message,{id:`trace:${id}`,kind:"trace",traceId:id}); } Object.assign(entry,patch); return entry; }
function appendReasoning(assistant,text,source="reasoning",traceId="reasoning") { if(!text)return;let buffers=reasoningBuffers.get(assistant);if(!buffers){buffers=new Map();reasoningBuffers.set(assistant,buffers);}const bufferKey=`${traceId}:${source}`;buffers.set(bufferKey,(buffers.get(bufferKey)||"")+text);const prefix=`${traceId}:`,main=[buffers.get(`${prefix}reasoning`)||"",buffers.get(`${prefix}reasoning-content`)||""].sort((a,b)=>b.length-a.length)[0],structured=[...new Set([...buffers.entries()].filter(([key])=>key.startsWith(`${prefix}detail:`)).map(([,value])=>value))].join("\n\n"),detail=structured.length>main.length?structured:main;let entry=assistant.trace.find(item=>item.id===traceId);if(!entry){entry={id:traceId,kind:"thought",status:"completed",title:"Reasoning",detail:""};assistant.trace.push(entry);ensureSegment(assistant,{id:`trace:${traceId}`,kind:"trace",traceId});}entry.detail=detail;const preview=detail.replace(/[\s*_`#>]+/g," ").trim();entry.title=preview?`Reasoning · ${preview.slice(0,100)}`:"Reasoning"; }
const InferenceAdapter = {
version: 1,
async complete({messages,model,provider,assistant,signal}) {
const body={ model, messages, temperature:Number(state.settings.temperature), top_p:Number(state.settings.topP), top_k:Number(state.settings.topK), repetition_penalty:Number(state.settings.repetitionPenalty), max_tokens:Number(state.settings.maxTokens), stream:true };
if (state.settings.seed) body.seed=Number(state.settings.seed);
const tools=enabledToolSchemas(); if (tools.length) body.tools=tools;
const modelInfo=state.models.find(entry=>entry.id===model) || {id:model}, reasoning=getReasoningSelection(modelInfo);
if(reasoning) {
if(provider.id==="lmstudio") { if(reasoning.mode==="off")body.reasoning_effort="none";else if(reasoning.mode==="effort")body.reasoning_effort=reasoning.effort;else if(reasoning.mode==="max_tokens")body.reasoning_tokens=reasoning.maxTokens; }
else { if(reasoning.mode==="off")body.reasoning={effort:"none"};else if(reasoning.mode==="effort")body.reasoning={effort:reasoning.effort};else if(reasoning.mode==="max_tokens")body.reasoning={max_tokens:reasoning.maxTokens};else body.reasoning={enabled:true}; }
}
const headers={"Content-Type":"application/json"},key=providerKey(provider.id);if(key)headers.Authorization=`Bearer ${key.trim()}`;if(provider.openRouter){headers["HTTP-Referer"]=location.origin.startsWith("http")?location.origin:"https://localhost";headers["X-Title"]="Agent Micro-App";}
const response=await fetch(provider.chatUrl,{method:"POST",headers,body:JSON.stringify(body),signal});
if (!response.ok) { const raw=await response.text(); throw new Error(`${provider.name} ${response.status}: ${extractProviderError(raw,response.statusText)}`); }
if (!response.body) throw new Error("The provider returned no response stream.");
const reader=response.body.getReader(),decoder=new TextDecoder(),streamId=uid("stream");let buffer="",callMap=[],content="",phase=0,lastStreamKind="";const phaseId=kind=>{if(lastStreamKind!==kind){lastStreamKind=kind;phase++;}return `${streamId}:${kind}-${phase}`;};
state.status="Thinking…"; updateControls();
while(true) {
const {done,value}=await reader.read(); if(done) break; buffer+=decoder.decode(value,{stream:true}); const lines=buffer.split("\n"); buffer=lines.pop() || "";
for(const rawLine of lines) { const line=rawLine.trim(); if(!line || line==="data: [DONE]") continue; const payload=line.startsWith("data:")?line.slice(5).trim():line; let data; try{data=JSON.parse(payload);}catch{continue;} const delta=data.choices?.[0]?.delta; if(!delta) continue;
const reasoningText=typeof delta.reasoning==="string"?delta.reasoning:"",reasoningContent=typeof delta.reasoning_content==="string"?delta.reasoning_content:"";
if(reasoningText) appendReasoning(assistant,reasoningText,"reasoning",phaseId("reasoning")); if(reasoningContent) appendReasoning(assistant,reasoningContent,"reasoning-content",phaseId("reasoning"));
if(Array.isArray(delta.reasoning_details)) delta.reasoning_details.forEach((d,n)=>{const detailText=d.text || d.summary || "";if(detailText)appendReasoning(assistant,detailText,`detail:${d.id ?? d.index ?? n}`,phaseId("reasoning"));});
const contentText=typeof delta.content==="string"?delta.content:"";if(contentText) { content+=contentText;appendAssistantText(assistant,phaseId("text"),contentText);state.status="Writing response…";scheduleStreamingRender(assistant);updateControls(); }
if(Array.isArray(delta.tool_calls)&&delta.tool_calls.length) { const parts=delta.tool_calls.filter(part=>part&&(part.id || part.function?.name || part.function?.arguments));if(parts.length){const toolPhaseId=phaseId("tool");parts.forEach(part=>{const idx=part.index ?? 0;if(!callMap[idx])callMap[idx]={id:part.id || `${streamId}:call-${idx}`,traceId:`${toolPhaseId}:${idx}`,type:"function",function:{name:"",arguments:""}};else if(part.id)callMap[idx].id=part.id;if(part.function?.name)callMap[idx].function.name+=part.function.name;if(part.function?.arguments)callMap[idx].function.arguments+=part.function.arguments;upsertToolTrace(assistant,callMap[idx].traceId,callMap[idx].function.name || "tool",{status:"running",rawInput:callMap[idx].function.arguments,title:`Preparing ${humanize(callMap[idx].function.name || "tool")}…`});});}}
scheduleStreamingRender(assistant);
}
}
return {content,toolCalls:callMap.filter(Boolean)};
}
};
function extractProviderError(raw,fallback) { const parsed=safeParse(raw,null); return parsed?.error?.message || parsed?.message || raw.trim().slice(0,700) || fallback || "Request failed"; }
function normalizeProviderModel(model) { if(typeof model==="string")return {id:model,name:model,modalities:[]};const id=model.id || model.key || model.name,capabilities=model.capabilities && typeof model.capabilities==="object"?model.capabilities:{},advertisedModalities=model.architecture?.input_modalities || model.input_modalities || model.modalities || [];const modalities=Array.isArray(advertisedModalities)&&advertisedModalities.length?advertisedModalities:capabilities.vision===true?["text","image"]:capabilities.vision===false?["text"]:[];return {id,name:model.name || model.display_name || id,modalities,capabilities,contextLength:model.context_length || model.max_context_length,reasoning:model.reasoning || capabilities.reasoning,supportedParameters:model.supported_parameters || []}; }
async function loadModels(force=false) {
const token=++state.modelLoadToken,provider=activeProvider(),key=providerKey(provider.id),cacheScope=key?"keyed":"anonymous";
if(!provider.modelsUrl){state.models=[{id:provider.defaultModel,name:provider.defaultModel}];els.modelLoading.classList.add("hidden");renderModels();return;}
if (!force) { const cached=safeParse(await readPreference(STORAGE.models,localStorage.getItem(STORAGE.models) || ""),null); if(cached && cached.providerId===provider.id && cached.signature===provider.modelsUrl && cached.scope===cacheScope && Date.now()-cached.savedAt<86400000) { state.models=cached.models;els.modelLoading.classList.add("hidden");renderModels();return; } }
els.modelLoading.classList.remove("hidden");
try { const headers={};if(key)headers.Authorization=`Bearer ${key}`;if(provider.openRouter){headers["HTTP-Referer"]=location.origin.startsWith("http")?location.origin:"https://localhost";headers["X-Title"]="Agent Micro-App";}const response=await fetch(provider.modelsUrl,{headers});if(!response.ok)throw new Error(`${response.status} ${response.statusText}`);const data=await response.json(),items=Array.isArray(data)?data:(data.data || data.models || []);let models=items.filter(item=>typeof item==="string" || !item?.type || item.type==="llm").map(normalizeProviderModel).filter(model=>model.id);if(provider.openRouter&&!key)models=models.filter(model=>model.id.endsWith(":free"));if(!models.length)throw new Error("No models returned");models.sort((a,b)=>(a.name || a.id).localeCompare(b.name || b.id));if(token!==state.modelLoadToken)return;state.models=models;saveJson(STORAGE.models,{models,savedAt:Date.now(),scope:cacheScope,providerId:provider.id,signature:provider.modelsUrl},false); }
catch { if(token!==state.modelLoadToken)return;state.models=provider.openRouter?FALLBACK_MODELS:[{id:provider.defaultModel,name:provider.defaultModel}];toast(`Could not load models from ${provider.name}.`,"error"); }
finally { if(token===state.modelLoadToken){els.modelLoading.classList.add("hidden");renderModels();} }
}
function openModelPicker() { if(!els.modelPopover.classList.contains("hidden")){els.modelPopover.classList.add("hidden");els.prompt.focus();return;}closeCommands(true);if(!closeComposerPopovers(els.modelPopover))return;state.modelQuery="";els.modelSearch.value="";renderModels();els.modelPopover.classList.remove("hidden");positionComposerPopover(els.modelPopover,els.modelPickerBtn);requestAnimationFrame(()=>els.modelSearch.focus()); }
function selectModel(modelId) { const chat=activeChat(),provider=activeProvider(chat);chat.model=modelId;chat.providerId=provider.id;provider.defaultModel=modelId;state.settings.model=modelId;touchChat(chat);saveJson(STORAGE.settings,state.settings);renderModels();els.modelPopover.classList.add("hidden");els.prompt.focus(); }
function showCommandPopover(popover) { closeCommands(true);if(!closeComposerPopovers(popover))return;popover.classList.remove("hidden");positionCenteredComposerPopover(popover); }
function openProviders() { state.providerDrafts=clone(state.settings.inferenceProviders);state.providerKeyDrafts={...state.providerKeys};state.providerDraftId=activeProvider()?.id || state.settings.activeProviderId;renderProviderEditor();showCommandPopover(els.providersPopover); }
function populateParameters(values=state.settings) { $("#systemPromptInput").value=values.systemPrompt;$("#temperatureInput").value=$("#temperatureRange").value=values.temperature;$("#topPInput").value=$("#topPRange").value=values.topP;$("#topKInput").value=$("#topKRange").value=values.topK;$("#repetitionPenaltyInput").value=$("#repetitionPenaltyRange").value=values.repetitionPenalty;$("#maxTokensInput").value=values.maxTokens;$("#seedInput").value=values.seed; }
function openParameters() { populateParameters();showCommandPopover(els.parametersPopover); }
function openMcpConfig() { renderMcpServerSettings();showCommandPopover(els.mcpConfigPopover); }
function openDataControls() { showCommandPopover(els.dataControlsPopover); }
function captureProviderDraft(validate=false) { const provider=state.providerDrafts.find(entry=>entry.id===state.providerDraftId);if(!provider)return;provider.name=$("#inferenceProviderNameInput").value.trim() || "Inference Provider";provider.defaultModel=$("#inferenceDefaultModelInput").value.trim();provider.chatUrl=validate?validateInferenceUrl($("#inferenceChatUrlInput").value,"chat completions URL"):$("#inferenceChatUrlInput").value.trim();provider.modelsUrl=validate?validateInferenceUrl($("#inferenceModelsUrlInput").value,"models URL",true):$("#inferenceModelsUrlInput").value.trim();if(validate&&!provider.defaultModel)throw new Error("Enter a default model ID.");provider.rememberKey=$("#rememberKeyInput").checked;state.providerKeyDrafts[provider.id]=$("#apiKeyInput").value.trim(); }
function renderProviderEditor() { const provider=state.providerDrafts.find(entry=>entry.id===state.providerDraftId) || state.providerDrafts[0];if(!provider)return;state.providerDraftId=provider.id;els.providerSelect.innerHTML=state.providerDrafts.map(entry=>`<button class="provider-card ${entry.id===provider.id?"active":""}" type="button" data-provider-id="${escapeHtml(entry.id)}" aria-pressed="${entry.id===provider.id}"><span class="provider-card-icon">${icon(entry.id==="openrouter"?"cloud":entry.id==="lmstudio"?"monitor-cog":"server")}</span><span class="provider-card-copy"><strong>${escapeHtml(entry.name)}</strong><span>${escapeHtml(entry.description || (entry.builtIn?"Built-in provider":"OpenAI-compatible endpoint"))}</span></span>${icon("circle-check","provider-card-check")}</button>`).join("");$("#providerEditorTitle").textContent=provider.name;$("#providerEditorBadge").textContent=provider.builtIn?"Built-in":"Custom";$("#inferenceProviderNameInput").value=provider.name;$("#inferenceDefaultModelInput").value=provider.defaultModel;$("#inferenceChatUrlInput").value=provider.chatUrl;$("#inferenceModelsUrlInput").value=provider.modelsUrl;$("#apiKeyInput").value=state.providerKeyDrafts[provider.id] || "";$("#rememberKeyInput").checked=provider.rememberKey;$("#removeInferenceProviderBtn").hidden=provider.builtIn;refreshIcons(els.providerSelect); }
function addInferenceProvider() { try{captureProviderDraft(false);}catch{}if(state.providerDrafts.length>=10){toast("Up to 10 inference providers can be configured.","error");return;}const customCount=state.providerDrafts.filter(provider=>!provider.builtIn).length+1,provider={id:uid("provider"),name:`Custom provider ${customCount}`,description:"OpenAI-compatible endpoint",chatUrl:"http://localhost:1234/v1/chat/completions",modelsUrl:"http://localhost:1234/v1/models",defaultModel:"local-model",rememberKey:false,openRouter:false,builtIn:false};state.providerDrafts.push(provider);state.providerDraftId=provider.id;renderProviderEditor();$("#inferenceProviderNameInput").select(); }
function removeInferenceProvider() { const current=state.providerDrafts.find(entry=>entry.id===state.providerDraftId);if(!current||current.builtIn)return;const index=state.providerDrafts.indexOf(current);delete state.providerKeyDrafts[state.providerDraftId];state.providerDrafts.splice(index,1);state.providerDraftId=state.providerDrafts[Math.max(0,index-1)].id;renderProviderEditor(); }
function renderMcpServerSettings(servers=state.settings.mcpServers) { els.mcpServerList.innerHTML=servers.length?servers.map(server=>`<div class="mcp-server-row" data-mcp-server-id="${escapeHtml(server.id)}"><input type="checkbox" data-mcp-field="enabled" ${server.enabled?"checked":""} aria-label="Enable ${escapeHtml(server.name)}"><input type="text" data-mcp-field="name" value="${escapeHtml(server.name)}" placeholder="Server name" aria-label="MCP server name"><input type="url" data-mcp-field="url" value="${escapeHtml(server.url)}" placeholder="https://server.example/mcp/" aria-label="MCP Streamable HTTP URL"><button class="icon-btn" type="button" data-action="remove-mcp-server" aria-label="Remove ${escapeHtml(server.name)}">${icon("trash-2")}</button></div>`).join(""):`<div class="empty-chats">No MCP servers configured.</div>`;refreshIcons(els.mcpServerList); }
function readMcpServerSettings(validate=true) { return $$('[data-mcp-server-id]',els.mcpServerList).map((row,index)=>{const rawUrl=$("[data-mcp-field='url']",row).value.trim();return {id:row.dataset.mcpServerId || uid("mcp"),name:$("[data-mcp-field='name']",row).value.trim() || `MCP Server ${index+1}`,url:validate?validateMcpServerUrl(rawUrl):rawUrl,enabled:$("[data-mcp-field='enabled']",row).checked};}); }
function addMcpServerSetting() { const servers=readMcpServerSettings(false);if(servers.length>=12){toast("Up to 12 MCP servers can be configured.","error");return;}servers.push({id:uid("mcp"),name:`MCP Server ${servers.length+1}`,url:"https://",enabled:true});renderMcpServerSettings(servers);const rows=$$('[data-mcp-server-id]',els.mcpServerList);$("[data-mcp-field='name']",rows.at(-1))?.select(); }
async function saveProviders() {
try{captureProviderDraft(true);}catch(error){toast(error.message,"error");return;}
const providersChanged=JSON.stringify(state.providerDrafts)!==JSON.stringify(state.settings.inferenceProviders),chat=activeChat(),previousProviderId=chat.providerId || state.settings.activeProviderId;
state.settings={...state.settings,activeProviderId:state.providerDraftId,inferenceProviders:clone(state.providerDrafts)};
state.providerKeys=Object.fromEntries(state.settings.inferenceProviders.map(provider=>[provider.id,state.providerKeyDrafts[provider.id] || ""]).filter(([,key])=>key));const provider=inferenceProvider(state.settings.activeProviderId);chat.providerId=provider.id;if(previousProviderId!==provider.id)chat.model=provider.defaultModel;state.settings.model=chat.model || provider.defaultModel;touchChat(chat);
const remembered={},session={};for(const item of state.settings.inferenceProviders){const key=state.providerKeys[item.id];if(!key)continue;(item.rememberKey?remembered:session)[item.id]=key;}
let keyPersisted=true;try{sessionStorage.removeItem(STORAGE.providerKeys);sessionStorage.removeItem(STORAGE.key);if(Object.keys(session).length)sessionStorage.setItem(STORAGE.providerKeys,JSON.stringify(session));}catch{keyPersisted=false;}try{await removeSecret(STORAGE.providerKeys);await removeSecret(STORAGE.key);if(Object.keys(remembered).length)await writeSecret(STORAGE.providerKeys,remembered);}catch{keyPersisted=false;}try{localStorage.removeItem(STORAGE.key);}catch{}
const settingsPersisted=saveJson(STORAGE.settings,state.settings,false);els.providersPopover.classList.add("hidden");if(providersChanged||previousProviderId!==provider.id)state.models=provider.openRouter?FALLBACK_MODELS:[{id:provider.defaultModel,name:provider.defaultModel}];loadModels(true);
if(!keyPersisted)toast("Provider settings are active, but one or more API keys could not be persisted.","error");else if(!settingsPersisted)toast("Providers are active for this tab, but local storage is full.","error");else toast("Providers saved.");
}
function persistParameters() { state.settings={...state.settings,systemPrompt:$("#systemPromptInput").value.trim() || DEFAULT_PROMPT,temperature:clamp($("#temperatureInput").value,0,2),topP:clamp($("#topPInput").value,0,1),topK:Math.round(clamp($("#topKInput").value,0,100)),repetitionPenalty:clamp($("#repetitionPenaltyInput").value,0,2),maxTokens:Math.round(clamp($("#maxTokensInput").value,64,32768)),seed:Math.max(0,Math.round(Number($("#seedInput").value)||0))};saveJson(STORAGE.settings,state.settings); }
function persistMcpConfig() { let mcpServers;try{mcpServers=readMcpServerSettings();}catch(error){toast(error.message,"error");return false;}const changed=JSON.stringify(mcpServers)!==JSON.stringify(state.settings.mcpServers);state.settings={...state.settings,mcpServers};saveJson(STORAGE.settings,state.settings);if(changed){resetMcpClients();void loadMcpTools(true).catch(error=>toast(cleanError(error.message),"error"));}return true; }
function clamp(value,min,max) { const n=Number(value); return Number.isFinite(n)?Math.min(max,Math.max(min,n)):min; }
function renderMcpTools() {
if(!els.toolGrid)return;
els.toolLoading.classList.toggle("hidden",!state.mcpLoading);
if(state.mcpLoading && !state.mcpTools.length)els.toolGrid.innerHTML=`<div class="empty-chats">Discovering tools…</div>`;
else if(!state.mcpTools.length)els.toolGrid.innerHTML=`<div class="empty-chats">No MCP tools available.</div>`;
else {
const groups=new Map();for(const tool of state.mcpTools){if(!groups.has(tool.serverId))groups.set(tool.serverId,[]);groups.get(tool.serverId).push(tool);}
els.toolGrid.innerHTML=[...groups.entries()].map(([serverId,tools])=>{const enabledCount=tools.filter(tool=>!isMcpToolDisabled(tool)).length,serverName=tools[0]?.serverName || serverId;return `<section class="tool-server-group" data-tool-server-group="${escapeHtml(serverId)}"><label class="tool-server-head"><input type="checkbox" data-server-tools="${escapeHtml(serverId)}" ${enabledCount===tools.length?"checked":""}><span><strong>${escapeHtml(serverName)}</strong><small data-server-tool-count>${enabledCount}/${tools.length} enabled</small></span></label>${tools.map(tool=>{const checked=!isMcpToolDisabled(tool),summary=tool.description || "MCP tool";return `<label class="tool-card ${checked?"":"disabled"}" title="${escapeHtml(summary)}"><input type="checkbox" data-tool-key value="${escapeHtml(tool.key)}" data-tool-server="${escapeHtml(serverId)}" ${checked?"checked":""}><span>${icon(mcpToolIcon(tool.name))}</span><span><strong>${escapeHtml(humanize(tool.name))}</strong><span>${escapeHtml(summary)}</span></span></label>`;}).join("")}</section>`;}).join("");
$$('[data-server-tools]',els.toolGrid).forEach(input=>syncMcpServerToggle(input.dataset.serverTools));
}
refreshIcons(els.toolGrid);
}
function syncMcpServerToggle(serverId) { const escaped=CSS.escape(serverId),tools=$$(`[data-tool-server="${escaped}"]`,els.toolGrid),toggle=$(`[data-server-tools="${escaped}"]`,els.toolGrid);if(!toggle||!tools.length)return;const enabled=tools.filter(input=>input.checked).length;toggle.checked=enabled===tools.length;toggle.indeterminate=enabled>0&&enabled<tools.length;const count=toggle.closest(".tool-server-head")?.querySelector("[data-server-tool-count]");if(count)count.textContent=`${enabled}/${tools.length} enabled`; }
function openTools() { if(!els.toolsPopover.classList.contains("hidden")){els.toolsPopover.classList.add("hidden");return;}closeCommands(true);if(!closeComposerPopovers(els.toolsPopover))return;renderMcpTools();els.toolsPopover.classList.remove("hidden");positionCenteredComposerPopover(els.toolsPopover);void loadMcpTools().catch(()=>{}); }
function saveTools() { if(state.mcpTools.length)state.settings.disabledMcpTools=state.mcpTools.filter(tool=>!$$('[data-tool-key]:checked',els.toolGrid).some(input=>input.value===tool.key)).map(tool=>tool.key);saveJson(STORAGE.settings,state.settings);els.toolsPopover.classList.add("hidden");toast(`${enabledMcpTools().length} MCP tools enabled.`); }
const COMMANDS = [
{icon:"paperclip",label:"Attach",description:"Add text, code, or images to the message",action:"attach",keywords:"file upload image"},
{icon:"database",label:"Data Controls",description:"Export, import, or clear local data",action:"data-controls",keywords:"backup download storage"},
{icon:"plug",label:"MCP Config",description:"Configure MCP servers and connections",action:"mcp-config",keywords:"tools server url integrations"},
{icon:"box",label:"Models",description:"Choose a model from the active provider",action:"model-picker",keywords:"provider llm"},
{icon:"plus",label:"New chat",description:"Start a fresh conversation",action:"new-chat",keywords:"reset conversation"},
{icon:"sliders-horizontal",label:"Parameters",description:"Configure the system prompt and generation",action:"parameters",keywords:"temperature top p top k tokens seed repetition"},
{icon:"server",label:"Providers",description:"Configure OpenRouter, LM Studio, and custom endpoints",action:"providers",keywords:"api key models inference"},
{icon:"brain-circuit",label:"Reasoning",description:"Configure reasoning for the selected model",action:"reasoning",keywords:"thinking effort"},
{icon:"refresh-cw",label:"Refresh models",description:"Reload models from the active provider",action:"refresh-models",keywords:"sync update"},
{icon:"wrench",label:"Tools",description:"Manage MCP tools",action:"tools",keywords:"mcp integrations"}
];
function renderCommands(query="") { const normalized=query.trim().toLowerCase();state.commandMatches=COMMANDS.filter(command=>!normalized||`${command.label} ${command.description} ${command.keywords}`.toLowerCase().includes(normalized));state.commandIndex=Math.min(state.commandIndex,Math.max(0,state.commandMatches.length-1));els.commandList.innerHTML=state.commandMatches.length?state.commandMatches.map((command,index)=>`<button class="command-row ${index===state.commandIndex?"selected":""}" type="button" role="option" aria-selected="${index===state.commandIndex}" data-command="${command.action}" data-command-index="${index}">${icon(command.icon)}<span class="command-copy"><strong>${escapeHtml(command.label)}</strong><span>${escapeHtml(command.description)}</span></span></button>`).join(""):`<div class="command-empty">No matching commands</div>`;refreshIcons(els.commandList); }
function commandQuery() { if(state.commandFromSlash)return els.prompt.value.slice(1);return els.prompt.value.startsWith(state.commandPromptBase)?els.prompt.value.slice(state.commandPromptBase.length):els.prompt.value; }
function openCommands() { if(state.commandOpen)return;if(!closeComposerPopovers())return;state.commandFromSlash=false;state.commandPromptBase=els.prompt.value;state.commandOpen=true;state.commandIndex=0;els.commandPanel.classList.remove("hidden");renderCommands();els.prompt.focus(); }
function closeCommands(clearQuery=false) { if(!state.commandOpen)return;const restore=clearQuery?(state.commandFromSlash?"":state.commandPromptBase):null;state.commandOpen=false;state.commandFromSlash=false;state.commandPromptBase="";els.commandPanel.classList.add("hidden");if(restore!==null){els.prompt.value=restore;autoSizePrompt();} }
function toggleCommands() { state.commandOpen?closeCommands(true):openCommands(); }
function moveCommandSelection(delta) { if(!state.commandMatches.length)return;state.commandIndex=(state.commandIndex+delta+state.commandMatches.length)%state.commandMatches.length;renderCommands(commandQuery());els.commandList.querySelector(".command-row.selected")?.scrollIntoView({block:"nearest"}); }
function runCommand(action) { closeCommands(true);if(action==="new-chat")newChat();if(action==="model-picker")openModelPicker();if(action==="reasoning")openReasoning();if(action==="tools")openTools();if(action==="attach")els.fileInput.click();if(action==="parameters")openParameters();if(action==="providers")openProviders();if(action==="data-controls")openDataControls();if(action==="mcp-config")openMcpConfig();if(action==="refresh-models")void loadModels(true).then(()=>toast(`Loaded ${state.models.length} models.`)); }
function runSelectedCommand() { const command=state.commandMatches[state.commandIndex];if(command)runCommand(command.action); }
function syncCommandPanelFromPrompt() { const slashQuery=els.prompt.value.startsWith("/")&&!els.prompt.value.includes("\n");if(slashQuery){if(!state.commandOpen){state.commandOpen=true;state.commandIndex=0;state.commandPromptBase="";els.commandPanel.classList.remove("hidden");}state.commandFromSlash=true;renderCommands(els.prompt.value.slice(1));}else if(state.commandFromSlash)closeCommands();else if(state.commandOpen)renderCommands(commandQuery()); }
function newChat() { if(state.generating) return; const chat=createChat(undefined,state.settings.activeProviderId); state.chats.unshift(chat); state.activeId=chat.id; state.attachments=[]; state.models=activeProvider(chat).openRouter?FALLBACK_MODELS:[{id:activeProvider(chat).defaultModel,name:activeProvider(chat).defaultModel}];saveChats();renderApp();void loadModels();closeMobileSidebar();els.prompt.focus(); }
function openChat(id) { if(state.generating) { toast("Stop the current response before switching chats.","error"); return; }const previous=activeProvider()?.id;state.activeId=id;state.attachments=[];const chat=activeChat(),provider=activeProvider(chat);chat.providerId=provider.id;state.settings.activeProviderId=provider.id;state.settings.model=chat.model;if(previous!==provider.id){state.models=provider.openRouter?FALLBACK_MODELS:[{id:provider.defaultModel,name:provider.defaultModel}];void loadModels();}saveJson(STORAGE.settings,state.settings);renderApp();closeMobileSidebar(); }
function deleteChat(id) { if(state.generating && id===state.activeId) return; const chat=state.chats.find(c=>c.id===id); if(!chat)return; state.chats=state.chats.filter(c=>c.id!==id); if(!state.chats.length)state.chats.push(createChat()); if(id===state.activeId)state.activeId=state.chats[0].id; saveChats(); renderApp(); toast(`Deleted “${chat.title}”.`); }
function exportChat() { const chat=activeChat(); if(!chat) return; const lines=[`# ${chat.title}`,"",`Provider: ${inferenceProvider(chat.providerId)?.name || chat.providerId}`,`Model: ${chat.model}`,""]; chat.messages.forEach(m=>{lines.push(`## ${m.role==="user"?"You":"Agent"}`,"",m.content || ""); if(m.attachments?.length) lines.push("",...m.attachments.map(a=>`- Attachment: ${a.name} (${formatBytes(a.size)})`)); if(m.trace?.length) lines.push("","> Agent trace",...m.trace.map(t=>`> - ${t.title || t.toolName}: ${t.status || "complete"}`)); lines.push("");}); download(`${slug(chat.title)}.md`,lines.join("\n"),"text/markdown"); }
function slug(value) { return String(value).toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"").slice(0,60)||"agent-chat"; }
function exportAll() { download(`agent-data-${new Date().toISOString().slice(0,10)}.json`,JSON.stringify({version:1,exportedAt:new Date().toISOString(),settings:state.settings,chats:state.chats},null,2),"application/json"); }
async function importAll(file) { try { const data=JSON.parse(await file.text()); if(data.version!==1 || !Array.isArray(data.chats)) throw new Error("Unsupported Agent export."); const incoming=data.chats.filter(validChat); if(!incoming.length) throw new Error("No valid chats were found.");state.chats=incoming.slice(0,MAX_CHATS);if(data.settings){const providers=normalizeInferenceProviders(data.settings.inferenceProviders,data.settings);state.settings={...DEFAULTS,...data.settings,inferenceProviders:providers,activeProviderId:providers.some(provider=>provider.id===data.settings.activeProviderId)?data.settings.activeProviderId:providers[0].id,mcpServers:normalizeMcpServers(data.settings.mcpServers)};}state.chats.forEach(chat=>{if(!chat.providerId||!state.settings.inferenceProviders.some(provider=>provider.id===chat.providerId))chat.providerId=state.settings.activeProviderId;});state.activeId=state.chats[0].id;state.settings.activeProviderId=activeChat().providerId;resetMcpClients();await persistChatsNow();saveJson(STORAGE.settings,state.settings);state.models=activeProvider().openRouter?FALLBACK_MODELS:[{id:activeProvider().defaultModel,name:activeProvider().defaultModel}];renderApp();void loadModels(true);void loadMcpTools(true).catch(()=>{});toast(`Imported ${state.chats.length} chats.`); } catch(error) { toast(error.message || "Could not import data.","error"); } }
async function clearAllData() { if(!confirm("Clear every saved Agent chat and preference from this browser?")) return;clearTimeout(state.persistTimer);state.persistTimer=null;await clearAgentDb();Object.values(STORAGE).forEach(key=>{localStorage.removeItem(key);sessionStorage.removeItem(key);});state.settings={...DEFAULTS,inferenceProviders:clone(DEFAULT_INFERENCE_PROVIDERS),mcpServers:clone(DEFAULT_MCP_SERVERS)};state.providerKeys={};state.chats=[createChat(DEFAULT_MODEL,"openrouter")];state.activeId=state.chats[0].id;resetMcpClients();state.models=FALLBACK_MODELS;state.attachments=[];state.chatDbAvailable=true;[els.modelPopover,els.reasoningPopover,els.toolsPopover,els.providersPopover,els.parametersPopover,els.mcpConfigPopover,els.dataControlsPopover].forEach(popover=>popover.classList.add("hidden"));await persistChatsNow();renderApp();void loadModels(true);void loadMcpTools(true).catch(()=>{});toast("Agent was reset to its default empty state."); }
function editMessage(index) { const chat=activeChat(),message=chat.messages[index]; if(!message || message.role!=="user" || state.generating) return; chat.messages=chat.messages.slice(0,index); els.prompt.value=message.content; state.attachments=clone(message.attachments || []); touchChat(chat); renderApp(); autoSizePrompt(); els.prompt.focus(); toast("Message loaded into the composer. Sending will replace later turns."); }
function retryMessage(index) { const chat=activeChat(); if(state.generating) return; let userIndex=index-1; while(userIndex>=0 && chat.messages[userIndex].role!=="user") userIndex--; if(userIndex<0) return; const user=clone(chat.messages[userIndex]); chat.messages=chat.messages.slice(0,userIndex); touchChat(chat); renderApp(); sendMessage(user.content,user.attachments || []); }
function messageCopy(index) { const message=activeChat().messages[index]; if(message) copyText(message.content || ""); }
function autoSizePrompt() { els.prompt.style.height="auto"; els.prompt.style.height=Math.min(170,Math.max(46,els.prompt.scrollHeight))+"px"; updateControls(); }
function openMobileSidebar() { els.sidebar.classList.add("open"); els.sidebarBackdrop.classList.remove("hidden"); }
function closeMobileSidebar() { els.sidebar.classList.remove("open"); els.sidebarBackdrop.classList.add("hidden"); }
function runArtifact(html) { state.artifactHtml=html; const title=html.match(/<title[^>]*>([^<]*)<\/title>/i)?.[1] || "HTML Preview"; els.artifactTitle.textContent=title; els.artifactFrame.srcdoc=html; els.artifact.classList.add("open"); els.artifactBackdrop.classList.remove("hidden"); }
function closeArtifact() { els.artifact.classList.remove("open"); els.artifactBackdrop.classList.add("hidden"); setTimeout(()=>{els.artifactFrame.srcdoc="";},220); }
function wireEvents() {
$("#newChatBtn").addEventListener("click",newChat); $("#commandBtn").addEventListener("click",toggleCommands); els.modelPickerBtn.addEventListener("click",openModelPicker);
$("#openSidebarBtn").addEventListener("click",openMobileSidebar); $("#closeSidebarBtn").addEventListener("click",closeMobileSidebar); els.sidebarBackdrop.addEventListener("click",closeMobileSidebar);
els.chatList.addEventListener("click",event=>{const row=event.target.closest("[data-chat-id]");const action=event.target.closest("[data-action]")?.dataset.action;if(!row)return;action==="delete-chat"?deleteChat(row.dataset.chatId):openChat(row.dataset.chatId);});
els.composer.addEventListener("submit",event=>{event.preventDefault();if(state.commandOpen)runSelectedCommand();else sendMessage();});els.prompt.addEventListener("input",()=>{autoSizePrompt();syncCommandPanelFromPrompt();});els.prompt.addEventListener("keydown",event=>{if(state.commandOpen){if(event.key==="ArrowDown"){event.preventDefault();moveCommandSelection(1);return;}if(event.key==="ArrowUp"){event.preventDefault();moveCommandSelection(-1);return;}if((event.key==="Enter"||event.key==="Tab")&&!event.isComposing){event.preventDefault();runSelectedCommand();return;}if(event.key==="Escape"){event.preventDefault();closeCommands(true);return;}}if(event.key==="Enter"&&!event.shiftKey&&!event.isComposing){event.preventDefault();sendMessage();}});
els.fileInput.addEventListener("change",event=>{addFiles(event.target.files);event.target.value="";}); els.pending.addEventListener("click",event=>{const btn=event.target.closest('[data-action="remove-attachment"]');if(!btn)return;const id=event.target.closest("[data-attachment-id]")?.dataset.attachmentId;state.attachments=state.attachments.filter(a=>a.id!==id);renderPending();updateControls();});
els.stop.addEventListener("click",()=>state.abort?.abort()); els.reasoningBtn.addEventListener("click",openReasoning);
els.reasoningControls.addEventListener("click",event=>{const button=event.target.closest("[data-reasoning-mode]");if(!button)return;const mode=button.dataset.reasoningMode;if(mode==="effort")setReasoningSelection({mode,effort:button.dataset.reasoningEffort});else if(mode==="max_tokens")setReasoningSelection({mode,maxTokens:Math.max(1,Math.round(Number($("#reasoningBudgetInput")?.value)||2000))});else setReasoningSelection({mode});});
els.modelSearch.addEventListener("input",()=>{state.modelQuery=els.modelSearch.value;renderModels();}); els.modelList.addEventListener("click",event=>{const option=event.target.closest("[data-model-id]");if(option)selectModel(option.dataset.modelId);});
els.messagesInner.addEventListener("click",event=>{const action=event.target.closest("[data-action]")?.dataset.action;if(action==="open-settings")openProviders();const prompt=event.target.closest("[data-prompt]")?.dataset.prompt;if(prompt){els.prompt.value=prompt;autoSizePrompt();els.prompt.focus();}const article=event.target.closest("[data-message-index]");if(article){const i=Number(article.dataset.messageIndex);if(action==="copy-message")messageCopy(i);if(action==="edit-message")editMessage(i);if(action==="retry-message")retryMessage(i);}const wrap=event.target.closest(".code-wrap");if(wrap&&event.target.closest("[data-code-copy]"))copyText(wrap.dataset.code||"");if(wrap&&event.target.closest("[data-run-html]"))runArtifact(wrap.dataset.code||"");});
els.messages.addEventListener("scroll",()=>{state.followOutput=isNearBottom();},{passive:true});
els.messagesInner.addEventListener("toggle",event=>{const details=event.target.closest?.("[data-trace-key]");if(!details)return;details.open?state.traceOpen.add(details.dataset.traceKey):state.traceOpen.delete(details.dataset.traceKey);if(state.generating)state.followOutput=false;},true);
$$('[data-close-popover]').forEach(btn=>btn.addEventListener("click",()=>dismissComposerPopover(btn.closest(".composer-popover"))));$("#saveProvidersBtn").addEventListener("click",saveProviders);$("#resetParametersBtn").addEventListener("click",()=>populateParameters(DEFAULTS));
els.providerSelect.addEventListener("click",event=>{const card=event.target.closest("[data-provider-id]");if(!card||card.dataset.providerId===state.providerDraftId)return;try{captureProviderDraft(false);}catch{}state.providerDraftId=card.dataset.providerId;renderProviderEditor();});$("#inferenceProviderNameInput").addEventListener("input",()=>{const provider=state.providerDrafts.find(entry=>entry.id===state.providerDraftId);if(!provider)return;provider.name=$("#inferenceProviderNameInput").value || "Custom provider";$("#providerEditorTitle").textContent=provider.name;const card=els.providerSelect.querySelector(`[data-provider-id="${CSS.escape(provider.id)}"] .provider-card-copy strong`);if(card)card.textContent=provider.name;});$("#addInferenceProviderBtn").addEventListener("click",addInferenceProvider);$("#removeInferenceProviderBtn").addEventListener("click",removeInferenceProvider);
$("#addMcpServerBtn").addEventListener("click",addMcpServerSetting);els.mcpServerList.addEventListener("click",event=>{const button=event.target.closest('[data-action="remove-mcp-server"]');if(!button)return;button.closest('[data-mcp-server-id]')?.remove();if(!$$('[data-mcp-server-id]',els.mcpServerList).length)renderMcpServerSettings([]);});
[["#temperatureRange","#temperatureInput"],["#temperatureInput","#temperatureRange"],["#topPRange","#topPInput"],["#topPInput","#topPRange"],["#topKRange","#topKInput"],["#topKInput","#topKRange"],["#repetitionPenaltyRange","#repetitionPenaltyInput"],["#repetitionPenaltyInput","#repetitionPenaltyRange"]].forEach(([a,b])=>$(a).addEventListener("input",()=>$(b).value=$(a).value));
$("#saveToolsBtn").addEventListener("click",saveTools); $("#disableToolsBtn").addEventListener("click",()=>{$$('[data-tool-key]',els.toolGrid).forEach(input=>{input.checked=false;input.closest(".tool-card")?.classList.add("disabled");});$$('[data-server-tools]',els.toolGrid).forEach(input=>syncMcpServerToggle(input.dataset.serverTools));}); $("#refreshToolsBtn").addEventListener("click",()=>{void loadMcpTools(true).then(()=>toast(`Loaded ${state.mcpTools.length} tools from ${Object.values(state.mcpConnections).filter(connection=>connection.status==="connected").length} servers.`)).catch(error=>toast(cleanError(error.message),"error"));});
els.toolGrid.addEventListener("change",event=>{const serverToggle=event.target.closest("[data-server-tools]");if(serverToggle){const serverId=serverToggle.dataset.serverTools,escaped=CSS.escape(serverId);$$(`[data-tool-server="${escaped}"]`,els.toolGrid).forEach(input=>{input.checked=serverToggle.checked;input.closest(".tool-card")?.classList.toggle("disabled",!input.checked);});syncMcpServerToggle(serverId);return;}const toolToggle=event.target.closest("[data-tool-key]");if(toolToggle){toolToggle.closest(".tool-card")?.classList.toggle("disabled",!toolToggle.checked);syncMcpServerToggle(toolToggle.dataset.toolServer);}});
els.commandList.addEventListener("pointermove",event=>{const row=event.target.closest("[data-command-index]");if(!row)return;const index=Number(row.dataset.commandIndex);if(index!==state.commandIndex){state.commandIndex=index;els.commandList.querySelectorAll(".command-row").forEach((button,buttonIndex)=>{button.classList.toggle("selected",buttonIndex===index);button.setAttribute("aria-selected",String(buttonIndex===index));});}});els.commandList.addEventListener("click",event=>{const command=event.target.closest("[data-command]")?.dataset.command;if(command)runCommand(command);});
$("#exportChatSettingsBtn").addEventListener("click",exportChat); $("#exportAllBtn").addEventListener("click",exportAll); $("#importAllBtn").addEventListener("click",()=>$("#importInput").click()); $("#importInput").addEventListener("change",event=>{if(event.target.files[0])importAll(event.target.files[0]);event.target.value="";}); $("#clearDataBtn").addEventListener("click",clearAllData);
$("#closeArtifactBtn").addEventListener("click",closeArtifact); els.artifactBackdrop.addEventListener("click",closeArtifact); $("#downloadArtifactBtn").addEventListener("click",()=>download(`${slug(els.artifactTitle.textContent)}.html`,state.artifactHtml,"text/html"));
document.addEventListener("keydown",event=>{const mod=event.ctrlKey||event.metaKey;if(mod&&event.key.toLowerCase()==="k"){event.preventDefault();toggleCommands();}if(mod&&event.shiftKey&&event.key.toLowerCase()==="n"){event.preventDefault();newChat();}if(event.key==="Escape"){if([els.modelPopover,els.reasoningPopover,els.toolsPopover,els.providersPopover,els.parametersPopover,els.mcpConfigPopover,els.dataControlsPopover].some(popover=>!popover.classList.contains("hidden"))){closeComposerPopovers();els.prompt.focus();}if(els.artifact.classList.contains("open"))closeArtifact();}});document.addEventListener("pointerdown",event=>{if(state.commandOpen&&!event.target.closest("#commandPanel")&&!event.target.closest("#commandBtn")&&event.target!==els.prompt)closeCommands();if(!event.target.closest("#modelPopover")&&!event.target.closest("#modelPickerBtn"))els.modelPopover.classList.add("hidden");if(!event.target.closest("#reasoningPopover")&&!event.target.closest("#reasoningBtn"))els.reasoningPopover.classList.add("hidden");if(!event.target.closest("#toolsPopover"))els.toolsPopover.classList.add("hidden");[["#providersPopover",els.providersPopover],["#parametersPopover",els.parametersPopover],["#mcpConfigPopover",els.mcpConfigPopover],["#dataControlsPopover",els.dataControlsPopover]].forEach(([selector,popover])=>{if(!event.target.closest(selector))dismissComposerPopover(popover);});});window.addEventListener("resize",()=>closeComposerPopovers());
window.addEventListener("dragenter",event=>{event.preventDefault();state.dragDepth++;els.dropOverlay.classList.remove("hidden");}); window.addEventListener("dragover",event=>event.preventDefault()); window.addEventListener("dragleave",event=>{event.preventDefault();state.dragDepth=Math.max(0,state.dragDepth-1);if(!state.dragDepth)els.dropOverlay.classList.add("hidden");}); window.addEventListener("drop",event=>{event.preventDefault();state.dragDepth=0;els.dropOverlay.classList.add("hidden");if(event.dataTransfer?.files?.length)addFiles(event.dataTransfer.files);});
}
async function init() { wireEvents(); await loadState(); renderApp(); autoSizePrompt(); refreshIcons(); loadModels(); void loadMcpTools().catch(()=>{}); }
void init();
})();
</script>
</body>
</html>