@songsid/agend 2.2.0-beta.3 → 2.2.0-beta.5

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 (139) hide show
  1. package/dist/agent-cli.js +6 -2
  2. package/dist/agent-cli.js.map +1 -1
  3. package/dist/backend/claude-code.d.ts +46 -2
  4. package/dist/backend/claude-code.js +125 -31
  5. package/dist/backend/claude-code.js.map +1 -1
  6. package/dist/backend/credential-profile.d.ts +33 -3
  7. package/dist/backend/credential-profile.js +107 -5
  8. package/dist/backend/credential-profile.js.map +1 -1
  9. package/dist/backend/types.d.ts +1 -1
  10. package/dist/cache-service.d.ts +2 -0
  11. package/dist/cache-service.js +2 -1
  12. package/dist/cache-service.js.map +1 -1
  13. package/dist/channel/adapters/discord.d.ts +13 -0
  14. package/dist/channel/adapters/discord.js +42 -0
  15. package/dist/channel/adapters/discord.js.map +1 -1
  16. package/dist/channel/adapters/telegram.d.ts +6 -1
  17. package/dist/channel/adapters/telegram.js +27 -2
  18. package/dist/channel/adapters/telegram.js.map +1 -1
  19. package/dist/channel/types.d.ts +22 -1
  20. package/dist/cli.js +35 -0
  21. package/dist/cli.js.map +1 -1
  22. package/dist/config-validator.js +7 -0
  23. package/dist/config-validator.js.map +1 -1
  24. package/dist/daemon.d.ts +96 -0
  25. package/dist/daemon.js +339 -16
  26. package/dist/daemon.js.map +1 -1
  27. package/dist/delivery-outbox.d.ts +18 -5
  28. package/dist/delivery-outbox.js +121 -14
  29. package/dist/delivery-outbox.js.map +1 -1
  30. package/dist/fleet-context.d.ts +1 -0
  31. package/dist/fleet-manager.d.ts +89 -3
  32. package/dist/fleet-manager.js +541 -86
  33. package/dist/fleet-manager.js.map +1 -1
  34. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +43 -10
  35. package/dist/host-memory.d.ts +10 -0
  36. package/dist/host-memory.js +33 -1
  37. package/dist/host-memory.js.map +1 -1
  38. package/dist/instance-lifecycle.d.ts +25 -6
  39. package/dist/instance-lifecycle.js +73 -26
  40. package/dist/instance-lifecycle.js.map +1 -1
  41. package/dist/locale.js +48 -18
  42. package/dist/locale.js.map +1 -1
  43. package/dist/login-controller.d.ts +20 -3
  44. package/dist/login-controller.js +71 -19
  45. package/dist/login-controller.js.map +1 -1
  46. package/dist/login-flows.d.ts +5 -2
  47. package/dist/login-flows.js +5 -4
  48. package/dist/login-flows.js.map +1 -1
  49. package/dist/memory-pressure.d.ts +13 -1
  50. package/dist/memory-pressure.js +50 -10
  51. package/dist/memory-pressure.js.map +1 -1
  52. package/dist/outbound-handlers.d.ts +61 -0
  53. package/dist/outbound-handlers.js +100 -35
  54. package/dist/outbound-handlers.js.map +1 -1
  55. package/dist/outbound-schemas.d.ts +1 -0
  56. package/dist/outbound-schemas.js +2 -0
  57. package/dist/outbound-schemas.js.map +1 -1
  58. package/dist/public-preview.d.ts +100 -0
  59. package/dist/public-preview.js +233 -0
  60. package/dist/public-preview.js.map +1 -0
  61. package/dist/public-web-gateway.js +6 -4
  62. package/dist/public-web-gateway.js.map +1 -1
  63. package/dist/public-web-link.d.ts +20 -0
  64. package/dist/public-web-link.js +26 -1
  65. package/dist/public-web-link.js.map +1 -1
  66. package/dist/restart-progress.d.ts +29 -0
  67. package/dist/restart-progress.js +23 -0
  68. package/dist/restart-progress.js.map +1 -1
  69. package/dist/settings-api.js +10 -4
  70. package/dist/settings-api.js.map +1 -1
  71. package/dist/spawn-gate.js +5 -1
  72. package/dist/spawn-gate.js.map +1 -1
  73. package/dist/spawn-hold-reply.d.ts +12 -0
  74. package/dist/spawn-hold-reply.js +39 -0
  75. package/dist/spawn-hold-reply.js.map +1 -0
  76. package/dist/topic-commands.js +9 -2
  77. package/dist/topic-commands.js.map +1 -1
  78. package/dist/tunnel/purpose-lane.d.ts +5 -1
  79. package/dist/tunnel/purpose-lane.js +14 -7
  80. package/dist/tunnel/purpose-lane.js.map +1 -1
  81. package/dist/turn-resume.d.ts +2 -0
  82. package/dist/turn-resume.js +1 -1
  83. package/dist/turn-resume.js.map +1 -1
  84. package/dist/types.d.ts +8 -0
  85. package/dist/ui/chat-mentions.js +63 -0
  86. package/dist/ui/chat-render.js +25 -0
  87. package/dist/ui/chat-strings.js +14 -2
  88. package/dist/ui/chat-thread.js +28 -10
  89. package/dist/ui/panel-chat.js +26 -11
  90. package/dist/ui/panel-details.js +2 -0
  91. package/dist/ui/panel-fleet.js +199 -57
  92. package/dist/ui/panel-needs.js +4 -3
  93. package/dist/ui/panel-settings.js +4 -2
  94. package/dist/ui/preview-panel.js +48 -14
  95. package/dist/ui/preview-status.js +28 -0
  96. package/dist/ui/preview.js +40 -3
  97. package/dist/ui/settings-token.js +1 -1
  98. package/dist/ui/shared/app-i18n.js +4 -0
  99. package/dist/ui/shared/app-prefs.js +1 -0
  100. package/dist/ui/shared/app-shell.js +146 -2
  101. package/dist/ui/shared/app-store.js +3 -1
  102. package/dist/ui/shared/app.css +47 -20
  103. package/dist/ui/shared/instance-nav.js +79 -23
  104. package/dist/ui/shared/view-strings.js +10 -0
  105. package/dist/unrecognised-prompt.d.ts +23 -0
  106. package/dist/unrecognised-prompt.js +74 -0
  107. package/dist/unrecognised-prompt.js.map +1 -0
  108. package/dist/update-install.d.ts +8 -0
  109. package/dist/update-install.js +9 -3
  110. package/dist/update-install.js.map +1 -1
  111. package/dist/update-marker.d.ts +40 -0
  112. package/dist/update-marker.js +49 -0
  113. package/dist/update-marker.js.map +1 -1
  114. package/dist/usage/i18n-keys.d.ts +1 -1
  115. package/dist/usage/i18n-keys.js +1 -1
  116. package/dist/usage/i18n-keys.js.map +1 -1
  117. package/dist/usage/providers.d.ts +34 -5
  118. package/dist/usage/providers.js +160 -27
  119. package/dist/usage/providers.js.map +1 -1
  120. package/dist/usage/statusline-usage.d.ts +6 -2
  121. package/dist/usage/statusline-usage.js +10 -4
  122. package/dist/usage/statusline-usage.js.map +1 -1
  123. package/dist/web-api.d.ts +20 -0
  124. package/dist/web-api.js +216 -30
  125. package/dist/web-api.js.map +1 -1
  126. package/dist/web-auth.js +3 -2
  127. package/dist/web-auth.js.map +1 -1
  128. package/dist/web-mentions.d.ts +44 -0
  129. package/dist/web-mentions.js +75 -0
  130. package/dist/web-mentions.js.map +1 -0
  131. package/dist/web-prefs.js +2 -0
  132. package/dist/web-prefs.js.map +1 -1
  133. package/dist/web-preview.d.ts +14 -1
  134. package/dist/web-preview.js +35 -13
  135. package/dist/web-preview.js.map +1 -1
  136. package/dist/web-schedules.d.ts +12 -0
  137. package/dist/web-schedules.js +34 -0
  138. package/dist/web-schedules.js.map +1 -0
  139. package/package.json +1 -1
