tina4-nodejs 3.10.73 → 3.10.75

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tina4-nodejs",
3
- "version": "3.10.73",
3
+ "version": "3.10.75",
4
4
  "type": "module",
5
5
  "description": "Tina4 for Node.js/TypeScript — 54 built-in features, zero dependencies",
6
6
  "keywords": ["tina4", "framework", "web", "api", "orm", "graphql", "websocket", "typescript"],
@@ -0,0 +1,426 @@
1
+ (function(){"use strict";var Je;const at="/__dev/api";async function z(e,t="GET",n){const i={method:t,headers:{}};return n&&(i.headers["Content-Type"]="application/json",i.body=JSON.stringify(n)),(await fetch(at+e,i)).json()}function a(e){const t=document.createElement("span");return t.textContent=e,t.innerHTML}const je={python:{color:"#3b82f6",name:"Python"},php:{color:"#8b5cf6",name:"PHP"},ruby:{color:"#ef4444",name:"Ruby"},nodejs:{color:"#22c55e",name:"Node.js"}};function rt(){const e=document.getElementById("app"),t=(e==null?void 0:e.dataset.framework)??"python",n=e==null?void 0:e.dataset.color,i=je[t]??je.python;return{framework:t,color:n??i.color,name:i.name}}function lt(e){const t=document.documentElement;t.style.setProperty("--primary",e.color),t.style.setProperty("--bg","#0f172a"),t.style.setProperty("--surface","#1e293b"),t.style.setProperty("--border","#334155"),t.style.setProperty("--text","#e2e8f0"),t.style.setProperty("--muted","#94a3b8"),t.style.setProperty("--success","#22c55e"),t.style.setProperty("--danger","#ef4444"),t.style.setProperty("--warn","#f59e0b"),t.style.setProperty("--info","#3b82f6")}const dt=`
2
+ * { margin: 0; padding: 0; box-sizing: border-box; }
3
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: var(--bg); color: var(--text); }
4
+
5
+ .dev-admin { display: flex; flex-direction: column; height: 100vh; }
6
+ .dev-header { display: flex; align-items: center; justify-content: space-between; padding: 0.5rem 1rem; background: var(--surface); border-bottom: 1px solid var(--border); }
7
+ .dev-header h1 { font-size: 1rem; font-weight: 700; }
8
+ .dev-header h1 span { color: var(--primary); }
9
+
10
+ .dev-tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border); background: var(--surface); padding: 0 0.5rem; overflow-x: auto; }
11
+ .dev-tab { padding: 0.5rem 0.75rem; border: none; background: none; color: var(--muted); cursor: pointer; font-size: 0.8rem; font-weight: 500; white-space: nowrap; border-bottom: 2px solid transparent; transition: all 0.15s; }
12
+ .dev-tab:hover { color: var(--text); }
13
+ .dev-tab.active { color: var(--primary); border-bottom-color: var(--primary); }
14
+
15
+ .dev-content { flex: 1; overflow-y: auto; }
16
+ .dev-panel { padding: 1rem; display: none; }
17
+ .dev-panel.active { display: block; }
18
+ .dev-panel-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem; }
19
+ .dev-panel-header h2 { font-size: 0.95rem; font-weight: 600; }
20
+
21
+ .btn { padding: 0.35rem 0.75rem; border: 1px solid var(--border); border-radius: 0.375rem; background: var(--surface); color: var(--text); cursor: pointer; font-size: 0.8rem; transition: all 0.15s; height: 30px; line-height: 1; }
22
+ .btn:hover { background: var(--border); }
23
+ .btn-primary { background: var(--primary); border-color: var(--primary); color: white; }
24
+ .btn-primary:hover { opacity: 0.9; }
25
+ .btn-danger { background: var(--danger); border-color: var(--danger); color: white; }
26
+ .btn-sm { padding: 0.2rem 0.5rem; font-size: 0.75rem; }
27
+
28
+ .input { padding: 0.35rem 0.5rem; border: 1px solid var(--border); border-radius: 0.375rem; background: var(--bg); color: var(--text); font-size: 0.8rem; height: 30px; }
29
+ select.input { height: 30px; }
30
+ input[type=number].input { -moz-appearance: textfield; }
31
+ input[type=number].input::-webkit-outer-spin-button, input[type=number].input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
32
+ .input:focus { outline: none; border-color: var(--primary); }
33
+ textarea.input { font-family: "SF Mono", "Fira Code", Consolas, monospace; resize: vertical; height: auto; }
34
+
35
+ table { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
36
+ th { text-align: left; padding: 0.5rem; color: var(--muted); font-weight: 600; border-bottom: 1px solid var(--border); }
37
+ td { padding: 0.5rem; border-bottom: 1px solid var(--border); }
38
+ tr:hover { background: rgba(255,255,255,0.03); }
39
+
40
+ .badge { display: inline-block; padding: 0.1rem 0.4rem; border-radius: 9999px; font-size: 0.7rem; font-weight: 600; }
41
+ .badge-success { background: rgba(34,197,94,0.15); color: var(--success); }
42
+ .badge-danger { background: rgba(239,68,68,0.15); color: var(--danger); }
43
+ .badge-warn { background: rgba(245,158,11,0.15); color: var(--warn); }
44
+ .badge-info { background: rgba(59,130,246,0.15); color: var(--info); }
45
+ .badge-muted { background: rgba(148,163,184,0.15); color: var(--muted); }
46
+
47
+ .method { font-weight: 700; font-size: 0.7rem; padding: 0.1rem 0.3rem; border-radius: 0.2rem; }
48
+ .method-get { color: var(--success); }
49
+ .method-post { color: var(--info); }
50
+ .method-put { color: var(--warn); }
51
+ .method-patch { color: var(--warn); }
52
+ .method-delete { color: var(--danger); }
53
+ .method-any { color: var(--muted); }
54
+
55
+ .flex { display: flex; }
56
+ .gap-sm { gap: 0.5rem; }
57
+ .items-center { align-items: center; }
58
+ .text-mono { font-family: "SF Mono", "Fira Code", Consolas, monospace; }
59
+ .text-sm { font-size: 0.8rem; }
60
+ .text-muted { color: var(--muted); }
61
+ .empty-state { text-align: center; padding: 2rem; color: var(--muted); }
62
+
63
+ .metric-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 0.75rem; margin-bottom: 1rem; }
64
+ .metric-card { background: var(--surface); border: 1px solid var(--border); border-radius: 0.5rem; padding: 0.75rem; }
65
+ .metric-card .label { font-size: 0.7rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; }
66
+ .metric-card .value { font-size: 1.5rem; font-weight: 700; margin-top: 0.25rem; }
67
+
68
+ .chat-container { display: flex; flex-direction: column; height: calc(100vh - 140px); }
69
+ .chat-messages { flex: 1; overflow-y: auto; padding: 0.75rem; }
70
+ .chat-msg { padding: 0.5rem 0.75rem; border-radius: 0.5rem; margin-bottom: 0.5rem; font-size: 0.85rem; line-height: 1.5; max-width: 85%; }
71
+ .chat-user { background: var(--primary); color: white; margin-left: auto; }
72
+ .chat-bot { background: var(--surface); border: 1px solid var(--border); }
73
+ .chat-input-row { display: flex; gap: 0.5rem; padding: 0.75rem; border-top: 1px solid var(--border); }
74
+ .chat-input-row input { flex: 1; }
75
+
76
+ .error-trace { background: var(--bg); border: 1px solid var(--border); border-radius: 0.375rem; padding: 0.5rem; font-family: monospace; font-size: 0.75rem; white-space: pre-wrap; max-height: 200px; overflow-y: auto; margin-top: 0.5rem; }
77
+
78
+ .bubble-chart { width: 100%; height: 400px; background: var(--surface); border: 1px solid var(--border); border-radius: 0.5rem; overflow: hidden; }
79
+ `;function ct(e){e.innerHTML=`
80
+ <div class="dev-panel-header">
81
+ <h2>Routes <span id="routes-count" class="text-muted text-sm"></span></h2>
82
+ <button class="btn btn-sm" onclick="window.__loadRoutes()">Refresh</button>
83
+ </div>
84
+ <table>
85
+ <thead><tr><th>Method</th><th>Path</th><th>Auth</th><th>Handler</th></tr></thead>
86
+ <tbody id="routes-body"></tbody>
87
+ </table>
88
+ `,He()}async function He(){const e=await z("/routes"),t=document.getElementById("routes-count");t&&(t.textContent=`(${e.count})`);const n=document.getElementById("routes-body");n&&(n.innerHTML=(e.routes||[]).map(i=>`
89
+ <tr>
90
+ <td><span class="method method-${i.method.toLowerCase()}">${a(i.method)}</span></td>
91
+ <td class="text-mono"><a href="${a(i.path)}" target="_blank" style="color:inherit;text-decoration:underline dotted">${a(i.path)}</a></td>
92
+ <td>${i.auth_required?'<span class="badge badge-warn">auth</span>':'<span class="badge badge-success">open</span>'}</td>
93
+ <td class="text-sm text-muted">${a(i.handler||"")} <small>(${a(i.module||"")})</small></td>
94
+ </tr>
95
+ `).join(""))}window.__loadRoutes=He;let W=[],G=[],H=JSON.parse(localStorage.getItem("tina4_query_history")||"[]");function mt(e){e.innerHTML=`
96
+ <div class="dev-panel-header">
97
+ <h2>Database</h2>
98
+ <button class="btn btn-sm" onclick="window.__loadTables()">Refresh</button>
99
+ </div>
100
+ <div style="display:flex;gap:1rem;height:calc(100vh - 140px)">
101
+ <div style="width:200px;flex-shrink:0;overflow-y:auto;border-right:1px solid var(--border);padding-right:0.75rem">
102
+ <div style="font-weight:600;font-size:0.75rem;color:var(--muted);text-transform:uppercase;margin-bottom:0.5rem">Tables</div>
103
+ <div id="db-table-list"></div>
104
+ <div style="margin-top:1.5rem;border-top:1px solid var(--border);padding-top:0.75rem">
105
+ <div style="font-weight:600;font-size:0.75rem;color:var(--muted);text-transform:uppercase;margin-bottom:0.5rem">Seed Data</div>
106
+ <select id="db-seed-table" class="input" style="width:100%;margin-bottom:0.5rem">
107
+ <option value="">Pick table...</option>
108
+ </select>
109
+ <div class="flex gap-sm">
110
+ <input type="number" id="db-seed-count" class="input" value="10" style="width:60px">
111
+ <button class="btn btn-sm btn-primary" onclick="window.__seedTable()">Seed</button>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ <div style="flex:1;display:flex;flex-direction:column;min-width:0">
116
+ <div class="flex gap-sm items-center" style="margin-bottom:0.5rem;flex-wrap:wrap">
117
+ <select id="db-type" class="input" style="width:80px">
118
+ <option value="sql">SQL</option>
119
+ <option value="graphql">GraphQL</option>
120
+ </select>
121
+ <span class="text-sm text-muted">Limit</span>
122
+ <select id="db-limit" class="input" style="width:60px">
123
+ <option value="20">20</option>
124
+ <option value="50">50</option>
125
+ <option value="100">100</option>
126
+ <option value="500">500</option>
127
+ </select>
128
+ <span class="text-sm text-muted">Offset</span>
129
+ <input type="number" id="db-offset" class="input" value="0" min="0" style="width:70px;height:30px;-moz-appearance:textfield;-webkit-appearance:none;margin:0">
130
+ <button class="btn btn-primary" onclick="window.__runQuery()">Run</button>
131
+ <button class="btn" onclick="window.__copyCSV()">Copy CSV</button>
132
+ <button class="btn" onclick="window.__copyJSON()">Copy JSON</button>
133
+ <button class="btn" onclick="window.__showPaste()">Paste</button>
134
+ <span class="text-sm text-muted">Ctrl+Enter</span>
135
+ </div>
136
+ <div class="flex gap-sm items-center" style="margin-bottom:0.25rem">
137
+ <select id="db-history" class="input text-mono" style="flex:1" onchange="window.__loadHistory(this.value)">
138
+ <option value="">Query history...</option>
139
+ </select>
140
+ <button class="btn btn-sm" onclick="window.__clearHistory()" title="Clear history" style="height:30px">Clear</button>
141
+ </div>
142
+ <textarea id="db-query" class="input text-mono" style="width:100%;height:80px;resize:vertical" placeholder="SELECT * FROM users" onkeydown="if(event.ctrlKey&&event.key==='Enter')window.__runQuery()"></textarea>
143
+ <div id="db-result" style="flex:1;overflow:auto;margin-top:0.75rem"></div>
144
+ </div>
145
+ </div>
146
+ <div id="db-paste-modal" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.6);z-index:1000;display:none;align-items:center;justify-content:center">
147
+ <div style="background:var(--surface);border:1px solid var(--border);border-radius:0.5rem;padding:1.5rem;width:600px;max-height:80vh;overflow:auto">
148
+ <h3 style="margin-bottom:0.75rem;font-size:0.9rem">Paste Data</h3>
149
+ <p class="text-sm text-muted" style="margin-bottom:0.5rem">Paste CSV or JSON array. First row = column headers for CSV.</p>
150
+ <div class="flex gap-sm items-center" style="margin-bottom:0.5rem">
151
+ <select id="paste-table" class="input" style="flex:1"><option value="">Select existing table...</option></select>
152
+ <span class="text-sm text-muted">or</span>
153
+ <input type="text" id="paste-new-table" class="input" placeholder="New table name..." style="flex:1">
154
+ </div>
155
+ <textarea id="paste-data" class="input text-mono" style="width:100%;height:200px" placeholder='CSV data or JSON'></textarea>
156
+ <div class="flex gap-sm" style="margin-top:0.75rem;justify-content:flex-end">
157
+ <button class="btn" onclick="window.__hidePaste()">Cancel</button>
158
+ <button class="btn btn-primary" onclick="window.__doPaste()">Import</button>
159
+ </div>
160
+ </div>
161
+ </div>
162
+ `,ge(),he()}async function ge(){const t=(await z("/tables")).tables||[],n=document.getElementById("db-table-list");n&&(n.innerHTML=t.length?t.map(s=>`<div style="padding:0.3rem 0.5rem;cursor:pointer;border-radius:0.25rem;font-size:0.8rem;font-family:monospace" class="db-table-item" onclick="window.__selectTable('${a(s)}')" onmouseover="this.style.background='var(--border)'" onmouseout="this.style.background=''">${a(s)}</div>`).join(""):'<div class="text-sm text-muted">No tables</div>');const i=document.getElementById("db-seed-table");i&&(i.innerHTML='<option value="">Pick table...</option>'+t.map(s=>`<option value="${a(s)}">${a(s)}</option>`).join(""));const o=document.getElementById("paste-table");o&&(o.innerHTML='<option value="">Select table...</option>'+t.map(s=>`<option value="${a(s)}">${a(s)}</option>`).join(""))}function be(e){var n;(n=document.getElementById("db-limit"))!=null&&n.value;const t=document.getElementById("db-query");t&&(t.value=`SELECT * FROM ${e}`),document.querySelectorAll(".db-table-item").forEach(i=>{i.style.background=i.textContent===e?"var(--border)":""}),Pe()}function ut(){var n;const e=document.getElementById("db-query"),t=((n=document.getElementById("db-limit"))==null?void 0:n.value)||"20";e!=null&&e.value&&(e.value=e.value.replace(/LIMIT\s+\d+/i,`LIMIT ${t}`))}function pt(e){const t=e.trim();t&&(H=H.filter(n=>n!==t),H.unshift(t),H.length>50&&(H=H.slice(0,50)),localStorage.setItem("tina4_query_history",JSON.stringify(H)),he())}function he(){const e=document.getElementById("db-history");e&&(e.innerHTML='<option value="">Query history...</option>'+H.map((t,n)=>`<option value="${n}">${a(t.length>80?t.substring(0,80)+"...":t)}</option>`).join(""))}function gt(e){const t=parseInt(e);if(isNaN(t)||!H[t])return;const n=document.getElementById("db-query");n&&(n.value=H[t]),document.getElementById("db-history").selectedIndex=0}function bt(){H=[],localStorage.removeItem("tina4_query_history"),he()}async function Pe(){var o,s,c;const e=document.getElementById("db-query"),t=(o=e==null?void 0:e.value)==null?void 0:o.trim();if(!t)return;pt(t);const n=document.getElementById("db-result"),i=((s=document.getElementById("db-type"))==null?void 0:s.value)||"sql";n&&(n.innerHTML='<p class="text-muted">Running...</p>');try{const d=parseInt(((c=document.getElementById("db-limit"))==null?void 0:c.value)||"20"),p=await z("/query","POST",{query:t,type:i,limit:d});if(p.error){n&&(n.innerHTML=`<p style="color:var(--danger)">${a(p.error)}</p>`);return}p.rows&&p.rows.length>0?(G=Object.keys(p.rows[0]),W=p.rows,n&&(n.innerHTML=`<p class="text-sm text-muted" style="margin-bottom:0.5rem">${p.count??p.rows.length} rows</p>
163
+ <div style="overflow-x:auto"><table><thead><tr>${G.map(u=>`<th>${a(u)}</th>`).join("")}</tr></thead>
164
+ <tbody>${p.rows.map(u=>`<tr>${G.map(_=>`<td class="text-sm">${a(String(u[_]??""))}</td>`).join("")}</tr>`).join("")}</tbody></table></div>`)):p.affected!==void 0?(n&&(n.innerHTML=`<p class="text-muted">${p.affected} rows affected. ${p.success?"Success.":""}</p>`),W=[],G=[]):(n&&(n.innerHTML='<p class="text-muted">No results</p>'),W=[],G=[])}catch(d){n&&(n.innerHTML=`<p style="color:var(--danger)">${a(d.message)}</p>`)}}function ht(){if(!W.length)return;const e=G.join(","),t=W.map(n=>G.map(i=>{const o=String(n[i]??"");return o.includes(",")||o.includes('"')?`"${o.replace(/"/g,'""')}"`:o}).join(","));navigator.clipboard.writeText([e,...t].join(`
165
+ `))}function ft(){W.length&&navigator.clipboard.writeText(JSON.stringify(W,null,2))}function yt(){const e=document.getElementById("db-paste-modal");e&&(e.style.display="flex")}function qe(){const e=document.getElementById("db-paste-modal");e&&(e.style.display="none")}async function vt(){var o,s,c,d,p;const e=(o=document.getElementById("paste-table"))==null?void 0:o.value,t=(c=(s=document.getElementById("paste-new-table"))==null?void 0:s.value)==null?void 0:c.trim(),n=t||e,i=(p=(d=document.getElementById("paste-data"))==null?void 0:d.value)==null?void 0:p.trim();if(!n||!i){alert("Select a table or enter a new table name, and paste data.");return}try{let u;try{u=JSON.parse(i),Array.isArray(u)||(u=[u])}catch{const k=i.split(`
166
+ `).map(E=>E.trim()).filter(Boolean);if(k.length<2){alert("CSV needs at least a header row and one data row.");return}const h=k[0].split(",").map(E=>E.trim().replace(/[^a-zA-Z0-9_]/g,""));u=k.slice(1).map(E=>{const T=E.split(",").map(j=>j.trim()),v={};return h.forEach((j,ee)=>{v[j]=T[ee]??""}),v})}if(!u.length){alert("No data rows found.");return}if(t){const h=["id INTEGER PRIMARY KEY AUTOINCREMENT",...Object.keys(u[0]).filter(T=>T.toLowerCase()!=="id").map(T=>`"${T}" TEXT`)],E=await z("/query","POST",{query:`CREATE TABLE IF NOT EXISTS "${t}" (${h.join(", ")})`,type:"sql"});if(E.error){alert("Create table failed: "+E.error);return}}let _=0;for(const k of u){const h=t?Object.keys(k).filter(j=>j.toLowerCase()!=="id"):Object.keys(k),E=h.map(j=>`"${j}"`).join(","),T=h.map(j=>`'${String(k[j]).replace(/'/g,"''")}'`).join(","),v=await z("/query","POST",{query:`INSERT INTO "${n}" (${E}) VALUES (${T})`,type:"sql"});if(v.error){alert(`Row ${_+1} failed: ${v.error}`);break}_++}document.getElementById("paste-data").value="",document.getElementById("paste-new-table").value="",document.getElementById("paste-table").selectedIndex=0,qe(),ge(),_>0&&be(n)}catch(u){alert("Import error: "+u.message)}}async function xt(){var n,i;const e=(n=document.getElementById("db-seed-table"))==null?void 0:n.value,t=parseInt(((i=document.getElementById("db-seed-count"))==null?void 0:i.value)||"10");if(e)try{const o=await z("/seed","POST",{table:e,count:t});o.error?alert(o.error):be(e)}catch(o){alert("Seed error: "+o.message)}}window.__loadTables=ge,window.__selectTable=be,window.__updateLimit=ut,window.__runQuery=Pe,window.__copyCSV=ht,window.__copyJSON=ft,window.__showPaste=yt,window.__hidePaste=qe,window.__doPaste=vt,window.__seedTable=xt,window.__loadHistory=gt,window.__clearHistory=bt;function wt(e){e.innerHTML=`
167
+ <div class="dev-panel-header">
168
+ <h2>Errors <span id="errors-count" class="text-muted text-sm"></span></h2>
169
+ <div class="flex gap-sm">
170
+ <button class="btn btn-sm" onclick="window.__loadErrors()">Refresh</button>
171
+ <button class="btn btn-sm btn-danger" onclick="window.__clearErrors()">Clear All</button>
172
+ </div>
173
+ </div>
174
+ <div id="errors-body"></div>
175
+ `,ie()}async function ie(){const e=await z("/broken"),t=document.getElementById("errors-count"),n=document.getElementById("errors-body");if(!n)return;const i=e.errors||[];if(t&&(t.textContent=`(${i.length})`),!i.length){n.innerHTML='<div class="empty-state">No errors</div>';return}n.innerHTML=i.map((o,s)=>{const c=o.error_type?`${o.error_type}: ${o.message}`:o.error||o.message||"Unknown error",d=o.context||{},p=o.last_seen||o.first_seen||o.timestamp||"",u=p?new Date(p).toLocaleString():"";return`
176
+ <div style="background:var(--surface);border:1px solid var(--border);border-radius:0.5rem;padding:0.75rem;margin-bottom:0.75rem">
177
+ <div class="flex items-center" style="justify-content:space-between;flex-wrap:wrap;gap:0.5rem">
178
+ <div style="flex:1;min-width:0">
179
+ <span class="badge ${o.resolved?"badge-success":"badge-danger"}">${o.resolved?"RESOLVED":"UNRESOLVED"}</span>
180
+ ${o.count>1?`<span class="badge badge-warn" style="margin-left:4px">x${o.count}</span>`:""}
181
+ <strong style="margin-left:0.5rem;font-size:0.85rem">${a(c)}</strong>
182
+ </div>
183
+ <div class="flex gap-sm" style="flex-shrink:0">
184
+ ${o.resolved?"":`<button class="btn btn-sm" onclick="window.__resolveError('${a(o.id||String(s))}')">Resolve</button>`}
185
+ <button class="btn btn-sm btn-primary" onclick="window.__askAboutError(${s})">Ask Tina4</button>
186
+ </div>
187
+ </div>
188
+ ${d.method?`<div class="text-sm text-mono" style="margin-top:0.5rem;color:var(--info)">${a(d.method)} ${a(d.path||"")}</div>`:""}
189
+ ${o.traceback?`<pre style="margin-top:0.5rem;padding:0.5rem;background:var(--bg);border:1px solid var(--border);border-radius:4px;font-size:0.7rem;overflow-x:auto;white-space:pre-wrap;max-height:200px;overflow-y:auto">${a(o.traceback)}</pre>`:""}
190
+ <div class="text-sm text-muted" style="margin-top:0.5rem">${a(u)}</div>
191
+ </div>
192
+ `}).join(""),window.__errorData=i}async function $t(e){await z("/broken/resolve","POST",{id:e}),ie()}async function _t(){await z("/broken/clear","POST"),ie()}function kt(e){const n=(window.__errorData||[])[e];if(!n)return;const i=n.error_type?`${n.error_type}: ${n.message}`:n.error||n.message||"Unknown error",o=n.context||{},s=o.method&&o.path?`
193
+ Route: ${o.method} ${o.path}`:"",c=`I have this error: ${i}${s}
194
+
195
+ ${n.traceback||""}`;window.__switchTab("chat"),setTimeout(()=>{window.__prefillChat(c)},150)}window.__loadErrors=ie,window.__clearErrors=_t,window.__resolveError=$t,window.__askAboutError=kt;function Et(e){e.innerHTML=`
196
+ <div class="dev-panel-header">
197
+ <h2>System</h2>
198
+ </div>
199
+ <div id="system-grid" class="metric-grid"></div>
200
+ <div id="system-env" style="margin-top:1rem"></div>
201
+ `,Oe()}function Tt(e){if(!e||e<0)return"?";const t=Math.floor(e/86400),n=Math.floor(e%86400/3600),i=Math.floor(e%3600/60),o=Math.floor(e%60),s=[];return t>0&&s.push(`${t}d`),n>0&&s.push(`${n}h`),i>0&&s.push(`${i}m`),s.length===0&&s.push(`${o}s`),s.join(" ")}function St(e){return e?e>=1024?`${(e/1024).toFixed(1)} GB`:`${e.toFixed(1)} MB`:"?"}async function Oe(){const e=await z("/system"),t=document.getElementById("system-grid"),n=document.getElementById("system-env");if(!t)return;const o=(e.python_version||e.php_version||e.ruby_version||e.node_version||e.runtime||"?").split("(")[0].trim(),s=[{label:"Framework",value:e.framework||"Tina4"},{label:"Runtime",value:o},{label:"Platform",value:e.platform||"?"},{label:"Architecture",value:e.architecture||"?"},{label:"PID",value:String(e.pid??"?")},{label:"Uptime",value:Tt(e.uptime_seconds)},{label:"Memory",value:St(e.memory_mb)},{label:"Database",value:e.database||"none"},{label:"DB Tables",value:String(e.db_tables??"?")},{label:"DB Connected",value:e.db_connected?"Yes":"No"},{label:"Debug",value:e.debug==="true"||e.debug===!0?"ON":"OFF"},{label:"Log Level",value:e.log_level||"?"},{label:"Modules",value:String(e.loaded_modules??"?")},{label:"Working Dir",value:e.cwd||"?"}],c=new Set(["Working Dir","Database"]);if(t.innerHTML=s.map(d=>`
202
+ <div class="metric-card" style="${c.has(d.label)?"grid-column:1/-1":""}">
203
+ <div class="label">${a(d.label)}</div>
204
+ <div class="value" style="font-size:${c.has(d.label)?"0.75rem":"1.1rem"}">${a(d.value)}</div>
205
+ </div>
206
+ `).join(""),n){const d=[];e.debug!==void 0&&d.push(["TINA4_DEBUG",String(e.debug)]),e.log_level&&d.push(["LOG_LEVEL",e.log_level]),e.database&&d.push(["DATABASE_URL",e.database]),d.length&&(n.innerHTML=`
207
+ <h3 style="font-size:0.85rem;margin-bottom:0.5rem">Environment</h3>
208
+ <table>
209
+ <thead><tr><th>Variable</th><th>Value</th></tr></thead>
210
+ <tbody>${d.map(([p,u])=>`<tr><td class="text-mono text-sm" style="padding:4px 8px">${a(p)}</td><td class="text-sm" style="padding:4px 8px">${a(u)}</td></tr>`).join("")}</tbody>
211
+ </table>
212
+ `)}}window.__loadSystem=Oe;function It(e){e.innerHTML=`
213
+ <div class="dev-panel-header">
214
+ <h2>Code Metrics</h2>
215
+ </div>
216
+ <div id="metrics-quick" class="metric-grid"></div>
217
+ <div id="metrics-scan-info" class="text-sm text-muted" style="margin:0.5rem 0"></div>
218
+ <div id="metrics-chart" style="display:none;margin:1rem 0"></div>
219
+ <div id="metrics-detail" style="margin-top:1rem"></div>
220
+ <div id="metrics-complex" style="margin-top:1rem"></div>
221
+ `,Mt()}async function Mt(){var s;const e=document.getElementById("metrics-chart"),t=document.getElementById("metrics-complex"),n=document.getElementById("metrics-scan-info");e&&(e.style.display="block",e.innerHTML='<p class="text-muted">Analyzing...</p>');const i=await z("/metrics/full");if(i.error||!i.file_metrics){e&&(e.innerHTML=`<p style="color:var(--danger)">${a(i.error||"No data")}</p>`);return}if(n){const c=i.scan_mode==="framework"?'<span style="color:#cba6f7;font-weight:600">(Framework)</span> Add code to src/ to see your project':"";n.innerHTML=`${i.files_analyzed} files analyzed | ${i.total_functions} functions ${c}`}const o=document.getElementById("metrics-quick");o&&(o.innerHTML=[N("Files Analyzed",i.files_analyzed),N("Total Functions",i.total_functions),N("Avg Complexity",i.avg_complexity),N("Avg Maintainability",i.avg_maintainability)].join("")),e&&i.file_metrics.length>0?Lt(i.file_metrics,e,i.dependency_graph||{},i.scan_mode||"project"):e&&(e.innerHTML='<p class="text-muted">No files to visualize</p>'),t&&((s=i.most_complex_functions)!=null&&s.length)&&(t.innerHTML=`
222
+ <h3 style="font-size:0.85rem;margin-bottom:0.5rem">Most Complex Functions</h3>
223
+ <table>
224
+ <thead><tr><th>Function</th><th>File</th><th>Line</th><th>CC</th><th>LOC</th></tr></thead>
225
+ <tbody>${i.most_complex_functions.slice(0,15).map(c=>`
226
+ <tr>
227
+ <td class="text-mono">${a(c.name)}</td>
228
+ <td class="text-sm text-muted" style="cursor:pointer;text-decoration:underline dotted" onclick="window.__drillDown('${a(c.file)}')">${a(c.file)}</td>
229
+ <td>${c.line}</td>
230
+ <td><span class="${c.complexity>10?"badge badge-danger":c.complexity>5?"badge badge-warn":"badge badge-success"}">${c.complexity}</span></td>
231
+ <td>${c.loc}</td>
232
+ </tr>`).join("")}
233
+ </tbody>
234
+ </table>
235
+ `)}function Lt(e,t,n,i){var ot,it,st;const o=t.offsetWidth||900,s=Math.max(450,Math.min(650,o*.45)),c=Math.max(...e.map(f=>f.loc))||1,d=Math.max(...e.map(f=>f.dep_count||0))||1,p=14,u=Math.min(70,o/10);function _(f){const g=Math.min((f.avg_complexity||0)/10,1),y=f.has_tests?0:1,w=Math.min((f.dep_count||0)/5,1),m=g*.4+y*.4+w*.2,r=Math.max(0,Math.min(1,m)),b=Math.round(120*(1-r)),x=Math.round(70+r*30),$=Math.round(42+18*(1-r));return`hsl(${b},${x}%,${$}%)`}function k(f){return f.loc/c*.4+(f.avg_complexity||0)/10*.4+(f.dep_count||0)/d*.2}const h=[...e].sort((f,g)=>k(f)-k(g)),E=o/2,T=s/2,v=[];let j=0,ee=0;for(const f of h){const g=p+Math.sqrt(k(f))*(u-p),y=_(f);let w=!1;for(let m=0;m<800;m++){const r=E+ee*Math.cos(j),b=T+ee*Math.sin(j);let x=!1;for(const $ of v){const L=r-$.x,B=b-$.y;if(Math.sqrt(L*L+B*B)<g+$.r+2){x=!0;break}}if(!x&&r>g+2&&r<o-g-2&&b>g+25&&b<s-g-2){v.push({x:r,y:b,vx:0,vy:0,r:g,color:y,f}),w=!0;break}j+=.2,ee+=.04}w||v.push({x:E+(Math.random()-.5)*o*.3,y:T+(Math.random()-.5)*s*.3,vx:0,vy:0,r:g,color:y,f})}const Be=[];function Ye(f){const g=f.split("/").pop()||"",y=g.lastIndexOf(".");return(y>0?g.substring(0,y):g).toLowerCase()}const ze={};v.forEach((f,g)=>{ze[Ye(f.f.path)]=g});for(const[f,g]of Object.entries(n)){let y=null;if(v.forEach((w,m)=>{w.f.path===f&&(y=m)}),y!==null)for(const w of g){const m=w.replace(/^\.\//,"").replace(/^\.\.\//,"").split(/[./]/);let r;for(let b=m.length-1;b>=0;b--){const x=m[b].toLowerCase();if(x&&x!=="js"&&x!=="py"&&x!=="rb"&&x!=="ts"&&x!=="index"&&(r=ze[x],r!==void 0))break}r===void 0&&(r=ze[Ye(w)]),r!==void 0&&y!==r&&Be.push([y,r])}}const S=document.createElement("canvas");S.width=o,S.height=s,S.style.cssText="display:block;border:1px solid var(--border);border-radius:8px;cursor:pointer;background:#0f172a";const en=i==="framework"?'<span style="color:#cba6f7;font-weight:600">(Framework)</span> Add code to src/ to see your project':"";t.innerHTML=`<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:0.5rem"><h3 style="margin:0;font-size:0.85rem">Code Landscape ${en}</h3><span style="font-size:0.65rem;color:var(--muted)">Drag bubbles | Dbl-click to drill down</span></div><div style="position:relative" id="metrics-canvas-wrap"></div>`,document.getElementById("metrics-canvas-wrap").appendChild(S);const Ae=document.createElement("div");Ae.style.cssText="position:absolute;top:8px;left:8px;z-index:2;display:flex;gap:4px;flex-direction:column",Ae.innerHTML=`
236
+ <button class="btn btn-sm" id="metrics-zoom-in" style="width:28px;height:28px;padding:0;font-size:14px;font-weight:700;line-height:1">+</button>
237
+ <button class="btn btn-sm" id="metrics-zoom-out" style="width:28px;height:28px;padding:0;font-size:14px;font-weight:700;line-height:1">&minus;</button>
238
+ <button class="btn btn-sm" id="metrics-zoom-fit" style="width:28px;height:28px;padding:0;font-size:10px;font-weight:700;line-height:1">Fit</button>
239
+ `,document.getElementById("metrics-canvas-wrap").appendChild(Ae),(ot=document.getElementById("metrics-zoom-in"))==null||ot.addEventListener("click",()=>{M=Math.min(5,M*1.3)}),(it=document.getElementById("metrics-zoom-out"))==null||it.addEventListener("click",()=>{M=Math.max(.3,M*.7)}),(st=document.getElementById("metrics-zoom-fit"))==null||st.addEventListener("click",()=>{M=1,V=0,J=0});const l=S.getContext("2d");let F=-1,I=-1,Ke=0,Xe=0,V=0,J=0,M=1,te=!1,Qe=0,Ze=0,et=0,tt=0;function tn(){for(let m=0;m<v.length;m++){if(m===I)continue;const r=v[m],b=E-r.x,x=T-r.y,$=.3+r.r/u*.7,L=.008*$*$;r.vx+=b*L,r.vy+=x*L}for(const[m,r]of Be){const b=v[m],x=v[r],$=x.x-b.x,L=x.y-b.y,B=Math.sqrt($*$+L*L)||1,O=b.r+x.r+20,R=(B-O)*.002,ne=$/B*R,oe=L/B*R;m!==I&&(b.vx+=ne,b.vy+=oe),r!==I&&(x.vx-=ne,x.vy-=oe)}for(let m=0;m<v.length;m++)for(let r=m+1;r<v.length;r++){const b=v[m],x=v[r],$=x.x-b.x,L=x.y-b.y,B=Math.sqrt($*$+L*L)||1,O=b.r+x.r+20;if(B<O){const R=40*(O-B)/O,ne=$/B*R,oe=L/B*R;m!==I&&(b.vx-=ne,b.vy-=oe),r!==I&&(x.vx+=ne,x.vy+=oe)}}for(let m=0;m<v.length;m++){if(m===I)continue;const r=v[m];r.vx*=.65,r.vy*=.65;const b=2;r.vx=Math.max(-b,Math.min(b,r.vx)),r.vy=Math.max(-b,Math.min(b,r.vy)),r.x+=r.vx,r.y+=r.vy,r.x=Math.max(r.r+2,Math.min(o-r.r-2,r.x)),r.y=Math.max(r.r+25,Math.min(s-r.r-2,r.y))}}function nt(){var f;tn(),l.clearRect(0,0,o,s),l.save(),l.translate(V,J),l.scale(M,M),l.strokeStyle="rgba(255,255,255,0.03)",l.lineWidth=1/M;for(let g=0;g<o/M;g+=50)l.beginPath(),l.moveTo(g,0),l.lineTo(g,s/M),l.stroke();for(let g=0;g<s/M;g+=50)l.beginPath(),l.moveTo(0,g),l.lineTo(o/M,g),l.stroke();for(const[g,y]of Be){const w=v[g],m=v[y],r=m.x-w.x,b=m.y-w.y,x=Math.sqrt(r*r+b*b)||1,$=F===g||F===y;l.beginPath(),l.moveTo(w.x+r/x*w.r,w.y+b/x*w.r);const L=m.x-r/x*m.r,B=m.y-b/x*m.r;l.lineTo(L,B),l.strokeStyle=$?"rgba(139,180,250,0.9)":"rgba(255,255,255,0.15)",l.lineWidth=$?3:1,l.stroke();const O=$?12:6,R=Math.atan2(b,r);l.beginPath(),l.moveTo(L,B),l.lineTo(L-O*Math.cos(R-.4),B-O*Math.sin(R-.4)),l.lineTo(L-O*Math.cos(R+.4),B-O*Math.sin(R+.4)),l.closePath(),l.fillStyle=l.strokeStyle,l.fill()}for(let g=0;g<v.length;g++){const y=v[g],w=g===F,m=w?y.r+4:y.r;w&&(l.beginPath(),l.arc(y.x,y.y,m+8,0,Math.PI*2),l.fillStyle="rgba(255,255,255,0.08)",l.fill()),l.beginPath(),l.arc(y.x,y.y,m,0,Math.PI*2),l.fillStyle=y.color,l.globalAlpha=w?1:.85,l.fill(),l.globalAlpha=1,l.strokeStyle=w?"rgba(255,255,255,0.6)":"rgba(255,255,255,0.25)",l.lineWidth=w?2.5:1.5,l.stroke();const r=((f=y.f.path.split("/").pop())==null?void 0:f.replace(/\.\w+$/,""))||"?";if(m>16){const $=Math.max(8,Math.min(13,m*.38));l.fillStyle="#fff",l.font=`600 ${$}px monospace`,l.textAlign="center",l.fillText(r,y.x,y.y-2),l.fillStyle="rgba(255,255,255,0.65)",l.font=`${$-1}px monospace`,l.fillText(`${y.f.loc} LOC`,y.x,y.y+$)}const b=Math.max(9,m*.3),x=b*.7;if(m>14&&y.f.dep_count>0){const $=y.y-m+x+3;l.beginPath(),l.arc(y.x,$,x,0,Math.PI*2),l.fillStyle="#ea580c",l.fill(),l.fillStyle="#fff",l.font=`bold ${b}px sans-serif`,l.textAlign="center",l.fillText("D",y.x,$+b*.35)}if(m>14&&y.f.has_tests){const $=y.y+m-x-3;l.beginPath(),l.arc(y.x,$,x,0,Math.PI*2),l.fillStyle="#16a34a",l.fill(),l.fillStyle="#fff",l.font=`bold ${b}px sans-serif`,l.textAlign="center",l.fillText("T",y.x,$+b*.35)}}l.restore(),requestAnimationFrame(nt)}S.addEventListener("mousemove",f=>{const g=S.getBoundingClientRect(),y=(f.clientX-g.left-V)/M,w=(f.clientY-g.top-J)/M;if(te){V=et+(f.clientX-Qe),J=tt+(f.clientY-Ze);return}if(I>=0){pe=!0,v[I].x=y+Ke,v[I].y=w+Xe,v[I].vx=0,v[I].vy=0;return}F=-1;for(let m=v.length-1;m>=0;m--){const r=v[m],b=y-r.x,x=w-r.y;if(Math.sqrt(b*b+x*x)<r.r+4){F=m;break}}S.style.cursor=F>=0?"grab":"default"}),S.addEventListener("mousedown",f=>{const g=S.getBoundingClientRect(),y=(f.clientX-g.left-V)/M,w=(f.clientY-g.top-J)/M;if(f.button===2){te=!0,Qe=f.clientX,Ze=f.clientY,et=V,tt=J,S.style.cursor="move";return}F>=0&&(I=F,Ke=v[I].x-y,Xe=v[I].y-w,pe=!1,S.style.cursor="grabbing")});let pe=!1;S.addEventListener("mouseup",f=>{if(te){te=!1,S.style.cursor="default";return}if(I>=0){pe||fe(v[I].f.path),S.style.cursor="grab",I=-1,pe=!1;return}}),S.addEventListener("mouseleave",()=>{F=-1,I=-1,te=!1}),S.addEventListener("dblclick",f=>{const g=S.getBoundingClientRect(),y=(f.clientX-g.left-V)/M,w=(f.clientY-g.top-J)/M;for(let m=v.length-1;m>=0;m--){const r=v[m],b=y-r.x,x=w-r.y;if(Math.sqrt(b*b+x*x)<r.r+4){fe(r.f.path);break}}}),S.addEventListener("contextmenu",f=>f.preventDefault()),requestAnimationFrame(nt)}async function fe(e){const t=document.getElementById("metrics-detail");if(!t)return;t.innerHTML='<p class="text-muted">Loading file analysis...</p>';const n=await z("/metrics/file?path="+encodeURIComponent(e));if(n.error){t.innerHTML=`<p style="color:var(--danger)">${a(n.error)}</p>`;return}const i=n.functions||[],o=Math.max(1,...i.map(s=>s.complexity));t.innerHTML=`
240
+ <div style="background:var(--surface);border:1px solid var(--border);border-radius:0.5rem;padding:1rem">
241
+ <div class="flex items-center" style="justify-content:space-between;margin-bottom:0.75rem">
242
+ <h3 style="font-size:0.9rem">${a(n.path)}</h3>
243
+ <button class="btn btn-sm" onclick="document.getElementById('metrics-detail').innerHTML=''">Close</button>
244
+ </div>
245
+ <div class="metric-grid" style="margin-bottom:0.75rem">
246
+ ${N("LOC",n.loc)}
247
+ ${N("Total Lines",n.total_lines)}
248
+ ${N("Classes",n.classes)}
249
+ ${N("Functions",i.length)}
250
+ ${N("Imports",n.imports?n.imports.length:0)}
251
+ </div>
252
+ ${i.length?`
253
+ <h4 style="font-size:0.8rem;color:var(--info);margin-bottom:0.5rem">Cyclomatic Complexity by Function</h4>
254
+ ${i.sort((s,c)=>c.complexity-s.complexity).map(s=>{const c=s.complexity/o*100,d=s.complexity>10?"#ef4444":s.complexity>5?"#f59e0b":"#22c55e";return`<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:3px;font-size:0.75rem">
255
+ <div style="width:200px;flex-shrink:0;text-align:right;font-family:monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${a(s.name)}">${a(s.name)}</div>
256
+ <div style="flex:1;height:14px;background:var(--bg);border-radius:2px;overflow:hidden"><div style="width:${c}%;height:100%;background:${d}"></div></div>
257
+ <div style="width:180px;flex-shrink:0;font-family:monospace;text-align:right"><span style="color:${d}">CC:${s.complexity}</span> <span style="color:var(--muted)">${s.loc} LOC L${s.line}</span></div>
258
+ </div>`}).join("")}
259
+ `:'<p class="text-muted">No functions</p>'}
260
+ </div>
261
+ `}function N(e,t){return`<div class="metric-card"><div class="label">${a(e)}</div><div class="value">${a(String(t??0))}</div></div>`}window.__drillDown=fe;const se={tina4:{model:"tina4-v1",url:"https://api.tina4.com/v1/chat/completions"},custom:{model:"",url:"http://localhost:11434"},anthropic:{model:"claude-sonnet-4-20250514",url:"https://api.anthropic.com"},openai:{model:"gpt-4o",url:"https://api.openai.com"}};function ae(e="tina4"){const t=se[e]||se.tina4;return{provider:e,model:t.model,url:t.url,apiKey:""}}function ye(e){const t={...ae(),...e||{}};return t.provider==="ollama"&&(t.provider="custom"),t}function Ct(){try{const e=JSON.parse(localStorage.getItem("tina4_chat_settings")||"{}");return{thinking:ye(e.thinking),vision:ye(e.vision),imageGen:ye(e.imageGen)}}catch{return{thinking:ae(),vision:ae(),imageGen:ae()}}}function Bt(e){localStorage.setItem("tina4_chat_settings",JSON.stringify(e)),C=e,U()}let C=Ct(),P="Idle";const re=[];function zt(e){var n,i,o,s,c,d,p,u,_,k;e.innerHTML=`
262
+ <div class="dev-panel-header">
263
+ <h2>Code With Me</h2>
264
+ <div class="flex gap-sm">
265
+ <button class="btn btn-sm" id="chat-thoughts-btn" title="Supervisor thoughts">Thoughts <span id="thoughts-dot" style="display:none;color:var(--info)">&#9679;</span></button>
266
+ <button class="btn btn-sm" id="chat-settings-btn" title="Settings">&#9881; Settings</button>
267
+ </div>
268
+ </div>
269
+ <div style="display:flex;gap:0.5rem;flex:1;min-height:0;overflow:hidden">
270
+ <div style="flex:1;display:flex;flex-direction:column;min-height:0">
271
+ <div style="display:flex;gap:0.5rem;align-items:flex-start;padding:0.5rem 0;flex-shrink:0">
272
+ <textarea id="chat-input" class="input" placeholder="Ask Tina4 to build something..." rows="2" style="flex:1;resize:vertical;min-height:36px;max-height:200px;font-family:inherit;font-size:inherit"></textarea>
273
+ <div style="display:flex;flex-direction:column;gap:4px">
274
+ <button class="btn btn-primary" id="chat-send-btn" style="white-space:nowrap">Send</button>
275
+ <div style="display:flex;gap:4px">
276
+ <input type="file" id="chat-file-input" multiple style="display:none" />
277
+ <button class="btn btn-sm" id="chat-file-btn" style="font-size:0.65rem;padding:2px 6px">File</button>
278
+ <button class="btn btn-sm" id="chat-mic-btn" style="font-size:0.65rem;padding:2px 6px">Mic</button>
279
+ </div>
280
+ </div>
281
+ </div>
282
+ <div id="chat-attachments" style="display:none;margin-bottom:0.375rem;font-size:0.75rem"></div>
283
+ <div id="chat-status-bar" style="display:none;padding:6px 12px;background:var(--surface);border:1px solid var(--info);border-radius:0.375rem;margin-bottom:0.5rem;font-size:0.75rem;color:var(--info);align-items:center;gap:8px;flex-shrink:0">
284
+ <span style="display:inline-block;width:12px;height:12px;border:2px solid var(--info);border-top-color:transparent;border-radius:50%;animation:t4spin 0.8s linear infinite"></span>
285
+ <span id="chat-status-text">Thinking...</span>
286
+ </div>
287
+ <style>@keyframes t4spin{to{transform:rotate(360deg)}}</style>
288
+ <div id="chat-messages" style="flex:1;overflow-y:auto;display:flex;flex-direction:column;gap:0.5rem;padding:0.25rem 0">
289
+ <div class="chat-msg chat-bot">Hi! I'm Tina4. Ask me to build routes, templates, models — or ask questions about your project.</div>
290
+ </div>
291
+ </div>
292
+ <div id="chat-summary" style="width:200px;flex-shrink:0;background:var(--surface);border:1px solid var(--border);border-radius:0.5rem;padding:0.75rem;font-size:0.75rem;overflow-y:auto"></div>
293
+ </div>
294
+
295
+ <!-- Thoughts Panel (slides in from right) -->
296
+ <div id="chat-thoughts-panel" style="display:none;position:absolute;top:0;right:0;bottom:0;width:300px;background:var(--surface);border-left:1px solid var(--border);z-index:50;overflow-y:auto;padding:0.75rem">
297
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.75rem">
298
+ <h3 style="font-size:0.85rem;margin:0">Thoughts</h3>
299
+ <button class="btn btn-sm" id="chat-thoughts-close" style="width:24px;height:24px;padding:0;font-size:14px;line-height:1">&times;</button>
300
+ </div>
301
+ <div id="thoughts-list"></div>
302
+ </div>
303
+
304
+ <!-- Settings Modal -->
305
+ <div id="chat-modal-overlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:100;align-items:center;justify-content:center">
306
+ <div style="background:var(--surface);border:1px solid var(--border);border-radius:0.75rem;padding:1.25rem;width:750px;max-width:90vw;box-shadow:0 8px 32px rgba(0,0,0,0.3)">
307
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem">
308
+ <h3 style="font-size:0.95rem;margin:0">AI Settings</h3>
309
+ <button class="btn btn-sm" id="chat-modal-close" style="width:28px;height:28px;padding:0;font-size:16px;line-height:1">&times;</button>
310
+ </div>
311
+ <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:0.75rem;margin-bottom:0.75rem">
312
+ ${["thinking","vision","imageGen"].map(h=>`
313
+ <fieldset style="border:1px solid var(--border);border-radius:0.375rem;padding:0.5rem 0.75rem;margin:0">
314
+ <legend class="text-sm" style="font-weight:600;padding:0 4px">${h==="imageGen"?"Image Generation":h.charAt(0).toUpperCase()+h.slice(1)}</legend>
315
+ <div style="margin-bottom:0.375rem"><label class="text-sm text-muted" style="display:block;margin-bottom:2px">Provider</label><select id="set-${h}-provider" class="input" style="width:100%"><option value="tina4">Tina4 Cloud</option><option value="custom">Custom / Local</option><option value="anthropic">Anthropic (Claude)</option><option value="openai">OpenAI</option></select></div>
316
+ <div style="margin-bottom:0.375rem"><label class="text-sm text-muted" style="display:block;margin-bottom:2px">URL</label><input type="text" id="set-${h}-url" class="input" style="width:100%" /></div>
317
+ <div id="set-${h}-key-row" style="margin-bottom:0.375rem"><label class="text-sm text-muted" style="display:block;margin-bottom:2px">API Key</label><input type="password" id="set-${h}-key" class="input" placeholder="sk-..." style="width:100%" /></div>
318
+ <button class="btn btn-sm btn-primary" id="set-${h}-connect" style="width:100%;margin-bottom:0.375rem">Connect</button>
319
+ <div id="set-${h}-result" class="text-sm" style="min-height:1.2em;margin-bottom:0.375rem"></div>
320
+ <div style="margin-bottom:0.375rem"><label class="text-sm text-muted" style="display:block;margin-bottom:2px">Model</label><select id="set-${h}-model" class="input" style="width:100%" disabled><option value="">-- connect first --</option></select></div>
321
+ <div id="set-${h}-result" class="text-sm" style="margin-top:4px;min-height:1.2em"></div>
322
+ </fieldset>`).join("")}
323
+ </div>
324
+ <button class="btn btn-primary" id="chat-modal-save" style="width:100%">Save Settings</button>
325
+ </div>
326
+ </div>
327
+ `,(n=document.getElementById("chat-send-btn"))==null||n.addEventListener("click",K),(i=document.getElementById("chat-thoughts-btn"))==null||i.addEventListener("click",Te),(o=document.getElementById("chat-thoughts-close"))==null||o.addEventListener("click",Te),(s=document.getElementById("chat-settings-btn"))==null||s.addEventListener("click",At),(c=document.getElementById("chat-modal-close"))==null||c.addEventListener("click",ke),(d=document.getElementById("chat-modal-save"))==null||d.addEventListener("click",jt),(p=document.getElementById("chat-modal-overlay"))==null||p.addEventListener("click",h=>{h.target===h.currentTarget&&ke()}),(u=document.getElementById("chat-file-btn"))==null||u.addEventListener("click",()=>{var h;(h=document.getElementById("chat-file-input"))==null||h.click()}),(_=document.getElementById("chat-file-input"))==null||_.addEventListener("change",Vt),(k=document.getElementById("chat-mic-btn"))==null||k.addEventListener("click",Yt);const t=document.getElementById("chat-input");t==null||t.addEventListener("keydown",h=>{h.key==="Enter"&&!h.shiftKey&&(h.preventDefault(),K())}),U()}function ve(e,t){document.getElementById(`set-${e}-provider`).value=t.provider;const n=document.getElementById(`set-${e}-model`);t.model&&(n.innerHTML=`<option value="${t.model}">${t.model}</option>`,n.value=t.model,n.disabled=!1),document.getElementById(`set-${e}-url`).value=t.url,document.getElementById(`set-${e}-key`).value=t.apiKey,we(e,t.provider)}function xe(e){var t,n,i,o;return{provider:((t=document.getElementById(`set-${e}-provider`))==null?void 0:t.value)||"custom",model:((n=document.getElementById(`set-${e}-model`))==null?void 0:n.value)||"",url:((i=document.getElementById(`set-${e}-url`))==null?void 0:i.value)||"",apiKey:((o=document.getElementById(`set-${e}-key`))==null?void 0:o.value)||""}}function we(e,t){const n=document.getElementById(`set-${e}-key-row`);n&&(n.style.display="block")}function $e(e){const t=document.getElementById(`set-${e}-provider`);t==null||t.addEventListener("change",()=>{const n=se[t.value]||se.tina4,i=document.getElementById(`set-${e}-model`);i.innerHTML=`<option value="${n.model}">${n.model}</option>`,i.value=n.model,document.getElementById(`set-${e}-url`).value=n.url,we(e,t.value)}),we(e,(t==null?void 0:t.value)||"custom")}async function _e(e){var c,d,p;const t=((c=document.getElementById(`set-${e}-provider`))==null?void 0:c.value)||"custom",n=((d=document.getElementById(`set-${e}-url`))==null?void 0:d.value)||"",i=((p=document.getElementById(`set-${e}-key`))==null?void 0:p.value)||"",o=document.getElementById(`set-${e}-model`),s=document.getElementById(`set-${e}-result`);s&&(s.textContent="Connecting...",s.style.color="var(--muted)");try{let u=[];const _=n.replace(/\/(v1|api)\/.*$/,"").replace(/\/+$/,"");if(t==="tina4"){const h={"Content-Type":"application/json"};i&&(h.Authorization=`Bearer ${i}`);try{u=((await(await fetch(`${_}/v1/models`,{headers:h})).json()).data||[]).map(v=>v.id)}catch{}u.length||(u=["tina4-v1"])}else if(t==="custom"){try{u=((await(await fetch(`${_}/api/tags`)).json()).models||[]).map(T=>T.name||T.model)}catch{}if(!u.length)try{u=((await(await fetch(`${_}/v1/models`)).json()).data||[]).map(T=>T.id)}catch{}}else if(t==="anthropic")u=["claude-sonnet-4-20250514","claude-opus-4-20250514","claude-haiku-4-20250514","claude-3-5-sonnet-20241022"];else if(t==="openai"){const h=n.replace(/\/v1\/.*$/,"");u=((await(await fetch(`${h}/v1/models`,{headers:i?{Authorization:`Bearer ${i}`}:{}})).json()).data||[]).map(v=>v.id).filter(v=>v.startsWith("gpt"))}if(u.length===0){s&&(s.innerHTML='<span style="color:var(--warn)">No models found</span>');return}const k=o.value;o.innerHTML=u.map(h=>`<option value="${h}">${h}</option>`).join(""),u.includes(k)&&(o.value=k),o.disabled=!1,s&&(s.innerHTML=`<span style="color:var(--success)">&#10003; ${u.length} models available</span>`)}catch{s&&(s.innerHTML='<span style="color:var(--danger)">&#10007; Connection failed</span>')}}function At(){var t,n,i;const e=document.getElementById("chat-modal-overlay");e&&(e.style.display="flex",ve("thinking",C.thinking),ve("vision",C.vision),ve("imageGen",C.imageGen),$e("thinking"),$e("vision"),$e("imageGen"),(t=document.getElementById("set-thinking-connect"))==null||t.addEventListener("click",()=>_e("thinking")),(n=document.getElementById("set-vision-connect"))==null||n.addEventListener("click",()=>_e("vision")),(i=document.getElementById("set-imageGen-connect"))==null||i.addEventListener("click",()=>_e("imageGen")))}function ke(){const e=document.getElementById("chat-modal-overlay");e&&(e.style.display="none")}function jt(){Bt({thinking:xe("thinking"),vision:xe("vision"),imageGen:xe("imageGen")}),ke()}function U(){const e=document.getElementById("chat-summary");if(!e)return;const t=Y.length?Y.map(o=>`<div style="margin-bottom:4px;font-size:0.65rem;line-height:1.3">
328
+ <span style="color:var(--muted)">${a(o.time)}</span>
329
+ <span style="color:var(--info);font-size:0.6rem">${a(o.agent)}</span>
330
+ <div>${a(o.text)}</div>
331
+ </div>`).join(""):'<div class="text-muted" style="font-size:0.65rem">No activity yet</div>',n=P==="Idle"?"var(--muted)":P==="Thinking..."?"var(--info)":"var(--success)",i=o=>o.model?'<span style="color:var(--success)">&#9679;</span>':'<span style="color:var(--muted)">&#9675;</span>';e.innerHTML=`
332
+ <div style="margin-bottom:0.5rem;font-size:0.7rem">
333
+ <span style="color:${n}">&#9679;</span> ${a(P)}
334
+ </div>
335
+ <div style="font-size:0.65rem;line-height:1.8">
336
+ ${i(C.thinking)} T: ${a(C.thinking.model||"—")}<br>
337
+ ${i(C.vision)} V: ${a(C.vision.model||"—")}<br>
338
+ ${i(C.imageGen)} I: ${a(C.imageGen.model||"—")}
339
+ </div>
340
+ ${re.length?`
341
+ <div style="margin-bottom:0.75rem">
342
+ <div class="text-muted" style="font-size:0.65rem;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:4px">Files Changed</div>
343
+ ${re.map(o=>`<div class="text-mono" style="font-size:0.65rem;color:var(--success);margin-bottom:2px">${a(o)}</div>`).join("")}
344
+ </div>
345
+ `:""}
346
+ <div>
347
+ <div class="text-muted" style="font-size:0.65rem;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:4px">Activity</div>
348
+ ${t}
349
+ </div>
350
+ `}let Ee=0;function A(e,t){const n=document.getElementById("chat-messages");if(!n)return;const i=`msg-${++Ee}`,o=document.createElement("div");if(o.className=`chat-msg chat-${t}`,o.id=i,o.innerHTML=`
351
+ <div class="chat-msg-content">${e}</div>
352
+ <div class="chat-msg-actions" style="display:flex;gap:4px;margin-top:4px;opacity:0.4">
353
+ <button class="btn btn-sm" style="font-size:0.6rem;padding:1px 6px" onclick="window.__copyMsg('${i}')" title="Copy">Copy</button>
354
+ <button class="btn btn-sm" style="font-size:0.6rem;padding:1px 6px" onclick="window.__replyMsg('${i}')" title="Reply">Reply</button>
355
+ <button class="btn btn-sm btn-primary" style="font-size:0.6rem;padding:1px 6px;display:none" onclick="window.__submitAnswers('${i}')" title="Submit answers" data-submit-btn>Submit Answers</button>
356
+ </div>
357
+ `,o.addEventListener("mouseenter",()=>{const s=o.querySelector(".chat-msg-actions");s&&(s.style.opacity="1")}),o.addEventListener("mouseleave",()=>{const s=o.querySelector(".chat-msg-actions");s&&(s.style.opacity="0.4")}),o.querySelector(".chat-answer-input")){const s=o.querySelector("[data-submit-btn]");s&&(s.style.display="inline-block")}n.prepend(o)}function Ht(e){const t=document.getElementById(e);if(!t)return;const n=t.querySelectorAll(".chat-answer-input"),i=[];if(n.forEach(c=>{const d=c.dataset.q||"?",p=c.value.trim();p&&(i.push(`${d}. ${p}`),c.disabled=!0,c.style.opacity="0.6")}),!i.length)return;const o=document.getElementById("chat-input");o&&(o.value=i.join(`
358
+ `),K());const s=t.querySelector("[data-submit-btn]");s&&(s.style.display="none")}function Pt(e,t){const n=e.parentElement;if(!n)return;const i=n.querySelector(".chat-answer-input");i&&(i.value=t,i.disabled=!0,i.style.opacity="0.5"),n.querySelectorAll("button").forEach(s=>s.remove());const o=document.createElement("span");o.style.cssText="font-size:0.65rem;padding:2px 8px;border-radius:3px;background:var(--info);color:white",o.textContent=t,n.appendChild(o)}window.__quickAnswer=Pt,window.__submitAnswers=Ht;function qt(e){const t=document.querySelector(`#${e} .chat-msg-content`);t&&navigator.clipboard.writeText(t.textContent||"").then(()=>{const n=document.querySelector(`#${e} .chat-msg-actions button`);if(n){const i=n.textContent;n.textContent="Copied!",setTimeout(()=>{n.textContent=i},1e3)}})}function Ot(e){const t=document.querySelector(`#${e} .chat-msg-content`);if(!t)return;const n=(t.textContent||"").substring(0,100),i=document.getElementById("chat-input");i&&(i.value=`> ${n}${n.length>=100?"...":""}
359
+
360
+ `,i.focus(),i.setSelectionRange(i.value.length,i.value.length))}function Rt(e){var i,o;const t=e.closest(".chat-checklist-item");if(!t||(i=t.nextElementSibling)!=null&&i.classList.contains("chat-comment-box"))return;const n=document.createElement("div");n.className="chat-comment-box",n.style.cssText="padding-left:1.8rem;margin:0.15rem 0;display:flex;gap:4px",n.innerHTML=`
361
+ <input type="text" class="input" placeholder="Your comment..." style="flex:1;font-size:0.7rem;padding:2px 6px;height:24px">
362
+ <button class="btn btn-sm" style="font-size:0.6rem;padding:1px 6px;height:24px" onclick="window.__submitComment(this)">Add</button>
363
+ `,t.after(n),(o=n.querySelector("input"))==null||o.focus()}function Nt(e){var s;const t=e.closest(".chat-comment-box");if(!t)return;const n=t.querySelector("input"),i=(s=n==null?void 0:n.value)==null?void 0:s.trim();if(!i)return;const o=document.createElement("div");o.style.cssText="padding-left:1.8rem;margin:0.1rem 0;font-size:0.7rem;color:var(--info);font-style:italic",o.textContent=`↳ ${i}`,t.replaceWith(o)}function Re(){const e=[],t=[],n=[];return document.querySelectorAll(".chat-checklist-item").forEach(i=>{var d,p;const o=i.querySelector("input[type=checkbox]"),s=((d=i.querySelector("label"))==null?void 0:d.textContent)||"";o!=null&&o.checked?e.push(s):t.push(s);const c=i.nextElementSibling;if(c&&!c.classList.contains("chat-checklist-item")&&!c.classList.contains("chat-comment-box")){const u=((p=c.textContent)==null?void 0:p.replace("↳ ",""))||"";u&&n.push(`${s}: ${u}`)}}),{accepted:e,rejected:t,comments:n}}let le=!1;function Te(){const e=document.getElementById("chat-thoughts-panel");e&&(le=!le,e.style.display=le?"block":"none",le&&Ne())}async function Ne(){const e=document.getElementById("thoughts-list");if(e)try{const i=(await(await fetch("/__dev/api/thoughts")).json()||[]).filter(s=>!s.dismissed),o=document.getElementById("thoughts-dot");if(o&&(o.style.display=i.length?"inline":"none"),!i.length){e.innerHTML='<div class="text-muted text-sm" style="text-align:center;padding:2rem 0">All clear. No observations.</div>';return}e.innerHTML=i.map(s=>`
364
+ <div style="background:var(--bg);border:1px solid var(--border);border-radius:0.375rem;padding:0.5rem;margin-bottom:0.5rem;font-size:0.75rem">
365
+ <div style="line-height:1.4">${a(s.message)}</div>
366
+ <div style="display:flex;gap:4px;margin-top:0.375rem">
367
+ ${(s.actions||[]).map(c=>c.action==="dismiss"?`<button class="btn btn-sm" style="font-size:0.6rem" onclick="window.__dismissThought('${a(s.id)}')">Dismiss</button>`:`<button class="btn btn-sm btn-primary" style="font-size:0.6rem" onclick="window.__actOnThought('${a(s.id)}','${a(c.action)}')">${a(c.label)}</button>`).join("")}
368
+ </div>
369
+ </div>
370
+ `).join("")}catch{e.innerHTML='<div class="text-muted text-sm" style="text-align:center;padding:1rem">Agent not connected</div>'}}async function De(e){await fetch("/__dev/api/thoughts/dismiss",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:e})}).catch(()=>{}),Ne()}function Dt(e,t){De(e),Te()}setInterval(async()=>{try{const n=(await(await fetch("/__dev/api/thoughts")).json()||[]).filter(o=>!o.dismissed),i=document.getElementById("thoughts-dot");i&&(i.style.display=n.length?"inline":"none")}catch{}},6e4),window.__dismissThought=De,window.__actOnThought=Dt,window.__commentOnItem=Rt,window.__submitComment=Nt,window.__getChecklist=Re,window.__copyMsg=qt,window.__replyMsg=Ot;const Y=[];function Fe(e){const t=document.getElementById("chat-status-bar"),n=document.getElementById("chat-status-text");t&&(t.style.display="flex"),n&&(n.textContent=e)}function We(){const e=document.getElementById("chat-status-bar");e&&(e.style.display="none")}function de(e,t){const n=new Date().toLocaleTimeString([],{hour:"2-digit",minute:"2-digit",second:"2-digit"});Y.unshift({time:n,text:e,agent:t}),Y.length>50&&(Y.length=50),U()}async function K(){var i;const e=document.getElementById("chat-input"),t=(i=e==null?void 0:e.value)==null?void 0:i.trim();if(!t)return;if(e.value="",A(a(t),"user"),D.length){const o=D.map(s=>s.name).join(", ");A(`<span class="text-sm text-muted">Attached: ${a(o)}</span>`,"user")}P="Thinking...",Fe("Analyzing request..."),de("Analyzing request...","supervisor");const n={message:t,settings:{thinking:C.thinking,vision:C.vision,imageGen:C.imageGen}};D.length&&(n.files=D.map(o=>({name:o.name,data:o.data})));try{const o=await fetch("/__dev/api/chat",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)});if(!o.ok||!o.body){A(`<span style="color:var(--danger)">Error: ${o.statusText}</span>`,"bot"),P="Error",U();return}const s=o.body.getReader(),c=new TextDecoder;let d="";for(;;){const{done:p,value:u}=await s.read();if(p)break;d+=c.decode(u,{stream:!0});const _=d.split(`
371
+ `);d=_.pop()||"";let k="";for(const h of _)if(h.startsWith("event: "))k=h.slice(7).trim();else if(h.startsWith("data: ")){const E=h.slice(6);try{const T=JSON.parse(E);Ge(k,T)}catch{}}}D.length=0,Se()}catch{A('<span style="color:var(--danger)">Connection failed</span>',"bot"),P="Error",U()}}function Ge(e,t){switch(e){case"status":P=t.text||"Working...",Fe(`${t.agent||"supervisor"}: ${t.text||"Working..."}`),de(t.text||"",t.agent||"supervisor");break;case"message":{const n=t.content||"",i=t.agent||"supervisor";let o=Kt(n);i!=="supervisor"&&(o=`<span class="badge" style="font-size:0.6rem;margin-right:4px">${a(i)}</span>`+o),t.files_changed&&t.files_changed.length>0&&(o+='<div style="margin-top:0.5rem;padding:0.5rem;background:var(--bg);border-radius:0.375rem;border:1px solid var(--border)">',o+='<div class="text-sm" style="color:var(--success);font-weight:600;margin-bottom:0.25rem">Files changed:</div>',t.files_changed.forEach(s=>{o+=`<div class="text-sm text-mono">${a(s)}</div>`,re.includes(s)||re.push(s)}),o+="</div>"),A(o,"bot");break}case"plan":if(t.approve){const n=`
372
+ <div style="padding:0.5rem;background:var(--surface);border:1px solid var(--info);border-radius:0.375rem;margin-top:0.25rem">
373
+ <div class="text-sm" style="color:var(--info);font-weight:600;margin-bottom:0.25rem">Plan ready: ${a(t.file||"")}</div>
374
+ <div class="text-sm text-muted" style="margin-bottom:0.5rem">Uncheck items you don't want. Click + to add comments. Then choose an action.</div>
375
+ <div class="flex gap-sm" style="flex-wrap:wrap">
376
+ <button class="btn btn-sm" onclick="window.__submitFeedback()">Submit Feedback</button>
377
+ <button class="btn btn-sm btn-primary" onclick="window.__approvePlan('${a(t.file||"")}')">Approve & Execute</button>
378
+ <button class="btn btn-sm" onclick="window.__keepPlan('${a(t.file||"")}');this.parentElement.parentElement.remove()">Keep for Later</button>
379
+ <button class="btn btn-sm" onclick="this.parentElement.parentElement.remove()">Dismiss</button>
380
+ </div>
381
+ </div>
382
+ `;A(n,"bot")}break;case"error":We(),A(`<span style="color:var(--danger)">${a(t.message||"Unknown error")}</span>`,"bot"),P="Error",U();break;case"done":P="Done",We(),de("Done","supervisor"),setTimeout(()=>{P="Idle",U()},3e3);break}}async function Ft(e){A(`<span style="color:var(--success)">Plan approved: ${a(e)}</span>`,"user"),P="Executing plan...",de("Plan approved — executing...","supervisor");const t={message:`Execute the plan in ${e}. Write all the files now.`,settings:{thinking:C.thinking,vision:C.vision,imageGen:C.imageGen}};try{const n=await fetch("/__dev/api/chat",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)});if(!n.ok||!n.body)return;const i=n.body.getReader(),o=new TextDecoder;let s="";for(;;){const{done:c,value:d}=await i.read();if(c)break;s+=o.decode(d,{stream:!0});const p=s.split(`
383
+ `);s=p.pop()||"";let u="";for(const _ of p)if(_.startsWith("event: "))u=_.slice(7).trim();else if(_.startsWith("data: "))try{Ge(u,JSON.parse(_.slice(6)))}catch{}}}catch{A('<span style="color:var(--danger)">Plan execution failed</span>',"bot")}}function Wt(e){A(`<span style="color:var(--muted)">Plan saved for later: ${a(e)}</span>`,"bot")}function Gt(){const{accepted:e,rejected:t,comments:n}=Re();let i=`Here's my feedback on the proposal:
384
+
385
+ `;e.length&&(i+=`**Keep these:**
386
+ `+e.map(s=>`- ${s}`).join(`
387
+ `)+`
388
+
389
+ `),t.length&&(i+=`**Remove these:**
390
+ `+t.map(s=>`- ${s}`).join(`
391
+ `)+`
392
+
393
+ `),n.length&&(i+=`**Comments:**
394
+ `+n.map(s=>`- ${s}`).join(`
395
+ `)+`
396
+
397
+ `),!t.length&&!n.length&&(i+="Everything looks good. "),i+="Please revise the plan based on this feedback.";const o=document.getElementById("chat-input");o&&(o.value=i,K())}window.__submitFeedback=Gt,window.__approvePlan=Ft,window.__keepPlan=Wt;async function Ut(){try{const e=await z("/chat/undo","POST");A(`<span style="color:var(--warn)">${a(e.message||"Undo complete")}</span>`,"bot")}catch{A('<span style="color:var(--warn)">Nothing to undo</span>',"bot")}}const D=[];function Vt(){const e=document.getElementById("chat-file-input");e!=null&&e.files&&(document.getElementById("chat-attachments"),Array.from(e.files).forEach(t=>{const n=new FileReader;n.onload=()=>{D.push({name:t.name,data:n.result}),Se()},n.readAsDataURL(t)}),e.value="")}function Se(){const e=document.getElementById("chat-attachments");if(e){if(!D.length){e.style.display="none";return}e.style.display="flex",e.style.cssText+="gap:0.375rem;flex-wrap:wrap;margin-bottom:0.375rem;font-size:0.75rem",e.innerHTML=D.map((t,n)=>`<span style="background:var(--surface);border:1px solid var(--border);border-radius:4px;padding:2px 8px;display:inline-flex;align-items:center;gap:4px">
398
+ ${a(t.name)} <span style="cursor:pointer;color:var(--danger)" onclick="window.__removeFile(${n})">&times;</span>
399
+ </span>`).join("")}}function Jt(e){D.splice(e,1),Se()}let X=!1,q=null;function Yt(){const e=document.getElementById("chat-mic-btn"),t=window.SpeechRecognition||window.webkitSpeechRecognition;if(!t){A('<span style="color:var(--warn)">Speech recognition not supported in this browser</span>',"bot");return}if(X&&q){q.stop(),X=!1,e&&(e.textContent="Mic",e.style.background="");return}q=new t,q.continuous=!1,q.interimResults=!1,q.lang="en-US",q.onresult=n=>{const i=n.results[0][0].transcript,o=document.getElementById("chat-input");o&&(o.value=(o.value?o.value+" ":"")+i)},q.onend=()=>{X=!1,e&&(e.textContent="Mic",e.style.background="")},q.onerror=()=>{X=!1,e&&(e.textContent="Mic",e.style.background="")},q.start(),X=!0,e&&(e.textContent="Stop",e.style.background="var(--danger)")}window.__removeFile=Jt;function Kt(e){let t=e.replace(/\\n/g,`
400
+ `);const n=[];t=t.replace(/```(\w*)\n([\s\S]*?)```/g,(c,d,p)=>{const u=n.length;return n.push(`<pre style="background:var(--bg);padding:0.75rem;border-radius:0.375rem;overflow-x:auto;margin:0.5rem 0;font-size:0.75rem;border:1px solid var(--border)"><code>${a(p)}</code></pre>`),`\0CODE${u}\0`});const i=t.split(`
401
+ `),o=[];for(const c of i){const d=c.trim();if(d.startsWith("\0CODE")){o.push(d);continue}if(d.startsWith("### ")){o.push(`<div style="font-weight:700;font-size:0.8rem;margin:0.75rem 0 0.25rem;color:var(--info)">${a(d.slice(4))}</div>`);continue}if(d.startsWith("## ")){o.push(`<div style="font-weight:700;font-size:0.9rem;margin:0.75rem 0 0.25rem">${a(d.slice(3))}</div>`);continue}if(d.startsWith("# ")){o.push(`<div style="font-weight:700;font-size:1rem;margin:0.75rem 0 0.25rem">${a(d.slice(2))}</div>`);continue}if(d==="---"||d==="***"){o.push('<hr style="border:none;border-top:1px solid var(--border);margin:0.5rem 0">');continue}const p=d.match(/^(\d+)[.)]\s+(.+)/);if(p){if(p[2].trim().endsWith("?")){const _=`q-${Ee}-${p[1]}`;o.push(`<div style="margin:0.3rem 0;padding-left:0.5rem">
402
+ <div style="margin-bottom:4px"><span style="color:var(--info);font-weight:600;margin-right:0.4rem">${p[1]}.</span>${Q(p[2])}</div>
403
+ <div style="display:flex;gap:4px;align-items:center;flex-wrap:wrap">
404
+ <input type="text" class="input chat-answer-input" id="${_}" data-q="${p[1]}" placeholder="Your answer..." style="font-size:0.75rem;padding:4px 8px;flex:1;max-width:350px">
405
+ <button class="btn btn-sm" style="font-size:0.6rem;padding:2px 6px" onclick="window.__quickAnswer(this,'Yes')">Yes</button>
406
+ <button class="btn btn-sm" style="font-size:0.6rem;padding:2px 6px" onclick="window.__quickAnswer(this,'No')">No</button>
407
+ <button class="btn btn-sm" style="font-size:0.6rem;padding:2px 6px" onclick="window.__quickAnswer(this,'Later')">Later</button>
408
+ <button class="btn btn-sm" style="font-size:0.6rem;padding:2px 6px" onclick="window.__quickAnswer(this,'Skip')">Skip</button>
409
+ </div>
410
+ </div>`)}else o.push(`<div style="margin:0.15rem 0;padding-left:1.5rem"><span style="color:var(--info);font-weight:600;margin-right:0.4rem">${p[1]}.</span>${Q(p[2])}</div>`);continue}if(d.startsWith("- ")){const u=`chk-${Ee}-${o.length}`,_=d.slice(2);o.push(`<div style="margin:0.15rem 0;padding-left:0.5rem;display:flex;align-items:flex-start;gap:6px" class="chat-checklist-item">
411
+ <input type="checkbox" id="${u}" checked style="margin-top:3px;cursor:pointer;accent-color:var(--success)">
412
+ <label for="${u}" style="flex:1;cursor:pointer">${Q(_)}</label>
413
+ <button class="btn btn-sm" style="font-size:0.55rem;padding:1px 4px;opacity:0.5;flex-shrink:0" onclick="window.__commentOnItem(this)" title="Add comment">+</button>
414
+ </div>`);continue}if(d.startsWith("> ")){o.push(`<div style="border-left:3px solid var(--info);padding-left:0.75rem;margin:0.3rem 0;color:var(--muted);font-style:italic">${Q(d.slice(2))}</div>`);continue}if(d===""){o.push('<div style="height:0.4rem"></div>');continue}o.push(`<div style="margin:0.1rem 0">${Q(d)}</div>`)}let s=o.join("");return n.forEach((c,d)=>{s=s.replace(`\0CODE${d}\0`,c)}),s}function Q(e){return a(e).replace(/\*\*(.+?)\*\*/g,"<strong>$1</strong>").replace(/\*(.+?)\*/g,"<em>$1</em>").replace(/`([^`]+)`/g,'<code style="background:var(--bg);padding:0.1rem 0.3rem;border-radius:0.2rem;font-size:0.8em;border:1px solid var(--border)">$1</code>')}function Xt(e){const t=document.getElementById("chat-input");t&&(t.value=e,t.focus(),t.scrollTop=t.scrollHeight)}window.__sendChat=K,window.__undoChat=Ut,window.__prefillChat=Xt;const Ue=document.createElement("style");Ue.textContent=dt,document.head.appendChild(Ue);const ce=rt();lt(ce);const Ie=[{id:"routes",label:"Routes",render:ct},{id:"database",label:"Database",render:mt},{id:"errors",label:"Errors",render:wt},{id:"metrics",label:"Metrics",render:It},{id:"system",label:"System",render:Et}],Ve={id:"chat",label:"Code With Me",render:zt};let me=localStorage.getItem("tina4_cwm_unlocked")==="true",ue=me?[Ve,...Ie]:[...Ie],Z=me?"chat":"routes";function Qt(){const e=document.getElementById("app");if(!e)return;e.innerHTML=`
415
+ <div class="dev-admin">
416
+ <div class="dev-header">
417
+ <h1><span>Tina4</span> Dev Admin</h1>
418
+ <div style="display:flex;align-items:center;gap:0.75rem">
419
+ <span class="text-sm text-muted" id="version-label" style="cursor:default;user-select:none">${ce.name} &bull; loading&hellip;</span>
420
+ <button class="btn btn-sm" onclick="window.__closeDevAdmin()" title="Close Dev Admin" style="font-size:14px;width:28px;height:28px;padding:0;line-height:1">&times;</button>
421
+ </div>
422
+ </div>
423
+ <div class="dev-tabs" id="tab-bar"></div>
424
+ <div class="dev-content" id="tab-content"></div>
425
+ </div>
426
+ `;const t=document.getElementById("tab-bar");t.innerHTML=ue.map(n=>`<button class="dev-tab ${n.id===Z?"active":""}" data-tab="${n.id}" onclick="window.__switchTab('${n.id}')">${n.label}</button>`).join(""),Me(Z)}function Me(e){Z=e,document.querySelectorAll(".dev-tab").forEach(o=>{o.classList.toggle("active",o.dataset.tab===e)});const t=document.getElementById("tab-content");if(!t)return;const n=document.createElement("div");n.className="dev-panel active",t.innerHTML="",t.appendChild(n);const i=ue.find(o=>o.id===e);i&&i.render(n)}function Zt(){if(window.parent!==window)try{const e=window.parent.document.getElementById("tina4-dev-panel");e&&e.remove()}catch{document.body.style.display="none"}}window.__closeDevAdmin=Zt,window.__switchTab=Me,Qt(),z("/system").then(e=>{const t=document.getElementById("version-label");t&&e.version&&(t.innerHTML=`${ce.name} &bull; v${a(e.version)}`)}).catch(()=>{const e=document.getElementById("version-label");e&&(e.innerHTML=`${ce.name}`)});let Le=0,Ce=null;(Je=document.getElementById("version-label"))==null||Je.addEventListener("click",()=>{if(!me&&(Le++,Ce&&clearTimeout(Ce),Ce=setTimeout(()=>{Le=0},2e3),Le>=5)){me=!0,localStorage.setItem("tina4_cwm_unlocked","true"),ue=[Ve,...Ie],Z="chat";const e=document.getElementById("tab-bar");e&&(e.innerHTML=ue.map(t=>`<button class="dev-tab ${t.id===Z?"active":""}" data-tab="${t.id}" onclick="window.__switchTab('${t.id}')">${t.label}</button>`).join("")),Me("chat")}})})();
@@ -1,4 +1,4 @@
1
- (function(){"use strict";var Ve;const ze={python:{color:"#3b82f6",name:"Python"},php:{color:"#8b5cf6",name:"PHP"},ruby:{color:"#ef4444",name:"Ruby"},nodejs:{color:"#22c55e",name:"Node.js"}};function st(){const e=document.getElementById("app"),t=(e==null?void 0:e.dataset.framework)??"python",n=e==null?void 0:e.dataset.color,i=ze[t]??ze.python;return{framework:t,color:n??i.color,name:i.name}}function at(e){const t=document.documentElement;t.style.setProperty("--primary",e.color),t.style.setProperty("--bg","#0f172a"),t.style.setProperty("--surface","#1e293b"),t.style.setProperty("--border","#334155"),t.style.setProperty("--text","#e2e8f0"),t.style.setProperty("--muted","#94a3b8"),t.style.setProperty("--success","#22c55e"),t.style.setProperty("--danger","#ef4444"),t.style.setProperty("--warn","#f59e0b"),t.style.setProperty("--info","#3b82f6")}const rt=`
1
+ (function(){"use strict";var Je;const Ae={python:{color:"#3b82f6",name:"Python"},php:{color:"#8b5cf6",name:"PHP"},ruby:{color:"#ef4444",name:"Ruby"},nodejs:{color:"#22c55e",name:"Node.js"}};function at(){const e=document.getElementById("app"),t=(e==null?void 0:e.dataset.framework)??"python",n=e==null?void 0:e.dataset.color,i=Ae[t]??Ae.python;return{framework:t,color:n??i.color,name:i.name}}function rt(e){const t=document.documentElement;t.style.setProperty("--primary",e.color),t.style.setProperty("--bg","#0f172a"),t.style.setProperty("--surface","#1e293b"),t.style.setProperty("--border","#334155"),t.style.setProperty("--text","#e2e8f0"),t.style.setProperty("--muted","#94a3b8"),t.style.setProperty("--success","#22c55e"),t.style.setProperty("--danger","#ef4444"),t.style.setProperty("--warn","#f59e0b"),t.style.setProperty("--info","#3b82f6")}const lt=`
2
2
  * { margin: 0; padding: 0; box-sizing: border-box; }
3
3
  body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: var(--bg); color: var(--text); }
4
4
 
@@ -76,7 +76,7 @@ tr:hover { background: rgba(255,255,255,0.03); }
76
76
  .error-trace { background: var(--bg); border: 1px solid var(--border); border-radius: 0.375rem; padding: 0.5rem; font-family: monospace; font-size: 0.75rem; white-space: pre-wrap; max-height: 200px; overflow-y: auto; margin-top: 0.5rem; }
77
77
 
78
78
  .bubble-chart { width: 100%; height: 400px; background: var(--surface); border: 1px solid var(--border); border-radius: 0.5rem; overflow: hidden; }
79
- `,lt="/__dev/api";async function z(e,t="GET",n){const i={method:t,headers:{}};return n&&(i.headers["Content-Type"]="application/json",i.body=JSON.stringify(n)),(await fetch(lt+e,i)).json()}function d(e){const t=document.createElement("span");return t.textContent=e,t.innerHTML}function dt(e){e.innerHTML=`
79
+ `,dt="/__dev/api";async function z(e,t="GET",n){const i={method:t,headers:{}};return n&&(i.headers["Content-Type"]="application/json",i.body=JSON.stringify(n)),(await fetch(dt+e,i)).json()}function d(e){const t=document.createElement("span");return t.textContent=e,t.innerHTML}function ct(e){e.innerHTML=`
80
80
  <div class="dev-panel-header">
81
81
  <h2>Routes <span id="routes-count" class="text-muted text-sm"></span></h2>
82
82
  <button class="btn btn-sm" onclick="window.__loadRoutes()">Refresh</button>
@@ -85,14 +85,14 @@ tr:hover { background: rgba(255,255,255,0.03); }
85
85
  <thead><tr><th>Method</th><th>Path</th><th>Auth</th><th>Handler</th></tr></thead>
86
86
  <tbody id="routes-body"></tbody>
87
87
  </table>
88
- `,Ae()}async function Ae(){const e=await z("/routes"),t=document.getElementById("routes-count");t&&(t.textContent=`(${e.count})`);const n=document.getElementById("routes-body");n&&(n.innerHTML=(e.routes||[]).map(i=>`
88
+ `,je()}async function je(){const e=await z("/routes"),t=document.getElementById("routes-count");t&&(t.textContent=`(${e.count})`);const n=document.getElementById("routes-body");n&&(n.innerHTML=(e.routes||[]).map(i=>`
89
89
  <tr>
90
90
  <td><span class="method method-${i.method.toLowerCase()}">${d(i.method)}</span></td>
91
91
  <td class="text-mono"><a href="${d(i.path)}" target="_blank" style="color:inherit;text-decoration:underline dotted">${d(i.path)}</a></td>
92
92
  <td>${i.auth_required?'<span class="badge badge-warn">auth</span>':'<span class="badge badge-success">open</span>'}</td>
93
93
  <td class="text-sm text-muted">${d(i.handler||"")} <small>(${d(i.module||"")})</small></td>
94
94
  </tr>
95
- `).join(""))}window.__loadRoutes=Ae;let W=[],G=[],P=JSON.parse(localStorage.getItem("tina4_query_history")||"[]");function ct(e){e.innerHTML=`
95
+ `).join(""))}window.__loadRoutes=je;let W=[],G=[],P=JSON.parse(localStorage.getItem("tina4_query_history")||"[]");function mt(e){e.innerHTML=`
96
96
  <div class="dev-panel-header">
97
97
  <h2>Database</h2>
98
98
  <button class="btn btn-sm" onclick="window.__loadTables()">Refresh</button>
@@ -159,11 +159,11 @@ tr:hover { background: rgba(255,255,255,0.03); }
159
159
  </div>
160
160
  </div>
161
161
  </div>
162
- `,pe(),be()}async function pe(){const t=(await z("/tables")).tables||[],n=document.getElementById("db-table-list");n&&(n.innerHTML=t.length?t.map(s=>`<div style="padding:0.3rem 0.5rem;cursor:pointer;border-radius:0.25rem;font-size:0.8rem;font-family:monospace" class="db-table-item" onclick="window.__selectTable('${d(s)}')" onmouseover="this.style.background='var(--border)'" onmouseout="this.style.background=''">${d(s)}</div>`).join(""):'<div class="text-sm text-muted">No tables</div>');const i=document.getElementById("db-seed-table");i&&(i.innerHTML='<option value="">Pick table...</option>'+t.map(s=>`<option value="${d(s)}">${d(s)}</option>`).join(""));const o=document.getElementById("paste-table");o&&(o.innerHTML='<option value="">Select table...</option>'+t.map(s=>`<option value="${d(s)}">${d(s)}</option>`).join(""))}function ge(e){var n;(n=document.getElementById("db-limit"))!=null&&n.value;const t=document.getElementById("db-query");t&&(t.value=`SELECT * FROM ${e}`),document.querySelectorAll(".db-table-item").forEach(i=>{i.style.background=i.textContent===e?"var(--border)":""}),je()}function mt(){var n;const e=document.getElementById("db-query"),t=((n=document.getElementById("db-limit"))==null?void 0:n.value)||"20";e!=null&&e.value&&(e.value=e.value.replace(/LIMIT\s+\d+/i,`LIMIT ${t}`))}function ut(e){const t=e.trim();t&&(P=P.filter(n=>n!==t),P.unshift(t),P.length>50&&(P=P.slice(0,50)),localStorage.setItem("tina4_query_history",JSON.stringify(P)),be())}function be(){const e=document.getElementById("db-history");e&&(e.innerHTML='<option value="">Query history...</option>'+P.map((t,n)=>`<option value="${n}">${d(t.length>80?t.substring(0,80)+"...":t)}</option>`).join(""))}function pt(e){const t=parseInt(e);if(isNaN(t)||!P[t])return;const n=document.getElementById("db-query");n&&(n.value=P[t]),document.getElementById("db-history").selectedIndex=0}function gt(){P=[],localStorage.removeItem("tina4_query_history"),be()}async function je(){var o,s,c;const e=document.getElementById("db-query"),t=(o=e==null?void 0:e.value)==null?void 0:o.trim();if(!t)return;ut(t);const n=document.getElementById("db-result"),i=((s=document.getElementById("db-type"))==null?void 0:s.value)||"sql";n&&(n.innerHTML='<p class="text-muted">Running...</p>');try{const r=parseInt(((c=document.getElementById("db-limit"))==null?void 0:c.value)||"20"),p=await z("/query","POST",{query:t,type:i,limit:r});if(p.error){n&&(n.innerHTML=`<p style="color:var(--danger)">${d(p.error)}</p>`);return}p.rows&&p.rows.length>0?(G=Object.keys(p.rows[0]),W=p.rows,n&&(n.innerHTML=`<p class="text-sm text-muted" style="margin-bottom:0.5rem">${p.count??p.rows.length} rows</p>
162
+ `,pe(),be()}async function pe(){const t=(await z("/tables")).tables||[],n=document.getElementById("db-table-list");n&&(n.innerHTML=t.length?t.map(s=>`<div style="padding:0.3rem 0.5rem;cursor:pointer;border-radius:0.25rem;font-size:0.8rem;font-family:monospace" class="db-table-item" onclick="window.__selectTable('${d(s)}')" onmouseover="this.style.background='var(--border)'" onmouseout="this.style.background=''">${d(s)}</div>`).join(""):'<div class="text-sm text-muted">No tables</div>');const i=document.getElementById("db-seed-table");i&&(i.innerHTML='<option value="">Pick table...</option>'+t.map(s=>`<option value="${d(s)}">${d(s)}</option>`).join(""));const o=document.getElementById("paste-table");o&&(o.innerHTML='<option value="">Select table...</option>'+t.map(s=>`<option value="${d(s)}">${d(s)}</option>`).join(""))}function ge(e){var n;(n=document.getElementById("db-limit"))!=null&&n.value;const t=document.getElementById("db-query");t&&(t.value=`SELECT * FROM ${e}`),document.querySelectorAll(".db-table-item").forEach(i=>{i.style.background=i.textContent===e?"var(--border)":""}),Pe()}function ut(){var n;const e=document.getElementById("db-query"),t=((n=document.getElementById("db-limit"))==null?void 0:n.value)||"20";e!=null&&e.value&&(e.value=e.value.replace(/LIMIT\s+\d+/i,`LIMIT ${t}`))}function pt(e){const t=e.trim();t&&(P=P.filter(n=>n!==t),P.unshift(t),P.length>50&&(P=P.slice(0,50)),localStorage.setItem("tina4_query_history",JSON.stringify(P)),be())}function be(){const e=document.getElementById("db-history");e&&(e.innerHTML='<option value="">Query history...</option>'+P.map((t,n)=>`<option value="${n}">${d(t.length>80?t.substring(0,80)+"...":t)}</option>`).join(""))}function gt(e){const t=parseInt(e);if(isNaN(t)||!P[t])return;const n=document.getElementById("db-query");n&&(n.value=P[t]),document.getElementById("db-history").selectedIndex=0}function bt(){P=[],localStorage.removeItem("tina4_query_history"),be()}async function Pe(){var o,s,c;const e=document.getElementById("db-query"),t=(o=e==null?void 0:e.value)==null?void 0:o.trim();if(!t)return;pt(t);const n=document.getElementById("db-result"),i=((s=document.getElementById("db-type"))==null?void 0:s.value)||"sql";n&&(n.innerHTML='<p class="text-muted">Running...</p>');try{const l=parseInt(((c=document.getElementById("db-limit"))==null?void 0:c.value)||"20"),p=await z("/query","POST",{query:t,type:i,limit:l});if(p.error){n&&(n.innerHTML=`<p style="color:var(--danger)">${d(p.error)}</p>`);return}p.rows&&p.rows.length>0?(G=Object.keys(p.rows[0]),W=p.rows,n&&(n.innerHTML=`<p class="text-sm text-muted" style="margin-bottom:0.5rem">${p.count??p.rows.length} rows</p>
163
163
  <div style="overflow-x:auto"><table><thead><tr>${G.map(u=>`<th>${d(u)}</th>`).join("")}</tr></thead>
164
- <tbody>${p.rows.map(u=>`<tr>${G.map(_=>`<td class="text-sm">${d(String(u[_]??""))}</td>`).join("")}</tr>`).join("")}</tbody></table></div>`)):p.affected!==void 0?(n&&(n.innerHTML=`<p class="text-muted">${p.affected} rows affected. ${p.success?"Success.":""}</p>`),W=[],G=[]):(n&&(n.innerHTML='<p class="text-muted">No results</p>'),W=[],G=[])}catch(r){n&&(n.innerHTML=`<p style="color:var(--danger)">${d(r.message)}</p>`)}}function bt(){if(!W.length)return;const e=G.join(","),t=W.map(n=>G.map(i=>{const o=String(n[i]??"");return o.includes(",")||o.includes('"')?`"${o.replace(/"/g,'""')}"`:o}).join(","));navigator.clipboard.writeText([e,...t].join(`
165
- `))}function ht(){W.length&&navigator.clipboard.writeText(JSON.stringify(W,null,2))}function ft(){const e=document.getElementById("db-paste-modal");e&&(e.style.display="flex")}function Pe(){const e=document.getElementById("db-paste-modal");e&&(e.style.display="none")}async function yt(){var o,s,c,r,p;const e=(o=document.getElementById("paste-table"))==null?void 0:o.value,t=(c=(s=document.getElementById("paste-new-table"))==null?void 0:s.value)==null?void 0:c.trim(),n=t||e,i=(p=(r=document.getElementById("paste-data"))==null?void 0:r.value)==null?void 0:p.trim();if(!n||!i){alert("Select a table or enter a new table name, and paste data.");return}try{let u;try{u=JSON.parse(i),Array.isArray(u)||(u=[u])}catch{const k=i.split(`
166
- `).map(E=>E.trim()).filter(Boolean);if(k.length<2){alert("CSV needs at least a header row and one data row.");return}const b=k[0].split(",").map(E=>E.trim().replace(/[^a-zA-Z0-9_]/g,""));u=k.slice(1).map(E=>{const T=E.split(",").map(j=>j.trim()),v={};return b.forEach((j,ee)=>{v[j]=T[ee]??""}),v})}if(!u.length){alert("No data rows found.");return}if(t){const b=["id INTEGER PRIMARY KEY AUTOINCREMENT",...Object.keys(u[0]).filter(T=>T.toLowerCase()!=="id").map(T=>`"${T}" TEXT`)],E=await z("/query","POST",{query:`CREATE TABLE IF NOT EXISTS "${t}" (${b.join(", ")})`,type:"sql"});if(E.error){alert("Create table failed: "+E.error);return}}let _=0;for(const k of u){const b=t?Object.keys(k).filter(j=>j.toLowerCase()!=="id"):Object.keys(k),E=b.map(j=>`"${j}"`).join(","),T=b.map(j=>`'${String(k[j]).replace(/'/g,"''")}'`).join(","),v=await z("/query","POST",{query:`INSERT INTO "${n}" (${E}) VALUES (${T})`,type:"sql"});if(v.error){alert(`Row ${_+1} failed: ${v.error}`);break}_++}document.getElementById("paste-data").value="",document.getElementById("paste-new-table").value="",document.getElementById("paste-table").selectedIndex=0,Pe(),pe(),_>0&&ge(n)}catch(u){alert("Import error: "+u.message)}}async function vt(){var n,i;const e=(n=document.getElementById("db-seed-table"))==null?void 0:n.value,t=parseInt(((i=document.getElementById("db-seed-count"))==null?void 0:i.value)||"10");if(e)try{const o=await z("/seed","POST",{table:e,count:t});o.error?alert(o.error):ge(e)}catch(o){alert("Seed error: "+o.message)}}window.__loadTables=pe,window.__selectTable=ge,window.__updateLimit=mt,window.__runQuery=je,window.__copyCSV=bt,window.__copyJSON=ht,window.__showPaste=ft,window.__hidePaste=Pe,window.__doPaste=yt,window.__seedTable=vt,window.__loadHistory=pt,window.__clearHistory=gt;function xt(e){e.innerHTML=`
164
+ <tbody>${p.rows.map(u=>`<tr>${G.map(_=>`<td class="text-sm">${d(String(u[_]??""))}</td>`).join("")}</tr>`).join("")}</tbody></table></div>`)):p.affected!==void 0?(n&&(n.innerHTML=`<p class="text-muted">${p.affected} rows affected. ${p.success?"Success.":""}</p>`),W=[],G=[]):(n&&(n.innerHTML='<p class="text-muted">No results</p>'),W=[],G=[])}catch(l){n&&(n.innerHTML=`<p style="color:var(--danger)">${d(l.message)}</p>`)}}function ht(){if(!W.length)return;const e=G.join(","),t=W.map(n=>G.map(i=>{const o=String(n[i]??"");return o.includes(",")||o.includes('"')?`"${o.replace(/"/g,'""')}"`:o}).join(","));navigator.clipboard.writeText([e,...t].join(`
165
+ `))}function ft(){W.length&&navigator.clipboard.writeText(JSON.stringify(W,null,2))}function yt(){const e=document.getElementById("db-paste-modal");e&&(e.style.display="flex")}function He(){const e=document.getElementById("db-paste-modal");e&&(e.style.display="none")}async function vt(){var o,s,c,l,p;const e=(o=document.getElementById("paste-table"))==null?void 0:o.value,t=(c=(s=document.getElementById("paste-new-table"))==null?void 0:s.value)==null?void 0:c.trim(),n=t||e,i=(p=(l=document.getElementById("paste-data"))==null?void 0:l.value)==null?void 0:p.trim();if(!n||!i){alert("Select a table or enter a new table name, and paste data.");return}try{let u;try{u=JSON.parse(i),Array.isArray(u)||(u=[u])}catch{const k=i.split(`
166
+ `).map(E=>E.trim()).filter(Boolean);if(k.length<2){alert("CSV needs at least a header row and one data row.");return}const h=k[0].split(",").map(E=>E.trim().replace(/[^a-zA-Z0-9_]/g,""));u=k.slice(1).map(E=>{const T=E.split(",").map(j=>j.trim()),v={};return h.forEach((j,ee)=>{v[j]=T[ee]??""}),v})}if(!u.length){alert("No data rows found.");return}if(t){const h=["id INTEGER PRIMARY KEY AUTOINCREMENT",...Object.keys(u[0]).filter(T=>T.toLowerCase()!=="id").map(T=>`"${T}" TEXT`)],E=await z("/query","POST",{query:`CREATE TABLE IF NOT EXISTS "${t}" (${h.join(", ")})`,type:"sql"});if(E.error){alert("Create table failed: "+E.error);return}}let _=0;for(const k of u){const h=t?Object.keys(k).filter(j=>j.toLowerCase()!=="id"):Object.keys(k),E=h.map(j=>`"${j}"`).join(","),T=h.map(j=>`'${String(k[j]).replace(/'/g,"''")}'`).join(","),v=await z("/query","POST",{query:`INSERT INTO "${n}" (${E}) VALUES (${T})`,type:"sql"});if(v.error){alert(`Row ${_+1} failed: ${v.error}`);break}_++}document.getElementById("paste-data").value="",document.getElementById("paste-new-table").value="",document.getElementById("paste-table").selectedIndex=0,He(),pe(),_>0&&ge(n)}catch(u){alert("Import error: "+u.message)}}async function xt(){var n,i;const e=(n=document.getElementById("db-seed-table"))==null?void 0:n.value,t=parseInt(((i=document.getElementById("db-seed-count"))==null?void 0:i.value)||"10");if(e)try{const o=await z("/seed","POST",{table:e,count:t});o.error?alert(o.error):ge(e)}catch(o){alert("Seed error: "+o.message)}}window.__loadTables=pe,window.__selectTable=ge,window.__updateLimit=ut,window.__runQuery=Pe,window.__copyCSV=ht,window.__copyJSON=ft,window.__showPaste=yt,window.__hidePaste=He,window.__doPaste=vt,window.__seedTable=xt,window.__loadHistory=gt,window.__clearHistory=bt;function wt(e){e.innerHTML=`
167
167
  <div class="dev-panel-header">
