@songsid/agend 2.3.0-beta.2 → 2.3.0-beta.4

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 (189) hide show
  1. package/dist/agent-whats-new.js +2 -0
  2. package/dist/agent-whats-new.js.map +1 -1
  3. package/dist/auth-api.d.ts +30 -1
  4. package/dist/auth-api.js +42 -12
  5. package/dist/auth-api.js.map +1 -1
  6. package/dist/backend/claude-code.d.ts +2 -2
  7. package/dist/backend/claude-code.js +5 -4
  8. package/dist/backend/claude-code.js.map +1 -1
  9. package/dist/backend/types.d.ts +6 -0
  10. package/dist/backend/types.js +15 -0
  11. package/dist/backend/types.js.map +1 -1
  12. package/dist/backend-availability.d.ts +120 -0
  13. package/dist/backend-availability.js +396 -0
  14. package/dist/backend-availability.js.map +1 -0
  15. package/dist/backend-command.d.ts +21 -3
  16. package/dist/backend-command.js +23 -4
  17. package/dist/backend-command.js.map +1 -1
  18. package/dist/channel/adapters/discord.d.ts +7 -0
  19. package/dist/channel/adapters/discord.js +35 -0
  20. package/dist/channel/adapters/discord.js.map +1 -1
  21. package/dist/channel/mcp-tools.js +8 -4
  22. package/dist/channel/mcp-tools.js.map +1 -1
  23. package/dist/channel/types.d.ts +6 -0
  24. package/dist/classic-channel-manager.d.ts +8 -3
  25. package/dist/classic-channel-manager.js +10 -3
  26. package/dist/classic-channel-manager.js.map +1 -1
  27. package/dist/config-validator.js +73 -2
  28. package/dist/config-validator.js.map +1 -1
  29. package/dist/delivery-outbox.js +5 -0
  30. package/dist/delivery-outbox.js.map +1 -1
  31. package/dist/fleet-context.d.ts +10 -0
  32. package/dist/fleet-manager.d.ts +238 -5
  33. package/dist/fleet-manager.js +878 -71
  34. package/dist/fleet-manager.js.map +1 -1
  35. package/dist/fleet-traffic.d.ts +38 -0
  36. package/dist/fleet-traffic.js +96 -0
  37. package/dist/fleet-traffic.js.map +1 -0
  38. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +13 -5
  39. package/dist/general-knowledge/skills/multi-channel/SKILL.md +1 -1
  40. package/dist/general-knowledge/skills/web-appearance/SKILL.md +56 -11
  41. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +398 -0
  42. package/dist/human-delivery-receipts.js +2 -1
  43. package/dist/human-delivery-receipts.js.map +1 -1
  44. package/dist/locale.js +48 -8
  45. package/dist/locale.js.map +1 -1
  46. package/dist/memory-hogs.d.ts +27 -0
  47. package/dist/memory-hogs.js +124 -0
  48. package/dist/memory-hogs.js.map +1 -0
  49. package/dist/outbound-handlers.d.ts +2 -0
  50. package/dist/outbound-handlers.js +40 -1
  51. package/dist/outbound-handlers.js.map +1 -1
  52. package/dist/outbound-schemas.d.ts +92 -0
  53. package/dist/outbound-schemas.js +55 -1
  54. package/dist/outbound-schemas.js.map +1 -1
  55. package/dist/public-web-gateway.js +1 -1
  56. package/dist/public-web-gateway.js.map +1 -1
  57. package/dist/public-web-link.d.ts +9 -0
  58. package/dist/public-web-link.js +8 -0
  59. package/dist/public-web-link.js.map +1 -1
  60. package/dist/quickstart-api.d.ts +10 -1
  61. package/dist/quickstart-api.js +12 -4
  62. package/dist/quickstart-api.js.map +1 -1
  63. package/dist/reply-buttons-controller.d.ts +2 -0
  64. package/dist/reply-buttons-controller.js +1 -1
  65. package/dist/reply-buttons-controller.js.map +1 -1
  66. package/dist/tmux-manager.d.ts +5 -0
  67. package/dist/tmux-manager.js +16 -0
  68. package/dist/tmux-manager.js.map +1 -1
  69. package/dist/tool-permissions.js +4 -0
  70. package/dist/tool-permissions.js.map +1 -1
  71. package/dist/topic-commands.d.ts +5 -0
  72. package/dist/topic-commands.js +16 -0
  73. package/dist/topic-commands.js.map +1 -1
  74. package/dist/types.d.ts +25 -1
  75. package/dist/ui/app-effects.js +33 -9
  76. package/dist/ui/chat-effects.js +62 -4
  77. package/dist/ui/chat-strings.js +40 -4
  78. package/dist/ui/chat-thread.js +17 -6
  79. package/dist/ui/effect-trial-panel.js +19 -16
  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 +12 -5
  85. package/dist/ui/panel-details.js +2 -1
  86. package/dist/ui/panel-fleet.js +32 -11
  87. package/dist/ui/panel-settings.js +6 -4
  88. package/dist/ui/preview.js +3 -2
  89. package/dist/ui/settings-appearance.js +34 -18
  90. package/dist/ui/settings-catalog.js +89 -0
  91. package/dist/ui/settings-dialogs.js +40 -12
  92. package/dist/ui/settings-effects.js +4 -3
  93. package/dist/ui/settings-learn.js +25 -0
  94. package/dist/ui/settings-model.js +6 -2
  95. package/dist/ui/settings-mods.js +4 -3
  96. package/dist/ui/settings-owner-effects.js +8 -8
  97. package/dist/ui/settings-strings.js +40 -38
  98. package/dist/ui/shared/app-i18n.js +2 -2
  99. package/dist/ui/shared/app-prefs.js +1 -1
  100. package/dist/ui/shared/app-shell.js +23 -1
  101. package/dist/ui/shared/app-store.js +12 -1
  102. package/dist/ui/shared/app-stream.js +2 -1
  103. package/dist/ui/shared/app.css +186 -11
  104. package/dist/ui/shared/card-strings.js +4 -0
  105. package/dist/ui/shared/effect-app-runtime.js +1 -1
  106. package/dist/ui/shared/effect-frame.js +10 -5
  107. package/dist/ui/shared/effect-geometry.js +24 -9
  108. package/dist/ui/shared/effect-policy.js +2 -1
  109. package/dist/ui/shared/effect-registry.js +71 -17
  110. package/dist/ui/shared/effect-stock-trust.js +23 -2
  111. package/dist/ui/shared/effect-trial-shadow.js +62 -0
  112. package/dist/ui/shared/effect-trial-stage.js +15 -9
  113. package/dist/ui/shared/effect-trial.css +40 -0
  114. package/dist/ui/shared/identity-card.js +75 -4
  115. package/dist/ui/shared/identity-color.js +3 -3
  116. package/dist/ui/shared/instance-nav.js +20 -4
  117. package/dist/ui/shared/panel-view.js +24 -4
  118. package/dist/ui/shared/view-strings.js +22 -0
  119. package/dist/view-api.d.ts +32 -0
  120. package/dist/view-api.js +55 -4
  121. package/dist/view-api.js.map +1 -1
  122. package/dist/web-api.d.ts +19 -4
  123. package/dist/web-api.js +178 -20
  124. package/dist/web-api.js.map +1 -1
  125. package/dist/web-appearance-policy.d.ts +4 -6
  126. package/dist/web-appearance-policy.js +5 -12
  127. package/dist/web-appearance-policy.js.map +1 -1
  128. package/dist/web-appearance-tools.js +5 -1
  129. package/dist/web-appearance-tools.js.map +1 -1
  130. package/dist/web-appearance-view.d.ts +14 -1
  131. package/dist/web-appearance-view.js +98 -10
  132. package/dist/web-appearance-view.js.map +1 -1
  133. package/dist/web-appearance.d.ts +2 -2
  134. package/dist/web-appearance.js +5 -6
  135. package/dist/web-appearance.js.map +1 -1
  136. package/dist/web-auth.js +1 -1
  137. package/dist/web-auth.js.map +1 -1
  138. package/dist/web-card.d.ts +14 -0
  139. package/dist/web-card.js +40 -6
  140. package/dist/web-card.js.map +1 -1
  141. package/dist/web-channel-echo.d.ts +7 -1
  142. package/dist/web-channel-echo.js +6 -2
  143. package/dist/web-channel-echo.js.map +1 -1
  144. package/dist/web-chat-history.d.ts +40 -0
  145. package/dist/web-chat-history.js +66 -3
  146. package/dist/web-chat-history.js.map +1 -1
  147. package/dist/web-commands.d.ts +1 -0
  148. package/dist/web-effect-tools.js +2 -3
  149. package/dist/web-effect-tools.js.map +1 -1
  150. package/dist/web-effect-trials.d.ts +2 -2
  151. package/dist/web-effect-trials.js +3 -3
  152. package/dist/web-effect-trials.js.map +1 -1
  153. package/dist/web-guest-access.d.ts +125 -0
  154. package/dist/web-guest-access.js +232 -0
  155. package/dist/web-guest-access.js.map +1 -0
  156. package/dist/web-guest-rooms.d.ts +82 -0
  157. package/dist/web-guest-rooms.js +135 -0
  158. package/dist/web-guest-rooms.js.map +1 -0
  159. package/dist/web-identity-palette.d.ts +1 -1
  160. package/dist/web-identity-palette.js +2 -2
  161. package/dist/web-identity-palette.js.map +1 -1
  162. package/dist/web-login.d.ts +47 -0
  163. package/dist/web-login.js +115 -0
  164. package/dist/web-login.js.map +1 -1
  165. package/dist/web-mod-agent.d.ts +1 -1
  166. package/dist/web-mod-agent.js +2 -4
  167. package/dist/web-mod-agent.js.map +1 -1
  168. package/dist/web-mod-store.d.ts +5 -0
  169. package/dist/web-mod-store.js +6 -0
  170. package/dist/web-mod-store.js.map +1 -1
  171. package/dist/web-prefs.d.ts +8 -0
  172. package/dist/web-prefs.js +9 -0
  173. package/dist/web-prefs.js.map +1 -1
  174. package/dist/web-preview.d.ts +6 -2
  175. package/dist/web-preview.js +4 -2
  176. package/dist/web-preview.js.map +1 -1
  177. package/dist/web-returning-device.d.ts +39 -0
  178. package/dist/web-returning-device.js +46 -5
  179. package/dist/web-returning-device.js.map +1 -1
  180. package/dist/web-session.d.ts +58 -10
  181. package/dist/web-session.js +104 -22
  182. package/dist/web-session.js.map +1 -1
  183. package/dist/web-settings-tools.d.ts +81 -0
  184. package/dist/web-settings-tools.js +257 -0
  185. package/dist/web-settings-tools.js.map +1 -0
  186. package/dist/web-theme.d.ts +2 -0
  187. package/dist/web-theme.js +10 -1
  188. package/dist/web-theme.js.map +1 -1
  189. package/package.json +1 -1