@@ -28,7 +28,10 @@
28
28
  var BANNER_ACK_KEY = "agend_html_preview_banner_ack";
29
29
  var BANNER = "Previews run the agent's HTML in an isolated frame. It cannot use your login, but it may be able to send data out. Only preview content you trust. A preview can slow or freeze this tab.";
30
30
 
31
- var cfg = { dashboardOrigin: "", previewOrigin: "", boot: "", reason: "", reasonCode: "" };
31
+ var cfg = { dashboardOrigin: "", previewOrigin: "", boot: "", reason: "", reasonCode: "", detail: "" };
32
+ // Why it failed, in English when the app's dictionary has no word for it (chat-strings.js pvFail_*).
33
+ var FAIL_EN = { exited: "its address closed unexpectedly", "spawn-failed": "cloudflared could not start for it", "no-address": "no address was given for it",
34
+ unreachable: "its address could not be reached", "lease-held": "an earlier preview tunnel is still being cleaned up", "gateway-failed": "its local listener could not start" };
32
35
  // #1554: the page's words. `say(key, english, …values)` asks the app's dictionary (init's opts.text) and keeps the
33
36
  // English here when it has none — so this file still works, and reads the same, without the app.
34
37
  var text = null;
@@ -66,6 +69,7 @@
66
69
  boot: String((data && data.previewBoot) || ""),
67
70
  reason: String((data && data.previewReason) || ""),
68
71
  reasonCode: String((data && data.previewReasonCode) || ""),
72
+ detail: String((data && data.previewDetail) || ""),
69
73
  };
70
74
  if (opts && typeof opts.text === "function") text = opts.text;
71
75
  if (!listening && typeof root.addEventListener === "function") {
@@ -76,6 +80,29 @@
76
80
  listening = true;
77
81
  }
78
82
  }