168
168
  <h2>Errors <span id="errors-count" class="text-muted text-sm"></span></h2>
169
169
  <div class="flex gap-sm">
@@ -172,7 +172,7 @@ tr:hover { background: rgba(255,255,255,0.03); }
172
172
  </div>
173
173
  </div>
174
174
  <div id="errors-body"></div>
175
- `,ie()}async function ie(){const e=await z("/broken"),t=document.getElementById("errors-count"),n=document.getElementById("errors-body");if(!n)return;const i=e.errors||[];if(t&&(t.textContent=`(${i.length})`),!i.length){n.innerHTML='<div class="empty-state">No errors</div>';return}n.innerHTML=i.map((o,s)=>{const c=o.error_type?`${o.error_type}: ${o.message}`:o.error||o.message||"Unknown error",r=o.context||{},p=o.last_seen||o.first_seen||o.timestamp||"",u=p?new Date(p).toLocaleString():"";return`
175
+ `,ie()}async function ie(){const e=await z("/broken"),t=document.getElementById("errors-count"),n=document.getElementById("errors-body");if(!n)return;const i=e.errors||[];if(t&&(t.textContent=`(${i.length})`),!i.length){n.innerHTML='<div class="empty-state">No errors</div>';return}n.innerHTML=i.map((o,s)=>{const c=o.error_type?`${o.error_type}: ${o.message}`:o.error||o.message||"Unknown error",l=o.context||{},p=o.last_seen||o.first_seen||o.timestamp||"",u=p?new Date(p).toLocaleString():"";return`
176
176
  <div style="background:var(--surface);border:1px solid var(--border);border-radius:0.5rem;padding:0.75rem;margin-bottom:0.75rem">
