@songsid/agend 2.3.0-beta.1 → 2.3.0-beta.3

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 (174) hide show
  1. package/dist/agent-cli.js +5 -5
  2. package/dist/agent-cli.js.map +1 -1
  3. package/dist/agent-file-read.d.ts +11 -6
  4. package/dist/agent-file-read.js +32 -17
  5. package/dist/agent-file-read.js.map +1 -1
  6. package/dist/agent-whats-new.js +10 -13
  7. package/dist/agent-whats-new.js.map +1 -1
  8. package/dist/auth-api.js +10 -5
  9. package/dist/auth-api.js.map +1 -1
  10. package/dist/backend/claude-code.d.ts +2 -2
  11. package/dist/backend/claude-code.js +10 -5
  12. package/dist/backend/claude-code.js.map +1 -1
  13. package/dist/backend/types.d.ts +13 -0
  14. package/dist/backend/types.js +10 -0
  15. package/dist/backend/types.js.map +1 -1
  16. package/dist/cache-analysis.d.ts +47 -1
  17. package/dist/cache-analysis.js +68 -8
  18. package/dist/cache-analysis.js.map +1 -1
  19. package/dist/cache-report-tool.d.ts +17 -0
  20. package/dist/cache-report-tool.js +40 -0
  21. package/dist/cache-report-tool.js.map +1 -0
  22. package/dist/cache-service.d.ts +15 -0
  23. package/dist/cache-service.js +26 -1
  24. package/dist/cache-service.js.map +1 -1
  25. package/dist/channel/mcp-tools.js +9 -3
  26. package/dist/channel/mcp-tools.js.map +1 -1
  27. package/dist/classic-channel-manager.d.ts +8 -3
  28. package/dist/classic-channel-manager.js +10 -3
  29. package/dist/classic-channel-manager.js.map +1 -1
  30. package/dist/cli.js +7 -3
  31. package/dist/cli.js.map +1 -1
  32. package/dist/config-validator.js +5 -1
  33. package/dist/config-validator.js.map +1 -1
  34. package/dist/daemon.js +5 -5
  35. package/dist/daemon.js.map +1 -1
  36. package/dist/delivery-outbox.js +5 -0
  37. package/dist/delivery-outbox.js.map +1 -1
  38. package/dist/fleet-manager.d.ts +71 -3
  39. package/dist/fleet-manager.js +256 -45
  40. package/dist/fleet-manager.js.map +1 -1
  41. package/dist/fleet-traffic.d.ts +38 -0
  42. package/dist/fleet-traffic.js +96 -0
  43. package/dist/fleet-traffic.js.map +1 -0
  44. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +20 -5
  45. package/dist/general-knowledge/skills/multi-channel/SKILL.md +1 -1
  46. package/dist/general-knowledge/skills/web-appearance/SKILL.md +149 -5
  47. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +398 -0
  48. package/dist/locale.js +6 -4
  49. package/dist/locale.js.map +1 -1
  50. package/dist/memory-hogs.d.ts +27 -0
  51. package/dist/memory-hogs.js +124 -0
  52. package/dist/memory-hogs.js.map +1 -0
  53. package/dist/outbound-handlers.d.ts +4 -0
  54. package/dist/outbound-handlers.js +74 -1
  55. package/dist/outbound-handlers.js.map +1 -1
  56. package/dist/outbound-schemas.d.ts +98 -0
  57. package/dist/outbound-schemas.js +56 -0
  58. package/dist/outbound-schemas.js.map +1 -1
  59. package/dist/public-web-gateway.js +1 -1
  60. package/dist/public-web-gateway.js.map +1 -1
  61. package/dist/scheduler/scheduler.d.ts +12 -0
  62. package/dist/scheduler/scheduler.js +83 -10
  63. package/dist/scheduler/scheduler.js.map +1 -1
  64. package/dist/service-installer.d.ts +2 -1
  65. package/dist/service-installer.js +18 -3
  66. package/dist/service-installer.js.map +1 -1
  67. package/dist/tmux-manager.d.ts +5 -0
  68. package/dist/tmux-manager.js +16 -0
  69. package/dist/tmux-manager.js.map +1 -1
  70. package/dist/tool-permissions.d.ts +5 -0
  71. package/dist/tool-permissions.js +15 -2
  72. package/dist/tool-permissions.js.map +1 -1
  73. package/dist/types.d.ts +1 -1
  74. package/dist/ui/app-effects.js +33 -9
  75. package/dist/ui/chat-effects.js +62 -4
  76. package/dist/ui/chat-strings.js +40 -4
  77. package/dist/ui/chat-thread.js +13 -5
  78. package/dist/ui/effect-trial-panel.js +19 -16
  79. package/dist/ui/fleet-cache.js +23 -1
  80. package/dist/ui/fleet-canvas-layout.js +111 -0
  81. package/dist/ui/fleet-canvas.js +489 -0
  82. package/dist/ui/fleet-org.js +27 -12
  83. package/dist/ui/fleet-traffic.js +224 -0
  84. package/dist/ui/panel-chat.js +8 -2
  85. package/dist/ui/panel-details.js +2 -1
  86. package/dist/ui/panel-fleet.js +17 -5
  87. package/dist/ui/panel-settings.js +3 -2
  88. package/dist/ui/preview.js +3 -2
  89. package/dist/ui/settings-appearance.js +34 -18
  90. package/dist/ui/settings-dialogs.js +3 -6
  91. package/dist/ui/settings-effects.js +4 -3
  92. package/dist/ui/settings-learn.js +25 -0
  93. package/dist/ui/settings-model.js +6 -2
  94. package/dist/ui/settings-mods.js +4 -3
  95. package/dist/ui/settings-owner-effects.js +8 -8
  96. package/dist/ui/settings-strings.js +34 -38
  97. package/dist/ui/shared/app-i18n.js +2 -2
  98. package/dist/ui/shared/app-prefs.js +1 -1
  99. package/dist/ui/shared/app-shell.js +23 -1
  100. package/dist/ui/shared/app-store.js +12 -1
  101. package/dist/ui/shared/app-stream.js +2 -1
  102. package/dist/ui/shared/app.css +163 -12
  103. package/dist/ui/shared/effect-app-runtime.js +1 -1
  104. package/dist/ui/shared/effect-frame.js +10 -5
  105. package/dist/ui/shared/effect-geometry.js +24 -9
  106. package/dist/ui/shared/effect-policy.js +2 -1
  107. package/dist/ui/shared/effect-registry.js +71 -17
  108. package/dist/ui/shared/effect-trial-shadow.js +62 -0
  109. package/dist/ui/shared/effect-trial-stage.js +15 -9
  110. package/dist/ui/shared/effect-trial.css +40 -0
  111. package/dist/ui/shared/identity-card.js +5 -2
  112. package/dist/ui/shared/identity-color.js +3 -3
  113. package/dist/ui/shared/instance-nav.js +35 -9
  114. package/dist/ui/shared/panel-view.js +2 -2
  115. package/dist/ui/shared/usage-dialog.js +14 -2
  116. package/dist/ui/shared/view-strings.js +22 -2
  117. package/dist/ui/signin.html +1 -1
  118. package/dist/usage/providers.d.ts +3 -0
  119. package/dist/usage/providers.js +4 -3
  120. package/dist/usage/providers.js.map +1 -1
  121. package/dist/view-api.d.ts +21 -0
  122. package/dist/view-api.js +28 -0
  123. package/dist/view-api.js.map +1 -1
  124. package/dist/web-api.d.ts +5 -6
  125. package/dist/web-api.js +60 -11
  126. package/dist/web-api.js.map +1 -1
  127. package/dist/web-appearance-view.d.ts +10 -0
  128. package/dist/web-appearance-view.js +77 -8
  129. package/dist/web-appearance-view.js.map +1 -1
  130. package/dist/web-asset-parts.d.ts +3 -8
  131. package/dist/web-asset-parts.js.map +1 -1
  132. package/dist/web-auth.js +1 -1
  133. package/dist/web-auth.js.map +1 -1
  134. package/dist/web-effect-service.js +3 -1
  135. package/dist/web-effect-service.js.map +1 -1
  136. package/dist/web-effect-tools.js +4 -1
  137. package/dist/web-effect-tools.js.map +1 -1
  138. package/dist/web-effect-trials.d.ts +4 -4
  139. package/dist/web-effect-trials.js +6 -6
  140. package/dist/web-effect-trials.js.map +1 -1
  141. package/dist/web-effect-upload.d.ts +2 -4
  142. package/dist/web-effect-upload.js +3 -8
  143. package/dist/web-effect-upload.js.map +1 -1
  144. package/dist/web-effect.d.ts +14 -0
  145. package/dist/web-effect.js +21 -0
  146. package/dist/web-effect.js.map +1 -1
  147. package/dist/web-guest-access.d.ts +67 -0
  148. package/dist/web-guest-access.js +126 -0
  149. package/dist/web-guest-access.js.map +1 -0
  150. package/dist/web-identity-palette.d.ts +1 -1
  151. package/dist/web-identity-palette.js +2 -2
  152. package/dist/web-identity-palette.js.map +1 -1
  153. package/dist/web-mod-agent.js +5 -3
  154. package/dist/web-mod-agent.js.map +1 -1
  155. package/dist/web-mod-bundle.d.ts +5 -0
  156. package/dist/web-mod-bundle.js +6 -0
  157. package/dist/web-mod-bundle.js.map +1 -1
  158. package/dist/web-mod-store.d.ts +5 -0
  159. package/dist/web-mod-store.js +8 -1
  160. package/dist/web-mod-store.js.map +1 -1
  161. package/dist/web-prefs.d.ts +8 -0
  162. package/dist/web-prefs.js +9 -0
  163. package/dist/web-prefs.js.map +1 -1
  164. package/dist/web-preview.d.ts +6 -2
  165. package/dist/web-preview.js +4 -2
  166. package/dist/web-preview.js.map +1 -1
  167. package/dist/web-session.d.ts +15 -7
  168. package/dist/web-session.js +25 -8
  169. package/dist/web-session.js.map +1 -1
  170. package/dist/web-settings-tools.d.ts +69 -0
  171. package/dist/web-settings-tools.js +247 -0
  172. package/dist/web-settings-tools.js.map +1 -0
  173. package/package.json +1 -1
  174. package/templates/systemd.service.ejs +8 -0