@@ -0,0 +1,224 @@
1
+ // #1918 phase 3: live cross-instance traffic on the org canvas (fleet-canvas.js) — what src/fleet-traffic.ts sends: the
2
+ // stream's `traffic` items, and GET /ui/traffic's snapshot when the tab opens (and on the public link, which has no
3
+ // stream, every POLL_MS while the page is shown — the user's decision 1). Ids are opaque; there is never any text.
4
+ //
5
+ // - The store keeps each message's latest state: queued (a dotted line), delivering (a dash and a pulse), delivered
6
+ // (the line stays FADE_MS while it fades, then goes); consumed, failed, uncertain or cancelled ends it at once (a
7
+ // failed or uncertain delivery is Needs you's to show, #1386). A finished message never comes back.
8
+ // - Edges are routed metro-style through the lanes the layout keeps empty (beside the columns and between card rows
9
+ // inside a box; the bus above each row of teams; the trunk at the left), so a line never crosses a card.
10
+ // - Near: one edge per (from, to, kind, phase), ×N when several share it. Far (zoomed out): one edge per team pair.
11
+ // A broadcast (one message per recipient, sharing an `op`) is a fan-out from its sender; more than BUNDLE_AT
12
+ // recipients, or far, it is one edge per recipient team, ×N (the user's decision 3). At most MAX_EDGES are drawn.
13
+ import { OC } from "./fleet-canvas-layout.js";
14
+
15
+ export const FADE_MS = 3000;
16
+ export const POLL_MS = 5000;
17
+ export const MAX_EDGES = 60;
18
+ export const BUNDLE_AT = 8;
19
+ export const FAR_K = 0.55;
20
+ const MAX_ITEMS = 400, MAX_DONE = 2000;
21
+ const LIVE = new Set(["queued", "delivering"]);
22
+ const ENDS = new Set(["consumed", "failed", "uncertain", "cancelled"]);
23
+ const KINDS = new Set(["task", "report", "query", "update", "send", "broadcast"]);
24
+ const mono = () => (typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now());
25
+
26
+ /** A traffic item as the server sends it, checked; null for anything else. */
27
+ export function itemOk(x) {
28
+ if (!x || typeof x !== "object") return null;
29
+ const { id, from, to, kind, state, op } = x;
30
+ if (typeof id !== "string" || !/^[0-9a-f]{16}$/.test(id) || typeof from !== "string" || !from || typeof to !== "string" || !to) return null;
31
+ if (!KINDS.has(kind) || !(LIVE.has(state) || state === "delivered" || ENDS.has(state))) return null;
32
+ return { id, from, to, kind, state, ...(typeof op === "string" && /^[0-9a-f]{16}$/.test(op) ? { op } : {}) };
33
+ }
34
+
35
+ /**
36
+ * The page's traffic: `apply(item, nowMs)` one stream item, `load(snapshot, nowMs)` a snapshot, `sweep(nowMs)` drops
37
+ * faded lines; each returns whether anything shown changed. `items()` what to draw: { …item, fading }.
38
+ */
39
+ export function createTrafficStore() {
40
+ const items = new Map(); // id → { item, fadeAt }
41
+ const done = new Set(); // ids that ended: a late item for one is ignored
42
+ const finish = (id) => {
43
+ items.delete(id);
44
+ done.add(id);
45
+ if (done.size > MAX_DONE) done.delete(done.values().next().value);
46
+ };
47
+ const store = {
48
+ apply(raw, now = mono()) {
49
+ const it = itemOk(raw);
50
+ if (!it || done.has(it.id)) return false;
51
+ const prev = items.get(it.id);
52
+ if (ENDS.has(it.state)) { const had = !!prev; finish(it.id); return had; }
53
+ if (prev && prev.item.state === "delivered") return false; // delivered never goes back
54
+ if (prev && prev.item.state === it.state) return false;
55
+ items.delete(it.id);
56
+ items.set(it.id, { item: it, fadeAt: it.state === "delivered" ? now + FADE_MS : null });
57
+ while (items.size > MAX_ITEMS) items.delete(items.keys().next().value);
58
+ return true;
59
+ },
60
+ /** A snapshot is the truth for what is in flight: anything live it no longer lists has ended (missed while away). */
61
+ load(snap, now = mono()) {
62
+ const list = snap && Array.isArray(snap.items) ? snap.items.map(itemOk).filter(Boolean) : [];
63
+ const listed = new Set(list.map((i) => i.id));
64
+ let changed = false;
65
+ for (const [id, e] of items) if (!listed.has(id) && LIVE.has(e.item.state)) { finish(id); changed = true; }
66
+ // Delivered, for a line drawn here: it fades now (with no stream, the snapshot is the only news of it). Any other
67
+ // finished message is not drawn (it finished up to two minutes ago), and an ended one goes.
68
+ for (const it of list) {
69
+ if (LIVE.has(it.state) || (it.state === "delivered" && items.has(it.id))) changed = store.apply(it, now) || changed;
70
+ else { const had = items.has(it.id); finish(it.id); changed = changed || had; }
71
+ }
72
+ return changed;
73
+ },
74
+ sweep(now = mono()) {
75
+ let changed = false;
76
+ for (const [id, e] of items) if (e.fadeAt !== null && now >= e.fadeAt) { finish(id); changed = true; }
77
+ return changed;
78
+ },
79
+ items: () => [...items.values()].map((e) => ({ ...e.item, fading: e.fadeAt !== null })),
80
+ fadingCount: () => [...items.values()].filter((e) => e.fadeAt !== null).length,
81
+ };
82
+ return store;
83
+ }
84
+
85
+ // ── Where a name is on the canvas ──
86
+
87
+ /** The team a name is drawn in (its first, if it is in two), null for General, undefined when it is not on the canvas. */
88
+ function teamOf(L, name) {
89
+ if (L.tops.some((c) => c.node.name === name)) return null;
90
+ const b = L.boxes.find((x) => x.nodes.some((n) => n.name === name));
91
+ return b ? b : undefined;
92
+ }
93
+ /** An end of a line: General's card, a member's card, or its folded team's box. */
94
+ function endOf(L, name) {
95
+ const top = L.tops.find((c) => c.node.name === name);
96
+ if (top) return { kind: "top", c: top, key: top.key };
97
+ const b = teamOf(L, name);
98
+ if (!b) return null;
99
+ if (b.folded) return { kind: "box", b, key: `box:${b.key}` };
100
+ const i = b.cells.findIndex((c) => c.node.name === name);
101
+ return { kind: "cell", b, i, c: b.cells[i], key: b.cells[i].key };
102
+ }
103
+
104
+ // ── Routing: lanes the layout keeps empty ──
105
+
106
+ const firstBusOf = (L) => Math.min(...L.boxes.map((b) => b.busY));
107
+ /** From an end up to its row's bus lane: the points, and the lane's y. `o` spreads lines sharing a lane. */
108
+ function outward(L, end, o) {
109
+ if (end.kind === "top") {
110
+ const x = end.c.x + end.c.w / 2 + Math.max(-end.c.w / 2 + 12, Math.min(end.c.w / 2 - 12, o * 2.4));
111
+ const bus = firstBusOf(L) + o;
112
+ return { pts: [{ x, y: end.c.y + end.c.h }, { x, y: bus }], bus };
113
+ }
114
+ const b = end.b;
115
+ if (end.kind === "box") {
116
+ const x = b.x + b.w / 2 + o * 1.5;
117
+ return { pts: [{ x, y: b.y }, { x, y: b.busY + o }], bus: b.busY + o };
118
+ }
119
+ const { PAD, GAP, NW, NH } = OC;
120
+ const card = end.c, col = end.i % b.cols, rowI = Math.floor(end.i / b.cols), lines = Math.ceil(b.cells.length / b.cols);
121
+ const io = Math.max(-PAD / 2 + 3, Math.min(PAD / 2 - 3, o * 0.5));
122
+ const leftLane = b.x + PAD / 2 + io, rightLane = b.x + b.w - PAD / 2 + io;
123
+ let pts;
124
+ if (col === 0 || col === b.cols - 1) {
125
+ const left = col === 0, lane = left ? leftLane : rightLane, cy = card.y + NH / 2 + io;
126
+ pts = [{ x: left ? card.x : card.x + NW, y: cy }, { x: lane, y: cy }, { x: lane, y: b.y }];
127
+ } else {
128
+ // A middle column: down into the gutter below its row, across to the nearer side lane.
129
+ const gy = card.y + NH + (rowI === lines - 1 ? PAD : GAP) / 2 + io / 2, cx = card.x + NW / 2 + o;
130
+ const lane = cx - b.x < b.w / 2 ? leftLane : rightLane;
131
+ pts = [{ x: cx, y: card.y + NH }, { x: cx, y: gy }, { x: lane, y: gy }, { x: lane, y: b.y }];
132
+ }
133
+ const top = pts[pts.length - 1];
134
+ pts.push({ x: top.x, y: b.busY + o });
135
+ return { pts, bus: b.busY + o };
136
+ }
137
+ /** The points of a line from one end to the other: up to the bus, along it (and the trunk, between rows), down. */
138
+ export function route(L, a, z, o) {
139
+ const p = outward(L, a, o), q = outward(L, z, o);
140
+ const back = [...q.pts].reverse();
141
+ if (Math.abs(p.bus - q.bus) < 1) return [...p.pts, ...back];
142
+ const tx = OC.TRUNK - 12 - o;
143
+ return [...p.pts, { x: tx, y: p.bus }, { x: tx, y: q.bus }, ...back];
144
+ }
145
+ /** An orthogonal polyline as SVG path data, its corners rounded. */
146
+ export function pathOf(pts, r = 8) {
147
+ const q = pts.filter((pt, i) => !i || pt.x !== pts[i - 1].x || pt.y !== pts[i - 1].y);
148
+ let d = `M${q[0].x} ${q[0].y}`;
149
+ for (let i = 1; i < q.length - 1; i++) {
150
+ const a = q[i - 1], b = q[i], c = q[i + 1];
151
+ const r1 = Math.min(r, Math.hypot(b.x - a.x, b.y - a.y) / 2, Math.hypot(c.x - b.x, c.y - b.y) / 2);
152
+ const u = { x: Math.sign(a.x - b.x), y: Math.sign(a.y - b.y) }, v = { x: Math.sign(c.x - b.x), y: Math.sign(c.y - b.y) };
153
+ d += ` L${b.x + u.x * r1} ${b.y + u.y * r1} Q${b.x} ${b.y} ${b.x + v.x * r1} ${b.y + v.y * r1}`;
154
+ }
155
+ const last = q[q.length - 1];
156
+ return `${d} L${last.x} ${last.y}`;
157
+ }
158
+ /** Where a line's ×N goes: the middle of its longest straight run. */
159
+ export function badgeAt(pts) {
160
+ let best = pts[0], len = -1;
161
+ for (let i = 1; i < pts.length; i++) {
162
+ const l = Math.hypot(pts[i].x - pts[i - 1].x, pts[i].y - pts[i - 1].y);
163
+ if (l > len) { len = l; best = { x: (pts[i].x + pts[i - 1].x) / 2, y: (pts[i].y + pts[i - 1].y) / 2 }; }
164
+ }
165
+ return best;
166
+ }
167
+
168
+ // ── Edges ──
169
+
170
+ /** The colour family of a kind: task, report, broadcast, and the rest (send, query, update). */
171
+ export const hue = (kind) => (kind === "task" || kind === "report" || kind === "broadcast" ? kind : "send");
172
+ const phaseOf = (it) => (it.fading ? "fading" : it.state === "queued" ? "queued" : "live");
173
+ const RANK = { live: 0, queued: 1, fading: 2 };
174
+
175
+ /**
176
+ * The lines to draw from the store's items on layout `L`. `far`: zoomed out (one line per team pair). Each edge:
177
+ * { key, hue, phase, n, from, to, d, pts, badge } — `from`/`to` the instance (or team) names it joins, for search.
178
+ */
179
+ export function trafficEdges(items, L, { far = false, max = MAX_EDGES } = {}) {
180
+ if (!L.boxes.length && L.tops.length < 2) return [];
181
+ const groups = new Map();
182
+ const add = (key, g) => { const cur = groups.get(key); if (cur) { cur.n += g.n; cur.names.push(...g.names); } else groups.set(key, g); };
183
+ const teamKey = (name) => { const t = teamOf(L, name); return t === null ? "\u0000general" : t ? t.key : undefined; };
184
+ const teamEnd = (name) => { const t = teamOf(L, name); return t === null ? endOf(L, name) : t ? { kind: "box", b: t, key: `box:${t.key}` } : null; };
185
+ // Broadcasts: one fan-out per op; big ones (or far) bundle per recipient team.
186
+ const byOp = new Map();
187
+ for (const it of items) if (it.kind === "broadcast" && it.op) { const l = byOp.get(it.op) || []; l.push(it); byOp.set(it.op, l); }
188
+ const bundled = new Set();
189
+ for (const list of byOp.values()) if (far || list.length > BUNDLE_AT) for (const it of list) bundled.add(it.id);
190
+ for (const it of items) {
191
+ const phase = phaseOf(it), names = [it.from, it.to];
192
+ if (far || bundled.has(it.id)) {
193
+ const ta = teamKey(it.from), tb = teamKey(it.to);
194
+ if (ta === tb) continue; // inside one team: not drawn far away
195
+ const k = far ? "team" : "broadcast";
196
+ const a = far ? teamEnd(it.from) : endOf(L, it.from), z = teamEnd(it.to);
197
+ if (!a || !z) continue;
198
+ add(`${k}|${a.key}>${z.key}|${phase === "fading" ? "fading" : "live"}`, { a, z, hue: k === "team" ? "team" : "broadcast", phase: phase === "fading" ? "fading" : phase, n: 1, names });
199
+ continue;
200
+ }
201
+ const a = endOf(L, it.from), z = endOf(L, it.to);
202
+ if (!a || !z || a.key === z.key) continue; // both in one folded team
203
+ add(`${a.key}>${z.key}|${hue(it.kind)}|${phase}`, { a, z, hue: hue(it.kind), phase, n: 1, names });
204
+ }
205
+ const list = [...groups.entries()].sort((x, y) => RANK[x[1].phase] - RANK[y[1].phase]).slice(0, max);
206
+ return list.map(([key, g], i) => {
207
+ const o = ((i % 7) - 3) * 7; // neighbours 7 px apart (within the ±22 px bus lane)
208
+ const pts = route(L, g.a, g.z, o);
209
+ return { key, hue: g.hue, phase: g.phase, n: g.n, names: [...new Set(g.names)], d: pathOf(pts), pts, badge: g.n > 1 ? badgeAt(pts) : null };
210
+ });
211
+ }
212
+
213
+ /** For the list: what each instance is sending now, in words ("task → quill"), live messages only. */
214
+ export function sendingBy(items) {
215
+ const out = new Map();
216
+ for (const it of items) {
217
+ if (it.fading) continue;
218
+ const l = out.get(it.from) || [];
219
+ const key = `${it.kind}>${it.to}>${it.state}`;
220
+ if (!l.some((x) => x.key === key)) l.push({ key, kind: it.kind, to: it.to, queued: it.state === "queued" });
221
+ out.set(it.from, l);
222
+ }
223
+ return out;
224
+ }
@@ -10,7 +10,7 @@ import { html, useEffect, useLayoutEffect, useRef, useState } from "/assets/app-
10
10
  import { t } from "/assets/app-i18n.js";