177
177
  <div class="flex items-center" style="justify-content:space-between;flex-wrap:wrap;gap:0.5rem">
178
178
  <div style="flex:1;min-width:0">
@@ -185,31 +185,31 @@ tr:hover { background: rgba(255,255,255,0.03); }
185
185
  <button class="btn btn-sm btn-primary" onclick="window.__askAboutError(${s})">Ask Tina4</button>
186
186
  </div>
187
187
  </div>
188
- ${r.method?`<div class="text-sm text-mono" style="margin-top:0.5rem;color:var(--info)">${d(r.method)} ${d(r.path||"")}</div>`:""}
188
+ ${l.method?`<div class="text-sm text-mono" style="margin-top:0.5rem;color:var(--info)">${d(l.method)} ${d(l.path||"")}</div>`:""}
189
189
  ${o.traceback?`<pre style="margin-top:0.5rem;padding:0.5rem;background:var(--bg);border:1px solid var(--border);border-radius:4px;font-size:0.7rem;overflow-x:auto;white-space:pre-wrap;max-height:200px;overflow-y:auto">${d(o.traceback)}</pre>`:""}
190
190
  <div class="text-sm text-muted" style="margin-top:0.5rem">${d(u)}</div>
191
191
  </div>
192
- `}).join(""),window.__errorData=i}async function wt(e){await z("/broken/resolve","POST",{id:e}),ie()}async function $t(){await z("/broken/clear","POST"),ie()}function _t(e){const n=(window.__errorData||[])[e];if(!n)return;const i=n.error_type?`${n.error_type}: ${n.message}`:n.error||n.message||"Unknown error",o=n.context||{},s=o.method&&o.path?`
192
+ `}).join(""),window.__errorData=i}async function $t(e){await z("/broken/resolve","POST",{id:e}),ie()}async function _t(){await z("/broken/clear","POST"),ie()}function kt(e){const n=(window.__errorData||[])[e];if(!n)return;const i=n.error_type?`${n.error_type}: ${n.message}`:n.error||n.message||"Unknown error",o=n.context||{},s=o.method&&o.path?`
193
193
  Route: ${o.method} ${o.path}`:"",c=`I have this error: ${i}${s}