@@ -0,0 +1,62 @@
1
+ /** #2039: fixed host-only closed shadow shell. Owner CSS has no exported parts or slots into it. */
2
+ const roots = new WeakMap();
3
+ const clear = root => { while (root.firstChild) root.removeChild(root.firstChild); };
4
+ function sheetOf(host, record) {
5
+ try {
6
+ const link = record.link, sheet = link?.sheet;
7
+ return record.ready && !record.dead && roots.get(record.panel)?.current === record && record.root.mode === "closed" && record.root.host === record.panel
8
+ && link.getRootNode() === record.root && link.isConnected && link.disabled === false && sheet?.ownerNode === link && sheet.disabled === false
9
+ && sheet.href === new URL("/assets/effect-trial.css", host.location.origin).href && typeof sheet.insertRule === "function" && typeof sheet.deleteRule === "function" && sheet.cssRules ? sheet : null;
10
+ } catch { return null; }
11
+ }
12
+ /** Reflect only a shell issued here, after its fixed same-origin stylesheet loaded. Never a serialized capability. */
13
+ export function effectTrialShadow(host, panel) {
14
+ const record = roots.get(panel)?.current;
15
+ return record && sheetOf(host, record) === record.proof?.sheet ? record.proof : null;
16
+ }
17
+ export function createEffectTrialShadow({ window: host, panel, title, labels, onStop, onClose, onReady, onError } = {}) {
18
+ if (!host?.document || panel?.ownerDocument !== host.document || typeof panel.attachShadow !== "function" || typeof onReady !== "function" || typeof onError !== "function") return null;
19
+ let slot = roots.get(panel), cleanup = null;
20
+ const clock = () => (host.performance ?? globalThis.performance).now(), later = host.setTimeout?.bind(host) ?? globalThis.setTimeout, cancel = host.clearTimeout?.bind(host) ?? globalThis.clearTimeout;
21
+ try {
22
+ if (slot?.current && !slot.current.dead) return null;
23
+ if (!slot) { slot = { root: panel.attachShadow({ mode: "closed" }), current: null }; roots.set(panel, slot); }
24
+ const root = slot.root; clear(root);
25
+ const doc = host.document, element = (tag, cls) => { const el = doc.createElement(tag); el.className = cls; return el; };
26
+ const link = element("link", "effect-trial-style"); link.setAttribute("rel", "stylesheet"); link.setAttribute("href", "/assets/effect-trial.css");
27
+ const head = element("header", "effect-trial-head"), label = element("strong", "effect-trial-label"), name = element("span", "effect-trial-title"), close = element("button", "effect-trial-close"), controls = element("div", "effect-trial-controls"), stop = element("button", "effect-trial-stop"), body = element("div", "effect-trial-body"), stage = element("div", "effect-trial-stage"), note = element("p", "effect-trial-note");
28
+ close.setAttribute("type", "button"); stop.setAttribute("type", "button"); stage.setAttribute("aria-hidden", "true");
29
+ head.append(label, name, close); controls.append(stop); body.append(stage);
30
+ const nodes = [head, controls, body, note]; for (const el of nodes) el.setAttribute("hidden", "");
31
+ const began = clock(); if (!Number.isFinite(began) || began < 0) return null;
32
+ let timer = null;
33
+ const record = { panel, root, link, stage, head, controls, body, note, close, stop, ready: false, dead: false, proof: null }; slot.current = record;
34
+ const text = (nextTitle, nextLabels) => {
35
+ if (record.dead || !nextLabels) return;
36
+ label.textContent = nextLabels.label; name.textContent = typeof nextTitle === "string" ? nextTitle : "";
37
+ close.textContent = nextLabels.close; stop.textContent = nextLabels.stop; note.textContent = nextLabels.note;
38
+ panel.setAttribute("data-effect-trial-theme", doc.documentElement.dataset.theme === "dark" ? "dark" : "light");
39
+ };
40
+ const failed = () => { if (!record.dead) { dispose(); try { onError("stage_unavailable"); } catch { /* cleanup already owns failure */ } } };
41
+ const loaded = () => {
42
+ if (record.dead || record.ready || slot.current !== record) return;
43
+ const at = clock(); if (!Number.isFinite(at) || at < began || at - began >= 10000) { failed(); return; }
44
+ record.ready = true; const sheet = sheetOf(host, record);
45
+ if (!sheet) { failed(); return; }
46
+ cancel(timer); timer = null;
47
+ record.proof = Object.freeze({ panel, root, link, sheet, stage, head, controls, body, note, close, stop });
48
+ for (const el of nodes) el.removeAttribute("hidden");
49
+ try { onReady(record.proof); } catch { failed(); }
50
+ };
51
+ const stopClick = () => { if (!record.dead) onStop?.(); }, closeClick = () => { if (!record.dead) onClose?.(); };
52
+ function dispose() {
53
+ if (record.dead) return; record.dead = true; cancel(timer); timer = null;
54
+ link.removeEventListener("load", loaded); link.removeEventListener("error", failed); stop.removeEventListener("click", stopClick); close.removeEventListener("click", closeClick);
55
+ if (slot.current === record) { slot.current = null; clear(root); }
56
+ }
57
+ cleanup = dispose;
58
+ link.addEventListener("load", loaded); link.addEventListener("error", failed); stop.addEventListener("click", stopClick); close.addEventListener("click", closeClick);
59
+ text(title, labels); timer = later(failed, 10000); root.append(link, ...nodes);
60
+ return Object.freeze({ dispose, text });
61
+ } catch { cleanup?.(); return null; }
62
+ }
@@ -1,4 +1,5 @@
1
1
  /** #1629: host-issued LOCAL trial paint receipt. No authority, loader, frame factory or pool is created here. */