11
11
  import { appStore, useStore } from "/assets/app-store.js";
12
12
  import { useLease } from "/assets/app-ctx.js";
13
- import { namesOf, namesStore } from "/assets/instance-nav.js";
13
+ import { modelText, namesOf, namesStore } from "/assets/instance-nav.js";
14
14
  import { PanelHeader, onPanelKey, setTitle, statusClass, statusLabel, openDrawer, requestNewInstance } from "/assets/app-shell.js";
15
15
  import { detailsPath, textLinkSegments } from "/assets/app-route.js";
16
16
  import { InstanceSwitch } from "/assets/instance-switch.js";
@@ -281,7 +281,7 @@ function ChatView({ name, inst, lease, navKey, exec, awaiting }) {
281
281
  // #1269 quick actions: the model and effort it runs, each a way to /model or /effort (the same pick list).
282
282
  const pick = (command) => store.runCommand(name, command, "", { alive: () => lease.current() });
283
283
  const sub = html`<span class="status"><span class=${`dot ${cls}`} aria-hidden="true"></span>${statusLabel(inst, exec, awaiting)}</span>
284
- ${inst.model ? html`<button type="button" class="hd-chip" title=${t("chat.chipModel", inst.model)} aria-label=${t("chat.chipModel", inst.model)} onClick=${() => pick("model")}>${inst.model}</button>` : null}
284
+ ${inst.model ? html`<button type="button" class="hd-chip" title=${t("chat.chipModel", modelText(inst))} aria-label=${t("chat.chipModel", modelText(inst))} onClick=${() => pick("model")}>${modelText(inst)}</button>` : null}
285
285
  ${inst.effort ? html`<button type="button" class="hd-chip" title=${t("chat.chipEffort", inst.effort)} aria-label=${t("chat.chipEffort", inst.effort)} onClick=${() => pick("effort")}>${inst.effort}</button>` : null}`;
286
286
  return html`<div class=${`panel p-chat${wrap ? " wrap-code" : ""}`} ref=${view}>
287
287
  <${PanelHeader} title=${headerName(namesOf(inst || { name }, names.primary))} sub=${sub} nav=${html`<${InstanceSwitch} name=${name} current="chat" />`}><${ChatEffectPosters} name=${name} lease=${lease} element=${effectSurface} /><${ChatEffects} name=${name} lease=${lease} element=${effectSurface} store=${store} open=${dialog === "effects"} onClose=${() => setDialog(null)} /><${HeaderTools} navKey=${navKey} /><${Menu} items=${items} label=${t("app.more")} /></${PanelHeader}>
@@ -312,6 +312,12 @@ function Thread({ name, th }) {
312
312
  const thread = createThread(list.current, scroller.current, {
313
313
  t, tf: t, isUser: (x) => store.isUser(x, name), onJump: setJump, onEmpty: setEmpty,
314
314
  setPreviewOptIn, copyText, download: downloadHtml, toggleWrap,
315
+ effectTrialStatus: x => getAppEffects()?.trialStatus(x) ?? "localAdmin",
316
+ fixEffectTrial: (x, reason) => {
317
+ if (reason === "busy") getAppEffects()?.stopTrial();
318
+ else if (reason === "localAdmin") location.href = "/signin?next=" + encodeURIComponent(location.pathname + location.search);
319
+ else navigate("/ui/settings?section=appearance");
320
+ },
315
321
  canTryEffect: x => getAppEffects()?.canTry(x) === true,
316
322
  startEffectTrial: x => { const seq = navStore.get().seq; void getAppEffects()?.startTrial(x).then(ok => { if (!ok && live && navStore.get().seq === seq && document.hasFocus?.() === true && document.visibilityState === "visible") toast(t("chat.effectTrialUnavailable")); }); },
317
323
  clickReplyButton: (id, index) => store.clickReplyButton(name, id, index), // #1266
@@ -355,8 +361,8 @@ function Thread({ name, th }) {
355
361
  return html`<div class="scroller" ref=${scroller}>
356
362
  <div class="thread" ref=${list} role="log" aria-live="off" aria-label=${t("chat.conversationWith", name)}></div>
357
363
  ${empty ? html`<${Empty} icon="chat" title=${t("chat.noMessages")} />` : null}
358
- ${jump.show ? html`<button data-trust="thread-control" type="button" class="jump-latest" onClick=${() => th.current && th.current.jumpLatest()}>
359
- <${Icon} name="down" size=${16} />${jump.unseen > 0 ? t("chat.newBelow", jump.unseen) : t("chat.jumpLatest")}</button>` : null}
364
+ <div class="jump-dock">${jump.show ? html`<button data-trust="thread-control" type="button" class="jump-latest" onClick=${() => th.current && th.current.jumpLatest()}>
365
+ <${Icon} name="down" size=${16} />${jump.unseen > 0 ? t("chat.newBelow", jump.unseen) : t("chat.jumpLatest")}</button>` : null}</div>
360
366
  </div>`;
361
367
  }
362
368
 
@@ -393,7 +399,8 @@ function CommandCard({ name, lease }) {
393
399
  : c.compact === "lost" ? t("chat.compactLost") : c.compact === "not_compacted" ? t("chat.compactNotCompacted") : t("chat.compactUnknown")}</p>` : null}