194
194
 
195
- ${n.traceback||""}`;window.__switchTab("chat"),setTimeout(()=>{window.__prefillChat(c)},150)}window.__loadErrors=ie,window.__clearErrors=$t,window.__resolveError=wt,window.__askAboutError=_t;function kt(e){e.innerHTML=`
195
+ ${n.traceback||""}`;window.__switchTab("chat"),setTimeout(()=>{window.__prefillChat(c)},150)}window.__loadErrors=ie,window.__clearErrors=_t,window.__resolveError=$t,window.__askAboutError=kt;function Et(e){e.innerHTML=`
196
196
  <div class="dev-panel-header">
197
197
  <h2>System</h2>
198
198
  </div>
199
199
  <div id="system-grid" class="metric-grid"></div>
200
200
  <div id="system-env" style="margin-top:1rem"></div>
201
- `,He()}function Et(e){if(!e||e<0)return"?";const t=Math.floor(e/86400),n=Math.floor(e%86400/3600),i=Math.floor(e%3600/60),o=Math.floor(e%60),s=[];return t>0&&s.push(`${t}d`),n>0&&s.push(`${n}h`),i>0&&s.push(`${i}m`),s.length===0&&s.push(`${o}s`),s.join(" ")}function Tt(e){return e?e>=1024?`${(e/1024).toFixed(1)} GB`:`${e.toFixed(1)} MB`:"?"}async function He(){const e=await z("/system"),t=document.getElementById("system-grid"),n=document.getElementById("system-env");if(!t)return;const o=(e.python_version||e.php_version||e.ruby_version||e.node_version||e.runtime||"?").split("(")[0].trim(),s=[{label:"Framework",value:e.framework||"Tina4"},{label:"Runtime",value:o},{label:"Platform",value:e.platform||"?"},{label:"Architecture",value:e.architecture||"?"},{label:"PID",value:String(e.pid??"?")},{label:"Uptime",value:Et(e.uptime_seconds)},{label:"Memory",value:Tt(e.memory_mb)},{label:"Database",value:e.database||"none"},{label:"DB Tables",value:String(e.db_tables??"?")},{label:"DB Connected",value:e.db_connected?"Yes":"No"},{label:"Debug",value:e.debug==="true"||e.debug===!0?"ON":"OFF"},{label:"Log Level",value:e.log_level||"?"},{label:"Modules",value:String(e.loaded_modules??"?")},{label:"Working Dir",value:e.cwd||"?"}],c=new Set(["Working Dir","Database"]);if(t.innerHTML=s.map(r=>`
202
- <div class="metric-card" style="${c.has(r.label)?"grid-column:1/-1":""}">
203
- <div class="label">${d(r.label)}</div>
204
- <div class="value" style="font-size:${c.has(r.label)?"0.75rem":"1.1rem"}">${d(r.value)}</div>
201
+ `,qe()}function Tt(e){if(!e||e<0)return"?";const t=Math.floor(e/86400),n=Math.floor(e%86400/3600),i=Math.floor(e%3600/60),o=Math.floor(e%60),s=[];return t>0&&s.push(`${t}d`),n>0&&s.push(`${n}h`),i>0&&s.push(`${i}m`),s.length===0&&s.push(`${o}s`),s.join(" ")}function St(e){return e?e>=1024?`${(e/1024).toFixed(1)} GB`:`${e.toFixed(1)} MB`:"?"}async function qe(){const e=await z("/system"),t=document.getElementById("system-grid"),n=document.getElementById("system-env");if(!t)return;const o=(e.python_version||e.php_version||e.ruby_version||e.node_version||e.runtime||"?").split("(")[0].trim(),s=[{label:"Framework",value:e.framework||"Tina4"},{label:"Runtime",value:o},{label:"Platform",value:e.platform||"?"},{label:"Architecture",value:e.architecture||"?"},{label:"PID",value:String(e.pid??"?")},{label:"Uptime",value:Tt(e.uptime_seconds)},{label:"Memory",value:St(e.memory_mb)},{label:"Database",value:e.database||"none"},{label:"DB Tables",value:String(e.db_tables??"?")},{label:"DB Connected",value:e.db_connected?"Yes":"No"},{label:"Debug",value:e.debug==="true"||e.debug===!0?"ON":"OFF"},{label:"Log Level",value:e.log_level||"?"},{label:"Modules",value:String(e.loaded_modules??"?")},{label:"Working Dir",value:e.cwd||"?"}],c=new Set(["Working Dir","Database"]);if(t.innerHTML=s.map(l=>`
202
+ <div class="metric-card" style="${c.has(l.label)?"grid-column:1/-1":""}">
203
+ <div class="label">${d(l.label)}</div>
204
+ <div class="value" style="font-size:${c.has(l.label)?"0.75rem":"1.1rem"}">${d(l.value)}</div>
205
205
  </div>