2
+ import { effectTrialShadow } from "./effect-trial-shadow.js";
2
3
  import { effectHostStyle } from "./effect-host-style.js";
3
4
  import { effectStockSnapshot } from "./effect-stock-snapshot.js";
4
5
  import { STOCK_EFFECT_BARRIERS } from "./effect-stock-trust.js";
@@ -58,10 +59,11 @@ export function effectTrialStage(receipt, expected, stock) {
58
59
  }
59
60
 
60
61
  /** Only the fixed host TrialPanel supplies these elements and a current server-run lease. Not an agent manifest. */
61
- export function createEffectTrialStage({ window: host, panel, stage, binding: raw, current, ownerCss, onStop = () => {} } = {}) {
62
+ export function createEffectTrialStage({ window: host, panel, stage, binding: raw, current, onStop = () => {} } = {}) {
62
63
  const own = binding(raw);
63
- if (!own || typeof current !== "function" || typeof ownerCss !== "function" || typeof onStop !== "function" || !host?.document || !panel || !stage || owners.has(stage)) return null;
64
- let sheet; try { sheet = effectHostStyle(host); if (!sheet || host.CSS?.supports("contain", "paint") !== true || host.CSS.supports("overflow", "clip") !== true) return null; } catch { return null; }
64
+ if (!own || typeof current !== "function" || typeof onStop !== "function" || !host?.document || !panel || !stage || owners.has(stage)) return null;
65
+ const shell = effectTrialShadow(host, panel), hostSheet = effectHostStyle(host);
66
+ let sheet; try { sheet = shell?.sheet; if (!shell || shell.stage !== stage || !hostSheet || !sheet || host.CSS?.supports("contain", "paint") !== true || host.CSS.supports("overflow", "clip") !== true) return null; } catch { return null; }
65
67
  const receipt = Object.freeze(Object.create(null)), frames = new Set(), listeners = new Set(); let dead = false, constraint = null;
66
68
  const identity = Object.freeze({ key: own.key, id: own.id, instance: own.instance, generation: own.generation, serial: ++serial });
67
69
  const invalidate = reason => {
@@ -76,23 +78,27 @@ export function createEffectTrialStage({ window: host, panel, stage, binding: ra
76
78
  function check(stock) {
77
79
  try {
78
80
  // This callback may reenter disposal or replace the owner. Recheck identity after it returns.
79
- if (dead || current(own) !== true || constraint !== null && constraint() !== true || dead || owners.get(stage) !== receipt || ownerCss() !== "off" || dead) return null;
81
+ if (dead || current(own) !== true || constraint !== null && constraint() !== true || dead || owners.get(stage) !== receipt || dead) return null;
80
82
  const proof = effectStockSnapshot(stock), doc = host.document;
81
- if (effectHostStyle(host) !== sheet) return null;
83
+ if (effectHostStyle(host) !== hostSheet || effectTrialShadow(host, panel) !== shell) return null;
82
84
  // The own panel MUST still suppress ordinary foreground effects. Never synthesize front:true.
83
85
  if (!proof || proof.front !== false || doc.visibilityState !== "visible" || doc.hasFocus?.() !== true || doc.fullscreenElement || doc.pictureInPictureElement) return null;
84
86
  const split = doc.querySelector(".main > .p-chat > .chat-split");
85
87
  if (!split || doc.querySelectorAll(".main > .p-chat").length !== 1 || doc.querySelectorAll(".chat-split > .pv-panel").length !== 1
86
88
  || panel.parentElement !== split || panel.ownerDocument !== doc || panel.isConnected !== true || panel.className !== "pv-panel effect-trial-panel" || panel.getAttribute("data-trust") !== "preview") return null;
87
- const children = [...panel.children], names = ["effect-trial-head", "effect-trial-controls", "effect-trial-body", "effect-trial-note"];
88
- if (children.length !== names.length || children.some((el, i) => el.className !== names[i]) || children[2].children.length !== 1 || children[2].firstElementChild !== stage
89
+ const children = [...shell.root.children], names = ["effect-trial-style", "effect-trial-head", "effect-trial-controls", "effect-trial-body", "effect-trial-note"];
90
+ if (panel.children.length !== 0 || children.length !== names.length || children.some((el, i) => el.className !== names[i]) || children[3].children.length !== 1 || children[3].firstElementChild !== stage
89
91
  || stage.className !== "effect-trial-stage" || stage.ownerDocument !== doc || stage.isConnected !== true || stage.hasAttribute("tabindex") || stage.hasAttribute("contenteditable")) return null;
92
+ for (const control of [shell.head, shell.controls, shell.close, shell.stop, shell.note]) {
93
+ const style = host.getComputedStyle(control);
94
+ if (style.display === "none" || style.visibility !== "visible" || style.opacity !== "1" || !rect(control)) return null;
95
+ }
90
96
  if ([...doc.querySelectorAll(STOCK_EFFECT_BARRIERS)].some(el => el !== panel && visible(host, el))
91
97
  || [...doc.querySelectorAll(".pv-panel")].some(el => el !== panel && visible(host, el))) return null;
92
98
  const active = doc.activeElement;
93
99
  if (active && active !== doc.body && active !== doc.documentElement && (active.matches("input,select,textarea,[contenteditable]") || active.isContentEditable)) return null;
94
100
  let el = stage, depth = 0;
95
- while (el) { const s = host.getComputedStyle(el); if (++depth > 128 || el.ownerDocument !== doc || !neutral(s) || s.opacity !== "1" || s.visibility !== "visible" || s.display === "none" || s.contain !== (el === stage ? "paint" : "none")) return null; if (el === doc.documentElement) break; el = el.parentElement; }
101
+ while (el) { const s = host.getComputedStyle(el); if (++depth > 128 || el.ownerDocument !== doc || !neutral(s) || s.opacity !== "1" || s.visibility !== "visible" || s.display === "none" || s.contain !== (el === stage ? "paint" : "none")) return null; if (el === doc.documentElement) break; el = el.parentElement ?? (el.getRootNode?.() === shell.root ? panel : null); }
96
102
  if (el !== doc.documentElement) return null;
97
103
  const s = host.getComputedStyle(stage), r = rect(stage), p = rect(panel);
98
104
  if (!r || !p || s.position !== "relative" || s.zIndex !== "0" || s.overflowX !== "clip" || s.overflowY !== "clip"
@@ -103,7 +109,7 @@ export function createEffectTrialStage({ window: host, panel, stage, binding: ra
103
109
  if ([...stage.children].some(child => ![...frames].some(f => f.element === child))) return null;
104
110
  const width = Math.floor(r.width), height = Math.floor(r.height);
105
111
  if (width < 1 || height < 1 || width > 4096 || height > 4096 || width * height > 8388608 || dead || owners.get(stage) !== receipt) return null;
106
- if (current(own) !== true || constraint !== null && constraint() !== true || dead || owners.get(stage) !== receipt || ownerCss() !== "off" || dead || stage.isConnected !== true || panel.parentElement !== split) return null;
112
+ if (current(own) !== true || constraint !== null && constraint() !== true || dead || owners.get(stage) !== receipt || dead || stage.isConnected !== true || panel.parentElement !== split) return null;
107
113
  return { x: r.x, y: r.y, width, height };
108
114
  } catch { return null; }
109
115
  }
@@ -0,0 +1,40 @@
1
+ /* #2039: fixed closed-shadow Trial chrome; no inherited owner-CSS typography, parts or slots. */
2
+ :host {
3
+ all: initial !important;
4
+ box-sizing: border-box !important;
5
+ display: flex !important;
6
+ flex: 0 0 45% !important;
7
+ flex-direction: column !important;
8
+ align-self: stretch !important;
9
+ width: 45% !important;
10
+ min-width: 320px !important;
11
+ max-width: calc(100% - 368px) !important;
12
+ min-height: 0 !important;
13
+ height: auto !important;
14
+ position: relative !important;
15
+ z-index: 1 !important;
16
+ overflow: clip !important;
17
+ background: #ffffff !important;
18
+ color: #1b1b1f !important;
19
+ font: 400 16px/1.4 system-ui, sans-serif !important;
20
+ direction: ltr !important;
21
+ unicode-bidi: isolate !important;
22
+ text-align: start !important;
23
+ visibility: visible !important;
24
+ opacity: 1 !important;
25
+ }
26
+ :host([data-effect-trial-theme="dark"]) { background: #202024 !important; color: #f0f0f4 !important; }
27
+ :host::before, :host::after { all: initial !important; content: none !important; display: none !important; }
28
+ *, *::before, *::after { box-sizing: border-box; }
29
+ [hidden] { display: none !important; }
30
+ .effect-trial-head, .effect-trial-controls, .effect-trial-note { position: relative; z-index: 1; padding: 12px; margin: 0; }
31
+ .effect-trial-head { display: flex; align-items: center; gap: 8px; border-bottom: 1px solid currentColor; }
32
+ .effect-trial-title { flex: 1; min-width: 0; overflow-wrap: anywhere; }
33
+ button { appearance: auto; display: inline-block; font: inherit; color: inherit; background: transparent; border: 1px solid currentColor; border-radius: 6px; padding: 8px 12px; cursor: pointer; }
34
+ button:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
35
+ .effect-trial-body { flex: 1; min-height: 0; display: flex; padding: 12px; }
36
+ .effect-trial-stage { position: relative; z-index: 0; flex: 1; min-width: 0; min-height: 0; contain: paint; overflow: clip; padding: 0; border: 0; outline: none; box-shadow: none; }
37
+ .effect-trial-note { font-size: 14px; }
38
+ @media (max-width: 899px) {
39
+ :host { position: fixed !important; inset: 0 !important; z-index: 37 !important; width: auto !important; min-width: 0 !important; max-width: none !important; padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left) !important; }
40
+ }
@@ -135,7 +135,8 @@ function SizeControl({ name, size }) {
135
135
  }
136
136
 
137
137
  /**
138
- * The card. `compact`: the chat header's own small size (no size control). Nothing when the card is empty. The
138
+ * The card. `compact`: the chat header's own small size (no size control; the banner still follows the size choice,
139
+ * #2038). Nothing when the card is empty. The
139
140
  * tagline and bio say whose they are (title), since a person's field wins over the agent's.
140
141
  */
141
142
  export function IdentityCard({ name, card, compact = false, sizeControl = !compact }) {
@@ -143,10 +144,12 @@ export function IdentityCard({ name, card, compact = false, sizeControl = !compa
143
144
  const [broken, setBroken] = useState({});
144
145
  if (!card) return null;
145
146
  const size = compact ? "c" : cardSizeFor(name);
147
+ // #2038: the chat header's card is its own small size, but its banner follows the person's size choice too.
148
+ const bannerSize = compact ? ` bs-${cardSizeFor(name)}` : "";
146
149
  const whose = (f) => t(f.by === "human" ? "card.byHuman" : "card.byAgent");
147
150
  const pic = (slot, cls, alt) => card[slot] && broken[slot] !== card[slot].src
148
151
  ? html`<img class=${cls} src=${card[slot].src} alt=${alt} title=${whose(card[slot])} decoding="async" onError=${() => setBroken({ ...broken, [slot]: card[slot].src })} />` : null;
149
- return html`<section class=${`id-card card-${size}`} aria-label=${t("card.label", name)}>
152
+ return html`<section class=${`id-card card-${size}${bannerSize}`} aria-label=${t("card.label", name)}>
150
153
  ${pic("banner", "id-banner", "")}
151
154
  <div class="id-row">
152
155
  ${pic("portrait", "id-portrait", t("card.portraitAlt", name))}
@@ -3,7 +3,7 @@
3
3
  // (`color`, `color_show`); this module resolves them as src/web-identity-color.ts does (a test pins the two equal),
4
4
  // paints the sidebar row through the CSSOM (no style attribute, #1300), and holds the Color dialog.
5
5
  // colour: a person's pick > the agent's (themes shown) > its own bot's role colour > automatic (a few soft colours)
6
- // places: a person's choice > the agent's (themes shown) > this host's default (color.show) > ring + bar
6
+ // places: a person's choice > the agent's (themes shown) > this host's default (color.show) > all three (#2056)
7
7
  // Settings → Web appearance can turn colours off for the host (color.off): nothing is painted, every pick is kept.
8
8
  import { html, useEffect, useState } from "./app-html.js";
9
9
  import { createStore, useStore } from "./app-store.js";
@@ -30,7 +30,7 @@ export const AUTO_PALETTE = [
30
30
  { id: "moss", light: "#5b7340", dark: "#a9c48d" }, { id: "slate", light: "#5f6b78", dark: "#aeb8c4" },
31
31
  ];
32
32
  export const PLACEMENTS = ["name", "bar", "ring"];
33
- export const DEFAULT_PLACEMENTS = ["bar", "ring"];
33
+ export const DEFAULT_PLACEMENTS = ["name", "bar", "ring"]; // #2056: all three where nobody chose (was ring + bar)
34
34
  const HEX = /^#[0-9a-f]{6}$/;
35
35
  const presetOf = (c) => (typeof c === "string" && c.startsWith("preset:") ? IDENTITY_PALETTE.find((p) => p.id === c.slice(7)) : undefined);
36
36
  const autoOf = (c) => (typeof c === "string" && c.startsWith("auto:") ? AUTO_PALETTE.find((p) => p.id === c.slice(5)) : undefined);
@@ -115,7 +115,7 @@ export function colorsOff() {
115
115
  const f = prefInForce("color.off");
116
116
  return f ? f.value === true : readLocal("agend_color_off") === "on";
117
117
  }
118
- /** The default places (null: back to ring + bar). */
118
+ /** The default places (null: back to all three, #2056). */
119
119
  export function setDefaultPlaces(list) {
120
120
  const v = list === null ? null : placesOk(list);
121
121
  const same = v && v.length === DEFAULT_PLACEMENTS.length && v.every((p, i) => p === DEFAULT_PLACEMENTS[i]);
@@ -36,6 +36,22 @@ export function groupOf(it) {
36
36
  if (tags.length) return tags[0];
37
37
  return "Other";
38
38
  }
39
+ /** A group's heading: the fallback group ("Other") is a key, shown in the page's language; a tag is shown as it is. */
40
+ export function groupLabel(g) { return g === "Other" ? tn("groupOther") : g; }
41
+ /**
42
+ * #2050: the model as a person reads it. The server's resolver writes English for a model it has not resolved yet
43
+ * ("default (detected when it starts)") and for a CLI's own default ("x (default)"); those are shown in the page's language.
44
+ */
45
+ export function modelText(it, raw = it && it.model) {
46
+ if (!raw || typeof raw !== "string") return raw;
47
+ if (it.model_source === "unresolved") {
48
+ const why = /^default \((.*)\)$/.exec(raw)?.[1];
49
+ const reason = why === "detected when it starts" ? tn("modelWhyStart") : why === "this CLI does not report a default" ? tn("modelWhyCli") : why;
50
+ return reason ? tn("modelDefaultWhy", { reason }) : raw === "default" ? tn("modelDefault") : raw;
51
+ }
52
+ if (it.model_source === "cli-default") { const m = /^(.*) \(default\)$/.exec(raw); if (m) return tn("modelCliDefault", { model: m[1] }); }
53
+ return raw;
54
+ }
39
55
  const BACKEND_LABELS = { "claude-code": "Claude Code", "kiro-cli": "Kiro CLI", codex: "Codex", grok: "Grok Build", antigravity: "Antigravity", opencode: "OpenCode" };
40
56
  export function backendLabel(backend) { return BACKEND_LABELS[backend] || backend || "Unknown"; }
41
57
  export const KNOWN_BACKENDS = new Set(Object.keys(BACKEND_LABELS));
@@ -93,8 +109,8 @@ export function instanceTooltip(it) {
93
109
  if (it.display_name && it.display_name !== name) lines.push(it.display_name, `(${name})`);
94
110
  else lines.push(name);
95
111
  lines.push(line("tooltipBackend", backendLabel(it.backend)));
96
- const src = (s) => (s === "instance" || s === "classic" ? " (configured)" : s === "fleet-default" ? " (fleet default)" : "");
97
- lines.push(line("tooltipModel", (it.model || tn("tooltipUnavailable")) + src(it.model_source)));
112
+ const src = (s) => (s === "instance" || s === "classic" ? tn("srcConfigured") : s === "fleet-default" ? tn("srcFleetDefault") : "");
113
+ lines.push(line("tooltipModel", (modelText(it) || tn("tooltipUnavailable")) + src(it.model_source)));
98
114
  lines.push(line("tooltipStatus", status));
99
115
  lines.push(line("tooltipContext", context));
100
116
  if (it.effort) lines.push(line("tooltipEffort", it.effort + src(it.effort_source)));
@@ -224,6 +240,8 @@ function writeFacetsOpen(open) { try { localStorage.setItem(FACETS_KEY, JSON.str
224
240
 
225
241
  /** The Favorites group's key among the tag groups: no tag can be it (a group name is never a control character). */
226
242
  const FAV_GROUP = "\u0000favorites";
243
+ /** #1936: the open instance, pinned on top while a filter is on. */
244
+ const PINNED_GROUP = "\u0000pinned";
227
245
  function persistOrder(groupNames, instByGroup) {
228
246
  const rows = [];
229
247
  groupNames.forEach((g, i) => rows.push({ item_type: "group", item_name: g, sort_index: i, group_name: null }));
@@ -356,13 +374,12 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
356
374
  // #1664: "☆ Edit" — the stars on every row (on a phone the only way to them) and ↑ ↓ on the favorites — only on the
357
375
  // full list, as drag: in a filtered one the row above may not be the one above in the list (#1677 review).
358
376
  const [editing, setEditing] = useState(false);
359
- // Where a row goes: the view the page is in (#1523 §3.1). View → View; Details, or a Fleet tab (Q4 = A), → Details;
360
- // everywhere else → Chat. The anonymous reader only ever has View.
377
+ // Where a row goes: the view the page is in (#1523 §3.1). View → View; Details → Details; everywhere else, a Fleet tab
378
+ // included (#1917: the user's call, replacing Q4 = A), → Chat. The anonymous reader only ever has View.
361
379
  const rowPath = viewOnly || (route && route.panel === "view") ? viewPath
362
380
  : route && route.panel === "log" ? logPath // #1218: Log → Log
363
- : route && (route.panel === "details" || route.panel === "fleet") ? detailsPath : chatPath;
381
+ : route && route.panel === "details" ? detailsPath : chatPath;
364
382
  const active = route && (route.panel === "chat" || route.panel === "view" || route.panel === "log" || route.panel === "details") ? route.instance : null;
365
- useLayoutEffect(() => { keepActiveInView(list.current); }, [active, items.length, loaded]);
366
383
  const own = (map, name) => Object.prototype.hasOwnProperty.call(map, name);
367
384
  const byName = new Map(items.map((it) => [nameOf(it), it]));
368
385
  const facetOf = (name) => statusFacet(byName.get(name), own(exec, name) ? exec[name] : null, own(awaiting, name) ? awaiting[name] : null);
@@ -379,6 +396,12 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
379
396
  if (favs.length) shownInsts.set(FAV_GROUP, favs);
380
397
  const f = nav.filter;
381
398
  const shown = filterSidebar(shownGroups, shownInsts, byName, f.q, backendLabel, { status: f.status, cli: f.cli, statusOf: facetOf });
399
+ // #1936: while a filter is on, the instance you have open stays on top, pinned and marked "Current", whether or not
400
+ // it matches; a match is not repeated below. "N / M" still counts the filter's matches (the pinned row counts only
401
+ // when it is one). Clearing the filter puts it back in its place, and the list scrolls to it again.
402
+ const pinned = shown.active && active && byName.has(active) ? active : null;
403
+ const listed = pinned ? shown.groups.map((x) => ({ ...x, names: x.names.filter((n) => n !== pinned) })).filter((x) => x.names.length) : shown.groups;
404
+ useLayoutEffect(() => { keepActiveInView(list.current); }, [active, items.length, loaded, shown.active]);
382
405
  const backends = [...new Set(items.map((it) => it.backend).filter((b) => typeof b === "string" && b))].sort((a, b) => backendLabel(a).localeCompare(backendLabel(b)));
383
406
  const toggle = (g) => { const c = new Set(navStore.get().collapsed); if (c.has(g)) c.delete(g); else c.add(g); setCollapsed(c); };
384
407
  // From the store, not this render's copy: two clicks before the next render must both count.
@@ -463,7 +486,10 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
463
486
  ${canStar && items.length ? html`<button type="button" class="chip-btn fav-edit" aria-pressed=${editing ? "true" : "false"} onClick=${() => setEditing(!editing)}>
464
487
  ${editing ? tn("favDone") : html`<${Icon} name="star" size=${12} />${tn("favEdit")}`}</button>` : null}</div>
465
488
  ${!loaded ? html`<${Skeleton} lines=${4} />` : !items.length ? html`<p class="side-empty">${t("app.noInstances")}</p>` : null}
466
- ${shown.groups.map(({ group: g, names }) => {
489
+ ${pinned ? html`<div key=${PINNED_GROUP} class="v-group v-pinned" role="group" aria-label=${tn("pinnedLabel")}>
490
+ <div class="v-group-head pinned-head"><span class="grow">${tn("pinnedCurrent")}</span></div>
491
+ <ul class="inst-list">${row(pinned, PINNED_GROUP, 0, 1)}</ul></div>` : null}
492
+ ${listed.map(({ group: g, names }) => {
467
493
  // #1664: Favorites — always open, not dragged (↑ ↓ in Edit), named in the page's language.
468
494
  if (g === FAV_GROUP) return html`<div key=${g} class="v-group v-fav" role="group" aria-label=${tn("favorites")}>
469
495
  <div class="v-group-head fav-head"><${Icon} name="star" size=${14} cls="fav-mark" /><span class="grow">${tn("favorites")}</span><span class="count">${names.length}</span></div>
@@ -471,8 +497,8 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
471
497
  const folded = nav.collapsed.has(g);
472
498
  return html`<div key=${g} class="v-group">
473
499
  <button type="button" class="v-group-head" aria-expanded=${folded ? "false" : "true"} onClick=${() => toggle(g)}
474
- title=${tn(folded ? "expandGroup" : "collapseGroup", { group: g })} ...${dragProps({ type: "group", name: g })}>
475
- <${Icon} name="chevron" size=${14} cls=${folded ? "caret folded" : "caret"} /><span class="grow">${g}</span><span class="count">${names.length}</span></button>
500
+ title=${tn(folded ? "expandGroup" : "collapseGroup", { group: groupLabel(g) })} ...${dragProps({ type: "group", name: g })}>
501
+ <${Icon} name="chevron" size=${14} cls=${folded ? "caret folded" : "caret"} /><span class="grow">${groupLabel(g)}</span><span class="count">${names.length}</span></button>
476
502
  ${folded ? null : html`<ul class="inst-list">${names.map((name, i) => row(name, g, i, names.length))}</ul>`}
477
503
  </div>`;
478
504
  })}
@@ -16,7 +16,7 @@ import { HeaderTools, VIEW_SCALE, textSizeStore } from "./header-tools.js";
16
16
  import { readStream } from "./read-stream.js";
17
17
  // The usage dialog's metric row lives with the dialog now (usage-dialog.js); re-exported for what imported it here.
18
18
  export { UsageMetric } from "./usage-dialog.js";
19
- import { backendLabel, STATUS_KEYS } from "./instance-nav.js";
19
+ import { backendLabel, modelText, STATUS_KEYS } from "./instance-nav.js";
20
20
  import { viewStore } from "./view-roster-store.js";
21
21
 
22
22
  /** #1580: the app's stream says the fleet is not answering (a restart): the View's polls skip their turn until it is. */
@@ -305,7 +305,7 @@ function Card({ it, viewOnly, onEdit }) {
305
305
  <div class="v-avatar">${it.has_avatar ? html`<img ref=${avatar} alt="" />` : html`<span class="v-initial" aria-hidden="true">${initial}</span>`}</div>
306
306
  <div class="v-card-body">
307
307
  <div class="v-card-line"><strong>${it.display_name || it.instance_name}</strong>${it.role ? html`<span class="v-role">${it.role}</span>` : null}</div>
308
- <div class="v-card-meta">${it.instance_name} · ${backendLabel(it.backend)}${it.model ? ` · ${it.model}` : ""}${ctx}</div>
308
+ <div class="v-card-meta">${it.instance_name} · ${backendLabel(it.backend)}${it.model ? ` · ${modelText(it)}` : ""}${ctx}</div>
309
309
  ${open && it.description ? html`<p class="v-card-desc">${it.description}</p>` : null}
310
310
  </div>
311
311
  <button type="button" class="icon-btn" onClick=${toggle} aria-expanded=${open ? "true" : "false"} aria-label=${tn(open ? "collapseProfile" : "expandProfile")} title=${tn(open ? "collapseProfile" : "expandProfile")}>
@@ -66,7 +66,17 @@ export function UsageMetric({ m }) {
66
66
  const sub = [note, expiryText(m.expiresAt)].filter(Boolean).join(" · ");
67
67
  return html`<div class="u-metric"><div class="u-row"><span class="u-label">${label}</span><span class="u-val">${val}</span></div>${sub ? html`<div class="u-sub">${sub}</div>` : null}</div>`;
68
68
  }
69
- /** "Updated 3 min ago" (#1585): how old the numbers are — the panel may show the last snapshot while a fresh one comes. */
69
+ /**
70
+ * #2014: a row's title. A credential profile's row (`profile`; its API `name` is "Claude (<profile>)") shows the
71
+ * provider name on the title line beside the plan badge, and the profile on its own full-width muted line under it;
72
+ * the full name is the tooltip, and the dialog shows the profile unclipped. Any other row: its name.
73
+ */
74
+ export function providerTitle(p) {
75
+ const name = String((p && p.name) || ""), profile = p && typeof p.profile === "string" && p.profile ? p.profile : "";
76
+ const suffix = ` (${profile})`;
77
+ return profile && name.endsWith(suffix) ? { base: name.slice(0, -suffix.length), profile } : { base: name, profile };
78
+ }
79
+
70
80
  /**
71
81
  * #1749: a plan as its badge reads — one short line. "Name [Tier] (N×)" (Claude's "Team Premium (6.25x)", "Max (20x)")
72
82
  * becomes "Name ×N"; anything else (Pro, Codex's and muse's labels) as it is. The full name stays on the badge's
@@ -76,6 +86,7 @@ export function planBadge(plan) {
76
86
  const m = /^(\S+)(?: [^()]+)? \((\d+(?:\.\d+)?)x\)$/i.exec(String(plan || "").trim());
77
87
  return m ? `${m[1]} ×${m[2]}` : String(plan || "");
78
88
  }
89
+ /** "Updated 3 min ago" (#1585): how old the numbers are — the panel may show the last snapshot while a fresh one comes. */
79
90
  export function updatedAgo(fetchedAt, now = Date.now()) {
80
91
  const ms = now - new Date(fetchedAt).getTime();
81
92
  if (!Number.isFinite(ms) || ms < 60_000) return tn("usageUpdatedJustNow");
@@ -134,11 +145,12 @@ export function UsageBody({ owner }) {
134
145
  const providers = ordered(data.providers);
135
146
  content = html`${!providers.length ? html`<p class="note">${tn("usage.empty_active")}</p>` : null}
136
147
  ${providers.map((p, i) => html`<section key=${key(p)} class="u-provider">
137
- <div class="u-head"><strong title=${p.name}>${p.name}</strong>${p.plan ? html`<span class="pill u-plan" title=${p.plan}><span aria-hidden="true">${planBadge(p.plan)}</span><span class="sr-only">${p.plan}</span></span>` : null}
148
+ <div class="u-head"><strong title=${p.name}>${providerTitle(p).base}</strong>${p.plan ? html`<span class="pill u-plan" title=${p.plan}><span aria-hidden="true">${planBadge(p.plan)}</span><span class="sr-only">${p.plan}</span></span>` : null}
138
149
  <span class="u-order">
139
150
  <button type="button" class="icon-btn" disabled=${i === 0} aria-label=${tn("usageMoveUp")} title=${tn("usageMoveUp")} onClick=${() => move(key(p), -1, data.providers)}><${Icon} name="up" size=${14} /></button>
140
151
  <button type="button" class="icon-btn" disabled=${i === providers.length - 1} aria-label=${tn("usageMoveDown")} title=${tn("usageMoveDown")} onClick=${() => move(key(p), 1, data.providers)}><${Icon} name="down" size=${14} /></button>
141
152
  </span></div>
153
+ ${providerTitle(p).profile ? html`<div class="u-profile" title=${p.name}>${providerTitle(p).profile}</div>` : null}
142
154
  ${p.stale ? html`<p class="note">${usageText(p.hint || "", p.hintI18n)}</p>` : null}
143
155
  ${p.status === "error" && !p.transient ? html`<p class="u-err">${usageText(p.error || tn("usage.error_fallback"), p.errorI18n)}</p>`
144
156
  : p.status === "error" && p.transient ? html`<p class="note">${usageText(p.error || tn("usage.error_fallback"), p.errorI18n)}</p>`
@@ -51,6 +51,14 @@ register("view", {
51
51
  "tooltipEffort": "Effort: {value}",
52
52
  "tooltipCost": "Cost: {value}",
53
53
  "tooltipUnavailable": "Unavailable",
54
+ "srcConfigured": " (configured)",
55
+ "srcFleetDefault": " (fleet default)",
56
+ "modelDefault": "default",
57
+ "modelDefaultWhy": "default ({reason})",
58
+ "modelCliDefault": "{model} (default)",
59
+ "modelWhyStart": "detected when it starts",
60
+ "modelWhyCli": "this CLI does not report a default",
61
+ "groupOther": "Other",
54
62
  "statusRunning": "Running",
55
63
  "statusPaused": "Paused",
56
64
  "statusStopped": "Stopped",
@@ -197,7 +205,9 @@ register("view", {
197
205
  "favUp": "Move {name} up",
198
206
  "favDown": "Move {name} down",
199
207
  "favFull": "Favorites are full ({max})",
200
- "favFiltered": "Clear the filter to reorder favorites"
208
+ "favFiltered": "Clear the filter to reorder favorites",
209
+ "pinnedCurrent": "Current",
210
+ "pinnedLabel": "The instance you have open"
201
211
  },
202
212
  "zh-TW": {
203
213
  "editProfile": "編輯個人資料",
@@ -246,6 +256,14 @@ register("view", {
246
256
  "tooltipEffort": "推理強度:{value}",
247
257
  "tooltipCost": "費用:{value}",
248
258
  "tooltipUnavailable": "無資料",
259
+ "srcConfigured": "(設定值)",
260
+ "srcFleetDefault": "(fleet 預設)",
261
+ "modelDefault": "預設",
262
+ "modelDefaultWhy": "預設({reason})",
263
+ "modelCliDefault": "{model}(預設)",
264
+ "modelWhyStart": "啟動時偵測",
265
+ "modelWhyCli": "這個 CLI 不提供預設值",
266
+ "groupOther": "其他",
249
267
  "statusRunning": "運行中",
250
268
  "statusPaused": "已暫停",
251
269
  "statusStopped": "已停止",
@@ -392,6 +410,8 @@ register("view", {
392
410
  "favUp": "把 {name} 上移",
393
411
  "favDown": "把 {name} 下移",
394
412
  "favFull": "最愛已滿({max} 個)",
395
- "favFiltered": "清除篩選後才能調整最愛順序"
413
+ "favFiltered": "清除篩選後才能調整最愛順序",
414
+ "pinnedCurrent": "目前",
415
+ "pinnedLabel": "你目前開著的 instance"
396
416
  }
397
417
  });
@@ -55,7 +55,7 @@
55
55
  <div id="msg" role="alert" aria-live="polite"></div>
56
56
  <div class="hint" data-i18n="hint">No code? Send /web in General, then press a sign-in button to get one.</div>
57
57
  <details class="help"><summary data-i18n="nobot">No chat bot?</summary><p data-i18n="nobot_body">Ask the operator to run <code>agend web --code</code> on the AgEnD computer.</p></details>
58
- <details class="help"><summary data-i18n="safe">Page looks wrong after signing in?</summary><p data-i18n="safe_body">Your custom CSS may be hiding something. Add ?safe=1 to the end of the address to load the page without it, then fix it or turn it off in Settings → Appearance.</p></details>
58
+ <details class="help"><summary data-i18n="safe">Page looks wrong after signing in?</summary><p data-i18n="safe_body">Your custom CSS may be hiding something. Add ?safe=1 to the end of the address to load the page without it, then fix it or turn it off in Settings → Web appearance.</p></details>
59
59
  </form>
60
60
  <noscript>This page needs JavaScript to sign you in.</noscript>
61
61
  </main>
@@ -34,6 +34,8 @@ export interface UsageMetric {
34
34
  export interface ProviderUsage {
35
35
  id: string;
36
36
  name: string;
37
+ /** #2014: the credential profile this row reads (its `name` ends in ` (<profile>)`); the page shows it on its own line. */
38
+ profile?: string;
37
39
  status: "ok" | "error" | "no-credentials";
38
40
  /** A non-metered subscription whose entitlement is unlimited (not 0/100%). */
39
41
  unlimited?: boolean;
@@ -221,6 +223,7 @@ export declare function fetchKiroUsage(storeHome?: string): Promise<Omit<Provide
221
223
  type UsageProvider = {
222
224
  id: string;
223
225
  name: string;
226
+ profile?: string;
224
227
  fetch: (storeHome?: string, scope?: ClaudeUsageScope) => Promise<Omit<ProviderUsage, "id" | "name">>;
225
228
  };
226
229
  type UsageFleetConfig = Parameters<typeof listConfiguredProfiles>[0];
@@ -1458,6 +1458,7 @@ export function providersForConfig(config, base = DEFAULT_PROVIDERS) {
1458
1458
  rows.push(...profiles.map(profile => ({
1459
1459
  id: `${provider.id}:${profile}`,
1460
1460
  name: `${provider.name} (${profile})`,
1461
+ profile,
1461
1462
  fetch: () => provider.fetch(credentialProfileStoreHome(getAgendHome(), backendName, profile), { profile }),
1462
1463
  })));
1463
1464
  return rows;
@@ -1644,16 +1645,16 @@ export async function fetchAllUsage(config) {
1644
1645
  // Fail soft: this row says it could not be read, the rest still answer,
1645
1646
  // and the snapshot returns in bounded time.
1646
1647
  return {
1647
- id: p.id, name: p.name, status: "error",
1648
+ id: p.id, name: p.name, ...(p.profile ? { profile: p.profile } : {}), status: "error",
1648
1649
  error: `Could not reach ${p.name}.`,
1649
1650
  errorI18n: i18n("usage.error.unreachable", p.name),
1650
1651
  metrics: [],
1651
1652
  };
1652
1653
  }
1653
- return { id: p.id, name: p.name, ...outcome };
1654
+ return { id: p.id, name: p.name, ...(p.profile ? { profile: p.profile } : {}), ...outcome };
1654
1655
  }
1655
1656
  catch (err) {
1656
- return { id: p.id, name: p.name, status: "error", error: String(err?.message ?? err), metrics: [] };
1657
+ return { id: p.id, name: p.name, ...(p.profile ? { profile: p.profile } : {}), status: "error", error: String(err?.message ?? err), metrics: [] };
1657
1658
  }
1658
1659
  }));
1659
1660
  return {