394
400
  ${c.choices && c.choices.prompt ? html`<pre class="cmd-text">${c.choices.prompt}</pre>` : null}
395
401
  ${c.choices ? html`<div class="cmd-choices" role="group" aria-label=${t("chat.cmdChoose")}>${c.choices.options.map((o) => html`<button key=${o.id} type="button"
396
- class=${`btn btn-sm${o.id === c.choices.current ? " current" : ""}`} aria-current=${o.id === c.choices.current ? "true" : undefined} onClick=${() => run(o.id)}>${o.label}</button>`)}</div>` : null}
402
+ class=${`btn btn-sm${o.id === c.choices.current ? " current" : ""}`} aria-current=${o.id === c.choices.current ? "true" : undefined}
403
+ disabled=${!!o.disabled} title=${o.disabled || undefined} onClick=${() => { if (!o.disabled) run(o.id); }}>${o.label}</button>`)}</div>` : null}
397
404
  </div>`;
398
405
  }
399
406
 
@@ -14,6 +14,7 @@ import { PanelHeader, setTitle, statusClass, statusLabel, openDrawer } from "/as
14
14
  import { navigate } from "/assets/app-nav.js";
15
15
  import { settingsPath } from "/assets/app-route.js";
16
16
  import { InstanceSwitch } from "/assets/instance-switch.js";
17
+ import { modelText } from "/assets/instance-nav.js";
17
18
  import { HeaderTools } from "/assets/header-tools.js";
18
19
  import { Menu } from "/assets/ui-menu.js";
19
20
  import { themeMenuItems, themesStore } from "/assets/app-themes.js";
@@ -143,7 +144,7 @@ function DetailsView({ name, inst, navKey, exec, awaiting }) {
143
144
  <button type="button" class="btn btn-sm d-edit-settings" onClick=${editInSettings}>${t("chat.dEditInSettings")}</button></div></section>
144
145
  <section class="card"><h3>${t("chat.dRuntime")}</h3>
145
146
  ${row(t("chat.dBackend"), d.backend || "--")}
146
- ${row(t("chat.dModel"), `${d.model || sl.model?.display_name || "--"}${d.model_source === "live" ? "" : src(d.model_source)}`)}
147
+ ${row(t("chat.dModel"), `${(d.model && modelText(d)) || sl.model?.display_name || "--"}${d.model_source === "live" ? "" : src(d.model_source)}`)}
147
148
  ${d.effort ? row(t("chat.dEffort"), `${d.effort}${src(d.effort_source)}`) : null}
148
149
  ${row(t("chat.dCost"), `$${cost.toFixed(2)} ${t("chat.dCostNote")}`)}
149
150
  ${row(t("chat.dContext"), html`<span class="pct">${ctx != null ? `${Math.round(ctx)}%` : "--"}</span>${ctx != null ? bar(ctx, ctx > 80 ? "warn" : "") : null}`)}
@@ -18,14 +18,24 @@ import { Icon } from "/assets/ui-icons.js";
18
18
  import { toast } from "/assets/ui-toast.js";
19
19
  import { confirmDialog } from "/assets/ui-confirm.js";
20
20
  import { confirmedWrite } from "./settings-confirm.js";
21
- import { OrgChart } from "./fleet-org.js";
21
+ import { OrgCanvas } from "./fleet-canvas.js";
22
22
  import { labelOf } from "/assets/instance-nav.js";
23
23
  import { CacheReport } from "./fleet-cache.js";
24
+ import { BackendSelect, ModelSelect } from "./settings-dialogs.js";
25
+
26
+ /** #2015: one of the largest processes as text: "python3 18.1 GB (dev-a)". */
27
+ export function memoryHogText(p) {
28
+ const size = p.rssBytes >= 1024 ** 3 ? `${(p.rssBytes / 1024 ** 3).toFixed(1)} GB` : `${Math.round(p.rssBytes / 1024 ** 2)} MB`;
29
+ const owner = p.owner === "instance" && p.instance ? p.instance
30
+ : t(p.owner === "fleet" ? "fleet.hostMemory_ownerFleet" : p.owner === "tmux" ? "fleet.hostMemory_ownerTmux" : "fleet.hostMemory_ownerOther");
31
+ return `${p.name} ${size} (${owner})`;
32
+ }
24
33
 
25
34
  register("fleet", {
26
35
  en: {
27
36
  hostMemory_elevated: "Host memory pressure: elevated — new agents start one at a time, at least 5 s apart",
28
37
  hostMemory_critical: "Host memory pressure: critical — new agents wait for memory to recover",
38
+ hostMemory_top: "Largest: {0}", hostMemory_ownerFleet: "the fleet", hostMemory_ownerTmux: "tmux", hostMemory_ownerOther: "not an instance",
29
39
  title: "Fleet", tasks: "Tasks", schedules: "Schedules", teams: "Teams", org: "Org chart", cache: "Cache", config: "Config",
30
40
  docs: "Docs", docsNewTab: "Docs (opens in a new tab)",
31
41
  newTask: "New task", newSchedule: "New schedule", newTeam: "New team",
@@ -52,11 +62,12 @@ register("fleet", {
52
62
  newInstance: "New instance", directory: "Directory", directoryHint: "Optional — a workspace is created if empty", topic: "Name", topicHint: "From the directory name",
53
63
  topicRequired: "Name is required when Directory is empty", description: "Description", descriptionHint: "What this instance does", model: "Model", modelHint: "e.g. sonnet, opus, gpt-5",
54
64
  branch: "Branch (git worktree)", branchHint: "e.g. feature-x", tags: "Tags", tagsHint: "comma-separated, e.g. dev, review", systemPrompt: "System prompt", systemPromptHint: "Custom instructions for this instance",
55
- fleetDefault: "Use the fleet default", notInstalled: "(not installed)", deprecated: "(deprecated)", instanceCreated: "Instance created",
65
+ fleetDefault: "Use the fleet default", notInstalled: "(not installed)", notSignedIn: "(not signed in — use /login)", tooOld: "(needs {0} or newer)", unchecked: "(not checked yet)", deprecated: "(deprecated)", instanceCreated: "Instance created",
56
66
  },
57
67
  "zh-TW": {
58
68
  hostMemory_elevated: "主機記憶體壓力偏高:新的 agent 一次啟動一個,至少間隔 5 秒",
59
69
  hostMemory_critical: "主機記憶體壓力嚴重:新的 agent 會等記憶體恢復再啟動",
70
+ hostMemory_top: "最大:{0}", hostMemory_ownerFleet: "fleet 本身", hostMemory_ownerTmux: "tmux", hostMemory_ownerOther: "不屬於任何 instance",
60
71
  title: "Fleet", tasks: "Tasks", schedules: "排程", teams: "Teams", org: "組織圖", cache: "快取", config: "設定",
61
72
  docs: "文件", docsNewTab: "文件(在新分頁開啟)",
62
73
  newTask: "新增 task", newSchedule: "新增排程", newTeam: "新增 team",
@@ -82,7 +93,7 @@ register("fleet", {
82
93
  newInstance: "新增 instance", directory: "目錄", directoryHint: "可留空——留空會自動建立工作區", topic: "名稱", topicHint: "預設取自目錄名稱",
83
94
  topicRequired: "目錄留空時必須填寫名稱", description: "說明", descriptionHint: "這個 instance 做什麼", model: "模型", modelHint: "例如 sonnet、opus、gpt-5",
84
95
  branch: "分支(git worktree)", branchHint: "例如 feature-x", tags: "標籤", tagsHint: "以逗號分隔,例如 dev, review", systemPrompt: "System prompt", systemPromptHint: "這個 instance 的自訂指示",
85
- fleetDefault: "使用 fleet 預設", notInstalled: "(未安裝)", deprecated: "(已淘汰)", instanceCreated: "Instance 已建立",
96
+ fleetDefault: "使用 fleet 預設", notInstalled: "(未安裝)", notSignedIn: "(未登入,請用 /login)", tooOld: "(需要 {0} 以上版本)", unchecked: "(尚未檢查)", deprecated: "(已淘汰)", instanceCreated: "Instance 已建立",
86
97
  },
87
98
  });
88
99
 
@@ -132,8 +143,10 @@ export function FleetPanel({ route, navKey }) {
132
143
  useEffect(() => { setTitle(`${t("fleet.title")} · ${t(`fleet.${tab}`)}`); }, [tab, navKey]);
133
144
  const Body = { tasks: Tasks, schedules: Schedules, teams: Teams, org: Org, cache: Cache, config: Config }[tab] || Tasks;
134
145
  // #1685: host memory pressure slowing or holding new agents — shown here, where macOS's routine "elevated" is not a chat notice.
135
- const { hostMemory } = useStore(appStore);
136
- const sub = hostMemory ? html`<span class=${`status host-mem ${hostMemory}`} role="status"><span class=${`dot ${hostMemory === "critical" ? "bad" : "warn"}`} aria-hidden="true"></span>${t(`fleet.hostMemory_${hostMemory}`)}</span>` : null;
146
+ const { hostMemory, hostMemoryTop } = useStore(appStore);
147
+ // #2015: and which processes hold the most, and whose — text only (never chat, #1712).
148
+ const top = hostMemory && hostMemoryTop?.length ? t("fleet.hostMemory_top", hostMemoryTop.map(memoryHogText).join(" · ")) : "";
149
+ const sub = hostMemory ? html`<span class=${`status host-mem ${hostMemory}`} role="status"><span class=${`dot ${hostMemory === "critical" ? "bad" : "warn"}`} aria-hidden="true"></span>${t(`fleet.hostMemory_${hostMemory}`)}${top ? html`<span class="host-mem-top">${top}</span>` : null}</span>` : null;
137
150
  return html`<div data-effect-panel="fleet" class="panel p-fleet">