83
+ /**
84
+ * #1723: the public link's own preview tunnel was still starting when this page loaded; preview-status.js asks the
85
+ * page's server and hands each answer here (this file reaches no network). Ready → previews run at the origin it
86
+ * names — only an https quick-tunnel origin other than this page's is ever taken: the page's frame-src allows no
87
+ * other; failed or off → the page says why. Returns true once there is nothing more to wait for.
88
+ */
89
+ function waitingForPublicPreview() { return cfg.reasonCode === "publicLinkStarting"; }
90
+ function applyPublicPreviewStatus(j) {
91
+ if (cfg.reasonCode !== "publicLinkStarting") return true;
92
+ var at = root.location && root.location.origin;
93
+ if (j && typeof j.previewOrigin === "string" && /^https:\/\/[a-z0-9-]+\.trycloudflare\.com$/.test(j.previewOrigin) && j.previewOrigin !== at
94
+ && typeof j.boot === "string" && /^[0-9a-f]{32}$/.test(j.boot)) {
95
+ cfg.previewOrigin = j.previewOrigin; cfg.boot = j.boot; cfg.reasonCode = ""; cfg.reason = "";
96
+ notify();
97
+ return true;
98
+ }
99
+ if (j && (j.code === "publicLinkPreviewFailed" || j.code === "publicLink" || j.code === "fleetOff")) {
100
+ cfg.reasonCode = j.code; cfg.reason = ""; cfg.detail = typeof j.detail === "string" ? j.detail : "";
101
+ notify();
102
+ return true;
103
+ }
104
+ return false;
105
+ }
79
106
  function onStorage(event) {
80
107
  // #1568: the banner acknowledged in another tab of this sign-in: hide it here too (nothing starts or stops).
81
108
  if (event && event.key === BANNER_ACK_KEY) { notify(); return; }
@@ -159,7 +186,11 @@
159
186
  function availability() {
160
187
  if (!cfg.previewOrigin || !cfg.boot) {
161
188
  // The server's reason, in the page's language when the server named it (a code), else as the server wrote it.
162
- var server = cfg.reasonCode ? say("pvServer_" + cfg.reasonCode, cfg.reason || "Previews are not available here.", cfg.dashboardOrigin) : "";
189
+ // #1723: a failed public-link preview names why ({0} is the reason, in the page's words).
190
+ 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;
191
+ var english = cfg.reasonCode === "publicLinkPreviewFailed" ? "Previews are unavailable on this public link: {0}. The link itself keeps working."
192
+ : cfg.reason || "Previews are not available here.";
193
+ var server = cfg.reasonCode ? say("pvServer_" + cfg.reasonCode, english, arg) : "";
163
194
  return { ok: false, why: "server", reason: server || cfg.reason || say("pvWhyServer", "Previews are not available here.") };
164
195
  }
165
196
  var at = root.location && root.location.origin;
@@ -197,11 +228,15 @@
197
228
  * opts.fill (#1481, the side panel): the frame fills its holder — the same frame from mountPreview, only sized by the
198
229
  * panel instead of by the frame's height messages, which it then ignores.
199
230
  */
231
+ /** How many previews this page has started: a click whose HTML arrives late asks whether another started meanwhile. */
232
+ var starts = 0;
233
+ function startCount() { return starts; }
200
234
  function start(key, holder, html, ui, opts) {
201
235
  var a = availability();
202
236
  if (!a.ok) { ui.state("unavailable", a.reason); return false; }
203
237
  if (typeof html !== "string" || utf8Bytes(html) > LIMITS.maxBytes) { ui.state("unavailable", say("pvHtmlOver", "This HTML is over 1 MiB; it is not previewed.")); return false; }
204
238
  stopAll("another");
239
+ starts++;
205
240
  var doc = holder.ownerDocument;
206
241
  var iframe = mountPreview(doc);
207
242
  var card = {
@@ -330,7 +365,7 @@
330
365
 
331
366
  return {
332
367
  init: init, availability: availability, optedIn: optedIn, setOptIn: setOptIn, never: never, setNever: setNever, onChange: onChange, listeners: listeners, onStorage: onStorage,
333
- start: start, stop: stop, stopAll: stopAll, stopIn: stopIn, running: running, liveCount: liveCount, liveFrame: liveFrame,
368
+ start: start, startCount: startCount, stop: stop, stopAll: stopAll, stopIn: stopIn, running: running, liveCount: liveCount, liveFrame: liveFrame,
334
369
  mountPreview: mountPreview, onMessage: onMessage, BANNER: BANNER, LIMITS: LIMITS,
335
370
  /** The banner every running preview carries, in the page's language (#1554); BANNER is its English. */
336
371
  banner: function () { return say("pvBanner", BANNER); },
@@ -338,5 +373,7 @@
338
373
  bannerAckLabel: function () { return say("pvBannerAck", "I understand"); },
339
374
  bannerAckTitle: function () { return say("pvBannerAckTitle", "Hide this notice until you sign in again"); },
340
375
  bannerAcked: bannerAcked, ackBanner: ackBanner, setBannerScope: setBannerScope,
376
+ /** #1723: the public link's preview, while it starts (preview-status.js asks; this only takes the answer). */
377
+ waitingForPublicPreview: waitingForPublicPreview, applyPublicPreviewStatus: applyPublicPreviewStatus,
341
378
  };
342
379
  });
@@ -31,7 +31,7 @@ export function TokenField({ id, platform, value, onInput, identity, busy, onVer
31
31
  const [shown, setShown] = useState(false);
32
32
  return html`<div class="field token-field"><label for=${id}>${tn("wizardToken")}</label>
33
33
  <div class="token-row">
34
- <input id=${id} type=${shown ? "text" : "password"} autocomplete="off" autocapitalize="off" spellcheck="false" disabled=${!!busy}
34
+ <input id=${id} type=${shown ? "text" : "password"} autocomplete="off" autocapitalize="off" spellcheck=${false} disabled=${!!busy}
35
35
  placeholder=${platform === "discord" ? "MTA…" : "123456:ABC-DEF…"} value=${value} onInput=${(e) => onInput(e.target.value.trim())} />
36
36
  <button type="button" class="icon-btn" aria-pressed=${shown ? "true" : "false"} title=${shown ? tn("tokenHide") : tn("tokenShow")}
37
37
  aria-label=${shown ? tn("tokenHide") : tn("tokenShow")} onClick=${() => setShown(!shown)}><${Icon} name="eye" size=${16} /></button>
@@ -62,6 +62,8 @@ register("app", {
62
62
  en: {
63
63
  brand: "AgEnD", fleet: "Fleet", fleetOverview: "Fleet overview", view: "View", settings: "Settings", instances: "Instances", newInstance: "New instance",
64
64
  collapse: "Hide the sidebar", expand: "Show the sidebar", openMenu: "Open the sidebar", closeMenu: "Close the sidebar",
65
+ resizeSidebar: "Sidebar width", resizeSidebarHint: "Drag to resize · double-click to reset",
66
+ names: "Names", namesNickname: "Nickname first", namesInstance: "Real name first",
65
67
  theme: "Theme", themeSystem: "System", themeLight: "Light", themeDark: "Dark", language: "Language",
66
68
  previews: "Allow HTML previews", tour: "Tour", tourTitle: "Show the web chat tour again",
67
69
  needsYou: "needs you", approxNote: "Read from the terminal, so approximate", chat: "Chat", details: "Details", instanceViews: "Views of {0}",
@@ -91,6 +93,8 @@ register("app", {
91
93
  "zh-TW": {
92
94
  brand: "AgEnD", fleet: "Fleet", fleetOverview: "Fleet 概覽", view: "檢視", settings: "設定", instances: "Instances", newInstance: "新增 instance",
93
95
  collapse: "隱藏側邊欄", expand: "顯示側邊欄", openMenu: "打開側邊欄", closeMenu: "關閉側邊欄",
96
+ resizeSidebar: "側邊欄寬度", resizeSidebarHint: "拖曳調整寬度 · 按兩下恢復預設",
97
+ names: "名稱", namesNickname: "暱稱優先", namesInstance: "實際名稱優先",
94
98
  theme: "主題", themeSystem: "跟隨系統", themeLight: "淺色", themeDark: "深色", language: "語言",
95
99
  previews: "允許預覽 HTML", tour: "導覽", tourTitle: "再看一次網頁聊天導覽",
96
100
  needsYou: "等你回應", approxNote: "從終端畫面判讀,僅供參考", chat: "聊天", details: "詳細資訊", instanceViews: "{0} 的檢視",
@@ -32,6 +32,7 @@ export const SYNCED = {
32
32
  "nav.filter": { ls: "agend_instance_filter", ...json },
33
33
  "nav.favorites": { ls: "agend_favorites", ...json }, // #1664
34
34
  "preview.optIn": { ls: "agend_html_preview", toLocal: (v) => (v ? "on" : null), fromLocal: (s) => (s === "on" ? true : undefined) },
35
+ "names.primary": { ls: "agend_names_primary", toLocal: (v) => (v === "instance" ? v : null), fromLocal: (s) => (s === "instance" ? s : undefined) }, // #1722
35
36
  };
36
37
  const own = (o, k) => !!o && Object.prototype.hasOwnProperty.call(o, k);
37
38
  const now = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
@@ -12,8 +12,8 @@ import { chatPath, viewPath, settingsPath, routeKey, NEEDS_PATH, detailsPath, fl
12
12
  import { SessionMenu } from "./app-session.js";
13
13
  import { savePref } from "./app-prefs.js";
14
14
  import { ErrorState, Skeleton } from "./ui-states.js";
15
- import { InstanceNav, RosterNav } from "./instance-nav.js";
16
- import { UsageRail } from "./header-tools.js";
15
+ import { InstanceNav, RosterNav, namesStore, setNamesPrimary } from "./instance-nav.js";
16
+ import { UsageRail, usageRailStore } from "./header-tools.js";
17
17
 
18
18
  /** Shell state shared with panels: the phone drawer, the collapsed sidebar, extensions a panel module adds. */
19
19
  export const shellStore = createStore({ drawer: false, collapsed: readCollapsed(), keyboard: false, dialog: null, side: null });
@@ -43,6 +43,144 @@ export function closeDialog() { shellStore.set({ dialog: null }); }
43
43
  function readCollapsed() { try { return localStorage.getItem("agend_sidebar") === "hidden"; } catch { return false; } }
44
44
  const narrow = () => typeof matchMedia === "function" && matchMedia("(max-width: 899px)").matches;
45
45
 
46
+ // ── #1722: the sidebar's width — this device's (the #1665 split: a width depends on the screen), through the CSSOM ──
47
+ export const SIDEBAR_W = { min: 200, max: 480, def: 260, step: 16, mainMin: 480 };
48
+ const SIDEBAR_W_KEY = "agend_sidebar_w";
49
+ const win = () => (typeof window !== "undefined" ? window : null);
50
+ const viewportWidth = () => { const w = win(); return w && typeof w.innerWidth === "number" && w.innerWidth > 0 ? w.innerWidth : 1366; };
51
+ /** The width the sidebar may have on a viewport `vw` px wide: within [min, max], and leaving the main pane mainMin. */
52
+ export function clampSidebarWidth(w, vw) {
53
+ const max = Math.max(SIDEBAR_W.min, Math.min(SIDEBAR_W.max, vw - SIDEBAR_W.mainMin));
54
+ return Math.round(Math.min(max, Math.max(SIDEBAR_W.min, w)));
55
+ }
56
+ function storedSidebarWidth() { try { const v = Number(localStorage.getItem(SIDEBAR_W_KEY)); return Number.isFinite(v) && v > 0 ? v : null; } catch { return null; } }
57
+ /** { width: px | null } — null: the default width (tokens.css). */
58
+ export const sidebarWidthStore = createStore({ width: null });
59
+ /** Set the width (clamped to this viewport); `save`: this device keeps it. null: back to the default, nothing kept. */
60
+ /**
61
+ * The room the main pane has beside the sidebar: the window, less the usage rail when it is on screen (#1673 — shown
62
+ * open, ≥ 1200 px; its CSS width when the layout gives none). #1722 review: the sidebar never leaves main under 480.
63
+ */
64
+ function mainBudget() {
65
+ const vw = viewportWidth();
66
+ if (vw < 1200 || typeof document === "undefined") return vw;
67
+ const rail = document.querySelector(".u-rail");
68
+ if (!rail) return vw;
69
+ const w = Math.round(rail.getBoundingClientRect ? rail.getBoundingClientRect().width : 0) || RAIL_W;
70
+ return vw - w;
71
+ }
72
+ const RAIL_W = 320;
73
+ /** Show a width (clamped to the room there is now) — this page only; null: the default width. Nothing is kept. */
74
+ function showSidebarWidth(w) {
75
+ const root = document.documentElement;
76
+ if (w == null) { root.style.removeProperty("--sidebar-w"); sidebarWidthStore.set({ width: null }); return null; }
77
+ const v = clampSidebarWidth(w, mainBudget());
78
+ root.style.setProperty("--sidebar-w", `${v}px`); // CSSOM: the page's CSP allows no style attribute
79
+ sidebarWidthStore.set({ width: v });
80
+ return v;
81
+ }
82
+ /** Set the width (clamped to the room there is now); `save`: this device keeps it. null: back to the default, nothing kept. */
83
+ export function setSidebarWidth(w, save = false) {
84
+ if (w == null) {
85
+ try { localStorage.removeItem(SIDEBAR_W_KEY); } catch { /* this page only */ }
86
+ return showSidebarWidth(null);
87
+ }
88
+ const v = showSidebarWidth(w);
89
+ // Kept: the choice within the window, not the squeeze beside the rail — it comes back when the rail goes.
90
+ if (save) { try { localStorage.setItem(SIDEBAR_W_KEY, String(clampSidebarWidth(w, viewportWidth()))); } catch { /* this page only */ } }
91
+ return v;
92
+ }
93
+ /** A drag in progress (one at a time): its end(commit). Only it may set the width until it ends. */
94
+ let activeDrag = null;
95
+ /**
96
+ * This device's kept width, shown within the room there is now — again whenever that changes (the window, the usage
97
+ * rail, the page). A squeeze is shown, never kept: the device's choice comes back when there is room. On a phone (the
98
+ * drawer) none applies, and a drag in progress is cancelled.
99
+ */
100
+ function applyKeptSidebarWidth() {
101
+ if (narrow()) {
102
+ if (activeDrag) activeDrag.end(false);
103
+ showSidebarWidth(null);
104
+ return;
105
+ }
106
+ if (activeDrag) return;
107
+ const w = storedSidebarWidth();
108
+ if (w != null) showSidebarWidth(w);
109
+ }
110
+ const afterPaint = (fn) => (typeof requestAnimationFrame === "function" ? requestAnimationFrame(() => fn()) : setTimeout(fn, 0));
111
+ /** The separator on the sidebar's right edge: drag, ← → (16 px), Home / End, Enter or a double-click resets. */
112
+ function SidebarResize() {
113
+ const { width } = useStore(sidebarWidthStore);
114
+ useStore(usageRailStore); // the rail's room is the separator's limit too
115
+ useLayoutEffect(() => {
116
+ applyKeptSidebarWidth();
117
+ // The room changes with the window, the rail (opened, closed) and the page (a page with no rail): shown again,
118
+ // once the page has drawn its change.
119
+ const again = () => afterPaint(applyKeptSidebarWidth);
120
+ const offRail = usageRailStore.subscribe(again);
121
+ const offNav = navStore.subscribe(again);
122
+ const w = win();
123
+ if (w && typeof w.addEventListener === "function") w.addEventListener("resize", applyKeptSidebarWidth);
124
+ return () => {
125
+ offRail(); offNav();
126
+ if (w && typeof w.removeEventListener === "function") w.removeEventListener("resize", applyKeptSidebarWidth);
127
+ if (activeDrag) activeDrag.end(false); // unmounted mid-drag: nothing kept, nothing left behind
128
+ };
129
+ }, []);
130
+ const now = width ?? SIDEBAR_W.def;
131
+ const onKey = (e) => {
132
+ const cur = sidebarWidthStore.get().width ?? SIDEBAR_W.def;
133
+ if (e.key === "ArrowRight") setSidebarWidth(cur + SIDEBAR_W.step, true);
134
+ else if (e.key === "ArrowLeft") setSidebarWidth(cur - SIDEBAR_W.step, true);
135
+ else if (e.key === "Home") setSidebarWidth(SIDEBAR_W.min, true);
136
+ else if (e.key === "End") setSidebarWidth(SIDEBAR_W.max, true);
137
+ else if (e.key === "Enter") setSidebarWidth(null);
138
+ else return;
139
+ e.preventDefault();
140
+ };
141
+ // One pointer owns a drag. Only its release keeps the width; a cancel, a lost capture, the drawer or an unmount
142
+ // puts back the width shown before it and keeps nothing — and no event of it can write after it ended. What is kept
143
+ // is where the pointer went (within the window), not what the room beside the rail let it show.
144
+ const onDown = (e) => {
145
+ if (e.button !== 0 || activeDrag || narrow()) return;
146
+ e.preventDefault();
147
+ const handle = e.currentTarget, pointerId = e.pointerId;
148
+ const before = sidebarWidthStore.get().width;
149
+ let chosen = null; // the pointer's own width this drag; null: it never moved
150
+ const left = handle.parentElement ? handle.parentElement.getBoundingClientRect().left : 0;
151
+ const mine = {};
152
+ const own = (ev) => activeDrag === mine && (ev.pointerId === undefined || ev.pointerId === pointerId);
153
+ const move = (ev) => { if (own(ev)) { chosen = ev.clientX - left; showSidebarWidth(chosen); } };
154
+ const up = (ev) => { if (own(ev)) mine.end(true); };
155
+ const cancel = (ev) => { if (own(ev)) mine.end(false); };
156
+ mine.end = (commit) => {
157
+ if (activeDrag !== mine) return;
158
+ activeDrag = null;
159
+ handle.removeEventListener("pointermove", move);
160
+ handle.removeEventListener("pointerup", up);
161
+ handle.removeEventListener("pointercancel", cancel);
162
+ handle.removeEventListener("lostpointercapture", cancel);
163
+ handle.classList.remove("dragging");
164
+ document.body.classList.remove("side-resizing");
165
+ try { handle.releasePointerCapture(pointerId); } catch { /* not held */ }
166
+ if (commit && chosen != null) setSidebarWidth(chosen, true);
167
+ else showSidebarWidth(before);
168
+ };
169
+ activeDrag = mine;
170
+ try { handle.setPointerCapture(pointerId); } catch { /* the move still works while over the handle */ }
171
+ handle.classList.add("dragging");
172
+ document.body.classList.add("side-resizing");
173
+ handle.addEventListener("pointermove", move);
174
+ handle.addEventListener("pointerup", up);
175
+ handle.addEventListener("pointercancel", cancel);
176
+ handle.addEventListener("lostpointercapture", cancel);
177
+ };
178
+ return html`<div class="side-resize" role="separator" aria-orientation="vertical" tabindex="0" aria-controls="sidebar"
179
+ aria-label=${t("app.resizeSidebar")} title=${t("app.resizeSidebarHint")} aria-valuemin=${String(SIDEBAR_W.min)}
180
+ aria-valuemax=${String(clampSidebarWidth(Infinity, mainBudget()))} aria-valuenow=${String(now)}
181
+ onKeyDown=${onKey} onPointerDown=${onDown} ondblclick=${() => setSidebarWidth(null)}></div>`;
182
+ }
183
+
46
184
  export function openDrawer() { shellStore.set({ drawer: true }); }
47
185
  export function closeDrawer() { if (shellStore.get().drawer) shellStore.set({ drawer: false }); }
48
186
  export function toggleSidebar() {
@@ -127,6 +265,7 @@ function Sidebar({ route, onNewInstance, viewOnly }) {
127
265
  <a class="side-row" href=${signInHref()}><${Icon} name="user" /><span>${t("app.signIn")}</span></a>
128
266
  <${PrefsPopover} />
129
267
  </div>
268
+ <${SidebarResize} />
130
269
  </aside>`;
131
270
  }
132
271
  // #1675: the whole fleet, on its Org chart (#1389), from anywhere — top left, so at the top of a phone's drawer too. The
@@ -152,6 +291,7 @@ function Sidebar({ route, onNewInstance, viewOnly }) {
152
291
  ${navLink("settings", settingsPath(), "settings", t("app.settings"))}
153
292
  <${SessionMenu} />
154
293
  </div>
294
+ <${SidebarResize} />
155
295
  </aside>`;
156
296
  }
157
297
 
@@ -167,6 +307,7 @@ function ViewOnlyNav({ route }) {
167
307
  export function DevicePrefs({ labels = false } = {}) {
168
308
  const [theme, setTheme] = useState(typeof AgendTheme !== "undefined" ? AgendTheme.get() : "system");
169
309
  const [l, setL] = useState(lang());
310
+ const names = useStore(namesStore); // #1722
170
311
  // #1665: what is in force, as it changes — another device's choice arriving while this is open is shown at once.
171
312
  useEffect(() => (typeof AgendTheme !== "undefined" && typeof AgendTheme.onChange === "function" ? AgendTheme.onChange(setTheme) : undefined), []);
172
313
  useEffect(() => onLang(setL), []);
@@ -177,6 +318,9 @@ export function DevicePrefs({ labels = false } = {}) {
177
318
  <label class="pref"><${Icon} name="globe" size=${16} /><span class=${labels ? "pref-label" : "sr-only"}>${t("app.language")}</span>
178
319
  <select value=${l} aria-label=${t("app.language")} onChange=${(e) => { setL(e.target.value); setLang(e.target.value); savePref("lang", e.target.value); }}>
179
320
  <option value="en">English</option><option value="zh-TW">中文</option></select></label>
321
+ <label class="pref"><${Icon} name="user" size=${16} /><span class=${labels ? "pref-label" : "sr-only"}>${t("app.names")}</span>
322
+ <select value=${names.primary} aria-label=${t("app.names")} onChange=${(e) => setNamesPrimary(e.target.value)}>
323
+ <option value="nickname">${t("app.namesNickname")}</option><option value="instance">${t("app.namesInstance")}</option></select></label>
180
324
  </div>`;
181
325
  }
182
326
 
@@ -38,7 +38,9 @@ export function applyStatus(d) {
38
38
  appStore.set({ ready: true, instances: d.instances, uptime: d.uptime ?? 0, exec, awaiting,
39
39
  connections: typeof d.connections === "number" ? d.connections : null,
40
40
  // #1665: the host preferences' revision (app-prefs.js re-reads them when it moves).
41
- prefsRev: typeof d.prefs_rev === "number" ? d.prefs_rev : null });
41
+ prefsRev: typeof d.prefs_rev === "number" ? d.prefs_rev : null,
42
+ // #1685: host memory pressure slowing or holding new agents ("elevated" | "critical"), else null.
43
+ hostMemory: d.host_memory === "elevated" || d.host_memory === "critical" ? d.host_memory : null });
42
44
  }
43
45
 
44
46
  /** One "activity" event: an instance started or stopped working. */
@@ -68,6 +68,7 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
68
68
 
69
69
  .sidebar {
70
70
  width: var(--sidebar-w); flex-shrink: 0; display: flex; flex-direction: column; background: var(--bg-sidebar);
71
+ position: relative; /* #1722: the resize handle sits on its right edge (the width above: tokens.css, or this device's) */
71
72
  padding: env(safe-area-inset-top) 0 env(safe-area-inset-bottom) env(safe-area-inset-left);
72
73
  }
73
74
  .sb-collapsed .sidebar { display: none; }
@@ -112,18 +113,23 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
112
113
  .av.ring { box-shadow: 0 0 0 2px var(--ring, transparent); }
113
114
  .av-text { overflow: hidden; }
114
115
  .av .av-dot { position: absolute; right: -2px; bottom: -2px; margin: 0; box-shadow: 0 0 0 2px var(--bg-sidebar); }
116
+ /* #1722: the CLI as a letter on the avatar's top-left corner — monochrome (the #1630 ring keeps the colour), cut out like the dot */
117
+ .av-cli { position: absolute; left: -4px; top: -4px; width: 14px; height: 14px; border-radius: 50%; box-sizing: border-box;
118
+ background: var(--surface); border: 1px solid var(--border-strong); box-shadow: 0 0 0 1.5px var(--bg-sidebar);
119
+ color: var(--text-2); font-size: 9px; line-height: 12px; font-weight: var(--fw-semibold); text-align: center; pointer-events: auto; }
120
+ .inst.active .av .av-cli, .inst:hover .av .av-cli { box-shadow: 0 0 0 1.5px var(--surface-2); }
115
121
  .inst.active .av .av-dot, .inst:hover .av .av-dot { box-shadow: 0 0 0 2px var(--surface-2); }
116
122
  .dot.hollow { background: transparent; border: 1.5px solid var(--text-3); }
117
123
  /* On a grey avatar a grey dot needs more contrast: stopped is a stronger grey, paused a clear ring around the sidebar colour. */
118
124
  .av .av-dot.off { background: var(--text-2); }
119
125
  .av .av-dot.hollow { box-sizing: border-box; width: 10px; height: 10px; background: var(--bg-sidebar); border: 2.5px solid var(--text-2); }
120
126
  .inst .av { margin-top: 1px; }
121
- .inst-id { font-family: var(--font-mono); }
122
127
  .inst-text { flex: 1; min-width: 0; display: flex; flex-direction: column; }
123
128
  .inst-name { font-size: var(--fs-sm); line-height: var(--lh-md); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
129
+ /* #1722: the other name, under the one that leads — smaller, muted, truncated on its own */
130
+ .inst-alt { font-size: var(--fs-xs); line-height: var(--lh-xs); color: var(--text-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
124
131
  .inst.active .inst-name { font-weight: var(--fw-semibold); }
125
132
  .inst-sub { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-1) var(--s-2); }
126
- .inst-alias { font-size: var(--fs-xs); line-height: var(--lh-xs); color: var(--text-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; max-width: 100%; }
127
133
  .badge-await {
128
134
  flex-shrink: 0; font-size: 11px; line-height: 16px; font-weight: var(--fw-semibold); padding: 0 var(--s-2); border-radius: var(--r-full);
129
135
  background: var(--warn-bg); color: var(--warn); border: 1px solid var(--warn-line); white-space: nowrap;
@@ -307,6 +313,16 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
307
313
  .pill.open { background: var(--accent-dim); color: var(--accent); }
308
314
  .pill.claimed { background: var(--warn-bg); color: var(--warn); }
309
315
  .pill.done { color: var(--ok); }
316
+ /* #1724: a disabled schedule — its own look (not the green of "done"): a dashed, muted badge and a dimmed row */
317
+ .pill.off { background: transparent; color: var(--text-3); border: 1px dashed var(--border-strong); }
318
+ .row-item.is-off { opacity: 0.62; }
319
+ .row-item.sched { flex-wrap: nowrap; }
320
+ .row-item.sched .sched-text { display: flex; flex-direction: column; flex: 1; min-width: 0; gap: 2px; }
321
+ .sched-actions { display: inline-flex; flex: none; gap: var(--s-1); }
322
+ .sched-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
323
+ .sched-sub { font-size: var(--fs-xs); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
324
+ @media (max-width: 899px) { .sched-sub { white-space: normal; } } /* a phone: the run time wraps rather than being cut */
325
+ .sched-mode { margin-bottom: var(--s-2); }
310
326
  .save-row { display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap; }
311
327
 
312
328
  /* ── Chat ── */
@@ -315,17 +331,21 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
315
331
  .msg { font-size: var(--fs-md); line-height: var(--lh-md); overflow-wrap: anywhere; min-width: 0; }
316
332
  .msg .meta { display: flex; align-items: baseline; gap: var(--s-2); font-size: var(--fs-xs); color: var(--text-3); margin-bottom: var(--s-1); }
317
333
  .msg .sender { font-weight: var(--fw-semibold); color: var(--text-2); }
318
- .msg .time { font-family: var(--font-mono); }
334
+ /* The time never wraps or gives up its width to a long neighbour: a flex child shrinks to its min-content, and with
335
+ the message's overflow-wrap: anywhere that is one character wide (the #1672 status line's time, one glyph per line). */
336
+ .msg .time { font-family: var(--font-mono); white-space: nowrap; flex: none; }
319
337
  .msg.user { align-self: flex-end; max-width: min(80%, 620px); }
320
338
  .msg.user .meta { justify-content: flex-end; }
321
339
  .msg.user .body { background: var(--user-bubble); border-radius: var(--r-xl); padding: 10px var(--s-4); }
322
340
  .msg.agent { align-self: stretch; }
323
- /* #1672: a status line (another instance's message, a daemon notice): muted plain text, no bubble; a long one folds. */
324
- .msg.status { align-self: stretch; font-size: var(--fs-sm); color: var(--text-3); }
325
- .msg.status .st-text { white-space: pre-wrap; border-left: 2px solid var(--border); padding-left: var(--s-3); }
326
- .msg.status.folded .st-text { max-height: 9em; overflow: hidden; mask-image: linear-gradient(to bottom, #000 60%, transparent); -webkit-mask-image: linear-gradient(to bottom, #000 60%, transparent); }
327
- .msg.status .st-more { border: 0; background: none; color: var(--text-2); cursor: pointer; font-size: var(--fs-xs); padding: var(--s-1) 0 0 var(--s-3); }
328
- .msg.status .st-more:hover { color: var(--text); }
341
+ /* #1672: a status line (another instance's message, a daemon notice): muted plain text, no bubble; a long one folds.
342
+ Its class is `status-line`, never `status`: that is the header's inline-flex badge class, and on a message it turned
343
+ the meta, body and tools into one flex row. */
344
+ .msg.status-line { align-self: stretch; font-size: var(--fs-sm); color: var(--text-3); }
345
+ .msg.status-line .st-text { white-space: pre-wrap; border-left: 2px solid var(--border); padding-left: var(--s-3); }
346
+ .msg.status-line.folded .st-text { max-height: 9em; overflow: hidden; mask-image: linear-gradient(to bottom, #000 60%, transparent); -webkit-mask-image: linear-gradient(to bottom, #000 60%, transparent); }
347
+ .msg.status-line .st-more { border: 0; background: none; color: var(--text-2); cursor: pointer; font-size: var(--fs-xs); padding: var(--s-1) 0 0 var(--s-3); }
348
+ .msg.status-line .st-more:hover { color: var(--text); }
329
349
  .msg-tools { display: flex; gap: var(--s-1); margin-top: var(--s-1); opacity: 0; transition: opacity var(--t-fast); }
330
350
  .msg:hover .msg-tools, .msg:focus-within .msg-tools { opacity: 1; }
331
351
  @media (hover: none) { .msg-tools { opacity: 1; } }
@@ -611,13 +631,16 @@ html.lb-open .scroller { overflow: hidden; }
611
631
  @media (hover: hover) { .inst-nav:not(.fav-editing) .v-li:hover .inst-star, .inst-nav:not(.fav-editing) .v-li:focus-within .inst-star { visibility: visible; } }
612
632
  @media (hover: none) { .inst-nav:not(.fav-editing) .inst-star { display: none; } }
613
633
  /* Edit: room for ↑ ↓ ☆ — the context and CLI marks step aside so the name keeps its width. */
614
- .fav-editing .v-li .v-meta, .fav-editing .v-li .cli-icon { display: none; }
634
+ .fav-editing .v-li .v-meta { display: none; }
615
635
  @media (max-width: 899px) { .fav-editing .inst-star, .fav-editing .inst-move { width: 36px; height: 36px; } }
616
636
  .v-inst .dot { margin-top: 0; }
617
637
  .v-meta { color: var(--text-3); font-size: var(--fs-xs); font-variant-numeric: tabular-nums; }
618
- .cli-icon { width: 10px; height: 10px; flex: 0 0 10px; border-radius: 50%; border: 1px solid var(--border-strong); }
619
- .cli-claude-code { background: #c678dd; } .cli-kiro-cli { background: #58a6ff; } .cli-codex { background: #3fb950; }
620
- .cli-grok { background: #0d1117; border-color: #f0f6fc; } .cli-antigravity { background: #f85149; } .cli-opencode { background: #f0f6fc; } .cli-other { background: #6e7681; }
638
+ /* #1722: the sidebar's width — a separator on its right edge (drag, arrows, Home/End, double-click resets); not on a phone */
639
+ .side-resize { position: absolute; top: 0; right: -3px; width: 6px; height: 100%; z-index: 5; cursor: col-resize; touch-action: none; }
640
+ .side-resize:hover, .side-resize:focus-visible, .side-resize.dragging { background: var(--accent-dim, var(--border-strong)); }
641
+ .side-resize:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
642
+ body.side-resizing, body.side-resizing * { cursor: col-resize !important; user-select: none; }
643
+ @media (max-width: 899px) { .side-resize { display: none; } }
621
644
  .dragging { opacity: 0.4; }
622
645
  .drop-before { box-shadow: inset 0 2px 0 var(--accent); }
623
646
  .drop-after { box-shadow: inset 0 -2px 0 var(--accent); }
@@ -628,15 +651,16 @@ html.lb-open .scroller { overflow: hidden; }
628
651
  .v-filter-box input::placeholder { color: var(--text-3); }
629
652
  .v-filter-clear { width: 28px; height: 28px; }
630
653
  .v-filter .note { padding: var(--s-1) var(--s-1) 0; font-variant-numeric: tabular-nums; }
631
- /* alpha.2 (N1): status and CLI chips under the filter box — each a <details> with checkboxes. */
654
+ /* alpha.2 (N1): status and CLI chips under the filter box — each a group of checkboxes under its header (#1700). */
632
655
  .nav-facets { display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--s-1) var(--s-2); padding: var(--s-2) var(--s-1) 0; }
633
656
  .nav-facet { font-size: var(--fs-xs); color: var(--text-2); }
634
- .nav-facet > summary { cursor: pointer; list-style: none; padding: 2px var(--s-2); border-radius: var(--r-sm); }
635
- .nav-facet > summary::-webkit-details-marker { display: none; }
636
- .nav-facet > summary::after { content: " ▾"; color: var(--text-3); }
637
- .nav-facet[open] > summary::after { content: " ▴"; }
638
- .nav-facet > summary:hover { background: var(--hover); }
639
- .nav-facet > summary:focus-visible { outline: 2px solid var(--accent); }
657
+ /* #1700: a facet group's header — the whole row toggles its list; folded with a filter set, a short summary. */
658
+ .nav-facet-head { display: inline-flex; align-items: center; gap: var(--s-1); max-width: 100%; padding: 2px var(--s-2); border: 0; border-radius: var(--r-sm);
659
+ background: transparent; color: inherit; font: inherit; cursor: pointer; text-align: left; }
660
+ .nav-facet-head:hover { background: var(--hover); }
661
+ .nav-facet-head:focus-visible { outline: 2px solid var(--accent); }
662
+ .nav-facet-head .caret { color: var(--text-3); flex: none; }
663
+ .nav-facet-sum { min-width: 0; max-width: 12em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--accent); }
640
664
  .nav-facet-n { display: inline-block; min-width: 16px; padding: 0 4px; border-radius: 8px; background: var(--accent); color: var(--on-accent, #fff); text-align: center; font-variant-numeric: tabular-nums; }
641
665
  .nav-facet-list { display: flex; flex-direction: column; gap: 2px; padding: var(--s-1) var(--s-2); }
642
666
  .nav-chip { display: flex; align-items: center; gap: var(--s-2); cursor: pointer; color: var(--text); }
@@ -989,3 +1013,6 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
989
1013
  }
990
1014
  /* #1630: a Discord server emoji in a chat message, the size of the text around it. */
991
1015
  .md img.emoji { display: inline-block; width: 1.375em; height: 1.375em; object-fit: contain; vertical-align: -0.3em; margin: 0 0.05em; }
1016
+ /* #1682: a mention, as Discord draws one — accent text on a tint of it; the id (or the Telegram handle) on hover. */
1017
+ .md .mention { display: inline; padding: 0 0.2em; border-radius: var(--r-sm); color: var(--accent); background: var(--accent-dim);
1018
+ font-weight: var(--fw-medium); white-space: nowrap; }