@oddessentials/agent-guild 0.1.1 → 0.3.0
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/README.md +20 -7
- package/config/providers.default.json +10 -0
- package/package.json +1 -1
- package/src/manager/config.mjs +7 -1
- package/src/manager/main.mjs +20 -7
- package/src/manager/model-stats.mjs +338 -0
- package/src/manager/providers.mjs +89 -5
- package/src/manager/self-update.mjs +205 -0
- package/src/manager/server.mjs +14 -1
- package/src/manager/session-manager.mjs +58 -7
- package/src/manager/session.mjs +4 -1
- package/src/manager/usage.mjs +106 -25
- package/web/app.js +663 -13
- package/web/art/art.css +52 -6
- package/web/index.html +49 -1
- package/web/styles.css +165 -18
- package/web/theme.js +10 -0
package/web/app.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
const TOKEN_KEY = 'agentGuild.token';
|
|
5
5
|
const CWD_KEY = 'agentGuild.cwd';
|
|
6
|
+
const ACCOUNTS_KEY = 'agentGuild.accounts';
|
|
7
|
+
const THEME_KEY = 'agentGuild.theme';
|
|
6
8
|
const isMac = /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
|
|
7
9
|
|
|
8
10
|
const $ = (id) => document.getElementById(id);
|
|
@@ -10,11 +12,18 @@ const state = {
|
|
|
10
12
|
token: null,
|
|
11
13
|
providers: [],
|
|
12
14
|
usage: new Map(),
|
|
15
|
+
accounts: {},
|
|
16
|
+
stats: null,
|
|
17
|
+
statsFor: new Map(),
|
|
13
18
|
sessions: new Map(),
|
|
14
19
|
views: new Map(),
|
|
15
20
|
activeId: null,
|
|
16
21
|
eventsSocket: null,
|
|
17
22
|
eventsRetry: 0,
|
|
23
|
+
/** True while the events socket is open. */
|
|
24
|
+
connected: false,
|
|
25
|
+
/** The manager's own version check, from `hello` and `manager.upgrade`. */
|
|
26
|
+
upgrade: null,
|
|
18
27
|
/** True from a shutdown request until the manager is reachable again. */
|
|
19
28
|
stopping: false,
|
|
20
29
|
/** After a stop: how many session processes did not confirm exiting, or null if the manager never said. */
|
|
@@ -41,8 +50,93 @@ function setConnection(kind, label) {
|
|
|
41
50
|
const el = $('connection');
|
|
42
51
|
el.className = `connection ${kind}`;
|
|
43
52
|
el.querySelector('.label').textContent = label;
|
|
44
|
-
// The manager can only be stopped while the page can reach it.
|
|
45
|
-
|
|
53
|
+
// The manager can only be stopped or upgraded while the page can reach it.
|
|
54
|
+
state.connected = kind === 'ok';
|
|
55
|
+
$('stop-manager').hidden = !state.connected;
|
|
56
|
+
renderUpgrade();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---- theme ----------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
/** theme.js applied the saved or system theme before the first paint; this keeps the button in step. */
|
|
62
|
+
function applyTheme(theme) {
|
|
63
|
+
document.documentElement.dataset.theme = theme;
|
|
64
|
+
const button = $('theme-toggle');
|
|
65
|
+
const other = theme === 'dark' ? 'light' : 'dark';
|
|
66
|
+
button.textContent = theme === 'dark' ? '☀ Light' : '☾ Dark';
|
|
67
|
+
button.title = `Switch to the ${other} theme`;
|
|
68
|
+
button.setAttribute('aria-label', `Switch to the ${other} theme`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function currentTheme() {
|
|
72
|
+
return document.documentElement.dataset.theme === 'dark' ? 'dark' : 'light';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function toggleTheme() {
|
|
76
|
+
const theme = currentTheme() === 'dark' ? 'light' : 'dark';
|
|
77
|
+
save(THEME_KEY, theme);
|
|
78
|
+
applyTheme(theme);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---- upgrading the manager ------------------------------------------------
|
|
82
|
+
|
|
83
|
+
function renderUpgrade() {
|
|
84
|
+
const u = state.upgrade;
|
|
85
|
+
const button = $('upgrade');
|
|
86
|
+
const note = $('upgrade-note');
|
|
87
|
+
const offer = Boolean(state.connected && u?.available && u.command);
|
|
88
|
+
button.hidden = !offer;
|
|
89
|
+
if (offer) {
|
|
90
|
+
button.textContent = `Upgrade to ${u.latestVersion}`;
|
|
91
|
+
button.title = `Run "${u.command}" in a session. Sessions keep running; the new version is used once the manager is restarted.`;
|
|
92
|
+
}
|
|
93
|
+
let text = '';
|
|
94
|
+
let title = '';
|
|
95
|
+
const last = u?.lastInstall;
|
|
96
|
+
if (u?.installing) {
|
|
97
|
+
text = `Upgrading${u.latestVersion ? ` to v${u.latestVersion}` : ''}…`;
|
|
98
|
+
title = 'npm is running in a session. Keep the manager running until it finishes.';
|
|
99
|
+
} else if (u?.pendingVersion) {
|
|
100
|
+
text = `v${u.pendingVersion} installed · restart to use it`;
|
|
101
|
+
title = `Agent Guild ${u.pendingVersion} is installed, but this manager is still ${u.version}. Stop the manager and run "agent-guild open" to use it.`;
|
|
102
|
+
} else if (last?.outcome === 'failed') {
|
|
103
|
+
text = last.exitCode === null ? 'Upgrade failed' : `Upgrade failed (exit ${last.exitCode})`;
|
|
104
|
+
title = 'See the upgrade session for npm\'s output, then run the upgrade again: the files on disk may be incomplete. On Windows, files in use cannot be replaced: stop the manager first and run the command yourself.';
|
|
105
|
+
} else if (last?.outcome === 'unchanged') {
|
|
106
|
+
text = 'Upgrade finished, but this copy was not replaced';
|
|
107
|
+
title = `npm did not replace the files this manager runs from. Run${u.command ? ` "${u.command}"` : ' the npm install'} where Agent Guild is installed.`;
|
|
108
|
+
} else if (u?.available && !u.command) {
|
|
109
|
+
text = `v${u.latestVersion} available`;
|
|
110
|
+
title = u.guidance || '';
|
|
111
|
+
}
|
|
112
|
+
note.hidden = !state.connected || !text;
|
|
113
|
+
note.textContent = text;
|
|
114
|
+
note.title = title;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function setUpgrade(upgrade) {
|
|
118
|
+
const before = state.upgrade;
|
|
119
|
+
state.upgrade = upgrade || null;
|
|
120
|
+
renderUpgrade();
|
|
121
|
+
const pending = state.upgrade?.pendingVersion;
|
|
122
|
+
if (pending && pending !== before?.pendingVersion) {
|
|
123
|
+
toast(`Agent Guild ${pending} is installed. Stop the manager and run "agent-guild open" to use it.`, 10000);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function upgradeManager() {
|
|
128
|
+
const button = $('upgrade');
|
|
129
|
+
button.disabled = true;
|
|
130
|
+
try {
|
|
131
|
+
const { session } = await api('POST', '/upgrade');
|
|
132
|
+
upsertSession(session);
|
|
133
|
+
openPanel(session.id);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
if (err instanceof AuthError) return showAuth(err.message);
|
|
136
|
+
toast(err.message, 8000);
|
|
137
|
+
} finally {
|
|
138
|
+
button.disabled = false;
|
|
139
|
+
}
|
|
46
140
|
}
|
|
47
141
|
|
|
48
142
|
function relativeTime(iso) {
|
|
@@ -146,6 +240,51 @@ function wsUrl(path) {
|
|
|
146
240
|
|
|
147
241
|
// ---- providers ------------------------------------------------------------
|
|
148
242
|
|
|
243
|
+
function selectedAccount(provider) {
|
|
244
|
+
const accounts = provider.accounts || [];
|
|
245
|
+
return accounts.find((a) => a.id === state.accounts[provider.id]) || accounts[0] || { id: 'default', label: 'Default' };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function selectAccount(provider, id) {
|
|
249
|
+
state.accounts[provider.id] = id;
|
|
250
|
+
save(ACCOUNTS_KEY, JSON.stringify(state.accounts));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function usageFor(provider, account = selectedAccount(provider)) {
|
|
254
|
+
return state.usage.get(`${provider.id}/${account.id}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function renderAccounts(card, provider) {
|
|
258
|
+
const host = card.querySelector('.accounts');
|
|
259
|
+
const accounts = provider.accounts || [];
|
|
260
|
+
host.hidden = accounts.length < 2;
|
|
261
|
+
if (host.hidden) return host.replaceChildren();
|
|
262
|
+
const selected = selectedAccount(provider).id;
|
|
263
|
+
const same = host.children.length === accounts.length && accounts.every((a, i) => host.children[i].dataset.account === a.id);
|
|
264
|
+
if (!same) {
|
|
265
|
+
host.replaceChildren(...accounts.map((account) => {
|
|
266
|
+
const chip = document.createElement('button');
|
|
267
|
+
chip.type = 'button';
|
|
268
|
+
chip.className = 'account-chip';
|
|
269
|
+
chip.setAttribute('role', 'tab');
|
|
270
|
+
chip.dataset.account = account.id;
|
|
271
|
+
chip.addEventListener('click', () => {
|
|
272
|
+
selectAccount(provider, account.id);
|
|
273
|
+
renderAccounts(card, provider);
|
|
274
|
+
renderUsage(card, provider);
|
|
275
|
+
});
|
|
276
|
+
return chip;
|
|
277
|
+
}));
|
|
278
|
+
}
|
|
279
|
+
accounts.forEach((account, i) => {
|
|
280
|
+
const chip = host.children[i];
|
|
281
|
+
chip.classList.toggle('unsigned', usageFor(provider, account)?.signedIn === false);
|
|
282
|
+
chip.setAttribute('aria-selected', String(account.id === selected));
|
|
283
|
+
chip.textContent = account.label;
|
|
284
|
+
chip.title = `Start new ${provider.tool} sessions as the ${account.label} account`;
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
149
288
|
function renderProviders() {
|
|
150
289
|
const list = $('providers');
|
|
151
290
|
const tpl = $('provider-template');
|
|
@@ -166,7 +305,6 @@ function renderProviders() {
|
|
|
166
305
|
const existing = node.querySelector('.existing');
|
|
167
306
|
const hint = node.querySelector('.hint');
|
|
168
307
|
start.hidden = !provider.available;
|
|
169
|
-
start.title = `Start a new ${provider.tool} session`;
|
|
170
308
|
start.addEventListener('click', () => startSession(provider, node));
|
|
171
309
|
existing.hidden = !provider.available || !provider.resumable;
|
|
172
310
|
existing.title = `Resume one of ${provider.tool}'s own sessions by its id`;
|
|
@@ -184,7 +322,9 @@ function renderProviders() {
|
|
|
184
322
|
renderHint(hint, provider);
|
|
185
323
|
renderCopies(node.querySelector('.copies'), provider);
|
|
186
324
|
renderConsoleLinks(node, provider);
|
|
325
|
+
renderAccounts(node, provider);
|
|
187
326
|
renderUsage(node, provider);
|
|
327
|
+
renderModelStats(node, provider);
|
|
188
328
|
return node;
|
|
189
329
|
}));
|
|
190
330
|
}
|
|
@@ -281,14 +421,21 @@ function creditsNote(usage) {
|
|
|
281
421
|
|
|
282
422
|
function renderUsage(card, provider) {
|
|
283
423
|
const host = card.querySelector('.usage');
|
|
284
|
-
const
|
|
424
|
+
const account = selectedAccount(provider);
|
|
425
|
+
const usage = usageFor(provider, account);
|
|
285
426
|
renderTier(card, provider, provider.usageSource ? usage : null);
|
|
427
|
+
const start = card.querySelector('.new');
|
|
428
|
+
const unsigned = Boolean(provider.available && usage?.signedIn === false);
|
|
429
|
+
start.textContent = unsigned ? 'Sign in' : 'New';
|
|
430
|
+
start.title = unsigned
|
|
431
|
+
? `Start a ${provider.tool} session and sign in as the ${account.label} account`
|
|
432
|
+
: `Start a new ${provider.tool} session${provider.accounts?.length > 1 ? ` as the ${account.label} account` : ''}`;
|
|
286
433
|
if (!provider.available || !provider.usageSource || !usage) return host.replaceChildren();
|
|
287
434
|
if (usage.error || usage.windows.length === 0) {
|
|
288
435
|
const note = document.createElement('div');
|
|
289
436
|
note.className = 'usage-note';
|
|
290
|
-
note.textContent = `Usage: ${usage.error || 'no limits reported'}`;
|
|
291
|
-
note.title = note.textContent;
|
|
437
|
+
note.textContent = unsigned ? 'Not signed in yet' : `Usage: ${usage.error || 'no limits reported'}`;
|
|
438
|
+
note.title = unsigned ? usage.error : note.textContent;
|
|
292
439
|
return host.replaceChildren(note, ...creditsNote(usage));
|
|
293
440
|
}
|
|
294
441
|
host.replaceChildren(...usage.windows.map((w) => {
|
|
@@ -310,11 +457,447 @@ function renderUsage(card, provider) {
|
|
|
310
457
|
async function loadUsage() {
|
|
311
458
|
let usage;
|
|
312
459
|
try { ({ usage } = await api('GET', '/usage')); } catch { return; }
|
|
313
|
-
state.usage = new Map(usage.map((u) => [u.providerId
|
|
460
|
+
state.usage = new Map(usage.map((u) => [`${u.providerId}/${u.accountId ?? 'default'}`, u]));
|
|
314
461
|
for (const card of $('providers').children) {
|
|
315
462
|
const provider = state.providers.find((p) => p.id === card.dataset.id);
|
|
316
|
-
if (provider)
|
|
463
|
+
if (!provider) continue;
|
|
464
|
+
renderAccounts(card, provider);
|
|
465
|
+
renderUsage(card, provider);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const ARTIFICIAL_ANALYSIS = 'Artificial Analysis';
|
|
470
|
+
const MODELS_SHOWN = 8;
|
|
471
|
+
const modelsView = { providerId: null, sessionId: null, focus: null, all: false, expanded: new Set() };
|
|
472
|
+
let modelsOpener = null;
|
|
473
|
+
let statsLoading = null;
|
|
474
|
+
let statsAgain = false;
|
|
475
|
+
let statsTimer;
|
|
476
|
+
|
|
477
|
+
function modelKey(s) {
|
|
478
|
+
return s.model ? `${s.model.name}\n${s.model.displayName ?? ''}` : '';
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function sessionModelId(s) {
|
|
482
|
+
return state.statsFor.get(s.id) === modelKey(s) ? state.stats?.sessions[s.id] ?? null : null;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function loadStats() {
|
|
486
|
+
if (statsLoading) {
|
|
487
|
+
statsAgain = true;
|
|
488
|
+
return statsLoading;
|
|
489
|
+
}
|
|
490
|
+
const asked = new Map([...state.sessions.values()].map((s) => [s.id, modelKey(s)]));
|
|
491
|
+
statsLoading = api('GET', '/model-stats').then((stats) => {
|
|
492
|
+
state.stats = stats;
|
|
493
|
+
state.statsFor = asked;
|
|
494
|
+
for (const card of $('providers').children) {
|
|
495
|
+
const provider = state.providers.find((p) => p.id === card.dataset.id);
|
|
496
|
+
if (provider) renderModelStats(card, provider);
|
|
497
|
+
}
|
|
498
|
+
renderSessions();
|
|
499
|
+
if ($('models').open) {
|
|
500
|
+
const body = document.querySelector('.models-body');
|
|
501
|
+
const top = body.scrollTop;
|
|
502
|
+
const focus = modelsFocus();
|
|
503
|
+
const withTip = Boolean(tipFor?.closest('#models-list'));
|
|
504
|
+
renderModels();
|
|
505
|
+
body.scrollTop = top;
|
|
506
|
+
restoreModelsFocus(focus, withTip);
|
|
507
|
+
}
|
|
508
|
+
if (tipFor && !tipFor.isConnected) hideTip();
|
|
509
|
+
}, () => {}).finally(() => {
|
|
510
|
+
statsLoading = null;
|
|
511
|
+
if (statsAgain) {
|
|
512
|
+
statsAgain = false;
|
|
513
|
+
loadStats();
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
return statsLoading;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function scheduleStats() {
|
|
520
|
+
clearTimeout(statsTimer);
|
|
521
|
+
statsTimer = setTimeout(loadStats, 300);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function indexStats() {
|
|
525
|
+
return state.stats?.stats.filter((stat) => stat.group === ARTIFICIAL_ANALYSIS) ?? [];
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function ordinal(n) {
|
|
529
|
+
const suffixes = ['th', 'st', 'nd', 'rd'];
|
|
530
|
+
const v = n % 100;
|
|
531
|
+
return `${n}${suffixes[(v - 20) % 10] || suffixes[v] || suffixes[0]}`;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function listJoin(items) {
|
|
535
|
+
return items.length < 2 ? items.join('') : `${items.slice(0, -1).join(', ')} and ${items.at(-1)}`;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function formatTokens(n) {
|
|
539
|
+
if (typeof n !== 'number' || !Number.isFinite(n) || n <= 0) return '—';
|
|
540
|
+
if (n >= 1e6) return `${Number((n / 1e6).toFixed(2))}M`;
|
|
541
|
+
if (n >= 1e3) return `${Math.round(n / 1e3)}K`;
|
|
542
|
+
return String(n);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function formatPrice(n) {
|
|
546
|
+
return `$${Number(n.toFixed(n < 1 ? 3 : 2))}`;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function formatDate(iso, options = { year: 'numeric', month: 'short', day: 'numeric' }) {
|
|
550
|
+
const date = new Date(iso);
|
|
551
|
+
return Number.isNaN(date.getTime()) ? '' : date.toLocaleDateString(undefined, options);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function statValue(stat, entry) {
|
|
555
|
+
return stat.group === ARTIFICIAL_ANALYSIS ? String(entry.value) : `Elo ${entry.value}`;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function statPlace(entry) {
|
|
559
|
+
return `${entry.tied ? 'tied ' : ''}${ordinal(entry.place)} of ${entry.of}`;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function statSummary(stat, entry) {
|
|
563
|
+
if (!entry) return `${stat.label}: no published result`;
|
|
564
|
+
const value = `${stat.label} ${statValue(stat, entry)}`;
|
|
565
|
+
if (entry.level === null) return `${value}: no other model has this result to compare with`;
|
|
566
|
+
const rank = entry.rank === null ? '' : `, Design Arena rank ${entry.rank}`;
|
|
567
|
+
return `${value}: level ${entry.level}, tier ${entry.tier}, ${statPlace(entry)}${rank}`;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function levelValue(entry) {
|
|
571
|
+
if (!entry) return ['—'];
|
|
572
|
+
if (entry.level === null) return [String(entry.value)];
|
|
573
|
+
const tier = document.createElement('span');
|
|
574
|
+
tier.className = 'stat-tier';
|
|
575
|
+
tier.textContent = entry.tier;
|
|
576
|
+
const level = document.createElement('span');
|
|
577
|
+
level.className = 'level';
|
|
578
|
+
level.textContent = entry.level;
|
|
579
|
+
return [tier, level];
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function tierClass(entry) {
|
|
583
|
+
return entry?.tier ? `tier-${entry.tier.toLowerCase()}` : '';
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function statRow(stat, card, { detail = false } = {}) {
|
|
587
|
+
const node = $('stat-template').content.firstElementChild.cloneNode(true);
|
|
588
|
+
const entry = card.stats[stat.id];
|
|
589
|
+
node.querySelector('.stat-name').textContent = detail ? stat.label : stat.short;
|
|
590
|
+
const info = node.querySelector('.info');
|
|
591
|
+
info.dataset.stat = stat.id;
|
|
592
|
+
info.setAttribute('aria-label', `${stat.label}: ${stat.about}`);
|
|
593
|
+
const reading = node.querySelector('.stat-reading');
|
|
594
|
+
reading.querySelector('.stat-value').replaceChildren(...levelValue(entry));
|
|
595
|
+
node.classList.toggle('unmeasured', !entry);
|
|
596
|
+
if (entry?.tier) {
|
|
597
|
+
node.classList.add(tierClass(entry));
|
|
598
|
+
reading.querySelector('.stat-fill').style.width = `${entry.level}%`;
|
|
599
|
+
}
|
|
600
|
+
if (detail) {
|
|
601
|
+
const rank = entry?.rank == null ? '' : ` · Rank ${entry.rank}`;
|
|
602
|
+
reading.querySelector('.stat-note').textContent = !entry ? 'No published result'
|
|
603
|
+
: entry.level === null ? statValue(stat, entry) : `${statPlace(entry)} · ${statValue(stat, entry)}${rank}`;
|
|
604
|
+
}
|
|
605
|
+
reading.title = statSummary(stat, entry);
|
|
606
|
+
reading.setAttribute('aria-label', reading.title);
|
|
607
|
+
return node;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
let tipFor = null;
|
|
611
|
+
let quietFocus = false;
|
|
612
|
+
|
|
613
|
+
function showTip(button) {
|
|
614
|
+
const stat = state.stats?.stats.find((s) => s.id === button.dataset.stat);
|
|
615
|
+
if (!stat) return;
|
|
616
|
+
const tip = $('tip');
|
|
617
|
+
$('tip-title').textContent = stat.label;
|
|
618
|
+
$('tip-source').textContent = stat.group;
|
|
619
|
+
$('tip-text').textContent = stat.about;
|
|
620
|
+
const host = $('models').open ? $('models') : document.body;
|
|
621
|
+
if (tip.parentElement !== host) host.append(tip);
|
|
622
|
+
tip.hidden = false;
|
|
623
|
+
const box = button.getBoundingClientRect();
|
|
624
|
+
const left = Math.min(Math.max(8, box.left + box.width / 2 - tip.offsetWidth / 2), innerWidth - tip.offsetWidth - 8);
|
|
625
|
+
const below = box.bottom + 8 + tip.offsetHeight <= innerHeight;
|
|
626
|
+
tip.style.left = `${Math.round(left)}px`;
|
|
627
|
+
tip.style.top = `${Math.round(below ? box.bottom + 8 : box.top - tip.offsetHeight - 8)}px`;
|
|
628
|
+
tipFor = button;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function hideTip() {
|
|
632
|
+
$('tip').hidden = true;
|
|
633
|
+
tipFor = null;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function renderModelStats(card, provider) {
|
|
637
|
+
const host = card.querySelector('.model-stats');
|
|
638
|
+
const stats = state.stats;
|
|
639
|
+
const entry = stats?.providers[provider.id];
|
|
640
|
+
if (!entry) {
|
|
641
|
+
if (!stats?.error || !provider.modelPattern) return host.replaceChildren();
|
|
642
|
+
const note = document.createElement('div');
|
|
643
|
+
note.className = 'usage-note';
|
|
644
|
+
note.textContent = `Benchmarks: ${stats.error}`;
|
|
645
|
+
note.title = note.textContent;
|
|
646
|
+
return host.replaceChildren(note);
|
|
647
|
+
}
|
|
648
|
+
const featured = stats.models[entry.featured];
|
|
649
|
+
const count = entry.models.length;
|
|
650
|
+
const complete = indexStats().every((stat) => featured.stats[stat.id]);
|
|
651
|
+
const head = document.createElement('button');
|
|
652
|
+
head.type = 'button';
|
|
653
|
+
head.className = 'model-stats-head';
|
|
654
|
+
head.textContent = `${featured.name} · ${count} model${count === 1 ? '' : 's'}`;
|
|
655
|
+
head.setAttribute('aria-label', head.textContent);
|
|
656
|
+
head.title = `${featured.name} is the newest ${provider.tool} model with ${complete ? 'all three Artificial Analysis indexes' : 'published benchmarks'}. Open to compare ${count === 1 ? 'it' : `all ${count}`}.`;
|
|
657
|
+
head.addEventListener('click', () => showModels({ providerId: provider.id, focus: featured.id }));
|
|
658
|
+
host.replaceChildren(head, ...indexStats().map((stat) => statRow(stat, featured)));
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function modelStatsLine(s) {
|
|
662
|
+
const id = sessionModelId(s);
|
|
663
|
+
const card = id ? state.stats.models[id] : null;
|
|
664
|
+
if (!card) return '';
|
|
665
|
+
return indexStats().map((stat) => {
|
|
666
|
+
const entry = card.stats[stat.id];
|
|
667
|
+
return `${stat.short} ${entry?.tier ? `${entry.tier} ${entry.level}` : '—'}`;
|
|
668
|
+
}).join(' · ');
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function modelsFocus() {
|
|
672
|
+
const active = document.activeElement;
|
|
673
|
+
const row = active?.closest?.('#models-list .model');
|
|
674
|
+
return row ? { id: row.dataset.id, stat: active.dataset.stat ?? null } : null;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function restoreModelsFocus(focus, withTip) {
|
|
678
|
+
const row = focus && [...$('models-list').children].find((el) => el.dataset.id === focus.id);
|
|
679
|
+
if (!row) return;
|
|
680
|
+
const info = focus.stat && row.querySelector(`.info[data-stat="${focus.stat}"]`);
|
|
681
|
+
quietFocus = true;
|
|
682
|
+
(info || row.querySelector('.model-toggle')).focus({ preventScroll: true });
|
|
683
|
+
quietFocus = false;
|
|
684
|
+
if (withTip && info) showTip(info);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function showModels({ providerId = null, sessionId = null, focus = null }) {
|
|
688
|
+
Object.assign(modelsView, { providerId, sessionId, focus, all: false, expanded: new Set(focus ? [focus] : []) });
|
|
689
|
+
const dialog = $('models');
|
|
690
|
+
renderModels();
|
|
691
|
+
if (!dialog.open) {
|
|
692
|
+
modelsOpener = document.activeElement;
|
|
693
|
+
dialog.showModal();
|
|
317
694
|
}
|
|
695
|
+
const toggle = $('models-list').querySelector('[aria-expanded="true"]');
|
|
696
|
+
toggle?.focus();
|
|
697
|
+
toggle?.scrollIntoView({ block: 'nearest' });
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function openSessionModel(id) {
|
|
701
|
+
const s = state.sessions.get(id);
|
|
702
|
+
if (!s?.model) return;
|
|
703
|
+
if (!state.stats || state.statsFor.get(id) !== modelKey(s)) scheduleStats();
|
|
704
|
+
showModels({ sessionId: id, focus: sessionModelId(s) });
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function unmatchedText(s) {
|
|
708
|
+
const stats = state.stats;
|
|
709
|
+
if (!stats) return 'Loading benchmarks from OpenRouter…';
|
|
710
|
+
if (!stats.pool) return `Benchmarks are unavailable: ${stats.error}.`;
|
|
711
|
+
if (!(s.id in stats.sessions) || state.statsFor.get(s.id) !== modelKey(s)) return 'Looking this model up in OpenRouter’s catalog…';
|
|
712
|
+
const names = [s.model.name, s.model.displayName].filter((name, i, all) => name && all.indexOf(name) === i);
|
|
713
|
+
return `OpenRouter’s catalog has no model named ${names.map((name) => `“${name}”`).join(' or ')}.`;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function sourceText(stats, { levels = true } = {}) {
|
|
717
|
+
if (!stats?.retrievedAt) return '';
|
|
718
|
+
const parts = [`Benchmarks from Artificial Analysis and Design Arena via OpenRouter, fetched ${relativeTime(stats.retrievedAt)}.`];
|
|
719
|
+
if (stats.stale) parts.push(`The last refresh failed (${stats.error}), so these results may be out of date.`);
|
|
720
|
+
if (stats.pool && levels) {
|
|
721
|
+
parts.push('Artificial Analysis results are index scores. Design Arena results are Elo ratings from real users\' head-to-head votes, and Rank is the model\'s place on Design Arena\'s own leaderboard.');
|
|
722
|
+
parts.push(`Level 0–100 is a model's standing on each benchmark among the models ${listJoin(stats.pool.tools)} run; 100 is the best result.`);
|
|
723
|
+
parts.push('Tier: S 90+, A 75+, B 50+, C 25+, D below 25.');
|
|
724
|
+
}
|
|
725
|
+
return parts.join(' ');
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function usedBy(id) {
|
|
729
|
+
return [...state.sessions.values()]
|
|
730
|
+
.filter((s) => s.status === 'running' && sessionModelId(s) === id)
|
|
731
|
+
.map((s) => s.name);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function badge(kind, label, title) {
|
|
735
|
+
const el = document.createElement('span');
|
|
736
|
+
el.className = `badge ${kind}`;
|
|
737
|
+
el.textContent = label;
|
|
738
|
+
el.title = title;
|
|
739
|
+
return el;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function modelFacts(card) {
|
|
743
|
+
const list = document.createElement('dl');
|
|
744
|
+
list.className = 'model-facts';
|
|
745
|
+
const fact = (term, value) => {
|
|
746
|
+
if (!value) return;
|
|
747
|
+
const row = document.createElement('div');
|
|
748
|
+
const dt = document.createElement('dt');
|
|
749
|
+
const dd = document.createElement('dd');
|
|
750
|
+
dt.textContent = term;
|
|
751
|
+
dd.textContent = value;
|
|
752
|
+
row.append(dt, dd);
|
|
753
|
+
list.append(row);
|
|
754
|
+
};
|
|
755
|
+
fact('Context', card.context ? `${formatTokens(card.context)} tokens` : '');
|
|
756
|
+
fact('Max output', card.maxOutput ? `${formatTokens(card.maxOutput)} tokens` : '');
|
|
757
|
+
fact('Input', card.input.join(', '));
|
|
758
|
+
fact('Reasoning effort', card.reasoning.join(', '));
|
|
759
|
+
if (card.price.input !== null && card.price.output !== null) {
|
|
760
|
+
fact('Price', `${formatPrice(card.price.input)} input · ${formatPrice(card.price.output)} output per 1M tokens`);
|
|
761
|
+
}
|
|
762
|
+
fact('Created', card.created ? formatDate(card.created) : '');
|
|
763
|
+
fact('Expires', card.expires ? formatDate(card.expires, { year: 'numeric', month: 'short', day: 'numeric', timeZone: 'UTC' }) : '');
|
|
764
|
+
const link = document.createElement('a');
|
|
765
|
+
link.className = 'console-link';
|
|
766
|
+
link.href = httpsHref(`https://openrouter.ai/${card.id}`) ?? '';
|
|
767
|
+
link.target = '_blank';
|
|
768
|
+
link.rel = 'noopener noreferrer';
|
|
769
|
+
link.textContent = 'OpenRouter ↗';
|
|
770
|
+
link.setAttribute('aria-label', `${card.name} on OpenRouter (opens in a new tab)`);
|
|
771
|
+
const facts = document.createElement('div');
|
|
772
|
+
facts.className = 'model-more';
|
|
773
|
+
facts.append(list, link);
|
|
774
|
+
return facts;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function modelDetail(card) {
|
|
778
|
+
const groups = [];
|
|
779
|
+
for (const stat of state.stats.stats) {
|
|
780
|
+
if (groups.at(-1)?.name !== stat.group) groups.push({ name: stat.group, stats: [] });
|
|
781
|
+
groups.at(-1).stats.push(stat);
|
|
782
|
+
}
|
|
783
|
+
const sections = groups.map(({ name, stats }) => {
|
|
784
|
+
const section = document.createElement('div');
|
|
785
|
+
section.className = 'model-group';
|
|
786
|
+
const heading = document.createElement('h4');
|
|
787
|
+
heading.textContent = name;
|
|
788
|
+
section.append(heading);
|
|
789
|
+
if (stats.some((stat) => card.stats[stat.id])) {
|
|
790
|
+
section.append(...stats.map((stat) => statRow(stat, card, { detail: true })));
|
|
791
|
+
} else {
|
|
792
|
+
const none = document.createElement('p');
|
|
793
|
+
none.className = 'model-none';
|
|
794
|
+
none.textContent = 'No published results';
|
|
795
|
+
section.append(none);
|
|
796
|
+
}
|
|
797
|
+
return section;
|
|
798
|
+
});
|
|
799
|
+
return [...sections, modelFacts(card)];
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function modelRow(card) {
|
|
803
|
+
const node = $('model-template').content.firstElementChild.cloneNode(true);
|
|
804
|
+
node.dataset.id = card.id;
|
|
805
|
+
const toggle = node.querySelector('.model-toggle');
|
|
806
|
+
const detail = node.querySelector('.model-detail');
|
|
807
|
+
node.querySelector('.model-name').textContent = card.name;
|
|
808
|
+
const users = usedBy(card.id);
|
|
809
|
+
const badges = [];
|
|
810
|
+
if (users.length) badges.push(badge('in-use', 'In use', `Used by ${listJoin(users)}`));
|
|
811
|
+
if (card.new) badges.push(badge('new', 'New', `Created ${formatDate(card.created)}`));
|
|
812
|
+
if (card.expires) {
|
|
813
|
+
const day = formatDate(card.expires, { month: 'short', day: 'numeric', timeZone: 'UTC' });
|
|
814
|
+
badges.push(badge('expires', `Expires ${day}`, `OpenRouter lists this model as expiring on ${formatDate(card.expires, { year: 'numeric', month: 'short', day: 'numeric', timeZone: 'UTC' })}`));
|
|
815
|
+
}
|
|
816
|
+
node.querySelector('.model-title').append(...badges);
|
|
817
|
+
for (const stat of indexStats()) {
|
|
818
|
+
const entry = card.stats[stat.id];
|
|
819
|
+
const cell = document.createElement('span');
|
|
820
|
+
cell.className = ['model-index', tierClass(entry), entry ? '' : 'unmeasured'].filter(Boolean).join(' ');
|
|
821
|
+
cell.replaceChildren(...levelValue(entry));
|
|
822
|
+
cell.title = statSummary(stat, entry);
|
|
823
|
+
toggle.append(cell);
|
|
824
|
+
}
|
|
825
|
+
const context = document.createElement('span');
|
|
826
|
+
context.className = 'model-context';
|
|
827
|
+
context.textContent = formatTokens(card.context);
|
|
828
|
+
context.title = card.context ? `Context: ${card.context.toLocaleString()} tokens` : 'Context: not listed';
|
|
829
|
+
toggle.append(context);
|
|
830
|
+
const summary = indexStats().map((stat) => statSummary(stat, card.stats[stat.id])).join('. ');
|
|
831
|
+
toggle.setAttribute('aria-description', summary);
|
|
832
|
+
const expand = (open) => {
|
|
833
|
+
toggle.setAttribute('aria-expanded', String(open));
|
|
834
|
+
if (open && !detail.childElementCount) detail.replaceChildren(...modelDetail(card));
|
|
835
|
+
detail.hidden = !open;
|
|
836
|
+
};
|
|
837
|
+
expand(modelsView.expanded.has(card.id));
|
|
838
|
+
toggle.addEventListener('click', () => {
|
|
839
|
+
const open = toggle.getAttribute('aria-expanded') !== 'true';
|
|
840
|
+
if (open) modelsView.expanded.add(card.id);
|
|
841
|
+
else modelsView.expanded.delete(card.id);
|
|
842
|
+
expand(open);
|
|
843
|
+
});
|
|
844
|
+
return node;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function renderModels() {
|
|
848
|
+
const stats = state.stats;
|
|
849
|
+
const session = modelsView.sessionId ? state.sessions.get(modelsView.sessionId) : null;
|
|
850
|
+
const provider = session?.provider ?? state.providers.find((p) => p.id === modelsView.providerId);
|
|
851
|
+
if (!provider || (modelsView.sessionId && !session?.model)) return $('models').close();
|
|
852
|
+
paintProviderIcon($('models-icon'), provider);
|
|
853
|
+
const list = stats?.providers[provider.id]?.models ?? [];
|
|
854
|
+
let ids = list;
|
|
855
|
+
let title = `${provider.tool} models`;
|
|
856
|
+
let note = '';
|
|
857
|
+
if (session) {
|
|
858
|
+
const id = sessionModelId(session);
|
|
859
|
+
if (id && !modelsView.focus) {
|
|
860
|
+
modelsView.focus = id;
|
|
861
|
+
modelsView.expanded.add(id);
|
|
862
|
+
}
|
|
863
|
+
if (!id) {
|
|
864
|
+
ids = [];
|
|
865
|
+
title = `${provider.tool} · ${modelText(session)}`;
|
|
866
|
+
note = unmatchedText(session);
|
|
867
|
+
} else if (!list.includes(id)) {
|
|
868
|
+
ids = [id];
|
|
869
|
+
title = `${provider.tool} · ${stats.models[id].name}`;
|
|
870
|
+
}
|
|
871
|
+
} else if (!stats) {
|
|
872
|
+
note = 'Loading benchmarks from OpenRouter…';
|
|
873
|
+
} else if (list.length === 0) {
|
|
874
|
+
note = stats.error ? `Benchmarks are unavailable: ${stats.error}.` : `OpenRouter lists no benchmarked ${provider.tool} models.`;
|
|
875
|
+
}
|
|
876
|
+
$('models-title').textContent = title;
|
|
877
|
+
$('models-sub').textContent = ids.length > 1
|
|
878
|
+
? `${ids.length} models with published benchmarks, newest first`
|
|
879
|
+
: session ? `Reported by ${session.name} as “${session.model.name}”` : '';
|
|
880
|
+
const all = modelsView.all || ids.length <= MODELS_SHOWN || ids.indexOf(modelsView.focus) >= MODELS_SHOWN;
|
|
881
|
+
const shown = all ? ids : ids.slice(0, MODELS_SHOWN);
|
|
882
|
+
const columns = $('models-columns');
|
|
883
|
+
columns.hidden = shown.length === 0;
|
|
884
|
+
columns.replaceChildren(...['Model', ...indexStats().map((stat) => stat.short), 'Context'].map((label) => {
|
|
885
|
+
const span = document.createElement('span');
|
|
886
|
+
span.textContent = label;
|
|
887
|
+
return span;
|
|
888
|
+
}));
|
|
889
|
+
$('models-list').replaceChildren(...shown.map((id) => modelRow(stats.models[id])));
|
|
890
|
+
const more = $('models-more');
|
|
891
|
+
more.hidden = all;
|
|
892
|
+
more.textContent = `Show ${ids.length - shown.length} older models`;
|
|
893
|
+
$('models-note').textContent = note;
|
|
894
|
+
$('models-note').hidden = !note;
|
|
895
|
+
$('models-source').textContent = sourceText(stats, { levels: shown.length > 0 });
|
|
896
|
+
$('models-source').hidden = !$('models-source').textContent;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function closeModels() {
|
|
900
|
+
if ($('models').open) $('models').close();
|
|
318
901
|
}
|
|
319
902
|
|
|
320
903
|
const CHANNEL_LABELS = {
|
|
@@ -381,7 +964,7 @@ async function startSession(provider, card, { resume } = {}) {
|
|
|
381
964
|
save(CWD_KEY, cwd);
|
|
382
965
|
card.classList.add('busy');
|
|
383
966
|
try {
|
|
384
|
-
const body = { providerId: provider.id, cwd: cwd || undefined, cols: 120, rows: 32, resume };
|
|
967
|
+
const body = { providerId: provider.id, account: selectedAccount(provider).id, cwd: cwd || undefined, cols: 120, rows: 32, resume };
|
|
385
968
|
const { session } = await api('POST', '/sessions', body);
|
|
386
969
|
upsertSession(session);
|
|
387
970
|
openPanel(session.id);
|
|
@@ -415,6 +998,12 @@ function modelTitle(s) {
|
|
|
415
998
|
return `Model ${modelText(s)}${id}, ${MODEL_SOURCES[s.model.source] || s.model.source}`;
|
|
416
999
|
}
|
|
417
1000
|
|
|
1001
|
+
function accountLabel(s) {
|
|
1002
|
+
if (!s.account) return '';
|
|
1003
|
+
const provider = state.providers.find((p) => p.id === s.provider.id);
|
|
1004
|
+
return (provider?.accounts?.length ?? 0) > 1 || s.account.id !== 'default' ? s.account.label : '';
|
|
1005
|
+
}
|
|
1006
|
+
|
|
418
1007
|
function statusText(s) {
|
|
419
1008
|
if (s.status === 'exited') {
|
|
420
1009
|
if (s.signal) return `Exited (${s.signal})`;
|
|
@@ -433,6 +1022,7 @@ function buildCard(session) {
|
|
|
433
1022
|
node.querySelector('.stop').addEventListener('click', () => stopSession(session.id));
|
|
434
1023
|
node.querySelector('.remove').addEventListener('click', () => removeSession(session.id));
|
|
435
1024
|
node.querySelector('.rename').addEventListener('click', () => renameSession(session.id));
|
|
1025
|
+
node.querySelector('.model-pill').addEventListener('click', () => openSessionModel(session.id));
|
|
436
1026
|
return node;
|
|
437
1027
|
}
|
|
438
1028
|
|
|
@@ -453,14 +1043,15 @@ function updateCard(node, s) {
|
|
|
453
1043
|
badge.title = `Level ${level}`;
|
|
454
1044
|
node.querySelector('.name').textContent = s.name;
|
|
455
1045
|
const resumed = s.resume ? ` · resumed ${s.resume}` : '';
|
|
456
|
-
node.querySelector('.meta').textContent =
|
|
1046
|
+
node.querySelector('.meta').textContent = [s.provider.vendor, s.provider.tool, accountLabel(s), `started ${relativeTime(s.createdAt)}${resumed}`].filter(Boolean).join(' · ');
|
|
457
1047
|
const pill = node.querySelector('.status-pill');
|
|
458
1048
|
pill.textContent = statusText(s);
|
|
459
1049
|
pill.className = `status-pill ${s.status === 'exited' ? 'exited' : s.activity}`;
|
|
460
1050
|
const model = node.querySelector('.model-pill');
|
|
461
1051
|
model.hidden = !s.model;
|
|
462
1052
|
model.textContent = modelText(s);
|
|
463
|
-
model.title = modelTitle(s);
|
|
1053
|
+
model.title = [modelTitle(s), modelStatsLine(s)].filter(Boolean).join('\n');
|
|
1054
|
+
model.setAttribute('aria-label', `Benchmarks for ${modelText(s)}`);
|
|
464
1055
|
model.className = `model-pill ${s.model?.source || ''}`;
|
|
465
1056
|
const cwd = node.querySelector('.cwd-line');
|
|
466
1057
|
// The LRM keeps a leading "/" in place under the right-to-left truncation style.
|
|
@@ -471,7 +1062,8 @@ function updateCard(node, s) {
|
|
|
471
1062
|
node.querySelector('.stop').hidden = s.status !== 'running';
|
|
472
1063
|
node.querySelector('.remove').hidden = s.status === 'running';
|
|
473
1064
|
const modelLabel = s.model ? `, model ${modelText(s)}` : '';
|
|
474
|
-
|
|
1065
|
+
const accountName = accountLabel(s) ? `, ${accountLabel(s)} account` : '';
|
|
1066
|
+
node.setAttribute('aria-label', `${s.name}, ${s.provider.vendor}${accountName}${modelLabel}, ${statusText(s)}, ${s.agents.length} agents`);
|
|
475
1067
|
}
|
|
476
1068
|
|
|
477
1069
|
function renderSessions() {
|
|
@@ -492,6 +1084,7 @@ function renderSessions() {
|
|
|
492
1084
|
$('session-count').textContent = sessions.length ? `· ${running} running` : '';
|
|
493
1085
|
$('empty').hidden = sessions.length > 0;
|
|
494
1086
|
if (state.activeId) updatePanel();
|
|
1087
|
+
if (state.stats && sessions.some((s) => s.model && state.statsFor.get(s.id) !== modelKey(s))) scheduleStats();
|
|
495
1088
|
}
|
|
496
1089
|
|
|
497
1090
|
function upsertSession(session) {
|
|
@@ -706,7 +1299,7 @@ function updatePanel() {
|
|
|
706
1299
|
if (!s) return;
|
|
707
1300
|
paintProviderIcon($('panel-icon'), s.provider);
|
|
708
1301
|
$('panel-title').textContent = s.name;
|
|
709
|
-
$('panel-sub').textContent = [s.provider.tool, modelText(s), statusText(s), s.cwd].filter(Boolean).join(' · ');
|
|
1302
|
+
$('panel-sub').textContent = [s.provider.tool, accountLabel(s), modelText(s), statusText(s), s.cwd].filter(Boolean).join(' · ');
|
|
710
1303
|
$('panel-sub').title = modelTitle(s);
|
|
711
1304
|
renderAgents($('panel-agents'), s.agents);
|
|
712
1305
|
const stop = $('panel-stop');
|
|
@@ -749,6 +1342,7 @@ function enterStopping(running = 0) {
|
|
|
749
1342
|
state.stopping = true;
|
|
750
1343
|
state.stopRemaining = null;
|
|
751
1344
|
closePanel();
|
|
1345
|
+
closeModels();
|
|
752
1346
|
for (const view of state.views.values()) view.dispose();
|
|
753
1347
|
state.views.clear();
|
|
754
1348
|
state.sessions.clear();
|
|
@@ -805,6 +1399,9 @@ function connectEvents() {
|
|
|
805
1399
|
state.sessions = new Map(msg.sessions.map((s) => [s.id, s]));
|
|
806
1400
|
for (const id of [...state.views.keys()]) if (!state.sessions.has(id)) dropSession(id);
|
|
807
1401
|
renderSessions();
|
|
1402
|
+
setUpgrade(msg.upgrade);
|
|
1403
|
+
} else if (msg.type === 'manager.upgrade') {
|
|
1404
|
+
setUpgrade(msg.upgrade);
|
|
808
1405
|
} else if (msg.type === 'manager.stopping') {
|
|
809
1406
|
enterStopping(msg.running);
|
|
810
1407
|
} else if (msg.type === 'manager.stopped') {
|
|
@@ -818,6 +1415,7 @@ function connectEvents() {
|
|
|
818
1415
|
} else if (msg.type === 'providers.updated') {
|
|
819
1416
|
state.providers = msg.providers;
|
|
820
1417
|
renderProviders();
|
|
1418
|
+
scheduleStats();
|
|
821
1419
|
}
|
|
822
1420
|
};
|
|
823
1421
|
ws.onclose = () => {
|
|
@@ -854,8 +1452,10 @@ function readTokenFromHash() {
|
|
|
854
1452
|
}
|
|
855
1453
|
|
|
856
1454
|
let usageTimer;
|
|
1455
|
+
let statsInterval;
|
|
857
1456
|
|
|
858
1457
|
function showAuth(message = '') {
|
|
1458
|
+
closeModels();
|
|
859
1459
|
$('app').hidden = true;
|
|
860
1460
|
$('terminal-panel').hidden = true;
|
|
861
1461
|
$('stopped').hidden = true;
|
|
@@ -885,6 +1485,9 @@ async function boot() {
|
|
|
885
1485
|
loadUsage();
|
|
886
1486
|
clearInterval(usageTimer);
|
|
887
1487
|
usageTimer = setInterval(loadUsage, 60000);
|
|
1488
|
+
loadStats();
|
|
1489
|
+
clearInterval(statsInterval);
|
|
1490
|
+
statsInterval = setInterval(loadStats, 60 * 60 * 1000);
|
|
888
1491
|
}
|
|
889
1492
|
|
|
890
1493
|
$('auth-form').addEventListener('submit', (e) => {
|
|
@@ -895,7 +1498,52 @@ $('auth-form').addEventListener('submit', (e) => {
|
|
|
895
1498
|
boot();
|
|
896
1499
|
});
|
|
897
1500
|
$('panel-close').addEventListener('click', closePanel);
|
|
1501
|
+
$('models-close').addEventListener('click', closeModels);
|
|
1502
|
+
$('models').addEventListener('click', (e) => { if (e.target === $('models')) closeModels(); });
|
|
1503
|
+
$('models').addEventListener('close', () => {
|
|
1504
|
+
hideTip();
|
|
1505
|
+
const opener = modelsOpener?.isConnected ? modelsOpener
|
|
1506
|
+
: modelsView.sessionId ? cards.get(modelsView.sessionId)?.querySelector('.model-pill')
|
|
1507
|
+
: $('providers').querySelector(`.provider[data-id="${modelsView.providerId}"] .model-stats-head`);
|
|
1508
|
+
opener?.focus();
|
|
1509
|
+
modelsOpener = null;
|
|
1510
|
+
});
|
|
1511
|
+
document.addEventListener('pointerover', (e) => {
|
|
1512
|
+
const info = e.target.closest?.('.info');
|
|
1513
|
+
if (info && e.pointerType !== 'touch') showTip(info);
|
|
1514
|
+
});
|
|
1515
|
+
document.addEventListener('pointerout', (e) => {
|
|
1516
|
+
if (tipFor && e.target === tipFor && document.activeElement !== tipFor) hideTip();
|
|
1517
|
+
});
|
|
1518
|
+
document.addEventListener('pointerdown', (e) => { if (tipFor && !e.target.closest?.('.info')) hideTip(); });
|
|
1519
|
+
document.addEventListener('click', (e) => {
|
|
1520
|
+
const info = e.target.closest?.('.info');
|
|
1521
|
+
if (info) showTip(info);
|
|
1522
|
+
});
|
|
1523
|
+
document.addEventListener('focusin', (e) => { if (!quietFocus && e.target.matches?.('.info')) showTip(e.target); });
|
|
1524
|
+
document.addEventListener('focusout', (e) => { if (e.target === tipFor) hideTip(); });
|
|
1525
|
+
document.addEventListener('keydown', (e) => {
|
|
1526
|
+
if (e.key !== 'Escape' || !tipFor) return;
|
|
1527
|
+
hideTip();
|
|
1528
|
+
e.preventDefault();
|
|
1529
|
+
e.stopPropagation();
|
|
1530
|
+
}, true);
|
|
1531
|
+
addEventListener('scroll', () => { if (tipFor) hideTip(); }, true);
|
|
1532
|
+
addEventListener('resize', () => { if (tipFor) hideTip(); });
|
|
1533
|
+
$('models-more').addEventListener('click', () => {
|
|
1534
|
+
const before = $('models-list').childElementCount;
|
|
1535
|
+
modelsView.all = true;
|
|
1536
|
+
renderModels();
|
|
1537
|
+
$('models-list').children[before]?.querySelector('.model-toggle')?.focus();
|
|
1538
|
+
});
|
|
898
1539
|
$('stop-manager').addEventListener('click', () => stopManager());
|
|
1540
|
+
$('upgrade').addEventListener('click', upgradeManager);
|
|
1541
|
+
$('theme-toggle').addEventListener('click', toggleTheme);
|
|
1542
|
+
applyTheme(currentTheme());
|
|
1543
|
+
// Follow the system setting until the user picks a theme.
|
|
1544
|
+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
|
1545
|
+
if (!load(THEME_KEY)) applyTheme(e.matches ? 'dark' : 'light');
|
|
1546
|
+
});
|
|
899
1547
|
$('panel-stop').addEventListener('click', () => {
|
|
900
1548
|
const s = state.sessions.get(state.activeId);
|
|
901
1549
|
if (!s) return;
|
|
@@ -903,6 +1551,8 @@ $('panel-stop').addEventListener('click', () => {
|
|
|
903
1551
|
else removeSession(s.id);
|
|
904
1552
|
});
|
|
905
1553
|
$('cwd').value = load(CWD_KEY) || '';
|
|
1554
|
+
try { state.accounts = JSON.parse(load(ACCOUNTS_KEY)) || {}; } catch { state.accounts = {}; }
|
|
1555
|
+
if (typeof state.accounts !== 'object' || Array.isArray(state.accounts)) state.accounts = {};
|
|
906
1556
|
setInterval(renderSessions, 30000);
|
|
907
1557
|
|
|
908
1558
|
// The terminal panel sits below the top bar, which wraps onto two rows on
|