@oddessentials/agent-guild 1.22.0 → 1.23.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.
Files changed (37) hide show
  1. package/bin/native/win32-arm64/agent-guild-report.exe +0 -0
  2. package/bin/native/win32-x64/agent-guild-report.exe +0 -0
  3. package/bin/windows/arm64/agent-guild-start.exe +0 -0
  4. package/bin/windows/x64/agent-guild-start.exe +0 -0
  5. package/docs/api.md +23 -12
  6. package/node_modules/node-pty/prebuilds/win32-arm64/conpty.node +0 -0
  7. package/node_modules/node-pty/prebuilds/win32-arm64/conpty_console_list.node +0 -0
  8. package/node_modules/node-pty/prebuilds/win32-x64/conpty.node +0 -0
  9. package/node_modules/node-pty/prebuilds/win32-x64/conpty_console_list.node +0 -0
  10. package/node_modules/smol-toml/LICENSE +24 -0
  11. package/node_modules/smol-toml/README.md +605 -0
  12. package/node_modules/smol-toml/dist/date.d.ts +41 -0
  13. package/node_modules/smol-toml/dist/date.js +156 -0
  14. package/node_modules/smol-toml/dist/error.d.ts +37 -0
  15. package/node_modules/smol-toml/dist/error.js +67 -0
  16. package/node_modules/smol-toml/dist/extract.js +293 -0
  17. package/node_modules/smol-toml/dist/index.cjs +771 -0
  18. package/node_modules/smol-toml/dist/index.d.ts +44 -0
  19. package/node_modules/smol-toml/dist/index.js +34 -0
  20. package/node_modules/smol-toml/dist/parse.d.ts +73 -0
  21. package/node_modules/smol-toml/dist/parse.js +164 -0
  22. package/node_modules/smol-toml/dist/primitive.js +151 -0
  23. package/node_modules/smol-toml/dist/stringify.d.ts +32 -0
  24. package/node_modules/smol-toml/dist/stringify.js +219 -0
  25. package/node_modules/smol-toml/dist/struct.js +156 -0
  26. package/node_modules/smol-toml/dist/util.d.ts +43 -0
  27. package/node_modules/smol-toml/dist/util.js +57 -0
  28. package/node_modules/smol-toml/package.json +68 -0
  29. package/package.json +2 -1
  30. package/src/manager/config.mjs +10 -10
  31. package/src/manager/mcp-connect.mjs +332 -98
  32. package/src/manager/server.mjs +6 -3
  33. package/web/app.js +180 -42
  34. package/web/index.html +23 -7
  35. package/web/styles.css +28 -9
  36. package/web/yard/art.json +1 -1
  37. package/web/yard/yard.css +1 -0
package/web/app.js CHANGED
@@ -47,6 +47,7 @@ const DOCK_WIDTH_KEY = 'agentGuild.dockWidth';
47
47
  const PANES_KEY = 'agentGuild.panes';
48
48
  const SPLIT_RATIO_KEY = 'agentGuild.splitRatio';
49
49
  const TERMINAL_FONT_KEY = 'agentGuild.terminalFontSize';
50
+ const TOOL_CARDS_KEY = 'agentGuild.toolCards';
50
51
  const RELEASES_URL = 'https://github.com/oddessentials/agent-guild/releases';
51
52
  const SUPPORT_URL = 'https://oddessentials.ai/donate/?ref=agent-guild';
52
53
  const isMac = /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
@@ -1624,6 +1625,7 @@ function selectAccount(provider, id) {
1624
1625
  renderAccounts(card, provider);
1625
1626
  renderUsage(card, provider);
1626
1627
  renderReportingSetup(card, provider);
1628
+ renderMcpSetup(card, provider);
1627
1629
  }
1628
1630
  refreshPendingActions();
1629
1631
  notifyViews();
@@ -1966,6 +1968,7 @@ function renderAccounts(card, provider) {
1966
1968
  renderAccounts(card, provider);
1967
1969
  renderUsage(card, provider);
1968
1970
  renderReportingSetup(card, provider);
1971
+ renderMcpSetup(card, provider);
1969
1972
  });