206
- `).join(""),n){const r=[];e.debug!==void 0&&r.push(["TINA4_DEBUG",String(e.debug)]),e.log_level&&r.push(["LOG_LEVEL",e.log_level]),e.database&&r.push(["DATABASE_URL",e.database]),r.length&&(n.innerHTML=`
206
+ `).join(""),n){const l=[];e.debug!==void 0&&l.push(["TINA4_DEBUG",String(e.debug)]),e.log_level&&l.push(["LOG_LEVEL",e.log_level]),e.database&&l.push(["DATABASE_URL",e.database]),l.length&&(n.innerHTML=`
207
207
  <h3 style="font-size:0.85rem;margin-bottom:0.5rem">Environment</h3>
208
208
  <table>
209
209
  <thead><tr><th>Variable</th><th>Value</th></tr></thead>
210
- <tbody>${r.map(([p,u])=>`<tr><td class="text-mono text-sm" style="padding:4px 8px">${d(p)}</td><td class="text-sm" style="padding:4px 8px">${d(u)}</td></tr>`).join("")}</tbody>
210
+ <tbody>${l.map(([p,u])=>`<tr><td class="text-mono text-sm" style="padding:4px 8px">${d(p)}</td><td class="text-sm" style="padding:4px 8px">${d(u)}</td></tr>`).join("")}</tbody>
211
211
  </table>
212
- `)}}window.__loadSystem=He;function St(e){e.innerHTML=`
212
+ `)}}window.__loadSystem=qe;function It(e){e.innerHTML=`
213
213
  <div class="dev-panel-header">
214
214
  <h2>Code Metrics</h2>
215
215
  </div>
