@oddessentials/agent-guild 0.2.0 → 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 +17 -7
- package/src/manager/providers.mjs +89 -5
- package/src/manager/self-update.mjs +205 -0
- package/src/manager/server.mjs +10 -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 +177 -12
- package/web/art/art.css +8 -6
- package/web/index.html +5 -0
- package/web/styles.css +29 -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,6 +12,7 @@ const state = {
|
|
|
10
12
|
token: null,
|
|
11
13
|
providers: [],
|
|
12
14
|
usage: new Map(),
|
|
15
|
+
accounts: {},
|
|
13
16
|
stats: null,
|
|
14
17
|
statsFor: new Map(),
|
|
15
18
|
sessions: new Map(),
|
|
@@ -17,6 +20,10 @@ const state = {
|
|
|
17
20
|
activeId: null,
|
|
18
21
|
eventsSocket: null,
|
|
19
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,
|
|
20
27
|
/** True from a shutdown request until the manager is reachable again. */
|
|
21
28
|
stopping: false,
|
|
22
29
|
/** After a stop: how many session processes did not confirm exiting, or null if the manager never said. */
|
|
@@ -43,8 +50,93 @@ function setConnection(kind, label) {
|
|
|
43
50
|
const el = $('connection');
|
|
44
51
|
el.className = `connection ${kind}`;
|
|
45
52
|
el.querySelector('.label').textContent = label;
|
|
46
|
-
// The manager can only be stopped while the page can reach it.
|
|
47
|
-
|
|
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
|
+
}
|
|
48
140
|
}
|
|
49
141
|
|
|
50
142
|
function relativeTime(iso) {
|
|
@@ -148,6 +240,51 @@ function wsUrl(path) {
|
|
|
148
240
|
|
|
149
241
|
// ---- providers ------------------------------------------------------------
|
|
150
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
|
+
|
|
151
288
|
function renderProviders() {
|
|
152
289
|
const list = $('providers');
|
|
153
290
|
const tpl = $('provider-template');
|
|
@@ -168,7 +305,6 @@ function renderProviders() {
|
|
|
168
305
|
const existing = node.querySelector('.existing');
|
|
169
306
|
const hint = node.querySelector('.hint');
|
|
170
307
|
start.hidden = !provider.available;
|
|
171
|
-
start.title = `Start a new ${provider.tool} session`;
|
|
172
308
|
start.addEventListener('click', () => startSession(provider, node));
|
|
173
309
|
existing.hidden = !provider.available || !provider.resumable;
|
|
174
310
|
existing.title = `Resume one of ${provider.tool}'s own sessions by its id`;
|
|
@@ -186,6 +322,7 @@ function renderProviders() {
|
|
|
186
322
|
renderHint(hint, provider);
|
|
187
323
|
renderCopies(node.querySelector('.copies'), provider);
|
|
188
324
|
renderConsoleLinks(node, provider);
|
|
325
|
+
renderAccounts(node, provider);
|
|
189
326
|
renderUsage(node, provider);
|
|
190
327
|
renderModelStats(node, provider);
|
|
191
328
|
return node;
|
|
@@ -284,14 +421,21 @@ function creditsNote(usage) {
|
|
|
284
421
|
|
|
285
422
|
function renderUsage(card, provider) {
|
|
286
423
|
const host = card.querySelector('.usage');
|
|
287
|
-
const
|
|
424
|
+
const account = selectedAccount(provider);
|
|
425
|
+
const usage = usageFor(provider, account);
|
|
288
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` : ''}`;
|
|
289
433
|
if (!provider.available || !provider.usageSource || !usage) return host.replaceChildren();
|
|
290
434
|
if (usage.error || usage.windows.length === 0) {
|
|
291
435
|
const note = document.createElement('div');
|
|
292
436
|
note.className = 'usage-note';
|
|
293
|
-
note.textContent = `Usage: ${usage.error || 'no limits reported'}`;
|
|
294
|
-
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;
|
|
295
439
|
return host.replaceChildren(note, ...creditsNote(usage));
|
|
296
440
|
}
|
|
297
441
|
host.replaceChildren(...usage.windows.map((w) => {
|
|
@@ -313,10 +457,12 @@ function renderUsage(card, provider) {
|
|
|
313
457
|
async function loadUsage() {
|
|
314
458
|
let usage;
|
|
315
459
|
try { ({ usage } = await api('GET', '/usage')); } catch { return; }
|
|
316
|
-
state.usage = new Map(usage.map((u) => [u.providerId
|
|
460
|
+
state.usage = new Map(usage.map((u) => [`${u.providerId}/${u.accountId ?? 'default'}`, u]));
|
|
317
461
|
for (const card of $('providers').children) {
|
|
318
462
|
const provider = state.providers.find((p) => p.id === card.dataset.id);
|
|
319
|
-
if (provider)
|
|
463
|
+
if (!provider) continue;
|
|
464
|
+
renderAccounts(card, provider);
|
|
465
|
+
renderUsage(card, provider);
|
|
320
466
|
}
|
|
321
467
|
}
|
|
322
468
|
|
|
@@ -818,7 +964,7 @@ async function startSession(provider, card, { resume } = {}) {
|
|
|
818
964
|
save(CWD_KEY, cwd);
|
|
819
965
|
card.classList.add('busy');
|
|
820
966
|
try {
|
|
821
|
-
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 };
|
|
822
968
|
const { session } = await api('POST', '/sessions', body);
|
|
823
969
|
upsertSession(session);
|
|
824
970
|
openPanel(session.id);
|
|
@@ -852,6 +998,12 @@ function modelTitle(s) {
|
|
|
852
998
|
return `Model ${modelText(s)}${id}, ${MODEL_SOURCES[s.model.source] || s.model.source}`;
|
|
853
999
|
}
|
|
854
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
|
+
|
|
855
1007
|
function statusText(s) {
|
|
856
1008
|
if (s.status === 'exited') {
|
|
857
1009
|
if (s.signal) return `Exited (${s.signal})`;
|
|
@@ -891,7 +1043,7 @@ function updateCard(node, s) {
|
|
|
891
1043
|
badge.title = `Level ${level}`;
|
|
892
1044
|
node.querySelector('.name').textContent = s.name;
|
|
893
1045
|
const resumed = s.resume ? ` · resumed ${s.resume}` : '';
|
|
894
|
-
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(' · ');
|
|
895
1047
|
const pill = node.querySelector('.status-pill');
|
|
896
1048
|
pill.textContent = statusText(s);
|
|
897
1049
|
pill.className = `status-pill ${s.status === 'exited' ? 'exited' : s.activity}`;
|
|
@@ -910,7 +1062,8 @@ function updateCard(node, s) {
|
|
|
910
1062
|
node.querySelector('.stop').hidden = s.status !== 'running';
|
|
911
1063
|
node.querySelector('.remove').hidden = s.status === 'running';
|
|
912
1064
|
const modelLabel = s.model ? `, model ${modelText(s)}` : '';
|
|
913
|
-
|
|
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`);
|
|
914
1067
|
}
|
|
915
1068
|
|
|
916
1069
|
function renderSessions() {
|
|
@@ -1146,7 +1299,7 @@ function updatePanel() {
|
|
|
1146
1299
|
if (!s) return;
|
|
1147
1300
|
paintProviderIcon($('panel-icon'), s.provider);
|
|
1148
1301
|
$('panel-title').textContent = s.name;
|
|
1149
|
-
$('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(' · ');
|
|
1150
1303
|
$('panel-sub').title = modelTitle(s);
|
|
1151
1304
|
renderAgents($('panel-agents'), s.agents);
|
|
1152
1305
|
const stop = $('panel-stop');
|
|
@@ -1246,6 +1399,9 @@ function connectEvents() {
|
|
|
1246
1399
|
state.sessions = new Map(msg.sessions.map((s) => [s.id, s]));
|
|
1247
1400
|
for (const id of [...state.views.keys()]) if (!state.sessions.has(id)) dropSession(id);
|
|
1248
1401
|
renderSessions();
|
|
1402
|
+
setUpgrade(msg.upgrade);
|
|
1403
|
+
} else if (msg.type === 'manager.upgrade') {
|
|
1404
|
+
setUpgrade(msg.upgrade);
|
|
1249
1405
|
} else if (msg.type === 'manager.stopping') {
|
|
1250
1406
|
enterStopping(msg.running);
|
|
1251
1407
|
} else if (msg.type === 'manager.stopped') {
|
|
@@ -1381,6 +1537,13 @@ $('models-more').addEventListener('click', () => {
|
|
|
1381
1537
|
$('models-list').children[before]?.querySelector('.model-toggle')?.focus();
|
|
1382
1538
|
});
|
|
1383
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
|
+
});
|
|
1384
1547
|
$('panel-stop').addEventListener('click', () => {
|
|
1385
1548
|
const s = state.sessions.get(state.activeId);
|
|
1386
1549
|
if (!s) return;
|
|
@@ -1388,6 +1551,8 @@ $('panel-stop').addEventListener('click', () => {
|
|
|
1388
1551
|
else removeSession(s.id);
|
|
1389
1552
|
});
|
|
1390
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 = {};
|
|
1391
1556
|
setInterval(renderSessions, 30000);
|
|
1392
1557
|
|
|
1393
1558
|
// The terminal panel sits below the top bar, which wraps onto two rows on
|
package/web/art/art.css
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
.level-badge { display: none; }
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
/* The guild look is the dark theme. The rules below nest under the theme
|
|
4
|
+
attribute that theme.js sets on <html>, so they apply whether the user
|
|
5
|
+
chose dark or the system did. :where() keeps their specificity as it was
|
|
6
|
+
inside the media query, so styles.css still loses to them only on order. */
|
|
7
|
+
:where(:root[data-theme="dark"]) {
|
|
8
|
+
--gold: #e8c47c;
|
|
9
|
+
--gold-dim: #8a6d3b;
|
|
10
|
+
--card-bg: rgba(18, 20, 26, 0.9);
|
|
9
11
|
|
|
10
12
|
.provider, .session-card {
|
|
11
13
|
--pc: #a67cf6;
|
package/web/index.html
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>Agent Guild</title>
|
|
7
7
|
<meta name="color-scheme" content="light dark">
|
|
8
|
+
<script src="/theme.js"></script>
|
|
8
9
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%235B5BD6'/%3E%3Cpath d='M9 22l7-12 7 12' stroke='white' stroke-width='3' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E">
|
|
9
10
|
<link rel="stylesheet" href="/vendor/xterm/xterm.css">
|
|
10
11
|
<link rel="stylesheet" href="/styles.css">
|
|
@@ -20,6 +21,9 @@
|
|
|
20
21
|
<div id="connection" class="connection" role="status" aria-live="polite">
|
|
21
22
|
<span class="dot"></span><span class="label">Connecting…</span>
|
|
22
23
|
</div>
|
|
24
|
+
<span id="upgrade-note" class="upgrade-note" hidden></span>
|
|
25
|
+
<button id="upgrade" class="btn upgrade" type="button" hidden></button>
|
|
26
|
+
<button id="theme-toggle" class="btn theme-toggle" type="button"></button>
|
|
23
27
|
<button id="stop-manager" class="btn danger stop-manager" type="button" hidden title="Stop the session manager. This ends every session.">Stop manager</button>
|
|
24
28
|
</div>
|
|
25
29
|
</header>
|
|
@@ -113,6 +117,7 @@
|
|
|
113
117
|
<span class="tool"></span>
|
|
114
118
|
<span class="state"></span>
|
|
115
119
|
</span>
|
|
120
|
+
<div class="accounts" role="tablist" aria-label="Accounts" hidden></div>
|
|
116
121
|
<div class="usage"></div>
|
|
117
122
|
<div class="model-stats"></div>
|
|
118
123
|
<div class="provider-links">
|
package/web/styles.css
CHANGED
|
@@ -18,21 +18,20 @@
|
|
|
18
18
|
color-scheme: light;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
21
|
+
/* theme.js sets data-theme from the saved choice or the system setting before the first paint. */
|
|
22
|
+
:root[data-theme="dark"] {
|
|
23
|
+
--bg: #0f1115;
|
|
24
|
+
--surface: #171a21;
|
|
25
|
+
--surface-2: #1e222b;
|
|
26
|
+
--border: #2a303b;
|
|
27
|
+
--text: #e6e9ef;
|
|
28
|
+
--muted: #9aa4b2;
|
|
29
|
+
--accent: #7c7cf0;
|
|
30
|
+
--danger: #ef6b66;
|
|
31
|
+
--ok: #4cc38a;
|
|
32
|
+
--warn: #e0a84a;
|
|
33
|
+
--shadow: none;
|
|
34
|
+
color-scheme: dark;
|
|
36
35
|
}
|
|
37
36
|
|
|
38
37
|
* { box-sizing: border-box; }
|
|
@@ -86,6 +85,16 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 16px 48px; display: grid
|
|
|
86
85
|
.provider .state.update-available { color: var(--warn); }
|
|
87
86
|
.provider.unavailable .state { color: var(--muted); }
|
|
88
87
|
.provider .update { color: var(--warn); border-color: var(--warn); }
|
|
88
|
+
.provider .accounts { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 6px; }
|
|
89
|
+
.account-chip {
|
|
90
|
+
font: inherit; font-size: 0.75rem; font-weight: 600; padding: 2px 10px; border-radius: 999px; cursor: pointer;
|
|
91
|
+
border: 1px solid var(--border); background: var(--surface-2); color: var(--muted); max-width: 100%;
|
|
92
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
93
|
+
}
|
|
94
|
+
.account-chip:hover { border-color: var(--muted); }
|
|
95
|
+
.account-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
96
|
+
.account-chip[aria-selected="true"] { background: var(--accent); border-color: var(--accent); color: var(--accent-text); }
|
|
97
|
+
.account-chip.unsigned { border-style: dashed; }
|
|
89
98
|
.provider .usage { grid-column: 1 / -1; display: grid; gap: 4px; }
|
|
90
99
|
.provider .usage:empty { display: none; }
|
|
91
100
|
.meter { display: grid; grid-template-columns: 5.5em 1fr auto; align-items: center; gap: 8px; font-size: 0.75rem; color: var(--muted); }
|
|
@@ -152,7 +161,7 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 16px 48px; display: grid
|
|
|
152
161
|
.tier-b { --tier: #3b82f6; --tier-ink: #1d4ed8; }
|
|
153
162
|
.tier-c { --tier: #22a35a; --tier-ink: #166534; }
|
|
154
163
|
.tier-d { --tier: #94a3b8; --tier-ink: #475569; }
|
|
155
|
-
|
|
164
|
+
:where(:root[data-theme="dark"]) {
|
|
156
165
|
.tier-s { --tier: #e0b45c; --tier-ink: #f6dea6; }
|
|
157
166
|
.tier-a { --tier: #a78bfa; --tier-ink: #ddd3ff; }
|
|
158
167
|
.tier-b { --tier: #60a5fa; --tier-ink: #cfe3ff; }
|
|
@@ -240,7 +249,9 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 16px 48px; display: grid
|
|
|
240
249
|
.btn.danger { color: var(--danger); }
|
|
241
250
|
.btn.danger:hover { border-color: var(--danger); }
|
|
242
251
|
.btn:disabled { opacity: 0.6; cursor: progress; }
|
|
243
|
-
.stop-manager { white-space: nowrap; }
|
|
252
|
+
.stop-manager, .theme-toggle, .upgrade { white-space: nowrap; }
|
|
253
|
+
.btn.upgrade { color: var(--warn); border-color: var(--warn); }
|
|
254
|
+
.upgrade-note { font-size: 0.85rem; color: var(--warn); }
|
|
244
255
|
|
|
245
256
|
.empty { color: var(--muted); margin: 8px 0 0; }
|
|
246
257
|
|
|
@@ -308,7 +319,7 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 16px 48px; display: grid
|
|
|
308
319
|
.badge.new { --badge: var(--ok); color: color-mix(in srgb, var(--badge) 72%, #000); }
|
|
309
320
|
.badge.expires { --badge: var(--warn); color: color-mix(in srgb, var(--badge) 62%, #000); }
|
|
310
321
|
.badge.in-use, .badge.new, .badge.expires { border-color: color-mix(in srgb, var(--badge) 45%, transparent); }
|
|
311
|
-
|
|
322
|
+
:where(:root[data-theme="dark"]) {
|
|
312
323
|
.badge.in-use, .badge.new, .badge.expires { color: var(--badge); }
|
|
313
324
|
}
|
|
314
325
|
.model-detail {
|
package/web/theme.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Applies the chosen theme before the first paint, so the page never flashes
|
|
2
|
+
// the other one. Loaded in <head> as a plain script; app.js owns the toggle.
|
|
3
|
+
(function () {
|
|
4
|
+
var theme = null;
|
|
5
|
+
try { theme = localStorage.getItem('agentGuild.theme'); } catch (e) { /* storage unavailable */ }
|
|
6
|
+
if (theme !== 'light' && theme !== 'dark') {
|
|
7
|
+
theme = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
8
|
+
}
|
|
9
|
+
document.documentElement.dataset.theme = theme;
|
|
10
|
+
})();
|