1970
1973
  return chip;
1971
1974
  }));
@@ -2265,51 +2268,89 @@ function renderReportingSetup(card, provider) {
2265
2268
  };
2266
2269
  }
2267
2270
 
2268
- const MCP_STATES = { on: 'on', off: 'off', disabled: 'turned off in the tool', other: 'another server is named agent-guild', unknown: 'settings could not be read' };
2271
+ const openMcp = new Set();
2272
+ const mcpBusy = new Set();
2269
2273
 
2270
- function mcpText(provider, mcp) {
2271
- if (mcp.state === 'checking') return 'Agent Guild MCP: checking';
2272
- if (mcp.state === 'on') return mcp.total > 1 ? `Agent Guild MCP on for all ${mcp.total} accounts` : 'Agent Guild MCP on';
2273
- if (mcp.state === 'partial') return `Agent Guild MCP on for ${mcp.on} of ${mcp.total} accounts`;
2274
- if (mcp.state === 'disabled') return `Agent Guild MCP turned off in ${provider.tool}`;
2275
- if (mcp.state === 'other') return 'Agent Guild MCP: another server is named agent-guild';
2276
- if (mcp.state === 'unknown') return 'Agent Guild MCP: settings could not be read';
2277
- return 'Agent Guild MCP off';
2274
+ /** What the card's Agent Guild MCP section says about one account, and the change it offers. */
2275
+ function mcpView(provider, entry) {
2276
+ const tool = provider.tool;
2277
+ const from = entry.from ? `${tool} takes it from ${entry.from}'s settings. ` : '';
2278
+ const yours = !entry.managed && !entry.from ? 'You added it yourself. ' : '';
2279
+ const typing = entry.allowInput ? ' It can also type into sessions.' : '';
2280
+ if (entry.state === 'checking') return { label: 'Checking…', status: `Reading ${tool}'s MCP settings.` };
2281
+ if (entry.state === 'off') {
2282
+ return { label: 'Off', status: entry.detail ? `${entry.detail} Turn on adds Agent Guild's own server to ${tool}'s settings.` : `Not added to ${tool}.`, action: 'on', button: 'Turn on' };
2283
+ }
2284
+ if (entry.state === 'on') {
2285
+ return {
2286
+ label: 'On', status: `${from}${yours}New ${tool} sessions can use it.${typing}`, action: 'off', button: 'Turn off',
2287
+ ask: yours ? { message: `Remove the agent-guild server you added to ${tool}'s settings?\n\n${entry.command}`, ok: 'Remove' } : null,
2288
+ };
2289
+ }
2290
+ if (entry.state === 'disabled') {
2291
+ const replace = provider.mcp.mode === 'codex' && !entry.managed;
2292
+ return {
2293
+ label: 'Turned off', status: `${from}${yours}It is turned off in ${tool}.`, action: replace ? 'replace' : 'on', button: replace ? 'Replace…' : 'Turn on',
2294
+ ask: replace ? { message: `${tool} can turn this server on only by replacing it with Agent Guild's own. Replace it?\n\nNow: ${entry.command}`, ok: 'Replace' } : null,
2295
+ };
2296
+ }
2297
+ if (entry.state === 'conflict') {
2298
+ return {
2299
+ label: 'Conflict', warn: true, action: 'replace', button: 'Replace…',
2300
+ status: `${entry.from ? `${tool} takes a server named agent-guild from ${entry.from}'s settings that` : `${tool} already has a server named agent-guild that`} ${entry.detail}.`,
2301
+ ask: entry.from
2302
+ ? { message: `Add Agent Guild's own MCP server to ${tool}'s settings? ${tool} then uses it instead of the one in ${entry.from}'s settings, which stays as it is.`, ok: 'Add' }
2303
+ : { message: `Replace the agent-guild server in ${tool}'s settings with Agent Guild's own?${entry.command ? `\n\nNow: ${entry.command}` : ''}`, ok: 'Replace' },
2304
+ };
2305
+ }
2306
+ return { label: 'Unreadable', warn: true, status: `Agent Guild can't read ${tool}'s MCP settings: ${entry.detail}.` };
2278
2307
  }
2279
2308
 
2280
2309
  function renderMcpSetup(card, provider) {
2281
- const row = card.querySelector('.mcp-row');
2282
- const mcp = provider.mcp;
2283
- row.hidden = !provider.available || !mcp;
2284
- if (row.hidden) return;
2285
- const text = row.querySelector('.mcp-text');
2286
- text.textContent = mcpText(provider, mcp);
2287
- text.title = mcp.accounts.map((a) => `${a.label}: ${MCP_STATES[a.state] ?? a.state}${a.detail ? `. ${a.detail}` : ''}`).join('\n');
2288
- const states = new Set(mcp.accounts.map((a) => a.state));
2289
- const turnOn = row.querySelector('.mcp-on');
2290
- const turnOff = row.querySelector('.mcp-off');
2291
- turnOn.hidden = !states.has('off') && !states.has('disabled');
2292
- turnOff.hidden = !states.has('on') && !states.has('disabled');
2293
- turnOn.title = `Lets ${provider.tool} see your other Agent Guild sessions, read their answers and screens, and start or stop sessions when you approve. Typing into sessions stays off. Adds agent-guild to ${provider.tool}'s MCP servers for every account.`;
2294
- turnOff.title = `Removes Agent Guild's MCP server from ${provider.tool}'s settings. A server named agent-guild that runs something else is left as it is.`;
2295
- const change = async (enabled) => {
2296
- turnOn.disabled = turnOff.disabled = true;
2297
- try {
2298
- const { results } = await api('POST', `/providers/${provider.id}/mcp`, { enabled });
2299
- const problems = results.filter((r) => r.outcome === 'failed' || r.outcome === 'skipped');
2300
- const done = results.length - problems.length;
2301
- const lead = !problems.length ? `Agent Guild MCP is ${enabled ? `on for ${provider.tool}. New sessions can use it` : `off for ${provider.tool}`}.`
2302
- : `Agent Guild MCP is ${enabled ? 'on' : 'off'} for ${done} of ${results.length} ${provider.tool} accounts.`;
2303
- toast([lead, ...problems.map((r) => `${r.label}: ${r.detail ?? MCP_STATES[r.state] ?? r.state}.`)].join(' '), problems.length ? 12000 : undefined);
2304
- } catch (err) {
2305
- if (err instanceof AuthError) return showAuth(err.message);
2306
- toast(err.message, 10000);
2307
- } finally {
2308
- turnOn.disabled = turnOff.disabled = false;
2309
- }
2310
- };
2311
- turnOn.onclick = () => change(true);
2312
- turnOff.onclick = () => change(false);
2310
+ const box = card.querySelector('.mcp');
2311
+ const account = selectedAccount(provider);
2312
+ const entry = provider.mcp?.accounts.find((a) => a.id === account.id);
2313
+ box.hidden = !provider.available || !entry;
2314
+ if (box.hidden) return;
2315
+ const view = mcpView(provider, entry);
2316
+ const key = `${provider.id}:${entry.id}`;
2317
+ box.open = openMcp.has(provider.id);
2318
+ box.ontoggle = () => (box.open ? openMcp.add(provider.id) : openMcp.delete(provider.id));
2319
+ box.classList.toggle('warned', Boolean(view.warn));
2320
+ box.querySelector('.mcp-state').textContent = view.label;
2321
+ box.querySelector('.mcp-about').textContent = `Lets ${provider.tool} see your Agent Guild sessions, read their replies and screens, and start or stop sessions when you approve.`;
2322
+ box.querySelector('.mcp-status').textContent = (provider.mcp.accounts.length > 1 ? `${entry.label} account: ` : '') + view.status;
2323
+ const command = box.querySelector('.mcp-command');
2324
+ command.hidden = !entry.command || entry.managed;
2325
+ command.textContent = entry.command ?? '';
2326
+ const button = box.querySelector('.mcp-action');
2327
+ button.hidden = !view.action;
2328
+ button.textContent = view.button ?? '';
2329
+ button.disabled = mcpBusy.has(key);
2330
+ button.onclick = () => changeMcp(provider, entry, view);
2331
+ }
2332
+
2333
+ function repaintMcp(id) {
2334
+ const provider = state.providers.find((p) => p.id === id);
2335
+ if (!provider) return;
2336
+ for (const card of document.querySelectorAll('.provider[data-id]')) if (card.dataset.id === id) renderMcpSetup(card, provider);
2337
+ }
2338
+
2339
+ async function changeMcp(provider, entry, view) {
2340
+ if (view.ask && !(await ask(view.ask.message, { ok: view.ask.ok }))) return;
2341
+ const key = `${provider.id}:${entry.id}`;
2342
+ mcpBusy.add(key);
2343
+ repaintMcp(provider.id);
2344
+ try {
2345
+ const { result } = await api('POST', `/providers/${provider.id}/mcp`, { account: entry.id, action: view.action, expect: entry.fingerprint });
2346
+ if (result.outcome === 'failed') toast(result.detail, 12000);
2347
+ } catch (err) {
2348
+ if (err instanceof AuthError) return showAuth(err.message);
2349
+ toast(err.message, 10000);
2350
+ } finally {
2351
+ mcpBusy.delete(key);
2352
+ repaintMcp(provider.id);
2353
+ }
2313
2354
  }
2314
2355
 
2315
2356
  let dealt = false;
@@ -2391,11 +2432,87 @@ function buildProvider(provider) {
2391
2432
  return node;
2392
2433
  }
2393
2434
 
2435
+ function readToolCards() {
2436
+ let saved = null;
2437
+ try { saved = JSON.parse(load(TOOL_CARDS_KEY)); } catch {}
2438
+ const ids = (list) => new Set(Array.isArray(list) ? list.filter((id) => typeof id === 'string') : []);
2439
+ return { collapsed: ids(saved?.collapsed), hidden: ids(saved?.hidden) };
2440
+ }
2441
+
2442
+ const toolCards = readToolCards();
2443
+
2444
+ function saveToolCards() {
2445
+ save(TOOL_CARDS_KEY, JSON.stringify({ collapsed: [...toolCards.collapsed], hidden: [...toolCards.hidden] }));
2446
+ }
2447
+
2448
+ function paintCollapse(card, provider) {
2449
+ const collapsed = toolCards.collapsed.has(provider.id);
2450
+ card.classList.toggle('collapsed', collapsed);
2451
+ const toggle = card.querySelector('.card-toggle');
2452
+ toggle.hidden = false;
2453
+ toggle.dataset.muxFocus = `${provider.id}:collapse`;
2454
+ toggle.setAttribute('aria-expanded', String(!collapsed));
2455
+ toggle.setAttribute('aria-label', `${collapsed ? 'Expand' : 'Collapse'} the ${provider.tool} card`);
2456
+ toggle.title = collapsed ? 'Show everything on this card' : 'Show only what starts a session';
2457
+ card.querySelector('.head-tools').hidden = false;
2458
+ }
2459
+
2460
+ function setCollapsed(ids, collapsed) {
2461
+ for (const id of ids) {
2462
+ if (collapsed) toolCards.collapsed.add(id);
2463
+ else toolCards.collapsed.delete(id);
2464
+ }
2465
+ saveToolCards();
2466
+ for (const card of $('providers').children) {
2467
+ const provider = state.providers.find((p) => p.id === card.dataset.id);
2468
+ if (provider && ids.includes(provider.id)) paintCollapse(card, provider);
2469
+ }
2470
+ }
2471
+
2472
+ function setHidden(ids, hidden) {
2473
+ for (const id of ids) {
2474
+ if (hidden) toolCards.hidden.add(id);
2475
+ else toolCards.hidden.delete(id);
2476
+ }
2477
+ saveToolCards();
2478
+ renderProviders();
2479
+ }
2480
+
2481
+ function renderCardsMenu() {
2482
+ const hidden = state.providers.filter((p) => toolCards.hidden.has(p.id)).length;
2483
+ const button = $('cards-open');
2484
+ button.textContent = hidden ? `Cards · ${hidden} hidden` : 'Cards';
2485
+ button.setAttribute('aria-label', hidden ? `Tool cards, ${hidden} hidden` : 'Tool cards');
2486
+ $('cards-none').hidden = !state.providers.length || hidden < state.providers.length;
2487
+ const box = $('cards-shown');
2488
+ const rows = JSON.stringify(state.providers.map((p) => [p.id, p.vendor, p.tool]));
2489
+ if (box.dataset.rows !== rows) {
2490
+ const focused = box.contains(document.activeElement) ? document.activeElement.value : null;
2491
+ box.dataset.rows = rows;
2492
+ box.replaceChildren(box.querySelector('legend'), ...state.providers.map((provider) => {
2493
+ const input = document.createElement('input');
2494
+ input.type = 'checkbox';
2495
+ input.name = 'card';
2496
+ input.value = provider.id;
2497
+ const label = el('label', 'choice', input, el('span', '', provider.tool));
2498
+ label.title = `${provider.vendor} ${provider.tool}`;
2499
+ return label;
2500
+ }));
2501
+ if (focused) [...box.querySelectorAll('input')].find((input) => input.value === focused)?.focus();
2502
+ }
2503
+ for (const input of box.querySelectorAll('input')) input.checked = !toolCards.hidden.has(input.value);
2504
+ }
2505
+
2394
2506
  function renderProviders() {
2395
2507
  const list = $('providers');
2396
2508
  const focused = document.activeElement;
2397
2509
  const focusKey = focused?.dataset?.muxFocus;
2398
- list.replaceChildren(...state.providers.map(buildProvider));
2510
+ list.replaceChildren(...state.providers.filter((p) => !toolCards.hidden.has(p.id)).map((provider) => {
2511
+ const card = buildProvider(provider);
2512
+ paintCollapse(card, provider);
2513
+ return card;
2514
+ }));
2515
+ renderCardsMenu();
2399
2516
  notifyViews();
2400
2517
  if (focusKey) {
2401
2518
  const controls = [...list.querySelectorAll('[data-mux-focus]')];
@@ -8421,6 +8538,26 @@ window.addEventListener('resize', () => {
8421
8538
  placeMenu($('manager-menu'), $('manager'));
8422
8539
  placeMenu($('panel-sessions'), sessionInvoker());
8423
8540
  placeMenu($('panel-text-size'), $('panel-text-toggle'));
8541
+ placeMenu($('cards-menu'), $('cards-open'));
8542
+ });
8543
+ onMenu($('cards-menu'), (menu) => {
8544
+ placeMenu(menu, $('cards-open'));
8545
+ menu.querySelector('input')?.focus();
8546
+ }, (menu) => returnFocus(menu, $('cards-open')));
8547
+ $('cards-menu').addEventListener('change', (e) => {
8548
+ if (e.target.name === 'card') setHidden([e.target.value], !e.target.checked);
8549
+ });
8550
+ $('cards-collapse').addEventListener('click', () => setCollapsed(state.providers.map((p) => p.id), true));
8551
+ $('cards-expand').addEventListener('click', () => setCollapsed(state.providers.map((p) => p.id), false));
8552
+ $('cards-show-all').addEventListener('click', () => {
8553
+ setHidden(state.providers.map((p) => p.id), false);
8554
+ $('providers').querySelector('.card-toggle')?.focus();
8555
+ });
8556
+ $('providers').addEventListener('click', (e) => {
8557
+ const toggle = e.target.closest('.card-toggle');
8558
+ if (!toggle) return;
8559
+ setCollapsed([toggle.closest('.provider').dataset.id], toggle.getAttribute('aria-expanded') === 'true');
8560
+ toggle.scrollIntoView({ block: 'nearest' });
8424
8561
  });
8425
8562
  $('providers').addEventListener('animationend', (e) => {
8426
8563
  if (e.target === e.currentTarget.lastElementChild) e.currentTarget.classList.remove('deal');
@@ -8595,6 +8732,7 @@ yardUi = initYard({
8595
8732
  renderLaunchFlags(node, provider);
8596
8733
  renderUsage(node, provider);
8597
8734
  renderReportingSetup(node, provider);
8735
+ renderMcpSetup(node, provider);
8598
8736
  renderModelStats(node, provider);
8599
8737
  node.dataset.contentSignature = contentSignature;
8600
8738
  }
package/web/index.html CHANGED
@@ -98,7 +98,17 @@
98
98
  <main id="app" hidden>
99
99
  <section class="launcher" aria-labelledby="launch-heading">
100
100
  <div class="section-head">
101
- <h2 id="launch-heading">Start a session</h2>
101
+ <div class="launch-title">
102
+ <h2 id="launch-heading">Start a session</h2>
103
+ <button id="cards-open" class="btn menu-button cards-open" type="button" popovertarget="cards-menu" aria-haspopup="dialog">Cards</button>
104
+ <div id="cards-menu" class="settings-menu menu cards-menu" popover role="dialog" aria-label="Tool cards">
105
+ <fieldset id="cards-shown"><legend>Show</legend></fieldset>
106
+ <fieldset>
107
+ <button id="cards-collapse" class="menu-item" type="button">Collapse all</button>
108
+ <button id="cards-expand" class="menu-item" type="button">Expand all</button>
109
+ </fieldset>
110
+ </div>
111
+ </div>
102
112
  <div class="launch-tools">
103
113
  <div class="cwd">
104
114
  <label for="cwd">Working folder</label>
@@ -124,6 +134,7 @@
124
134
  </div>
125
135
  </div>
126
136
  <div id="providers" class="providers" role="list"></div>
137
+ <p id="cards-none" class="cards-none" hidden>All tool cards are hidden. <button id="cards-show-all" class="btn" type="button">Show all</button></p>
127
138
  </section>
128
139
 
129
140
  <section id="yard" hidden aria-label="Yard">
@@ -799,6 +810,7 @@
799
810
  <span class="state"></span>
800
811
  </span>
801
812
  <span class="head-tools">
813
+ <button class="card-toggle" type="button" aria-expanded="true" hidden></button>
802
814
  <a class="cloud-link" target="_blank" rel="noopener noreferrer" hidden></a>
803
815
  <button class="accounts-link" type="button" aria-haspopup="dialog" aria-label="Accounts" hidden></button>
804
816
  </span>
@@ -817,12 +829,6 @@
817
829
  <span class="reporting-text"></span>
818
830
  <button class="btn reporting-toggle" type="button"></button>
819
831
  </div>
820
- <div class="reporting-row mcp-row" hidden>
821
- <span class="mcp-text"></span>
822
- <a class="console-link mcp-docs" href="https://github.com/oddessentials/agent-guild/blob/main/docs/mcp.md" target="_blank" rel="noopener noreferrer" title="What Agent Guild's MCP server lets a coding agent do">About</a>
823
- <button class="btn mcp-on" type="button">Turn on</button>
824
- <button class="btn mcp-off" type="button">Turn off</button>
825
- </div>
826
832
  <div class="provider-links">
827
833
  <a class="console-link usage-link" target="_blank" rel="noopener noreferrer">Usage</a>
828
834
  <a class="console-link billing-link" target="_blank" rel="noopener noreferrer">Billing</a>
@@ -838,6 +844,16 @@
838
844
  <button class="btn multiplexer-refresh" type="button">Refresh</button>
839
845
  <div class="multiplexer-list"></div>
840
846
  </details>
847
+ <details class="mcp" hidden>
848
+ <summary><span class="mcp-title">Agent Guild MCP</span><span class="mcp-state"></span></summary>
849
+ <p class="mcp-about"></p>
850
+ <p class="mcp-status"></p>
851
+ <code class="mcp-command" hidden></code>
852
+ <div class="mcp-actions">
853
+ <a class="console-link" href="https://github.com/oddessentials/agent-guild/blob/main/docs/mcp.md" target="_blank" rel="noopener noreferrer">How it works</a>
854
+ <button class="btn mcp-action" type="button" hidden></button>
855
+ </div>
856
+ </details>
841
857
  <p class="hint" hidden></p>
842
858
  <div class="provider-actions">
843
859
  <button class="btn primary new" type="button">New</button>
package/web/styles.css CHANGED
@@ -243,7 +243,7 @@ main { max-width: calc(1200px + var(--dock-space, 0px)); margin: 0 auto; padding
243
243
  font: inherit; font-size: 0.78rem; text-align: left; cursor: pointer;
244
244
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
245
245
  }
246
- .model-stats-head::after, .console-link::after, .menu-button::after, .model-name::before, .provider .copies summary::before {
246
+ .model-stats-head::after, .console-link::after, .menu-button::after, .model-name::before, .provider .copies summary::before, .provider .mcp summary::before {
247
247
  content: ""; display: inline-block; flex: none; width: 0.75em; height: 0.75em; margin-left: 0.3em; vertical-align: -0.05em;
248
248
  background: currentColor; mask: var(--icon-chevron) center / contain no-repeat;
249
249
  }
@@ -308,8 +308,6 @@ main { max-width: calc(1200px + var(--dock-space, 0px)); margin: 0 auto; padding
308
308
  }
309
309
  .reporting-row { display: flex; align-items: center; gap: 10px; font-size: 0.78rem; color: var(--muted); }
310
310
  .reporting-row .btn { padding: 2px 10px; font-size: 0.75rem; white-space: nowrap; }
311
- .mcp-row { flex-wrap: wrap; row-gap: 4px; }
312
- .mcp-row .mcp-text { flex-basis: 100%; }
313
311
  .console-link { color: var(--accent); text-decoration: none; }
314
312
  .console-link:hover { text-decoration: underline; }
315
313
  .console-link:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: 3px; }
@@ -317,13 +315,23 @@ main { max-width: calc(1200px + var(--dock-space, 0px)); margin: 0 auto; padding
317
315
  .memory-link, .hint-details { padding: 0; border: 0; background: none; color: var(--accent); font: inherit; cursor: pointer; }
318
316
  .memory-link:hover, .hint-details:hover { text-decoration: underline; }
319
317
  .memory-link:focus-visible, .hint-details:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: 3px; }
320
- /* The bottom margin lets the second icon's padding overlap the gap below, so the header is no taller. */
321
318
  .head-tools { display: flex; flex-direction: column; flex: none; align-self: flex-start; margin: -4px -6px -6px 0; }
322
- .cloud-link, .accounts-link { display: block; width: 32px; height: 32px; padding: 0; border: 0; border-radius: 8px; background: none; color: var(--muted); cursor: pointer; }
323
- .cloud-link::before, .accounts-link::before { content: ""; display: block; width: 100%; height: 100%; background: currentColor; mask: var(--icon-cloud) center / 20px no-repeat; }
319
+ .card-toggle, .cloud-link, .accounts-link { display: block; width: 32px; height: 32px; padding: 0; border: 0; border-radius: 8px; background: none; color: var(--muted); cursor: pointer; }
320
+ .card-toggle::before, .cloud-link::before, .accounts-link::before { content: ""; display: block; width: 100%; height: 100%; background: currentColor; mask: var(--icon-cloud) center / 20px no-repeat; }
324
321
  .accounts-link::before { mask-image: var(--icon-accounts); }
325
- .cloud-link:hover, .accounts-link:hover { color: var(--accent); }
326
- .cloud-link:focus-visible, .accounts-link:focus-visible { color: var(--accent); outline: 2px solid var(--focus); outline-offset: 0; }
322
+ .card-toggle::before { mask-image: var(--icon-chevron); mask-size: 16px; rotate: -90deg; transition: rotate 0.15s var(--ease); }
323
+ .card-toggle[aria-expanded="false"]::before { rotate: 90deg; }
324
+ .providers > .provider.collapsed { padding-top: 16px; align-self: start; }
325
+ :root[data-skin="guild"][data-art="on"] .providers > .provider.collapsed { padding-top: 18px; }
326
+ .providers > .provider.collapsed :is(.card-art, .usage, .model-stats, .reporting-row, .provider-links, .copies, .multiplexers, .mcp, .environment-summary, .hint-details) { display: none; }
327
+ .providers > .provider.collapsed::after { background: var(--brackets, none); background-repeat: no-repeat; }
328
+ .providers > .provider.collapsed.spent .provider-icon { filter: grayscale(1); }
329
+ .launch-title { display: flex; align-items: center; gap: 10px; min-width: 0; }
330
+ .cards-open { padding: 3px 10px; font-size: 0.78rem; }
331
+ #cards-shown .choice > span::before { content: none; }
332
+ .cards-none { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin: 0; color: var(--muted); font-size: 0.85rem; }
333
+ .card-toggle:hover, .cloud-link:hover, .accounts-link:hover { color: var(--accent); }
334
+ .card-toggle:focus-visible, .cloud-link:focus-visible, .accounts-link:focus-visible { color: var(--accent); outline: 2px solid var(--focus); outline-offset: 0; }
327
335
  .provider-actions { margin-top: auto; display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
328
336
  /* A card's note sits with its buttons at the foot of the card, above them. */
329
337
  .provider .hint { margin: auto 0 0; font-size: 0.78rem; color: var(--muted); overflow-wrap: anywhere; }
@@ -338,6 +346,17 @@ main { max-width: calc(1200px + var(--dock-space, 0px)); margin: 0 auto; padding
338
346
  .provider .copies li span { display: block; }
339
347
  .provider .copies .copy-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 24px; }
340
348
  .provider .copies .copy-name { color: var(--text); min-width: 0; }
349
+ .provider .mcp { font-size: 0.78rem; color: var(--muted); overflow-wrap: anywhere; }
350
+ .provider .mcp summary { display: flex; align-items: baseline; cursor: pointer; list-style: none; white-space: nowrap; }
351
+ .provider .mcp summary::-webkit-details-marker { display: none; }
352
+ .provider .mcp summary::before { margin: 0 0.3em 0 0; align-self: center; transition: transform 0.15s var(--ease); }
353
+ .provider .mcp[open] summary::before { transform: rotate(90deg); }
354
+ .provider .mcp .mcp-state { margin-left: auto; padding-left: 8px; font-weight: 600; white-space: nowrap; }
355
+ .provider .mcp.warned .mcp-state { color: var(--warn); }
356
+ .provider .mcp p { margin: 6px 0 0; line-height: 1.45; }
357
+ .provider .mcp .mcp-command { display: block; margin-top: 4px; font-size: 0.72rem; color: var(--text); }
358
+ .provider .mcp .mcp-actions { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 8px; }
359
+ .provider .mcp .mcp-action { padding: 2px 10px; font-size: 0.75rem; white-space: nowrap; }
341
360
  .provider .copies .copy-path { font-family: var(--font-mono); font-size: 0.72rem; }
342
361
  .provider .copies .copy-uninstall { flex: none; padding: 2px 10px; font-size: 0.75rem; }
343
362
  .provider.busy { opacity: 0.6; pointer-events: none; }
@@ -1318,7 +1337,7 @@ body:has(#terminal-panel:not([hidden]) .terminal-controls:not([hidden])) .toast
1318
1337
 
1319
1338
  @media (prefers-reduced-motion: reduce) {
1320
1339
  .status-pill.active::before, .agent.working::after, .model-detail .stat-fill, .toast:not([hidden]), .tip:not([hidden]), .run-failed:not([hidden]), .menu:popover-open { animation: none; }
1321
- .model-name::before, .provider .copies summary::before, .menu-button::after, .btn, .videos-play img, .videos-play-icon, .models, .news-panel, .models::backdrop, .news-panel::backdrop { transition: none; }
1340
+ .model-name::before, .provider .copies summary::before, .provider .mcp summary::before, .menu-button::after, .btn, .videos-play img, .videos-play-icon, .models, .news-panel, .models::backdrop, .news-panel::backdrop { transition: none; }
1322
1341
  .github-waiting::before, .github-running, body:has(#app[hidden]):has(#auth[hidden]):has(#stopped[hidden])::after { animation: none; }
1323
1342
  .btn:active:not(:disabled):not([aria-disabled="true"]) { transform: none; }
1324
1343
  .btn.voice[aria-pressed="true"]::before { animation: none; }