(cwd: string)
| 288 | function toast(msg, ok){ const t=document.getElementById('toast'); t.textContent=msg; t.style.borderColor=ok?'var(--green)':'#ff6b6b'; t.classList.add('show'); setTimeout(()=>t.classList.remove('show'),2600); } |
| 289 | async function act(action, params){ |
| 290 | try{ |
| 291 | const r = await fetch('/api/action?k='+encodeURIComponent(TOKEN),{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({action,params})}); |
| 292 | const j = await r.json(); |
| 293 | toast(j.message || (j.ok?'Done':'Failed'), j.ok); |
| 294 | if(j.ok) setTimeout(()=>location.reload(), 700); |
| 295 | }catch(e){ toast(String(e), false); } |
| 296 | } |
| 297 | async function recallRun(){ |
| 298 | const q = document.getElementById('recall-q').value.trim(); |
| 299 | if(!q) return; |
| 300 | const out = document.getElementById('recall-out'); |
| 301 | out.style.display = 'block'; out.textContent = 'Searching your history…'; |
| 302 | try{ |
| 303 | const r = await fetch('/api/action?k='+encodeURIComponent(TOKEN),{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({action:'recall.query',params:{query:q}})}); |
| 304 | const j = await r.json(); |
| 305 | out.textContent = j.message || (j.ok ? '(no result)' : 'Failed'); |
| 306 | }catch(e){ out.textContent = String(e); } |
| 307 | } |
| 308 | ` : ''} |
| 309 | </script></body></html>`; |
| 310 | } |
| 311 | |
| 312 | /** Gather the dashboard's data from the live stores + config. Defensive — a missing source is empty. */ |
| 313 | export async function gatherDashboardData(cwd: string): Promise<DashboardData> { |
| 314 | const { loadConfig } = await import('../config/loader.js'); |
| 315 | const { getSessionStore } = await import('../session/store.js'); |
| 316 | const config: any = await loadConfig(cwd).catch(() => ({ defaults: {}, providers: {} })); |
| 317 | const store = getSessionStore(); |
| 318 | |
| 319 | const project = (() => { try { return store.getProject(cwd)?.name || path.basename(cwd); } catch { return path.basename(cwd); } })(); |
| 320 | const defModel = config?.defaults?.model ?? '(unset)'; |
| 321 | const defProvider = config?.defaults?.provider; |
| 322 | const roleModels = await (async () => { |
| 323 | try { |
| 324 | const { looksVisionCapable } = await import('../setup/model-detector.js'); |
| 325 | return { |
| 326 | subagent: config?.roles?.subagent?.model as string | undefined, |
| 327 | vision: config?.roles?.vision?.model as string | undefined, |
| 328 | mainHasVision: typeof defModel === 'string' && looksVisionCapable(defModel), |
| 329 | }; |
| 330 | } catch { return { subagent: undefined, vision: undefined, mainHasVision: false }; } |
| 331 | })(); |
| 332 | |
| 333 | // Providers: built-ins present in config + user customs. |
| 334 | const providers: DashboardData['providers'] = []; |
| 335 | const pcfg = config?.providers ?? {}; |
| 336 | for (const [name, v] of Object.entries<any>(pcfg)) { |
| 337 | if (name === 'custom') continue; |
| 338 | providers.push({ |
| 339 | name, baseUrl: v?.baseUrl ?? '', keyEnv: v?.apiKeyEnv, |
| 340 | keySet: v?.apiKeyEnv ? !!process.env[v.apiKeyEnv] : undefined, |
| 341 | models: (v?.extraModels ?? []).map((m: any) => m.id).filter(Boolean), |
| 342 | isDefault: name === defProvider, |
| 343 | }); |
| 344 | } |
| 345 | for (const c of (pcfg.custom ?? [])) { |
| 346 | providers.push({ |
| 347 | name: c.name, baseUrl: c.baseUrl ?? '', keyEnv: c.apiKeyEnv, |
no test coverage detected