@@ -218,7 +218,7 @@ ${n.traceback||""}`;window.__switchTab("chat"),setTimeout(()=>{window.__prefillC
218
218
  <div id="metrics-chart" style="display:none;margin:1rem 0"></div>
219
219
  <div id="metrics-detail" style="margin-top:1rem"></div>
220
220
  <div id="metrics-complex" style="margin-top:1rem"></div>
221
- `,It()}async function It(){var s;const e=document.getElementById("metrics-chart"),t=document.getElementById("metrics-complex"),n=document.getElementById("metrics-scan-info");e&&(e.style.display="block",e.innerHTML='<p class="text-muted">Analyzing...</p>');const i=await z("/metrics/full");if(i.error||!i.file_metrics){e&&(e.innerHTML=`<p style="color:var(--danger)">${d(i.error||"No data")}</p>`);return}if(n){const c=i.scan_mode==="framework"?'<span style="color:#cba6f7;font-weight:600">(Framework)</span> Add code to src/ to see your project':"";n.innerHTML=`${i.files_analyzed} files analyzed | ${i.total_functions} functions ${c}`}const o=document.getElementById("metrics-quick");o&&(o.innerHTML=[N("Files Analyzed",i.files_analyzed),N("Total Functions",i.total_functions),N("Avg Complexity",i.avg_complexity),N("Avg Maintainability",i.avg_maintainability)].join("")),e&&i.file_metrics.length>0?Mt(i.file_metrics,e,i.dependency_graph||{},i.scan_mode||"project"):e&&(e.innerHTML='<p class="text-muted">No files to visualize</p>'),t&&((s=i.most_complex_functions)!=null&&s.length)&&(t.innerHTML=`
221
+ `,Mt()}async function Mt(){var s;const e=document.getElementById("metrics-chart"),t=document.getElementById("metrics-complex"),n=document.getElementById("metrics-scan-info");e&&(e.style.display="block",e.innerHTML='<p class="text-muted">Analyzing...</p>');const i=await z("/metrics/full");if(i.error||!i.file_metrics){e&&(e.innerHTML=`<p style="color:var(--danger)">${d(i.error||"No data")}</p>`);return}if(n){const c=i.scan_mode==="framework"?'<span style="color:#cba6f7;font-weight:600">(Framework)</span> Add code to src/ to see your project':"";n.innerHTML=`${i.files_analyzed} files analyzed | ${i.total_functions} functions ${c}`}const o=document.getElementById("metrics-quick");o&&(o.innerHTML=[N("Files Analyzed",i.files_analyzed),N("Total Functions",i.total_functions),N("Avg Complexity",i.avg_complexity),N("Avg Maintainability",i.avg_maintainability)].join("")),e&&i.file_metrics.length>0?Ct(i.file_metrics,e,i.dependency_graph||{},i.scan_mode||"project"):e&&(e.innerHTML='<p class="text-muted">No files to visualize</p>'),t&&((s=i.most_complex_functions)!=null&&s.length)&&(t.innerHTML=`
222
222
  <h3 style="font-size:0.85rem;margin-bottom:0.5rem">Most Complex Functions</h3>
223
223
  <table>
224
224
  <thead><tr><th>Function</th><th>File</th><th>Line</th><th>CC</th><th>LOC</th></tr></thead>
@@ -232,11 +232,11 @@ ${n.traceback||""}`;window.__switchTab("chat"),setTimeout(()=>{window.__prefillC
232
232
  </tr>`).join("")}
233
233
  </tbody>
234
234
  </table>
235
- `)}function Mt(e,t,n,i){var nt,ot,it;const o=t.offsetWidth||900,s=Math.max(450,Math.min(650,o*.45)),c=Math.max(...e.map(h=>h.loc))||1,r=Math.max(...e.map(h=>h.dep_count||0))||1,p=14,u=Math.min(70,o/10);function _(h){const g=Math.min((h.avg_complexity||0)/10,1),f=h.has_tests?0:1,$=Math.min((h.dep_count||0)/5,1),m=g*.4+f*.4+$*.2,l=Math.max(0,Math.min(1,m)),y=Math.round(120*(1-l)),x=Math.round(70+l*30),w=Math.round(42+18*(1-l));return`hsl(${y},${x}%,${w}%)`}function k(h){return h.loc/c*.4+(h.avg_complexity||0)/10*.4+(h.dep_count||0)/r*.2}const b=[...e].sort((h,g)=>k(h)-k(g)),E=o/2,T=s/2,v=[];let j=0,ee=0;for(const h of b){const g=p+Math.sqrt(k(h))*(u-p),f=_(h);let $=!1;for(let m=0;m<800;m++){const l=E+ee*Math.cos(j),y=T+ee*Math.sin(j);let x=!1;for(const w of v){const C=l-w.x,B=y-w.y;if(Math.sqrt(C*C+B*B)<g+w.r+2){x=!0;break}}if(!x&&l>g+2&&l<o-g-2&&y>g+25&&y<s-g-2){v.push({x:l,y,vx:0,vy:0,r:g,color:f,f:h}),$=!0;break}j+=.2,ee+=.04}$||v.push({x:E+(Math.random()-.5)*o*.3,y:T+(Math.random()-.5)*s*.3,vx:0,vy:0,r:g,color:f,f:h})}const Le=[];function Zt(h){const g=h.split("/").pop()||"",f=g.lastIndexOf(".");return(f>0?g.substring(0,f):g).toLowerCase()}const Je={};v.forEach((h,g)=>{Je[Zt(h.f.path)]=g});for(const[h,g]of Object.entries(n)){let f=null;if(v.forEach(($,m)=>{$.f.path===h&&(f=m)}),f!==null)for(const $ of g){const m=$.split(".").pop().toLowerCase(),l=Je[m];l!==void 0&&f!==l&&Le.push([f,l])}}const S=document.createElement("canvas");S.width=o,S.height=s,S.style.cssText="display:block;border:1px solid var(--border);border-radius:8px;cursor:pointer;background:#0f172a";const en=i==="framework"?'<span style="color:#cba6f7;font-weight:600">(Framework)</span> Add code to src/ to see your project':"";t.innerHTML=`<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:0.5rem"><h3 style="margin:0;font-size:0.85rem">Code Landscape ${en}</h3><span style="font-size:0.65rem;color:var(--muted)">Drag bubbles | Dbl-click to drill down</span></div><div style="position:relative" id="metrics-canvas-wrap"></div>`,document.getElementById("metrics-canvas-wrap").appendChild(S);const Be=document.createElement("div");Be.style.cssText="position:absolute;top:8px;left:8px;z-index:2;display:flex;gap:4px;flex-direction:column",Be.innerHTML=`
235
+ `)}function Ct(e,t,n,i){var ot,it,st;const o=t.offsetWidth||900,s=Math.max(450,Math.min(650,o*.45)),c=Math.max(...e.map(f=>f.loc))||1,l=Math.max(...e.map(f=>f.dep_count||0))||1,p=14,u=Math.min(70,o/10);function _(f){const g=Math.min((f.avg_complexity||0)/10,1),y=f.has_tests?0:1,w=Math.min((f.dep_count||0)/5,1),m=g*.4+y*.4+w*.2,a=Math.max(0,Math.min(1,m)),b=Math.round(120*(1-a)),x=Math.round(70+a*30),$=Math.round(42+18*(1-a));return`hsl(${b},${x}%,${$}%)`}function k(f){return f.loc/c*.4+(f.avg_complexity||0)/10*.4+(f.dep_count||0)/l*.2}const h=[...e].sort((f,g)=>k(f)-k(g)),E=o/2,T=s/2,v=[];let j=0,ee=0;for(const f of h){const g=p+Math.sqrt(k(f))*(u-p),y=_(f);let w=!1;for(let m=0;m<800;m++){const a=E+ee*Math.cos(j),b=T+ee*Math.sin(j);let x=!1;for(const $ of v){const C=a-$.x,B=b-$.y;if(Math.sqrt(C*C+B*B)<g+$.r+2){x=!0;break}}if(!x&&a>g+2&&a<o-g-2&&b>g+25&&b<s-g-2){v.push({x:a,y:b,vx:0,vy:0,r:g,color:y,f}),w=!0;break}j+=.2,ee+=.04}w||v.push({x:E+(Math.random()-.5)*o*.3,y:T+(Math.random()-.5)*s*.3,vx:0,vy:0,r:g,color:y,f})}const Le=[];function Ye(f){const g=f.split("/").pop()||"",y=g.lastIndexOf(".");return(y>0?g.substring(0,y):g).toLowerCase()}const Be={};v.forEach((f,g)=>{Be[Ye(f.f.path)]=g});for(const[f,g]of Object.entries(n)){let y=null;if(v.forEach((w,m)=>{w.f.path===f&&(y=m)}),y!==null)for(const w of g){const m=w.replace(/^\.\//,"").replace(/^\.\.\//,"").split(/[./]/);let a;for(let b=m.length-1;b>=0;b--){const x=m[b].toLowerCase();if(x&&x!=="js"&&x!=="py"&&x!=="rb"&&x!=="ts"&&x!=="index"&&(a=Be[x],a!==void 0))break}a===void 0&&(a=Be[Ye(w)]),a!==void 0&&y!==a&&Le.push([y,a])}}const S=document.createElement("canvas");S.width=o,S.height=s,S.style.cssText="display:block;border:1px solid var(--border);border-radius:8px;cursor:pointer;background:#0f172a";const en=i==="framework"?'<span style="color:#cba6f7;font-weight:600">(Framework)</span> Add code to src/ to see your project':"";t.innerHTML=`<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:0.5rem"><h3 style="margin:0;font-size:0.85rem">Code Landscape ${en}</h3><span style="font-size:0.65rem;color:var(--muted)">Drag bubbles | Dbl-click to drill down</span></div><div style="position:relative" id="metrics-canvas-wrap"></div>`,document.getElementById("metrics-canvas-wrap").appendChild(S);const ze=document.createElement("div");ze.style.cssText="position:absolute;top:8px;left:8px;z-index:2;display:flex;gap:4px;flex-direction:column",ze.innerHTML=`
236
236
  <button class="btn btn-sm" id="metrics-zoom-in" style="width:28px;height:28px;padding:0;font-size:14px;font-weight:700;line-height:1">+</button>
237
237
  <button class="btn btn-sm" id="metrics-zoom-out" style="width:28px;height:28px;padding:0;font-size:14px;font-weight:700;line-height:1">&minus;</button>
238
238
  <button class="btn btn-sm" id="metrics-zoom-fit" style="width:28px;height:28px;padding:0;font-size:10px;font-weight:700;line-height:1">Fit</button>
239
- `,document.getElementById("metrics-canvas-wrap").appendChild(Be),(nt=document.getElementById("metrics-zoom-in"))==null||nt.addEventListener("click",()=>{M=Math.min(5,M*1.3)}),(ot=document.getElementById("metrics-zoom-out"))==null||ot.addEventListener("click",()=>{M=Math.max(.3,M*.7)}),(it=document.getElementById("metrics-zoom-fit"))==null||it.addEventListener("click",()=>{M=1,V=0,J=0});const a=S.getContext("2d");let F=-1,I=-1,Ye=0,Ke=0,V=0,J=0,M=1,te=!1,Xe=0,Qe=0,Ze=0,et=0;function tn(){for(let m=0;m<v.length;m++){if(m===I)continue;const l=v[m],y=E-l.x,x=T-l.y,w=.3+l.r/u*.7,C=.008*w*w;l.vx+=y*C,l.vy+=x*C}for(const[m,l]of Le){const y=v[m],x=v[l],w=x.x-y.x,C=x.y-y.y,B=Math.sqrt(w*w+C*C)||1,O=y.r+x.r+20,R=(B-O)*.002,ne=w/B*R,oe=C/B*R;m!==I&&(y.vx+=ne,y.vy+=oe),l!==I&&(x.vx-=ne,x.vy-=oe)}for(let m=0;m<v.length;m++)for(let l=m+1;l<v.length;l++){const y=v[m],x=v[l],w=x.x-y.x,C=x.y-y.y,B=Math.sqrt(w*w+C*C)||1,O=y.r+x.r+20;if(B<O){const R=40*(O-B)/O,ne=w/B*R,oe=C/B*R;m!==I&&(y.vx-=ne,y.vy-=oe),l!==I&&(x.vx+=ne,x.vy+=oe)}}for(let m=0;m<v.length;m++){if(m===I)continue;const l=v[m];l.vx*=.65,l.vy*=.65;const y=2;l.vx=Math.max(-y,Math.min(y,l.vx)),l.vy=Math.max(-y,Math.min(y,l.vy)),l.x+=l.vx,l.y+=l.vy,l.x=Math.max(l.r+2,Math.min(o-l.r-2,l.x)),l.y=Math.max(l.r+25,Math.min(s-l.r-2,l.y))}}function tt(){var h;tn(),a.clearRect(0,0,o,s),a.save(),a.translate(V,J),a.scale(M,M),a.strokeStyle="rgba(255,255,255,0.03)",a.lineWidth=1/M;for(let g=0;g<o/M;g+=50)a.beginPath(),a.moveTo(g,0),a.lineTo(g,s/M),a.stroke();for(let g=0;g<s/M;g+=50)a.beginPath(),a.moveTo(0,g),a.lineTo(o/M,g),a.stroke();for(const[g,f]of Le){const $=v[g],m=v[f],l=m.x-$.x,y=m.y-$.y,x=Math.sqrt(l*l+y*y)||1,w=F===g||F===f;a.beginPath(),a.moveTo($.x+l/x*$.r,$.y+y/x*$.r);const C=m.x-l/x*m.r,B=m.y-y/x*m.r;a.lineTo(C,B),a.strokeStyle=w?"rgba(139,180,250,0.9)":"rgba(255,255,255,0.15)",a.lineWidth=w?3:1,a.stroke();const O=w?12:6,R=Math.atan2(y,l);a.beginPath(),a.moveTo(C,B),a.lineTo(C-O*Math.cos(R-.4),B-O*Math.sin(R-.4)),a.lineTo(C-O*Math.cos(R+.4),B-O*Math.sin(R+.4)),a.closePath(),a.fillStyle=a.strokeStyle,a.fill()}for(let g=0;g<v.length;g++){const f=v[g],$=g===F,m=$?f.r+4:f.r;$&&(a.beginPath(),a.arc(f.x,f.y,m+8,0,Math.PI*2),a.fillStyle="rgba(255,255,255,0.08)",a.fill()),a.beginPath(),a.arc(f.x,f.y,m,0,Math.PI*2),a.fillStyle=f.color,a.globalAlpha=$?1:.85,a.fill(),a.globalAlpha=1,a.strokeStyle=$?"rgba(255,255,255,0.6)":"rgba(255,255,255,0.25)",a.lineWidth=$?2.5:1.5,a.stroke();const l=((h=f.f.path.split("/").pop())==null?void 0:h.replace(/\.\w+$/,""))||"?";if(m>16){const w=Math.max(8,Math.min(13,m*.38));a.fillStyle="#fff",a.font=`600 ${w}px monospace`,a.textAlign="center",a.fillText(l,f.x,f.y-2),a.fillStyle="rgba(255,255,255,0.65)",a.font=`${w-1}px monospace`,a.fillText(`${f.f.loc} LOC`,f.x,f.y+w)}const y=Math.max(9,m*.3),x=y*.7;if(m>14&&f.f.dep_count>0){const w=f.y-m+x+3;a.beginPath(),a.arc(f.x,w,x,0,Math.PI*2),a.fillStyle="#ea580c",a.fill(),a.fillStyle="#fff",a.font=`bold ${y}px sans-serif`,a.textAlign="center",a.fillText("D",f.x,w+y*.35)}if(m>14&&f.f.has_tests){const w=f.y+m-x-3;a.beginPath(),a.arc(f.x,w,x,0,Math.PI*2),a.fillStyle="#16a34a",a.fill(),a.fillStyle="#fff",a.font=`bold ${y}px sans-serif`,a.textAlign="center",a.fillText("T",f.x,w+y*.35)}}a.restore(),requestAnimationFrame(tt)}S.addEventListener("mousemove",h=>{const g=S.getBoundingClientRect(),f=(h.clientX-g.left-V)/M,$=(h.clientY-g.top-J)/M;if(te){V=Ze+(h.clientX-Xe),J=et+(h.clientY-Qe);return}if(I>=0){ue=!0,v[I].x=f+Ye,v[I].y=$+Ke,v[I].vx=0,v[I].vy=0;return}F=-1;for(let m=v.length-1;m>=0;m--){const l=v[m],y=f-l.x,x=$-l.y;if(Math.sqrt(y*y+x*x)<l.r+4){F=m;break}}S.style.cursor=F>=0?"grab":"default"}),S.addEventListener("mousedown",h=>{const g=S.getBoundingClientRect(),f=(h.clientX-g.left-V)/M,$=(h.clientY-g.top-J)/M;if(h.button===2){te=!0,Xe=h.clientX,Qe=h.clientY,Ze=V,et=J,S.style.cursor="move";return}F>=0&&(I=F,Ye=v[I].x-f,Ke=v[I].y-$,ue=!1,S.style.cursor="grabbing")});let ue=!1;S.addEventListener("mouseup",h=>{if(te){te=!1,S.style.cursor="default";return}if(I>=0){ue||he(v[I].f.path),S.style.cursor="grab",I=-1,ue=!1;return}}),S.addEventListener("mouseleave",()=>{F=-1,I=-1,te=!1}),S.addEventListener("dblclick",h=>{const g=S.getBoundingClientRect(),f=(h.clientX-g.left-V)/M,$=(h.clientY-g.top-J)/M;for(let m=v.length-1;m>=0;m--){const l=v[m],y=f-l.x,x=$-l.y;if(Math.sqrt(y*y+x*x)<l.r+4){he(l.f.path);break}}}),S.addEventListener("contextmenu",h=>h.preventDefault()),requestAnimationFrame(tt)}async function he(e){const t=document.getElementById("metrics-detail");if(!t)return;t.innerHTML='<p class="text-muted">Loading file analysis...</p>';const n=await z("/metrics/file?path="+encodeURIComponent(e));if(n.error){t.innerHTML=`<p style="color:var(--danger)">${d(n.error)}</p>`;return}const i=n.functions||[],o=Math.max(1,...i.map(s=>s.complexity));t.innerHTML=`
239
+ `,document.getElementById("metrics-canvas-wrap").appendChild(ze),(ot=document.getElementById("metrics-zoom-in"))==null||ot.addEventListener("click",()=>{M=Math.min(5,M*1.3)}),(it=document.getElementById("metrics-zoom-out"))==null||it.addEventListener("click",()=>{M=Math.max(.3,M*.7)}),(st=document.getElementById("metrics-zoom-fit"))==null||st.addEventListener("click",()=>{M=1,V=0,J=0});const r=S.getContext("2d");let F=-1,I=-1,Ke=0,Xe=0,V=0,J=0,M=1,te=!1,Qe=0,Ze=0,et=0,tt=0;function tn(){for(let m=0;m<v.length;m++){if(m===I)continue;const a=v[m],b=E-a.x,x=T-a.y,$=.3+a.r/u*.7,C=.008*$*$;a.vx+=b*C,a.vy+=x*C}for(const[m,a]of Le){const b=v[m],x=v[a],$=x.x-b.x,C=x.y-b.y,B=Math.sqrt($*$+C*C)||1,O=b.r+x.r+20,R=(B-O)*.002,ne=$/B*R,oe=C/B*R;m!==I&&(b.vx+=ne,b.vy+=oe),a!==I&&(x.vx-=ne,x.vy-=oe)}for(let m=0;m<v.length;m++)for(let a=m+1;a<v.length;a++){const b=v[m],x=v[a],$=x.x-b.x,C=x.y-b.y,B=Math.sqrt($*$+C*C)||1,O=b.r+x.r+20;if(B<O){const R=40*(O-B)/O,ne=$/B*R,oe=C/B*R;m!==I&&(b.vx-=ne,b.vy-=oe),a!==I&&(x.vx+=ne,x.vy+=oe)}}for(let m=0;m<v.length;m++){if(m===I)continue;const a=v[m];a.vx*=.65,a.vy*=.65;const b=2;a.vx=Math.max(-b,Math.min(b,a.vx)),a.vy=Math.max(-b,Math.min(b,a.vy)),a.x+=a.vx,a.y+=a.vy,a.x=Math.max(a.r+2,Math.min(o-a.r-2,a.x)),a.y=Math.max(a.r+25,Math.min(s-a.r-2,a.y))}}function nt(){var f;tn(),r.clearRect(0,0,o,s),r.save(),r.translate(V,J),r.scale(M,M),r.strokeStyle="rgba(255,255,255,0.03)",r.lineWidth=1/M;for(let g=0;g<o/M;g+=50)r.beginPath(),r.moveTo(g,0),r.lineTo(g,s/M),r.stroke();for(let g=0;g<s/M;g+=50)r.beginPath(),r.moveTo(0,g),r.lineTo(o/M,g),r.stroke();for(const[g,y]of Le){const w=v[g],m=v[y],a=m.x-w.x,b=m.y-w.y,x=Math.sqrt(a*a+b*b)||1,$=F===g||F===y;r.beginPath(),r.moveTo(w.x+a/x*w.r,w.y+b/x*w.r);const C=m.x-a/x*m.r,B=m.y-b/x*m.r;r.lineTo(C,B),r.strokeStyle=$?"rgba(139,180,250,0.9)":"rgba(255,255,255,0.15)",r.lineWidth=$?3:1,r.stroke();const O=$?12:6,R=Math.atan2(b,a);r.beginPath(),r.moveTo(C,B),r.lineTo(C-O*Math.cos(R-.4),B-O*Math.sin(R-.4)),r.lineTo(C-O*Math.cos(R+.4),B-O*Math.sin(R+.4)),r.closePath(),r.fillStyle=r.strokeStyle,r.fill()}for(let g=0;g<v.length;g++){const y=v[g],w=g===F,m=w?y.r+4:y.r;w&&(r.beginPath(),r.arc(y.x,y.y,m+8,0,Math.PI*2),r.fillStyle="rgba(255,255,255,0.08)",r.fill()),r.beginPath(),r.arc(y.x,y.y,m,0,Math.PI*2),r.fillStyle=y.color,r.globalAlpha=w?1:.85,r.fill(),r.globalAlpha=1,r.strokeStyle=w?"rgba(255,255,255,0.6)":"rgba(255,255,255,0.25)",r.lineWidth=w?2.5:1.5,r.stroke();const a=((f=y.f.path.split("/").pop())==null?void 0:f.replace(/\.\w+$/,""))||"?";if(m>16){const $=Math.max(8,Math.min(13,m*.38));r.fillStyle="#fff",r.font=`600 ${$}px monospace`,r.textAlign="center",r.fillText(a,y.x,y.y-2),r.fillStyle="rgba(255,255,255,0.65)",r.font=`${$-1}px monospace`,r.fillText(`${y.f.loc} LOC`,y.x,y.y+$)}const b=Math.max(9,m*.3),x=b*.7;if(m>14&&y.f.dep_count>0){const $=y.y-m+x+3;r.beginPath(),r.arc(y.x,$,x,0,Math.PI*2),r.fillStyle="#ea580c",r.fill(),r.fillStyle="#fff",r.font=`bold ${b}px sans-serif`,r.textAlign="center",r.fillText("D",y.x,$+b*.35)}if(m>14&&y.f.has_tests){const $=y.y+m-x-3;r.beginPath(),r.arc(y.x,$,x,0,Math.PI*2),r.fillStyle="#16a34a",r.fill(),r.fillStyle="#fff",r.font=`bold ${b}px sans-serif`,r.textAlign="center",r.fillText("T",y.x,$+b*.35)}}r.restore(),requestAnimationFrame(nt)}S.addEventListener("mousemove",f=>{const g=S.getBoundingClientRect(),y=(f.clientX-g.left-V)/M,w=(f.clientY-g.top-J)/M;if(te){V=et+(f.clientX-Qe),J=tt+(f.clientY-Ze);return}if(I>=0){ue=!0,v[I].x=y+Ke,v[I].y=w+Xe,v[I].vx=0,v[I].vy=0;return}F=-1;for(let m=v.length-1;m>=0;m--){const a=v[m],b=y-a.x,x=w-a.y;if(Math.sqrt(b*b+x*x)<a.r+4){F=m;break}}S.style.cursor=F>=0?"grab":"default"}),S.addEventListener("mousedown",f=>{const g=S.getBoundingClientRect(),y=(f.clientX-g.left-V)/M,w=(f.clientY-g.top-J)/M;if(f.button===2){te=!0,Qe=f.clientX,Ze=f.clientY,et=V,tt=J,S.style.cursor="move";return}F>=0&&(I=F,Ke=v[I].x-y,Xe=v[I].y-w,ue=!1,S.style.cursor="grabbing")});let ue=!1;S.addEventListener("mouseup",f=>{if(te){te=!1,S.style.cursor="default";return}if(I>=0){ue||he(v[I].f.path),S.style.cursor="grab",I=-1,ue=!1;return}}),S.addEventListener("mouseleave",()=>{F=-1,I=-1,te=!1}),S.addEventListener("dblclick",f=>{const g=S.getBoundingClientRect(),y=(f.clientX-g.left-V)/M,w=(f.clientY-g.top-J)/M;for(let m=v.length-1;m>=0;m--){const a=v[m],b=y-a.x,x=w-a.y;if(Math.sqrt(b*b+x*x)<a.r+4){he(a.f.path);break}}}),S.addEventListener("contextmenu",f=>f.preventDefault()),requestAnimationFrame(nt)}async function he(e){const t=document.getElementById("metrics-detail");if(!t)return;t.innerHTML='<p class="text-muted">Loading file analysis...</p>';const n=await z("/metrics/file?path="+encodeURIComponent(e));if(n.error){t.innerHTML=`<p style="color:var(--danger)">${d(n.error)}</p>`;return}const i=n.functions||[],o=Math.max(1,...i.map(s=>s.complexity));t.innerHTML=`
240
240
  <div style="background:var(--surface);border:1px solid var(--border);border-radius:0.5rem;padding:1rem">
241
241
  <div class="flex items-center" style="justify-content:space-between;margin-bottom:0.75rem">
242
242
  <h3 style="font-size:0.9rem">${d(n.path)}</h3>
@@ -251,14 +251,14 @@ ${n.traceback||""}`;window.__switchTab("chat"),setTimeout(()=>{window.__prefillC
251
251
  </div>
252
252
  ${i.length?`
253
253
  <h4 style="font-size:0.8rem;color:var(--info);margin-bottom:0.5rem">Cyclomatic Complexity by Function</h4>
254
- ${i.sort((s,c)=>c.complexity-s.complexity).map(s=>{const c=s.complexity/o*100,r=s.complexity>10?"#ef4444":s.complexity>5?"#f59e0b":"#22c55e";return`<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:3px;font-size:0.75rem">
254
+ ${i.sort((s,c)=>c.complexity-s.complexity).map(s=>{const c=s.complexity/o*100,l=s.complexity>10?"#ef4444":s.complexity>5?"#f59e0b":"#22c55e";return`<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:3px;font-size:0.75rem">
255
255
  <div style="width:200px;flex-shrink:0;text-align:right;font-family:monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${d(s.name)}">${d(s.name)}</div>
256
- <div style="flex:1;height:14px;background:var(--bg);border-radius:2px;overflow:hidden"><div style="width:${c}%;height:100%;background:${r}"></div></div>
257
- <div style="width:180px;flex-shrink:0;font-family:monospace;text-align:right"><span style="color:${r}">CC:${s.complexity}</span> <span style="color:var(--muted)">${s.loc} LOC L${s.line}</span></div>
256
+ <div style="flex:1;height:14px;background:var(--bg);border-radius:2px;overflow:hidden"><div style="width:${c}%;height:100%;background:${l}"></div></div>
257
+ <div style="width:180px;flex-shrink:0;font-family:monospace;text-align:right"><span style="color:${l}">CC:${s.complexity}</span> <span style="color:var(--muted)">${s.loc} LOC L${s.line}</span></div>
258
258
  </div>`}).join("")}
259
259
  `:'<p class="text-muted">No functions</p>'}
260
260
  </div>
261
- `}function N(e,t){return`<div class="metric-card"><div class="label">${d(e)}</div><div class="value">${d(String(t??0))}</div></div>`}window.__drillDown=he;const se={tina4:{model:"tina4-v1",url:"https://api.tina4.com/v1/chat/completions"},custom:{model:"",url:"http://localhost:11434"},anthropic:{model:"claude-sonnet-4-20250514",url:"https://api.anthropic.com"},openai:{model:"gpt-4o",url:"https://api.openai.com"}};function ae(e="tina4"){const t=se[e]||se.tina4;return{provider:e,model:t.model,url:t.url,apiKey:""}}function fe(e){const t={...ae(),...e||{}};return t.provider==="ollama"&&(t.provider="custom"),t}function Ct(){try{const e=JSON.parse(localStorage.getItem("tina4_chat_settings")||"{}");return{thinking:fe(e.thinking),vision:fe(e.vision),imageGen:fe(e.imageGen)}}catch{return{thinking:ae(),vision:ae(),imageGen:ae()}}}function Lt(e){localStorage.setItem("tina4_chat_settings",JSON.stringify(e)),L=e,U()}let L=Ct(),H="Idle";const re=[];function Bt(e){var n,i,o,s,c,r,p,u,_,k;e.innerHTML=`
261
+ `}function N(e,t){return`<div class="metric-card"><div class="label">${d(e)}</div><div class="value">${d(String(t??0))}</div></div>`}window.__drillDown=he;const se={tina4:{model:"tina4-v1",url:"https://api.tina4.com/v1/chat/completions"},custom:{model:"",url:"http://localhost:11434"},anthropic:{model:"claude-sonnet-4-20250514",url:"https://api.anthropic.com"},openai:{model:"gpt-4o",url:"https://api.openai.com"}};function ae(e="tina4"){const t=se[e]||se.tina4;return{provider:e,model:t.model,url:t.url,apiKey:""}}function fe(e){const t={...ae(),...e||{}};return t.provider==="ollama"&&(t.provider="custom"),t}function Lt(){try{const e=JSON.parse(localStorage.getItem("tina4_chat_settings")||"{}");return{thinking:fe(e.thinking),vision:fe(e.vision),imageGen:fe(e.imageGen)}}catch{return{thinking:ae(),vision:ae(),imageGen:ae()}}}function Bt(e){localStorage.setItem("tina4_chat_settings",JSON.stringify(e)),L=e,U()}let L=Lt(),H="Idle";const re=[];function zt(e){var n,i,o,s,c,l,p,u,_,k;e.innerHTML=`
262
262
  <div class="dev-panel-header">
263
263
  <h2>Code With Me</h2>
264
264
  <div class="flex gap-sm">
@@ -309,22 +309,22 @@ ${n.traceback||""}`;window.__switchTab("chat"),setTimeout(()=>{window.__prefillC
309
309
  <button class="btn btn-sm" id="chat-modal-close" style="width:28px;height:28px;padding:0;font-size:16px;line-height:1">&times;</button>
310
310
  </div>
311
311
  <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:0.75rem;margin-bottom:0.75rem">
312
- ${["thinking","vision","imageGen"].map(b=>`
312
+ ${["thinking","vision","imageGen"].map(h=>`
313
313
  <fieldset style="border:1px solid var(--border);border-radius:0.375rem;padding:0.5rem 0.75rem;margin:0">
314
- <legend class="text-sm" style="font-weight:600;padding:0 4px">${b==="imageGen"?"Image Generation":b.charAt(0).toUpperCase()+b.slice(1)}</legend>
315
- <div style="margin-bottom:0.375rem"><label class="text-sm text-muted" style="display:block;margin-bottom:2px">Provider</label><select id="set-${b}-provider" class="input" style="width:100%"><option value="tina4">Tina4 Cloud</option><option value="custom">Custom / Local</option><option value="anthropic">Anthropic (Claude)</option><option value="openai">OpenAI</option></select></div>
316
- <div style="margin-bottom:0.375rem"><label class="text-sm text-muted" style="display:block;margin-bottom:2px">URL</label><input type="text" id="set-${b}-url" class="input" style="width:100%" /></div>
317
- <div id="set-${b}-key-row" style="margin-bottom:0.375rem"><label class="text-sm text-muted" style="display:block;margin-bottom:2px">API Key</label><input type="password" id="set-${b}-key" class="input" placeholder="sk-..." style="width:100%" /></div>
318
- <button class="btn btn-sm btn-primary" id="set-${b}-connect" style="width:100%;margin-bottom:0.375rem">Connect</button>
319
- <div id="set-${b}-result" class="text-sm" style="min-height:1.2em;margin-bottom:0.375rem"></div>
320
- <div style="margin-bottom:0.375rem"><label class="text-sm text-muted" style="display:block;margin-bottom:2px">Model</label><select id="set-${b}-model" class="input" style="width:100%" disabled><option value="">-- connect first --</option></select></div>
321
- <div id="set-${b}-result" class="text-sm" style="margin-top:4px;min-height:1.2em"></div>
314
+ <legend class="text-sm" style="font-weight:600;padding:0 4px">${h==="imageGen"?"Image Generation":h.charAt(0).toUpperCase()+h.slice(1)}</legend>
315
+ <div style="margin-bottom:0.375rem"><label class="text-sm text-muted" style="display:block;margin-bottom:2px">Provider</label><select id="set-${h}-provider" class="input" style="width:100%"><option value="tina4">Tina4 Cloud</option><option value="custom">Custom / Local</option><option value="anthropic">Anthropic (Claude)</option><option value="openai">OpenAI</option></select></div>
316
+ <div style="margin-bottom:0.375rem"><label class="text-sm text-muted" style="display:block;margin-bottom:2px">URL</label><input type="text" id="set-${h}-url" class="input" style="width:100%" /></div>
317
+ <div id="set-${h}-key-row" style="margin-bottom:0.375rem"><label class="text-sm text-muted" style="display:block;margin-bottom:2px">API Key</label><input type="password" id="set-${h}-key" class="input" placeholder="sk-..." style="width:100%" /></div>
318
+ <button class="btn btn-sm btn-primary" id="set-${h}-connect" style="width:100%;margin-bottom:0.375rem">Connect</button>
319
+ <div id="set-${h}-result" class="text-sm" style="min-height:1.2em;margin-bottom:0.375rem"></div>
320
+ <div style="margin-bottom:0.375rem"><label class="text-sm text-muted" style="display:block;margin-bottom:2px">Model</label><select id="set-${h}-model" class="input" style="width:100%" disabled><option value="">-- connect first --</option></select></div>
321
+ <div id="set-${h}-result" class="text-sm" style="margin-top:4px;min-height:1.2em"></div>
322
322
  </fieldset>`).join("")}
323
323
  </div>
324
324
  <button class="btn btn-primary" id="chat-modal-save" style="width:100%">Save Settings</button>
325
325
  </div>
326
326
  </div>
327
- `,(n=document.getElementById("chat-send-btn"))==null||n.addEventListener("click",K),(i=document.getElementById("chat-thoughts-btn"))==null||i.addEventListener("click",Ee),(o=document.getElementById("chat-thoughts-close"))==null||o.addEventListener("click",Ee),(s=document.getElementById("chat-settings-btn"))==null||s.addEventListener("click",zt),(c=document.getElementById("chat-modal-close"))==null||c.addEventListener("click",_e),(r=document.getElementById("chat-modal-save"))==null||r.addEventListener("click",At),(p=document.getElementById("chat-modal-overlay"))==null||p.addEventListener("click",b=>{b.target===b.currentTarget&&_e()}),(u=document.getElementById("chat-file-btn"))==null||u.addEventListener("click",()=>{var b;(b=document.getElementById("chat-file-input"))==null||b.click()}),(_=document.getElementById("chat-file-input"))==null||_.addEventListener("change",Ut),(k=document.getElementById("chat-mic-btn"))==null||k.addEventListener("click",Jt);const t=document.getElementById("chat-input");t==null||t.addEventListener("keydown",b=>{b.key==="Enter"&&!b.shiftKey&&(b.preventDefault(),K())}),U()}function ye(e,t){document.getElementById(`set-${e}-provider`).value=t.provider;const n=document.getElementById(`set-${e}-model`);t.model&&(n.innerHTML=`<option value="${t.model}">${t.model}</option>`,n.value=t.model,n.disabled=!1),document.getElementById(`set-${e}-url`).value=t.url,document.getElementById(`set-${e}-key`).value=t.apiKey,xe(e,t.provider)}function ve(e){var t,n,i,o;return{provider:((t=document.getElementById(`set-${e}-provider`))==null?void 0:t.value)||"custom",model:((n=document.getElementById(`set-${e}-model`))==null?void 0:n.value)||"",url:((i=document.getElementById(`set-${e}-url`))==null?void 0:i.value)||"",apiKey:((o=document.getElementById(`set-${e}-key`))==null?void 0:o.value)||""}}function xe(e,t){const n=document.getElementById(`set-${e}-key-row`);n&&(n.style.display="block")}function we(e){const t=document.getElementById(`set-${e}-provider`);t==null||t.addEventListener("change",()=>{const n=se[t.value]||se.tina4,i=document.getElementById(`set-${e}-model`);i.innerHTML=`<option value="${n.model}">${n.model}</option>`,i.value=n.model,document.getElementById(`set-${e}-url`).value=n.url,xe(e,t.value)}),xe(e,(t==null?void 0:t.value)||"custom")}async function $e(e){var c,r,p;const t=((c=document.getElementById(`set-${e}-provider`))==null?void 0:c.value)||"custom",n=((r=document.getElementById(`set-${e}-url`))==null?void 0:r.value)||"",i=((p=document.getElementById(`set-${e}-key`))==null?void 0:p.value)||"",o=document.getElementById(`set-${e}-model`),s=document.getElementById(`set-${e}-result`);s&&(s.textContent="Connecting...",s.style.color="var(--muted)");try{let u=[];const _=n.replace(/\/(v1|api)\/.*$/,"").replace(/\/+$/,"");if(t==="tina4"){const b={"Content-Type":"application/json"};i&&(b.Authorization=`Bearer ${i}`);try{u=((await(await fetch(`${_}/v1/models`,{headers:b})).json()).data||[]).map(v=>v.id)}catch{}u.length||(u=["tina4-v1"])}else if(t==="custom"){try{u=((await(await fetch(`${_}/api/tags`)).json()).models||[]).map(T=>T.name||T.model)}catch{}if(!u.length)try{u=((await(await fetch(`${_}/v1/models`)).json()).data||[]).map(T=>T.id)}catch{}}else if(t==="anthropic")u=["claude-sonnet-4-20250514","claude-opus-4-20250514","claude-haiku-4-20250514","claude-3-5-sonnet-20241022"];else if(t==="openai"){const b=n.replace(/\/v1\/.*$/,"");u=((await(await fetch(`${b}/v1/models`,{headers:i?{Authorization:`Bearer ${i}`}:{}})).json()).data||[]).map(v=>v.id).filter(v=>v.startsWith("gpt"))}if(u.length===0){s&&(s.innerHTML='<span style="color:var(--warn)">No models found</span>');return}const k=o.value;o.innerHTML=u.map(b=>`<option value="${b}">${b}</option>`).join(""),u.includes(k)&&(o.value=k),o.disabled=!1,s&&(s.innerHTML=`<span style="color:var(--success)">&#10003; ${u.length} models available</span>`)}catch{s&&(s.innerHTML='<span style="color:var(--danger)">&#10007; Connection failed</span>')}}function zt(){var t,n,i;const e=document.getElementById("chat-modal-overlay");e&&(e.style.display="flex",ye("thinking",L.thinking),ye("vision",L.vision),ye("imageGen",L.imageGen),we("thinking"),we("vision"),we("imageGen"),(t=document.getElementById("set-thinking-connect"))==null||t.addEventListener("click",()=>$e("thinking")),(n=document.getElementById("set-vision-connect"))==null||n.addEventListener("click",()=>$e("vision")),(i=document.getElementById("set-imageGen-connect"))==null||i.addEventListener("click",()=>$e("imageGen")))}function _e(){const e=document.getElementById("chat-modal-overlay");e&&(e.style.display="none")}function At(){Lt({thinking:ve("thinking"),vision:ve("vision"),imageGen:ve("imageGen")}),_e()}function U(){const e=document.getElementById("chat-summary");if(!e)return;const t=Y.length?Y.map(o=>`<div style="margin-bottom:4px;font-size:0.65rem;line-height:1.3">
327
+ `,(n=document.getElementById("chat-send-btn"))==null||n.addEventListener("click",K),(i=document.getElementById("chat-thoughts-btn"))==null||i.addEventListener("click",Ee),(o=document.getElementById("chat-thoughts-close"))==null||o.addEventListener("click",Ee),(s=document.getElementById("chat-settings-btn"))==null||s.addEventListener("click",At),(c=document.getElementById("chat-modal-close"))==null||c.addEventListener("click",_e),(l=document.getElementById("chat-modal-save"))==null||l.addEventListener("click",jt),(p=document.getElementById("chat-modal-overlay"))==null||p.addEventListener("click",h=>{h.target===h.currentTarget&&_e()}),(u=document.getElementById("chat-file-btn"))==null||u.addEventListener("click",()=>{var h;(h=document.getElementById("chat-file-input"))==null||h.click()}),(_=document.getElementById("chat-file-input"))==null||_.addEventListener("change",Vt),(k=document.getElementById("chat-mic-btn"))==null||k.addEventListener("click",Yt);const t=document.getElementById("chat-input");t==null||t.addEventListener("keydown",h=>{h.key==="Enter"&&!h.shiftKey&&(h.preventDefault(),K())}),U()}function ye(e,t){document.getElementById(`set-${e}-provider`).value=t.provider;const n=document.getElementById(`set-${e}-model`);t.model&&(n.innerHTML=`<option value="${t.model}">${t.model}</option>`,n.value=t.model,n.disabled=!1),document.getElementById(`set-${e}-url`).value=t.url,document.getElementById(`set-${e}-key`).value=t.apiKey,xe(e,t.provider)}function ve(e){var t,n,i,o;return{provider:((t=document.getElementById(`set-${e}-provider`))==null?void 0:t.value)||"custom",model:((n=document.getElementById(`set-${e}-model`))==null?void 0:n.value)||"",url:((i=document.getElementById(`set-${e}-url`))==null?void 0:i.value)||"",apiKey:((o=document.getElementById(`set-${e}-key`))==null?void 0:o.value)||""}}function xe(e,t){const n=document.getElementById(`set-${e}-key-row`);n&&(n.style.display="block")}function we(e){const t=document.getElementById(`set-${e}-provider`);t==null||t.addEventListener("change",()=>{const n=se[t.value]||se.tina4,i=document.getElementById(`set-${e}-model`);i.innerHTML=`<option value="${n.model}">${n.model}</option>`,i.value=n.model,document.getElementById(`set-${e}-url`).value=n.url,xe(e,t.value)}),xe(e,(t==null?void 0:t.value)||"custom")}async function $e(e){var c,l,p;const t=((c=document.getElementById(`set-${e}-provider`))==null?void 0:c.value)||"custom",n=((l=document.getElementById(`set-${e}-url`))==null?void 0:l.value)||"",i=((p=document.getElementById(`set-${e}-key`))==null?void 0:p.value)||"",o=document.getElementById(`set-${e}-model`),s=document.getElementById(`set-${e}-result`);s&&(s.textContent="Connecting...",s.style.color="var(--muted)");try{let u=[];const _=n.replace(/\/(v1|api)\/.*$/,"").replace(/\/+$/,"");if(t==="tina4"){const h={"Content-Type":"application/json"};i&&(h.Authorization=`Bearer ${i}`);try{u=((await(await fetch(`${_}/v1/models`,{headers:h})).json()).data||[]).map(v=>v.id)}catch{}u.length||(u=["tina4-v1"])}else if(t==="custom"){try{u=((await(await fetch(`${_}/api/tags`)).json()).models||[]).map(T=>T.name||T.model)}catch{}if(!u.length)try{u=((await(await fetch(`${_}/v1/models`)).json()).data||[]).map(T=>T.id)}catch{}}else if(t==="anthropic")u=["claude-sonnet-4-20250514","claude-opus-4-20250514","claude-haiku-4-20250514","claude-3-5-sonnet-20241022"];else if(t==="openai"){const h=n.replace(/\/v1\/.*$/,"");u=((await(await fetch(`${h}/v1/models`,{headers:i?{Authorization:`Bearer ${i}`}:{}})).json()).data||[]).map(v=>v.id).filter(v=>v.startsWith("gpt"))}if(u.length===0){s&&(s.innerHTML='<span style="color:var(--warn)">No models found</span>');return}const k=o.value;o.innerHTML=u.map(h=>`<option value="${h}">${h}</option>`).join(""),u.includes(k)&&(o.value=k),o.disabled=!1,s&&(s.innerHTML=`<span style="color:var(--success)">&#10003; ${u.length} models available</span>`)}catch{s&&(s.innerHTML='<span style="color:var(--danger)">&#10007; Connection failed</span>')}}function At(){var t,n,i;const e=document.getElementById("chat-modal-overlay");e&&(e.style.display="flex",ye("thinking",L.thinking),ye("vision",L.vision),ye("imageGen",L.imageGen),we("thinking"),we("vision"),we("imageGen"),(t=document.getElementById("set-thinking-connect"))==null||t.addEventListener("click",()=>$e("thinking")),(n=document.getElementById("set-vision-connect"))==null||n.addEventListener("click",()=>$e("vision")),(i=document.getElementById("set-imageGen-connect"))==null||i.addEventListener("click",()=>$e("imageGen")))}function _e(){const e=document.getElementById("chat-modal-overlay");e&&(e.style.display="none")}function jt(){Bt({thinking:ve("thinking"),vision:ve("vision"),imageGen:ve("imageGen")}),_e()}function U(){const e=document.getElementById("chat-summary");if(!e)return;const t=Y.length?Y.map(o=>`<div style="margin-bottom:4px;font-size:0.65rem;line-height:1.3">
328
328
  <span style="color:var(--muted)">${d(o.time)}</span>
329
329
  <span style="color:var(--info);font-size:0.6rem">${d(o.agent)}</span>
330
330
  <div>${d(o.text)}</div>
@@ -354,21 +354,21 @@ ${n.traceback||""}`;window.__switchTab("chat"),setTimeout(()=>{window.__prefillC
354
354
  <button class="btn btn-sm" style="font-size:0.6rem;padding:1px 6px" onclick="window.__replyMsg('${i}')" title="Reply">Reply</button>
355
355
  <button class="btn btn-sm btn-primary" style="font-size:0.6rem;padding:1px 6px;display:none" onclick="window.__submitAnswers('${i}')" title="Submit answers" data-submit-btn>Submit Answers</button>
356
356
  </div>
357
- `,o.addEventListener("mouseenter",()=>{const s=o.querySelector(".chat-msg-actions");s&&(s.style.opacity="1")}),o.addEventListener("mouseleave",()=>{const s=o.querySelector(".chat-msg-actions");s&&(s.style.opacity="0.4")}),o.querySelector(".chat-answer-input")){const s=o.querySelector("[data-submit-btn]");s&&(s.style.display="inline-block")}n.prepend(o)}function jt(e){const t=document.getElementById(e);if(!t)return;const n=t.querySelectorAll(".chat-answer-input"),i=[];if(n.forEach(c=>{const r=c.dataset.q||"?",p=c.value.trim();p&&(i.push(`${r}. ${p}`),c.disabled=!0,c.style.opacity="0.6")}),!i.length)return;const o=document.getElementById("chat-input");o&&(o.value=i.join(`
358
- `),K());const s=t.querySelector("[data-submit-btn]");s&&(s.style.display="none")}function Pt(e,t){const n=e.parentElement;if(!n)return;const i=n.querySelector(".chat-answer-input");i&&(i.value=t,i.disabled=!0,i.style.opacity="0.5"),n.querySelectorAll("button").forEach(s=>s.remove());const o=document.createElement("span");o.style.cssText="font-size:0.65rem;padding:2px 8px;border-radius:3px;background:var(--info);color:white",o.textContent=t,n.appendChild(o)}window.__quickAnswer=Pt,window.__submitAnswers=jt;function Ht(e){const t=document.querySelector(`#${e} .chat-msg-content`);t&&navigator.clipboard.writeText(t.textContent||"").then(()=>{const n=document.querySelector(`#${e} .chat-msg-actions button`);if(n){const i=n.textContent;n.textContent="Copied!",setTimeout(()=>{n.textContent=i},1e3)}})}function qt(e){const t=document.querySelector(`#${e} .chat-msg-content`);if(!t)return;const n=(t.textContent||"").substring(0,100),i=document.getElementById("chat-input");i&&(i.value=`> ${n}${n.length>=100?"...":""}
357
+ `,o.addEventListener("mouseenter",()=>{const s=o.querySelector(".chat-msg-actions");s&&(s.style.opacity="1")}),o.addEventListener("mouseleave",()=>{const s=o.querySelector(".chat-msg-actions");s&&(s.style.opacity="0.4")}),o.querySelector(".chat-answer-input")){const s=o.querySelector("[data-submit-btn]");s&&(s.style.display="inline-block")}n.prepend(o)}function Pt(e){const t=document.getElementById(e);if(!t)return;const n=t.querySelectorAll(".chat-answer-input"),i=[];if(n.forEach(c=>{const l=c.dataset.q||"?",p=c.value.trim();p&&(i.push(`${l}. ${p}`),c.disabled=!0,c.style.opacity="0.6")}),!i.length)return;const o=document.getElementById("chat-input");o&&(o.value=i.join(`
358
+ `),K());const s=t.querySelector("[data-submit-btn]");s&&(s.style.display="none")}function Ht(e,t){const n=e.parentElement;if(!n)return;const i=n.querySelector(".chat-answer-input");i&&(i.value=t,i.disabled=!0,i.style.opacity="0.5"),n.querySelectorAll("button").forEach(s=>s.remove());const o=document.createElement("span");o.style.cssText="font-size:0.65rem;padding:2px 8px;border-radius:3px;background:var(--info);color:white",o.textContent=t,n.appendChild(o)}window.__quickAnswer=Ht,window.__submitAnswers=Pt;function qt(e){const t=document.querySelector(`#${e} .chat-msg-content`);t&&navigator.clipboard.writeText(t.textContent||"").then(()=>{const n=document.querySelector(`#${e} .chat-msg-actions button`);if(n){const i=n.textContent;n.textContent="Copied!",setTimeout(()=>{n.textContent=i},1e3)}})}function Ot(e){const t=document.querySelector(`#${e} .chat-msg-content`);if(!t)return;const n=(t.textContent||"").substring(0,100),i=document.getElementById("chat-input");i&&(i.value=`> ${n}${n.length>=100?"...":""}
359
359
 
360
- `,i.focus(),i.setSelectionRange(i.value.length,i.value.length))}function Ot(e){var i,o;const t=e.closest(".chat-checklist-item");if(!t||(i=t.nextElementSibling)!=null&&i.classList.contains("chat-comment-box"))return;const n=document.createElement("div");n.className="chat-comment-box",n.style.cssText="padding-left:1.8rem;margin:0.15rem 0;display:flex;gap:4px",n.innerHTML=`
360
+ `,i.focus(),i.setSelectionRange(i.value.length,i.value.length))}function Rt(e){var i,o;const t=e.closest(".chat-checklist-item");if(!t||(i=t.nextElementSibling)!=null&&i.classList.contains("chat-comment-box"))return;const n=document.createElement("div");n.className="chat-comment-box",n.style.cssText="padding-left:1.8rem;margin:0.15rem 0;display:flex;gap:4px",n.innerHTML=`
361
361
  <input type="text" class="input" placeholder="Your comment..." style="flex:1;font-size:0.7rem;padding:2px 6px;height:24px">
362
362
  <button class="btn btn-sm" style="font-size:0.6rem;padding:1px 6px;height:24px" onclick="window.__submitComment(this)">Add</button>
363
- `,t.after(n),(o=n.querySelector("input"))==null||o.focus()}function Rt(e){var s;const t=e.closest(".chat-comment-box");if(!t)return;const n=t.querySelector("input"),i=(s=n==null?void 0:n.value)==null?void 0:s.trim();if(!i)return;const o=document.createElement("div");o.style.cssText="padding-left:1.8rem;margin:0.1rem 0;font-size:0.7rem;color:var(--info);font-style:italic",o.textContent=`↳ ${i}`,t.replaceWith(o)}function qe(){const e=[],t=[],n=[];return document.querySelectorAll(".chat-checklist-item").forEach(i=>{var r,p;const o=i.querySelector("input[type=checkbox]"),s=((r=i.querySelector("label"))==null?void 0:r.textContent)||"";o!=null&&o.checked?e.push(s):t.push(s);const c=i.nextElementSibling;if(c&&!c.classList.contains("chat-checklist-item")&&!c.classList.contains("chat-comment-box")){const u=((p=c.textContent)==null?void 0:p.replace("↳ ",""))||"";u&&n.push(`${s}: ${u}`)}}),{accepted:e,rejected:t,comments:n}}let le=!1;function Ee(){const e=document.getElementById("chat-thoughts-panel");e&&(le=!le,e.style.display=le?"block":"none",le&&Oe())}async function Oe(){const e=document.getElementById("thoughts-list");if(e)try{const i=(await(await fetch("/__dev/api/thoughts")).json()||[]).filter(s=>!s.dismissed),o=document.getElementById("thoughts-dot");if(o&&(o.style.display=i.length?"inline":"none"),!i.length){e.innerHTML='<div class="text-muted text-sm" style="text-align:center;padding:2rem 0">All clear. No observations.</div>';return}e.innerHTML=i.map(s=>`
363
+ `,t.after(n),(o=n.querySelector("input"))==null||o.focus()}function Nt(e){var s;const t=e.closest(".chat-comment-box");if(!t)return;const n=t.querySelector("input"),i=(s=n==null?void 0:n.value)==null?void 0:s.trim();if(!i)return;const o=document.createElement("div");o.style.cssText="padding-left:1.8rem;margin:0.1rem 0;font-size:0.7rem;color:var(--info);font-style:italic",o.textContent=`↳ ${i}`,t.replaceWith(o)}function Oe(){const e=[],t=[],n=[];return document.querySelectorAll(".chat-checklist-item").forEach(i=>{var l,p;const o=i.querySelector("input[type=checkbox]"),s=((l=i.querySelector("label"))==null?void 0:l.textContent)||"";o!=null&&o.checked?e.push(s):t.push(s);const c=i.nextElementSibling;if(c&&!c.classList.contains("chat-checklist-item")&&!c.classList.contains("chat-comment-box")){const u=((p=c.textContent)==null?void 0:p.replace("↳ ",""))||"";u&&n.push(`${s}: ${u}`)}}),{accepted:e,rejected:t,comments:n}}let le=!1;function Ee(){const e=document.getElementById("chat-thoughts-panel");e&&(le=!le,e.style.display=le?"block":"none",le&&Re())}async function Re(){const e=document.getElementById("thoughts-list");if(e)try{const i=(await(await fetch("/__dev/api/thoughts")).json()||[]).filter(s=>!s.dismissed),o=document.getElementById("thoughts-dot");if(o&&(o.style.display=i.length?"inline":"none"),!i.length){e.innerHTML='<div class="text-muted text-sm" style="text-align:center;padding:2rem 0">All clear. No observations.</div>';return}e.innerHTML=i.map(s=>`
364
364
  <div style="background:var(--bg);border:1px solid var(--border);border-radius:0.375rem;padding:0.5rem;margin-bottom:0.5rem;font-size:0.75rem">
365
365
  <div style="line-height:1.4">${d(s.message)}</div>
366
366
  <div style="display:flex;gap:4px;margin-top:0.375rem">
367
367
  ${(s.actions||[]).map(c=>c.action==="dismiss"?`<button class="btn btn-sm" style="font-size:0.6rem" onclick="window.__dismissThought('${d(s.id)}')">Dismiss</button>`:`<button class="btn btn-sm btn-primary" style="font-size:0.6rem" onclick="window.__actOnThought('${d(s.id)}','${d(c.action)}')">${d(c.label)}</button>`).join("")}
368
368
  </div>
369
369
  </div>
370
- `).join("")}catch{e.innerHTML='<div class="text-muted text-sm" style="text-align:center;padding:1rem">Agent not connected</div>'}}async function Re(e){await fetch("/__dev/api/thoughts/dismiss",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:e})}).catch(()=>{}),Oe()}function Nt(e,t){Re(e),Ee()}setInterval(async()=>{try{const n=(await(await fetch("/__dev/api/thoughts")).json()||[]).filter(o=>!o.dismissed),i=document.getElementById("thoughts-dot");i&&(i.style.display=n.length?"inline":"none")}catch{}},6e4),window.__dismissThought=Re,window.__actOnThought=Nt,window.__commentOnItem=Ot,window.__submitComment=Rt,window.__getChecklist=qe,window.__copyMsg=Ht,window.__replyMsg=qt;const Y=[];function Ne(e){const t=document.getElementById("chat-status-bar"),n=document.getElementById("chat-status-text");t&&(t.style.display="flex"),n&&(n.textContent=e)}function De(){const e=document.getElementById("chat-status-bar");e&&(e.style.display="none")}function de(e,t){const n=new Date().toLocaleTimeString([],{hour:"2-digit",minute:"2-digit",second:"2-digit"});Y.unshift({time:n,text:e,agent:t}),Y.length>50&&(Y.length=50),U()}async function K(){var i;const e=document.getElementById("chat-input"),t=(i=e==null?void 0:e.value)==null?void 0:i.trim();if(!t)return;if(e.value="",A(d(t),"user"),D.length){const o=D.map(s=>s.name).join(", ");A(`<span class="text-sm text-muted">Attached: ${d(o)}</span>`,"user")}H="Thinking...",Ne("Analyzing request..."),de("Analyzing request...","supervisor");const n={message:t,settings:{thinking:L.thinking,vision:L.vision,imageGen:L.imageGen}};D.length&&(n.files=D.map(o=>({name:o.name,data:o.data})));try{const o=await fetch("/__dev/api/chat",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)});if(!o.ok||!o.body){A(`<span style="color:var(--danger)">Error: ${o.statusText}</span>`,"bot"),H="Error",U();return}const s=o.body.getReader(),c=new TextDecoder;let r="";for(;;){const{done:p,value:u}=await s.read();if(p)break;r+=c.decode(u,{stream:!0});const _=r.split(`
371
- `);r=_.pop()||"";let k="";for(const b of _)if(b.startsWith("event: "))k=b.slice(7).trim();else if(b.startsWith("data: ")){const E=b.slice(6);try{const T=JSON.parse(E);Fe(k,T)}catch{}}}D.length=0,Te()}catch{A('<span style="color:var(--danger)">Connection failed</span>',"bot"),H="Error",U()}}function Fe(e,t){switch(e){case"status":H=t.text||"Working...",Ne(`${t.agent||"supervisor"}: ${t.text||"Working..."}`),de(t.text||"",t.agent||"supervisor");break;case"message":{const n=t.content||"",i=t.agent||"supervisor";let o=Yt(n);i!=="supervisor"&&(o=`<span class="badge" style="font-size:0.6rem;margin-right:4px">${d(i)}</span>`+o),t.files_changed&&t.files_changed.length>0&&(o+='<div style="margin-top:0.5rem;padding:0.5rem;background:var(--bg);border-radius:0.375rem;border:1px solid var(--border)">',o+='<div class="text-sm" style="color:var(--success);font-weight:600;margin-bottom:0.25rem">Files changed:</div>',t.files_changed.forEach(s=>{o+=`<div class="text-sm text-mono">${d(s)}</div>`,re.includes(s)||re.push(s)}),o+="</div>"),A(o,"bot");break}case"plan":if(t.approve){const n=`
370
+ `).join("")}catch{e.innerHTML='<div class="text-muted text-sm" style="text-align:center;padding:1rem">Agent not connected</div>'}}async function Ne(e){await fetch("/__dev/api/thoughts/dismiss",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:e})}).catch(()=>{}),Re()}function Dt(e,t){Ne(e),Ee()}setInterval(async()=>{try{const n=(await(await fetch("/__dev/api/thoughts")).json()||[]).filter(o=>!o.dismissed),i=document.getElementById("thoughts-dot");i&&(i.style.display=n.length?"inline":"none")}catch{}},6e4),window.__dismissThought=Ne,window.__actOnThought=Dt,window.__commentOnItem=Rt,window.__submitComment=Nt,window.__getChecklist=Oe,window.__copyMsg=qt,window.__replyMsg=Ot;const Y=[];function De(e){const t=document.getElementById("chat-status-bar"),n=document.getElementById("chat-status-text");t&&(t.style.display="flex"),n&&(n.textContent=e)}function Fe(){const e=document.getElementById("chat-status-bar");e&&(e.style.display="none")}function de(e,t){const n=new Date().toLocaleTimeString([],{hour:"2-digit",minute:"2-digit",second:"2-digit"});Y.unshift({time:n,text:e,agent:t}),Y.length>50&&(Y.length=50),U()}async function K(){var i;const e=document.getElementById("chat-input"),t=(i=e==null?void 0:e.value)==null?void 0:i.trim();if(!t)return;if(e.value="",A(d(t),"user"),D.length){const o=D.map(s=>s.name).join(", ");A(`<span class="text-sm text-muted">Attached: ${d(o)}</span>`,"user")}H="Thinking...",De("Analyzing request..."),de("Analyzing request...","supervisor");const n={message:t,settings:{thinking:L.thinking,vision:L.vision,imageGen:L.imageGen}};D.length&&(n.files=D.map(o=>({name:o.name,data:o.data})));try{const o=await fetch("/__dev/api/chat",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)});if(!o.ok||!o.body){A(`<span style="color:var(--danger)">Error: ${o.statusText}</span>`,"bot"),H="Error",U();return}const s=o.body.getReader(),c=new TextDecoder;let l="";for(;;){const{done:p,value:u}=await s.read();if(p)break;l+=c.decode(u,{stream:!0});const _=l.split(`
371
+ `);l=_.pop()||"";let k="";for(const h of _)if(h.startsWith("event: "))k=h.slice(7).trim();else if(h.startsWith("data: ")){const E=h.slice(6);try{const T=JSON.parse(E);We(k,T)}catch{}}}D.length=0,Te()}catch{A('<span style="color:var(--danger)">Connection failed</span>',"bot"),H="Error",U()}}function We(e,t){switch(e){case"status":H=t.text||"Working...",De(`${t.agent||"supervisor"}: ${t.text||"Working..."}`),de(t.text||"",t.agent||"supervisor");break;case"message":{const n=t.content||"",i=t.agent||"supervisor";let o=Kt(n);i!=="supervisor"&&(o=`<span class="badge" style="font-size:0.6rem;margin-right:4px">${d(i)}</span>`+o),t.files_changed&&t.files_changed.length>0&&(o+='<div style="margin-top:0.5rem;padding:0.5rem;background:var(--bg);border-radius:0.375rem;border:1px solid var(--border)">',o+='<div class="text-sm" style="color:var(--success);font-weight:600;margin-bottom:0.25rem">Files changed:</div>',t.files_changed.forEach(s=>{o+=`<div class="text-sm text-mono">${d(s)}</div>`,re.includes(s)||re.push(s)}),o+="</div>"),A(o,"bot");break}case"plan":if(t.approve){const n=`
372
372
  <div style="padding:0.5rem;background:var(--surface);border:1px solid var(--info);border-radius:0.375rem;margin-top:0.25rem">
373
373
  <div class="text-sm" style="color:var(--info);font-weight:600;margin-bottom:0.25rem">Plan ready: ${d(t.file||"")}</div>
374
374
  <div class="text-sm text-muted" style="margin-bottom:0.5rem">Uncheck items you don't want. Click + to add comments. Then choose an action.</div>
@@ -379,8 +379,8 @@ ${n.traceback||""}`;window.__switchTab("chat"),setTimeout(()=>{window.__prefillC
379
379
  <button class="btn btn-sm" onclick="this.parentElement.parentElement.remove()">Dismiss</button>
380
380
  </div>
381
381
  </div>
382
- `;A(n,"bot")}break;case"error":De(),A(`<span style="color:var(--danger)">${d(t.message||"Unknown error")}</span>`,"bot"),H="Error",U();break;case"done":H="Done",De(),de("Done","supervisor"),setTimeout(()=>{H="Idle",U()},3e3);break}}async function Dt(e){A(`<span style="color:var(--success)">Plan approved: ${d(e)}</span>`,"user"),H="Executing plan...",de("Plan approved — executing...","supervisor");const t={message:`Execute the plan in ${e}. Write all the files now.`,settings:{thinking:L.thinking,vision:L.vision,imageGen:L.imageGen}};try{const n=await fetch("/__dev/api/chat",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)});if(!n.ok||!n.body)return;const i=n.body.getReader(),o=new TextDecoder;let s="";for(;;){const{done:c,value:r}=await i.read();if(c)break;s+=o.decode(r,{stream:!0});const p=s.split(`
383
- `);s=p.pop()||"";let u="";for(const _ of p)if(_.startsWith("event: "))u=_.slice(7).trim();else if(_.startsWith("data: "))try{Fe(u,JSON.parse(_.slice(6)))}catch{}}}catch{A('<span style="color:var(--danger)">Plan execution failed</span>',"bot")}}function Ft(e){A(`<span style="color:var(--muted)">Plan saved for later: ${d(e)}</span>`,"bot")}function Wt(){const{accepted:e,rejected:t,comments:n}=qe();let i=`Here's my feedback on the proposal:
382
+ `;A(n,"bot")}break;case"error":Fe(),A(`<span style="color:var(--danger)">${d(t.message||"Unknown error")}</span>`,"bot"),H="Error",U();break;case"done":H="Done",Fe(),de("Done","supervisor"),setTimeout(()=>{H="Idle",U()},3e3);break}}async function Ft(e){A(`<span style="color:var(--success)">Plan approved: ${d(e)}</span>`,"user"),H="Executing plan...",de("Plan approved — executing...","supervisor");const t={message:`Execute the plan in ${e}. Write all the files now.`,settings:{thinking:L.thinking,vision:L.vision,imageGen:L.imageGen}};try{const n=await fetch("/__dev/api/chat",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)});if(!n.ok||!n.body)return;const i=n.body.getReader(),o=new TextDecoder;let s="";for(;;){const{done:c,value:l}=await i.read();if(c)break;s+=o.decode(l,{stream:!0});const p=s.split(`
383
+ `);s=p.pop()||"";let u="";for(const _ of p)if(_.startsWith("event: "))u=_.slice(7).trim();else if(_.startsWith("data: "))try{We(u,JSON.parse(_.slice(6)))}catch{}}}catch{A('<span style="color:var(--danger)">Plan execution failed</span>',"bot")}}function Wt(e){A(`<span style="color:var(--muted)">Plan saved for later: ${d(e)}</span>`,"bot")}function Gt(){const{accepted:e,rejected:t,comments:n}=Oe();let i=`Here's my feedback on the proposal:
384
384
 
385
385
  `;e.length&&(i+=`**Keep these:**
386
386
  `+e.map(s=>`- ${s}`).join(`
@@ -394,11 +394,11 @@ ${n.traceback||""}`;window.__switchTab("chat"),setTimeout(()=>{window.__prefillC
394
394
  `+n.map(s=>`- ${s}`).join(`
395
395
  `)+`
396
396
 
397
- `),!t.length&&!n.length&&(i+="Everything looks good. "),i+="Please revise the plan based on this feedback.";const o=document.getElementById("chat-input");o&&(o.value=i,K())}window.__submitFeedback=Wt,window.__approvePlan=Dt,window.__keepPlan=Ft;async function Gt(){try{const e=await z("/chat/undo","POST");A(`<span style="color:var(--warn)">${d(e.message||"Undo complete")}</span>`,"bot")}catch{A('<span style="color:var(--warn)">Nothing to undo</span>',"bot")}}const D=[];function Ut(){const e=document.getElementById("chat-file-input");e!=null&&e.files&&(document.getElementById("chat-attachments"),Array.from(e.files).forEach(t=>{const n=new FileReader;n.onload=()=>{D.push({name:t.name,data:n.result}),Te()},n.readAsDataURL(t)}),e.value="")}function Te(){const e=document.getElementById("chat-attachments");if(e){if(!D.length){e.style.display="none";return}e.style.display="flex",e.style.cssText+="gap:0.375rem;flex-wrap:wrap;margin-bottom:0.375rem;font-size:0.75rem",e.innerHTML=D.map((t,n)=>`<span style="background:var(--surface);border:1px solid var(--border);border-radius:4px;padding:2px 8px;display:inline-flex;align-items:center;gap:4px">
397
+ `),!t.length&&!n.length&&(i+="Everything looks good. "),i+="Please revise the plan based on this feedback.";const o=document.getElementById("chat-input");o&&(o.value=i,K())}window.__submitFeedback=Gt,window.__approvePlan=Ft,window.__keepPlan=Wt;async function Ut(){try{const e=await z("/chat/undo","POST");A(`<span style="color:var(--warn)">${d(e.message||"Undo complete")}</span>`,"bot")}catch{A('<span style="color:var(--warn)">Nothing to undo</span>',"bot")}}const D=[];function Vt(){const e=document.getElementById("chat-file-input");e!=null&&e.files&&(document.getElementById("chat-attachments"),Array.from(e.files).forEach(t=>{const n=new FileReader;n.onload=()=>{D.push({name:t.name,data:n.result}),Te()},n.readAsDataURL(t)}),e.value="")}function Te(){const e=document.getElementById("chat-attachments");if(e){if(!D.length){e.style.display="none";return}e.style.display="flex",e.style.cssText+="gap:0.375rem;flex-wrap:wrap;margin-bottom:0.375rem;font-size:0.75rem",e.innerHTML=D.map((t,n)=>`<span style="background:var(--surface);border:1px solid var(--border);border-radius:4px;padding:2px 8px;display:inline-flex;align-items:center;gap:4px">
398
398
  ${d(t.name)} <span style="cursor:pointer;color:var(--danger)" onclick="window.__removeFile(${n})">&times;</span>
399
- </span>`).join("")}}function Vt(e){D.splice(e,1),Te()}let X=!1,q=null;function Jt(){const e=document.getElementById("chat-mic-btn"),t=window.SpeechRecognition||window.webkitSpeechRecognition;if(!t){A('<span style="color:var(--warn)">Speech recognition not supported in this browser</span>',"bot");return}if(X&&q){q.stop(),X=!1,e&&(e.textContent="Mic",e.style.background="");return}q=new t,q.continuous=!1,q.interimResults=!1,q.lang="en-US",q.onresult=n=>{const i=n.results[0][0].transcript,o=document.getElementById("chat-input");o&&(o.value=(o.value?o.value+" ":"")+i)},q.onend=()=>{X=!1,e&&(e.textContent="Mic",e.style.background="")},q.onerror=()=>{X=!1,e&&(e.textContent="Mic",e.style.background="")},q.start(),X=!0,e&&(e.textContent="Stop",e.style.background="var(--danger)")}window.__removeFile=Vt;function Yt(e){let t=e.replace(/\\n/g,`
400
- `);const n=[];t=t.replace(/```(\w*)\n([\s\S]*?)```/g,(c,r,p)=>{const u=n.length;return n.push(`<pre style="background:var(--bg);padding:0.75rem;border-radius:0.375rem;overflow-x:auto;margin:0.5rem 0;font-size:0.75rem;border:1px solid var(--border)"><code>${p}</code></pre>`),`\0CODE${u}\0`});const i=t.split(`
401
- `),o=[];for(const c of i){const r=c.trim();if(r.startsWith("\0CODE")){o.push(r);continue}if(r.startsWith("### ")){o.push(`<div style="font-weight:700;font-size:0.8rem;margin:0.75rem 0 0.25rem;color:var(--info)">${r.slice(4)}</div>`);continue}if(r.startsWith("## ")){o.push(`<div style="font-weight:700;font-size:0.9rem;margin:0.75rem 0 0.25rem">${r.slice(3)}</div>`);continue}if(r.startsWith("# ")){o.push(`<div style="font-weight:700;font-size:1rem;margin:0.75rem 0 0.25rem">${r.slice(2)}</div>`);continue}if(r==="---"||r==="***"){o.push('<hr style="border:none;border-top:1px solid var(--border);margin:0.5rem 0">');continue}const p=r.match(/^(\d+)[.)]\s+(.+)/);if(p){if(p[2].trim().endsWith("?")){const _=`q-${ke}-${p[1]}`;o.push(`<div style="margin:0.3rem 0;padding-left:0.5rem">
399
+ </span>`).join("")}}function Jt(e){D.splice(e,1),Te()}let X=!1,q=null;function Yt(){const e=document.getElementById("chat-mic-btn"),t=window.SpeechRecognition||window.webkitSpeechRecognition;if(!t){A('<span style="color:var(--warn)">Speech recognition not supported in this browser</span>',"bot");return}if(X&&q){q.stop(),X=!1,e&&(e.textContent="Mic",e.style.background="");return}q=new t,q.continuous=!1,q.interimResults=!1,q.lang="en-US",q.onresult=n=>{const i=n.results[0][0].transcript,o=document.getElementById("chat-input");o&&(o.value=(o.value?o.value+" ":"")+i)},q.onend=()=>{X=!1,e&&(e.textContent="Mic",e.style.background="")},q.onerror=()=>{X=!1,e&&(e.textContent="Mic",e.style.background="")},q.start(),X=!0,e&&(e.textContent="Stop",e.style.background="var(--danger)")}window.__removeFile=Jt;function Kt(e){let t=e.replace(/\\n/g,`
400
+ `);const n=[];t=t.replace(/```(\w*)\n([\s\S]*?)```/g,(c,l,p)=>{const u=n.length;return n.push(`<pre style="background:var(--bg);padding:0.75rem;border-radius:0.375rem;overflow-x:auto;margin:0.5rem 0;font-size:0.75rem;border:1px solid var(--border)"><code>${p}</code></pre>`),`\0CODE${u}\0`});const i=t.split(`
401
+ `),o=[];for(const c of i){const l=c.trim();if(l.startsWith("\0CODE")){o.push(l);continue}if(l.startsWith("### ")){o.push(`<div style="font-weight:700;font-size:0.8rem;margin:0.75rem 0 0.25rem;color:var(--info)">${l.slice(4)}</div>`);continue}if(l.startsWith("## ")){o.push(`<div style="font-weight:700;font-size:0.9rem;margin:0.75rem 0 0.25rem">${l.slice(3)}</div>`);continue}if(l.startsWith("# ")){o.push(`<div style="font-weight:700;font-size:1rem;margin:0.75rem 0 0.25rem">${l.slice(2)}</div>`);continue}if(l==="---"||l==="***"){o.push('<hr style="border:none;border-top:1px solid var(--border);margin:0.5rem 0">');continue}const p=l.match(/^(\d+)[.)]\s+(.+)/);if(p){if(p[2].trim().endsWith("?")){const _=`q-${ke}-${p[1]}`;o.push(`<div style="margin:0.3rem 0;padding-left:0.5rem">
402
402
  <div style="margin-bottom:4px"><span style="color:var(--info);font-weight:600;margin-right:0.4rem">${p[1]}.</span>${Q(p[2])}</div>
403
403
  <div style="display:flex;gap:4px;align-items:center;flex-wrap:wrap">
404
404
  <input type="text" class="input chat-answer-input" id="${_}" data-q="${p[1]}" placeholder="Your answer..." style="font-size:0.75rem;padding:4px 8px;flex:1;max-width:350px">
@@ -407,20 +407,20 @@ ${n.traceback||""}`;window.__switchTab("chat"),setTimeout(()=>{window.__prefillC
407
407
  <button class="btn btn-sm" style="font-size:0.6rem;padding:2px 6px" onclick="window.__quickAnswer(this,'Later')">Later</button>
408
408
  <button class="btn btn-sm" style="font-size:0.6rem;padding:2px 6px" onclick="window.__quickAnswer(this,'Skip')">Skip</button>
409
409
  </div>
410
- </div>`)}else o.push(`<div style="margin:0.15rem 0;padding-left:1.5rem"><span style="color:var(--info);font-weight:600;margin-right:0.4rem">${p[1]}.</span>${Q(p[2])}</div>`);continue}if(r.startsWith("- ")){const u=`chk-${ke}-${o.length}`,_=r.slice(2);o.push(`<div style="margin:0.15rem 0;padding-left:0.5rem;display:flex;align-items:flex-start;gap:6px" class="chat-checklist-item">
410
+ </div>`)}else o.push(`<div style="margin:0.15rem 0;padding-left:1.5rem"><span style="color:var(--info);font-weight:600;margin-right:0.4rem">${p[1]}.</span>${Q(p[2])}</div>`);continue}if(l.startsWith("- ")){const u=`chk-${ke}-${o.length}`,_=l.slice(2);o.push(`<div style="margin:0.15rem 0;padding-left:0.5rem;display:flex;align-items:flex-start;gap:6px" class="chat-checklist-item">
411
411
  <input type="checkbox" id="${u}" checked style="margin-top:3px;cursor:pointer;accent-color:var(--success)">
412
412
  <label for="${u}" style="flex:1;cursor:pointer">${Q(_)}</label>
413
413
  <button class="btn btn-sm" style="font-size:0.55rem;padding:1px 4px;opacity:0.5;flex-shrink:0" onclick="window.__commentOnItem(this)" title="Add comment">+</button>
414
- </div>`);continue}if(r.startsWith("> ")){o.push(`<div style="border-left:3px solid var(--info);padding-left:0.75rem;margin:0.3rem 0;color:var(--muted);font-style:italic">${Q(r.slice(2))}</div>`);continue}if(r===""){o.push('<div style="height:0.4rem"></div>');continue}o.push(`<div style="margin:0.1rem 0">${Q(r)}</div>`)}let s=o.join("");return n.forEach((c,r)=>{s=s.replace(`\0CODE${r}\0`,c)}),s}function Q(e){return e.replace(/\*\*(.+?)\*\*/g,"<strong>$1</strong>").replace(/\*(.+?)\*/g,"<em>$1</em>").replace(/`([^`]+)`/g,'<code style="background:var(--bg);padding:0.1rem 0.3rem;border-radius:0.2rem;font-size:0.8em;border:1px solid var(--border)">$1</code>')}function Kt(e){const t=document.getElementById("chat-input");t&&(t.value=e,t.focus(),t.scrollTop=t.scrollHeight)}window.__sendChat=K,window.__undoChat=Gt,window.__prefillChat=Kt;const We=document.createElement("style");We.textContent=rt,document.head.appendChild(We);const Ge=st();at(Ge);const Se=[{id:"routes",label:"Routes",render:dt},{id:"database",label:"Database",render:ct},{id:"errors",label:"Errors",render:xt},{id:"metrics",label:"Metrics",render:St},{id:"system",label:"System",render:kt}],Ue={id:"chat",label:"Code With Me",render:Bt};let ce=localStorage.getItem("tina4_cwm_unlocked")==="true",me=ce?[Ue,...Se]:[...Se],Z=ce?"chat":"routes";function Xt(){const e=document.getElementById("app");if(!e)return;e.innerHTML=`
414
+ </div>`);continue}if(l.startsWith("> ")){o.push(`<div style="border-left:3px solid var(--info);padding-left:0.75rem;margin:0.3rem 0;color:var(--muted);font-style:italic">${Q(l.slice(2))}</div>`);continue}if(l===""){o.push('<div style="height:0.4rem"></div>');continue}o.push(`<div style="margin:0.1rem 0">${Q(l)}</div>`)}let s=o.join("");return n.forEach((c,l)=>{s=s.replace(`\0CODE${l}\0`,c)}),s}function Q(e){return e.replace(/\*\*(.+?)\*\*/g,"<strong>$1</strong>").replace(/\*(.+?)\*/g,"<em>$1</em>").replace(/`([^`]+)`/g,'<code style="background:var(--bg);padding:0.1rem 0.3rem;border-radius:0.2rem;font-size:0.8em;border:1px solid var(--border)">$1</code>')}function Xt(e){const t=document.getElementById("chat-input");t&&(t.value=e,t.focus(),t.scrollTop=t.scrollHeight)}window.__sendChat=K,window.__undoChat=Ut,window.__prefillChat=Xt;const Ge=document.createElement("style");Ge.textContent=lt,document.head.appendChild(Ge);const Ue=at();rt(Ue);const Se=[{id:"routes",label:"Routes",render:ct},{id:"database",label:"Database",render:mt},{id:"errors",label:"Errors",render:wt},{id:"metrics",label:"Metrics",render:It},{id:"system",label:"System",render:Et}],Ve={id:"chat",label:"Code With Me",render:zt};let ce=localStorage.getItem("tina4_cwm_unlocked")==="true",me=ce?[Ve,...Se]:[...Se],Z=ce?"chat":"routes";function Qt(){const e=document.getElementById("app");if(!e)return;e.innerHTML=`
415
415
  <div class="dev-admin">
416
416
  <div class="dev-header">
417
417
  <h1><span>Tina4</span> Dev Admin</h1>
418
418
  <div style="display:flex;align-items:center;gap:0.75rem">
419
- <span class="text-sm text-muted" id="version-label" style="cursor:default;user-select:none">${Ge.name} &bull; v3.10.70</span>
419
+ <span class="text-sm text-muted" id="version-label" style="cursor:default;user-select:none">${Ue.name} &bull; v3.10.70</span>
420
420
  <button class="btn btn-sm" onclick="window.__closeDevAdmin()" title="Close Dev Admin" style="font-size:14px;width:28px;height:28px;padding:0;line-height:1">&times;</button>
421
421
  </div>
422
422
  </div>
423
423
  <div class="dev-tabs" id="tab-bar"></div>
424
424
  <div class="dev-content" id="tab-content"></div>
425
425
  </div>
426
- `;const t=document.getElementById("tab-bar");t.innerHTML=me.map(n=>`<button class="dev-tab ${n.id===Z?"active":""}" data-tab="${n.id}" onclick="window.__switchTab('${n.id}')">${n.label}</button>`).join(""),Ie(Z)}function Ie(e){Z=e,document.querySelectorAll(".dev-tab").forEach(o=>{o.classList.toggle("active",o.dataset.tab===e)});const t=document.getElementById("tab-content");if(!t)return;const n=document.createElement("div");n.className="dev-panel active",t.innerHTML="",t.appendChild(n);const i=me.find(o=>o.id===e);i&&i.render(n)}function Qt(){if(window.parent!==window)try{const e=window.parent.document.getElementById("tina4-dev-panel");e&&e.remove()}catch{document.body.style.display="none"}}window.__closeDevAdmin=Qt,window.__switchTab=Ie,Xt();let Me=0,Ce=null;(Ve=document.getElementById("version-label"))==null||Ve.addEventListener("click",()=>{if(!ce&&(Me++,Ce&&clearTimeout(Ce),Ce=setTimeout(()=>{Me=0},2e3),Me>=5)){ce=!0,localStorage.setItem("tina4_cwm_unlocked","true"),me=[Ue,...Se],Z="chat";const e=document.getElementById("tab-bar");e&&(e.innerHTML=me.map(t=>`<button class="dev-tab ${t.id===Z?"active":""}" data-tab="${t.id}" onclick="window.__switchTab('${t.id}')">${t.label}</button>`).join("")),Ie("chat")}})})();
426
+ `;const t=document.getElementById("tab-bar");t.innerHTML=me.map(n=>`<button class="dev-tab ${n.id===Z?"active":""}" data-tab="${n.id}" onclick="window.__switchTab('${n.id}')">${n.label}</button>`).join(""),Ie(Z)}function Ie(e){Z=e,document.querySelectorAll(".dev-tab").forEach(o=>{o.classList.toggle("active",o.dataset.tab===e)});const t=document.getElementById("tab-content");if(!t)return;const n=document.createElement("div");n.className="dev-panel active",t.innerHTML="",t.appendChild(n);const i=me.find(o=>o.id===e);i&&i.render(n)}function Zt(){if(window.parent!==window)try{const e=window.parent.document.getElementById("tina4-dev-panel");e&&e.remove()}catch{document.body.style.display="none"}}window.__closeDevAdmin=Zt,window.__switchTab=Ie,Qt();let Me=0,Ce=null;(Je=document.getElementById("version-label"))==null||Je.addEventListener("click",()=>{if(!ce&&(Me++,Ce&&clearTimeout(Ce),Ce=setTimeout(()=>{Me=0},2e3),Me>=5)){ce=!0,localStorage.setItem("tina4_cwm_unlocked","true"),me=[Ve,...Se],Z="chat";const e=document.getElementById("tab-bar");e&&(e.innerHTML=me.map(t=>`<button class="dev-tab ${t.id===Z?"active":""}" data-tab="${t.id}" onclick="window.__switchTab('${t.id}')">${t.label}</button>`).join("")),Ie("chat")}})})();
@@ -63,56 +63,70 @@ function hasMatchingTest(relPath: string): boolean {
63
63
  const parts = relPath.split('/');
64
64
  const basename = parts[parts.length - 1] || '';
65
65
  const name = basename.replace(/\.(ts|js)$/, '');
66
- // Parent directory name (e.g. "adapters" from "adapters/sqlite.ts")
67
66
  const parentModule = parts.length > 1 ? parts[parts.length - 2] : '';
68
67
 
69
- // Stage 1: Filename matching name.test, name.spec, test_name patterns
70
- const testDirs = ['test', 'tests', 'spec'];
71
- for (const td of testDirs) {
72
- const patterns = [
73
- `${td}/${name}.test.ts`,
74
- `${td}/${name}.test.js`,
75
- `${td}/${name}.spec.ts`,
76
- `${td}/${name}.spec.js`,
77
- // Also check parent-named tests (test/database.test.ts covers adapters/sqlite.ts)
78
- ...(parentModule && parentModule !== name ? [
79
- `${td}/${parentModule}.test.ts`,
80
- `${td}/${parentModule}.test.js`,
81
- `${td}/${parentModule}.spec.ts`,
82
- ] : []),
83
- ];
84
- if (patterns.some(p => fs.existsSync(p))) {
85
- return true;
68
+ // Search both CWD and the scan root (framework dir when in fallback mode)
69
+ const searchRoots = [process.cwd()];
70
+ if (_lastScanRoot && _lastScanRoot !== process.cwd()) {
71
+ // Go up from scan root to find the repo root (where test/ lives)
72
+ let repoRoot = _lastScanRoot;
73
+ // Walk up until we find a test/ or tests/ dir, max 5 levels
74
+ for (let i = 0; i < 5; i++) {
75
+ if (fs.existsSync(path.join(repoRoot, 'test')) || fs.existsSync(path.join(repoRoot, 'tests')) || fs.existsSync(path.join(repoRoot, 'spec'))) {
76
+ searchRoots.push(repoRoot);
77
+ break;
78
+ }
79
+ const parent = path.dirname(repoRoot);
80
+ if (parent === repoRoot) break;
81
+ repoRoot = parent;
86
82
  }
87
83
  }
88
84
 
89
- // Build the module import path for content matching
90
- // e.g. "packages/core/src/metrics.ts" → "metrics" or "src/metrics"
91
- const pathWithoutExt = relPath.replace(/\.(ts|js)$/, '');
92
- // Build CamelCase class name from camelCase/kebab module name
93
- // e.g. "sqlTranslation" "SqlTranslation", "sql-translation" → "SqlTranslation"
94
- const className = name
95
- .replace(/[-_](.)/g, (_, c: string) => c.toUpperCase())
96
- .replace(/^(.)/, (_, c: string) => c.toUpperCase());
97
-
98
- // Stage 2+3: Content scan — check if any test file imports or references this module
99
- for (const td of testDirs) {
100
- if (!fs.existsSync(td)) continue;
101
- const testFiles = walkFiles(td, ['.ts', '.js']);
102
- for (const testFile of testFiles) {
103
- const content = readFileSafe(testFile);
104
- if (content === null) continue;
105
- // Stage 2: import path matching
106
- if (content.includes(name) && (
107
- content.includes(`"${name}"`) || content.includes(`'${name}'`) ||
108
- content.includes(`/${name}"`) || content.includes(`/${name}'`) ||
109
- content.includes(pathWithoutExt)
110
- )) {
111
- return true;
112
- }
113
- // Stage 3: class name mention
114
- if (className !== name && new RegExp(`\\b${className.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(content)) {
115
- return true;
85
+ const testDirs = ['test', 'tests', 'spec'];
86
+
87
+ for (const root of searchRoots) {
88
+ // Stage 1: Filename matching
89
+ for (const td of testDirs) {
90
+ const patterns = [
91
+ path.join(root, td, `${name}.test.ts`),
92
+ path.join(root, td, `${name}.test.js`),
93
+ path.join(root, td, `${name}.spec.ts`),
94
+ path.join(root, td, `${name}.spec.js`),
95
+ path.join(root, td, `test_${name}.ts`),
96
+ path.join(root, td, `test_${name}.js`),
97
+ path.join(root, td, `test_${name}.py`),
98
+ path.join(root, td, `${name}_test.rb`),
99
+ path.join(root, td, `${name}_spec.rb`),
100
+ ...(parentModule && parentModule !== name ? [
101
+ path.join(root, td, `${parentModule}.test.ts`),
102
+ path.join(root, td, `${parentModule}.test.js`),
103
+ path.join(root, td, `${parentModule}.spec.ts`),
104
+ ] : []),
105
+ ];
106
+ if (patterns.some(p => fs.existsSync(p))) return true;
107
+ }
108
+
109
+ // Stage 2+3: Content scan
110
+ const pathWithoutExt = relPath.replace(/\.(ts|js|py|rb|php)$/, '');
111
+ const className = name
112
+ .replace(/[-_](.)/g, (_: string, c: string) => c.toUpperCase())
113
+ .replace(/^(.)/, (_: string, c: string) => c.toUpperCase());
114
+
115
+ for (const td of testDirs) {
116
+ const fullTd = path.join(root, td);
117
+ if (!fs.existsSync(fullTd)) continue;
118
+ const testFiles = walkFiles(fullTd, ['.ts', '.js', '.py', '.rb']);
119
+ for (const testFile of testFiles) {
120
+ const content = readFileSafe(testFile);
121
+ if (content === null) continue;
122
+ if (content.includes(name) && (
123
+ content.includes(`"${name}"`) || content.includes(`'${name}'`) ||
124
+ content.includes(`/${name}"`) || content.includes(`/${name}'`) ||
125
+ content.includes(pathWithoutExt)
126
+ )) return true;
127
+ if (className !== name && new RegExp(`\\b${className.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(content)) {
128
+ return true;
129
+ }
116
130
  }
117
131
  }
118
132
  }
@@ -113,10 +113,12 @@ export class Router {
113
113
  routes.splice(existingIndex, 1);
114
114
  }
115
115
 
116
- // Write methods (POST/PUT/PATCH/DELETE) are secure by default
116
+ // Write methods (POST/PUT/PATCH/DELETE) are secure by default,
117
+ // unless custom middleware is registered (developer handles auth themselves)
117
118
  const WRITE_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
118
119
  const isWrite = WRITE_METHODS.has(method);
119
- const secureDefault = isWrite ? (definition.secure ?? true) : definition.secure;
120
+ const hasMiddleware = definition.middlewares && definition.middlewares.length > 0;
121
+ const secureDefault = isWrite && !hasMiddleware ? (definition.secure ?? true) : definition.secure;
120
122
 
121
123
  const compiled: CompiledRoute = {
122
124
  pattern: definition.pattern,