138
151
  <${PanelHeader} title=${t("fleet.title")} sub=${sub}><${DocsLink} /><${HeaderTools} navKey=${navKey} /></${PanelHeader}>
139
152
  <nav data-trust="panel-navigation" class="seg" aria-label=${t("fleet.title")}>${FLEET_TABS.map(k => html`<a key=${k} href=${fleetPath(k)} class=${`seg-item${k === tab ? " active" : ""}`}
@@ -255,11 +268,11 @@ function Teams({ lease }) {
255
268
  ${editing ? html`<${CreateTeam} team=${editing} onClose=${() => setEditing(null)} onDone=${() => setVersion(v => v + 1)} />` : null}`;
256
269
  }
257
270
 
258
- /** #1389: the org chart — its structure read once per opening, its live state from the app store (fleet-org.js). */
271
+ /** #1389: the org chart — its structure read once per opening, its live state from the app store. #1918: a canvas, or the list (fleet-canvas.js). */
259
272
  function Org({ lease }) {
260
273
  const state = useLoad(lease, "/ui/org", 0);
261
274
  const app = useStore(appStore);
262
- return html`<${Loaded} state=${state}>${(org) => html`<${OrgChart} org=${org} app=${app} />`}</${Loaded}>`;
275
+ return html`<${Loaded} state=${state}>${(org) => html`<${OrgCanvas} org=${org} app=${app} lease=${lease} />`}</${Loaded}>`;
263
276
  }
264
277
 
265
278
  /** #1468: the prompt-cache analysis — one read per opening and per window chosen; Refresh reads again (fleet-cache.js). */
@@ -322,8 +335,7 @@ function Config({ lease }) {
322
335
  <div class="field"><label for="cfg-users">${t("fleet.allowedUsers")}</label><${Secret} id="cfg-users" value=${form.users} onInput=${set("users")} label=${t("fleet.allowedUsers")} />
323
336
  <p class="note">${t("fleet.allowedUsersNote")}</p></div></section>
324
337
  <section class="card"><h3>${t("fleet.defaults")}</h3>
325
- <label class="field"><span>${t("fleet.backend")}</span><select value=${form.backend} onChange=${set("backend")}>
326
- ${[...(BACKENDS.includes(form.backend) ? [] : [form.backend]), ...BACKENDS].map(b => html`<option key=${b} value=${b}>${b}</option>`)}</select></label></section>
338
+ <div class="field"><label for="cfg-be">${t("fleet.backend")}</label><${BackendSelect} id="cfg-be" value=${form.backend} onChange=${(v) => setForm({ ...form, backend: v })} /></div></section>
327
339
  <section class="card"><h3>${t("fleet.roots")}</h3>
328
340
  ${form.roots.map((r, i) => html`<div key=${i} class="field-row"><input value=${r} placeholder="/path/to/projects" aria-label=${t("fleet.roots")}
329
341
  onInput=${(e) => { const roots = [...form.roots]; roots[i] = e.target.value; setForm({ ...form, roots }); }} />
@@ -547,14 +559,23 @@ export function CreateInstanceDialog({ onClose }) {
547
559
  ${field(`${t("fleet.topic")}${f.dir.trim() ? "" : " *"}`, html`<input value=${f.topic} onInput=${set("topic")} placeholder=${t("fleet.topicHint")} />`)}
548
560
  ${field(t("fleet.description"), html`<input value=${f.desc} onInput=${set("desc")} placeholder=${t("fleet.descriptionHint")} />`)}
549
561
  ${field(t("fleet.backend"), backends ? html`<select value=${f.backend} onChange=${set("backend")}><option value="">${t("fleet.fleetDefault")}</option>
550
- ${backends.map(b => html`<option key=${b.name} value=${b.name} disabled=${!b.installed}>${b.name}${b.deprecated ? ` ${t("fleet.deprecated")}` : ""}${b.installed ? "" : ` ${t("fleet.notInstalled")}`}</option>`)}</select>` : html`<${Skeleton} lines=${1} />`)}
551
- ${field(t("fleet.model"), html`<input value=${f.model} onInput=${set("model")} placeholder=${t("fleet.modelHint")} />`)}
562
+ ${backends.map(b => html`<option key=${b.name} value=${b.name} disabled=${backendBlocked(b)}>${b.name}${b.deprecated ? ` ${t("fleet.deprecated")}` : ""}${backendNote(b)}</option>`)}</select>` : html`<${Skeleton} lines=${1} />`)}
563
+ ${field(t("fleet.model"), html`<${ModelSelect} value=${f.model} onChange=${(v) => setF(x => ({ ...x, model: v }))} backend=${f.backend || null} inherit=${t("fleet.fleetDefault")} />`)}
552
564
  ${field(t("fleet.branch"), html`<input value=${f.branch} onInput=${set("branch")} placeholder=${t("fleet.branchHint")} />`)}
553
565
  ${field(t("fleet.tags"), html`<input value=${f.tags} onInput=${set("tags")} placeholder=${t("fleet.tagsHint")} />`)}
554
566
  ${field(t("fleet.systemPrompt"), html`<textarea rows="3" value=${f.prompt} onInput=${set("prompt")} placeholder=${t("fleet.systemPromptHint")}></textarea>`)}
555
567
  </${FormDialog}>`;
556
568
  }
557
569
 
570
+ // #2072: a backend this host cannot run is shown, disabled, with why; one not checked yet stays selectable.
571
+ const backendState = (b) => b.state ?? (b.installed ? "usable" : b.unknown ? "unknown" : "not_installed");
572
+ function backendBlocked(b) { return ["not_installed", "not_signed_in", "too_old"].includes(backendState(b)); }
573
+ function backendNote(b) {
574
+ const s = backendState(b);
575
+ return s === "not_installed" ? ` ${t("fleet.notInstalled")}` : s === "not_signed_in" ? ` ${t("fleet.notSignedIn")}`
576
+ : s === "too_old" ? ` ${t("fleet.tooOld", b.minVersion ?? "?")}` : s === "unknown" ? ` ${t("fleet.unchecked")}` : "";
577
+ }
578
+
558
579
  // ── Cron in words (#1724: in the page's language) ──
559
580
  /**
560
581
  * A cron expression as a person says it — "Weekdays 18:00", 「每週一到五 18:00」, "Every 15 minutes" — or the expression
@@ -20,6 +20,7 @@ import { Empty, ErrorState, Skeleton } from "/assets/ui-states.js";
20
20
  import { Icon } from "/assets/ui-icons.js";
21
21
  import { Menu } from "/assets/ui-menu.js";
22
22
  import { toast } from "/assets/ui-toast.js";
23
+ import { groupLabel, modelText } from "/assets/instance-nav.js";
23
24
  import "./settings-strings.js";
24
25
  import { api, attach, confirmedWrite, newKey } from "./settings-confirm.js";
25
26
  import { dismissOperation, operationActive, restartFleet, startOperation, takeLeftover } from "./settings-apply.js";
@@ -30,7 +31,7 @@ import {
30
31
  visibilityDefault, worstImpact,
31
32
  } from "./settings-model.js";
32
33
  import {
33
- AgentDialog, BackendSelect, BotDialog, ChipList, ClassicDialog, Drawer, Feedback, Impact, Label, NewBotDialog, Override,
34
+ AgentDialog, BackendSelect, BotDialog, ChipList, ClassicDialog, Drawer, Feedback, Impact, Label, ModelSelect, NewBotDialog, Override,
34
35
  Select, ask, impactText,
35
36
  } from "./settings-dialogs.js";
36
37
  import { SetupWizard } from "./settings-wizard.js";
@@ -87,7 +88,7 @@ async function loadLive(lease) {
87
88
  if (!lease.current()) return null;
88
89
  const live = {}, classicLive = {};
89
90
  if (f.ok && f.body && Array.isArray(f.body.instances)) for (const i of f.body.instances) {
90
- live[i.name] = { status: i.status, state: i.state, backend: i.backend, model: i.model, model_display: i.model_display, effort: i.effort, effort_supported: i.effort_supported };
91
+ live[i.name] = { status: i.status, state: i.state, backend: i.backend, model: i.model, model_display: modelText(i, i.model_display), effort: i.effort, effort_supported: i.effort_supported };
91
92
  if (i.classic) classicLive[i.name] = { status: i.status, state: i.state };
92
93
  }
93
94
  // /api/fleet carries live state for ClassicBot rooms; the profiles are the fallback for one not listed there yet.
@@ -473,7 +474,7 @@ function Agents({ ctx, search, openDialog }) {
473
474
  return html`<section key=${group} class="s-group">
474
475
  <button type="button" class="s-group-head" aria-expanded=${folded ? "false" : "true"}
475
476
  onClick=${() => setCollapsed((c) => { const n = new Set(c); n.has(group) ? n.delete(group) : n.add(group); return n; })}>
476
- <${Icon} name="chevron" size=${14} cls=${folded ? "caret folded" : "caret"} /><span class="grow">${group}</span><span class="count">${names.length}</span></button>
477
+ <${Icon} name="chevron" size=${14} cls=${folded ? "caret folded" : "caret"} /><span class="grow">${groupLabel(group)}</span><span class="count">${names.length}</span></button>
477
478
  ${folded ? null : names.map((name) => {
478
479
  const inst = insts[name];
479
480
  const st = (ctx.live[name] && ctx.live[name].status) || "stopped";
@@ -706,7 +707,8 @@ function General({ ctx }) {
706
707
  <div class="field">${L(tn("language"), "defaults.locale", "g-locale")}<${Select} id="g-locale" value=${f.locale} onChange=${set("locale")}
707
708
  options=${localeOptions(f.locale).map((o) => (o.auto ? { value: "", label: tn("localeAuto") } : o))} /></div>
708
709
  <div class="field">${L(tn("defaultBackend"), "defaults.backend", "g-be")}<${BackendSelect} id="g-be" value=${f.backend} onChange=${set("backend")} /></div>
709
- <div class="field">${L(tn("defaultModel"), "defaults.model", "g-model")}<input id="g-model" type="text" placeholder="e.g. claude-opus-4-8" value=${f.model} onInput=${(e) => set("model")(e.target.value)} /></div>
710
+ <div class="field">${L(tn("defaultModel"), "defaults.model", "g-model")}<${ModelSelect} id="g-model" value=${f.model} onChange=${set("model")}
711
+ backend=${f.backend || "claude-code"} inherit=${tn("modelCliDefault")} /></div>
710
712
  <div class="field">${L(`${tn("autoPause")} (${tn("minutes")})`, "defaults.auto_pause_after", "g-auto")}<input id="g-auto" type="number" min="0" step="1" class=${errors.auto ? "invalid" : ""} value=${f.auto} onInput=${(e) => set("auto")(e.target.value)} /><${Feedback} error=${errors.auto} /></div>
711
713
  <div class="field">${L(tn("toolProgress"), "defaults.tool_progress", "g-tp")}<${Select} id="g-tp" value=${f.tool_progress} onChange=${set("tool_progress")} options=${TOOL_PROGRESS} /></div>
712
714
  <div class="field">${L(tn("crossInstanceVisibility"), "defaults.cross_instance_visibility", "g-vis")}<${Select} id="g-vis" value=${f.visibility} onChange=${set("visibility")} options=${VISIBILITY_MODES} />
@@ -100,7 +100,7 @@
100
100
  notify();
101
101
  return true;
102
102
  }
103
- if (j && (j.code === "publicLinkPreviewFailed" || j.code === "publicLink" || j.code === "fleetOff")) {
103
+ if (j && (j.code === "publicLinkPreviewFailed" || j.code === "publicLink" || j.code === "fleetOff" || j.code === "listenFailed" || j.code === "notListening")) {
104
104
  cfg.reasonCode = j.code; cfg.reason = ""; cfg.detail = typeof j.detail === "string" ? j.detail : "";
105
105
  notify();
106
106
  return true;
@@ -191,7 +191,8 @@
191
191
  if (!cfg.previewOrigin || !cfg.boot) {
192
192
  // The server's reason, in the page's language when the server named it (a code), else as the server wrote it.
193
193
  // #1723: a failed public-link preview names why ({0} is the reason, in the page's words).
194
- var arg = cfg.reasonCode === "publicLinkPreviewFailed" ? say("pvFail_" + (cfg.detail || "unknown"), Object.prototype.hasOwnProperty.call(FAIL_EN, cfg.detail) ? FAIL_EN[cfg.detail] : "an unexpected error") : cfg.dashboardOrigin;
194
+ var arg = cfg.reasonCode === "publicLinkPreviewFailed" ? say("pvFail_" + (cfg.detail || "unknown"), Object.prototype.hasOwnProperty.call(FAIL_EN, cfg.detail) ? FAIL_EN[cfg.detail] : "an unexpected error")
195
+ : cfg.reasonCode === "listenFailed" ? cfg.detail : cfg.dashboardOrigin;
195
196
  var english = cfg.reasonCode === "publicLinkPreviewFailed" ? "Previews are unavailable on this public link: {0}. The link itself keeps working."
196
197
  : cfg.reason || "Previews are not available here.";
197
198
  var server = cfg.reasonCode ? say("pvServer_" + cfg.reasonCode, english, arg) : "";