@songsid/agend 2.2.0-alpha.2 → 2.2.0-alpha.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 (239) hide show
  1. package/README.md +4 -4
  2. package/README.zh-TW.md +4 -4
  3. package/dist/agent-endpoint.d.ts +4 -1
  4. package/dist/agent-endpoint.js +14 -2
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/atomic-write.d.ts +21 -0
  7. package/dist/atomic-write.js +65 -0
  8. package/dist/atomic-write.js.map +1 -0
  9. package/dist/auth-api.js +2 -0
  10. package/dist/auth-api.js.map +1 -1
  11. package/dist/backend/codex.d.ts +7 -0
  12. package/dist/backend/codex.js +7 -0
  13. package/dist/backend/codex.js.map +1 -1
  14. package/dist/backend/muse.d.ts +7 -0
  15. package/dist/backend/muse.js +7 -0
  16. package/dist/backend/muse.js.map +1 -1
  17. package/dist/backend/types.d.ts +6 -0
  18. package/dist/backend/types.js.map +1 -1
  19. package/dist/binary-probe.d.ts +80 -0
  20. package/dist/binary-probe.js +182 -0
  21. package/dist/binary-probe.js.map +1 -0
  22. package/dist/cache-service.d.ts +17 -1
  23. package/dist/cache-service.js +39 -4
  24. package/dist/cache-service.js.map +1 -1
  25. package/dist/channel/adapters/discord.d.ts +5 -1
  26. package/dist/channel/adapters/discord.js +56 -11
  27. package/dist/channel/adapters/discord.js.map +1 -1
  28. package/dist/channel/adapters/telegram.d.ts +38 -1
  29. package/dist/channel/adapters/telegram.js +82 -0
  30. package/dist/channel/adapters/telegram.js.map +1 -1
  31. package/dist/channel/mcp-tools.js +1 -1
  32. package/dist/channel/mcp-tools.js.map +1 -1
  33. package/dist/channel/tool-router.d.ts +8 -1
  34. package/dist/channel/tool-router.js +4 -2
  35. package/dist/channel/tool-router.js.map +1 -1
  36. package/dist/channel/types.d.ts +21 -0
  37. package/dist/cli.js +133 -38
  38. package/dist/cli.js.map +1 -1
  39. package/dist/connection-secrets.d.ts +5 -0
  40. package/dist/connection-secrets.js.map +1 -1
  41. package/dist/cpu-profile.js +50 -0
  42. package/dist/cpu-profile.js.map +1 -1
  43. package/dist/daemon.d.ts +12 -0
  44. package/dist/daemon.js +166 -26
  45. package/dist/daemon.js.map +1 -1
  46. package/dist/delivery-outbox.d.ts +1 -0
  47. package/dist/delivery-outbox.js +12 -0
  48. package/dist/delivery-outbox.js.map +1 -1
  49. package/dist/discord-permissions.d.ts +33 -0
  50. package/dist/discord-permissions.js +42 -0
  51. package/dist/discord-permissions.js.map +1 -0
  52. package/dist/env-file.d.ts +11 -0
  53. package/dist/env-file.js +27 -0
  54. package/dist/env-file.js.map +1 -0
  55. package/dist/fleet-context.d.ts +5 -0
  56. package/dist/fleet-context.js.map +1 -1
  57. package/dist/fleet-level-config.d.ts +8 -1
  58. package/dist/fleet-level-config.js +31 -0
  59. package/dist/fleet-level-config.js.map +1 -1
  60. package/dist/fleet-manager.d.ts +76 -7
  61. package/dist/fleet-manager.js +599 -127
  62. package/dist/fleet-manager.js.map +1 -1
  63. package/dist/fleet-stop-budget.d.ts +22 -0
  64. package/dist/fleet-stop-budget.js +95 -0
  65. package/dist/fleet-stop-budget.js.map +1 -0
  66. package/dist/kiro-transcript-lane.d.ts +8 -4
  67. package/dist/kiro-transcript-lane.js +66 -44
  68. package/dist/kiro-transcript-lane.js.map +1 -1
  69. package/dist/locale.js +82 -0
  70. package/dist/locale.js.map +1 -1
  71. package/dist/next-steps.d.ts +5 -0
  72. package/dist/next-steps.js +26 -0
  73. package/dist/next-steps.js.map +1 -0
  74. package/dist/outbound-handlers.js +8 -0
  75. package/dist/outbound-handlers.js.map +1 -1
  76. package/dist/outbound-schemas.d.ts +4 -0
  77. package/dist/outbound-schemas.js +6 -1
  78. package/dist/outbound-schemas.js.map +1 -1
  79. package/dist/outbox-open-error.d.ts +7 -0
  80. package/dist/outbox-open-error.js +15 -0
  81. package/dist/outbox-open-error.js.map +1 -0
  82. package/dist/pane-context-cache.d.ts +25 -0
  83. package/dist/pane-context-cache.js +73 -0
  84. package/dist/pane-context-cache.js.map +1 -0
  85. package/dist/paths.d.ts +0 -7
  86. package/dist/paths.js +8 -2
  87. package/dist/paths.js.map +1 -1
  88. package/dist/provider-probe.d.ts +15 -0
  89. package/dist/provider-probe.js +20 -0
  90. package/dist/provider-probe.js.map +1 -1
  91. package/dist/public-link-progress.d.ts +73 -0
  92. package/dist/public-link-progress.js +190 -0
  93. package/dist/public-link-progress.js.map +1 -0
  94. package/dist/public-web-gateway.js +1 -1
  95. package/dist/public-web-gateway.js.map +1 -1
  96. package/dist/public-web-link.d.ts +6 -1
  97. package/dist/public-web-link.js +87 -15
  98. package/dist/public-web-link.js.map +1 -1
  99. package/dist/quickstart-api.d.ts +93 -20
  100. package/dist/quickstart-api.js +194 -62
  101. package/dist/quickstart-api.js.map +1 -1
  102. package/dist/quickstart.d.ts +15 -0
  103. package/dist/quickstart.js +100 -29
  104. package/dist/quickstart.js.map +1 -1
  105. package/dist/reply-buttons-controller.d.ts +98 -0
  106. package/dist/reply-buttons-controller.js +174 -0
  107. package/dist/reply-buttons-controller.js.map +1 -0
  108. package/dist/reply-buttons.d.ts +113 -0
  109. package/dist/reply-buttons.js +208 -0
  110. package/dist/reply-buttons.js.map +1 -0
  111. package/dist/reply-dedup.js +3 -1
  112. package/dist/reply-dedup.js.map +1 -1
  113. package/dist/restart-guard.d.ts +1 -0
  114. package/dist/restart-guard.js +8 -3
  115. package/dist/restart-guard.js.map +1 -1
  116. package/dist/service-activation.d.ts +15 -2
  117. package/dist/service-activation.js +81 -4
  118. package/dist/service-activation.js.map +1 -1
  119. package/dist/service-installer.js +19 -1
  120. package/dist/service-installer.js.map +1 -1
  121. package/dist/settings-api.js +53 -12
  122. package/dist/settings-api.js.map +1 -1
  123. package/dist/settings-authority.d.ts +11 -0
  124. package/dist/settings-authority.js +128 -0
  125. package/dist/settings-authority.js.map +1 -0
  126. package/dist/settings-change.d.ts +5 -0
  127. package/dist/settings-change.js +4 -3
  128. package/dist/settings-change.js.map +1 -1
  129. package/dist/settings-confirmation.d.ts +3 -0
  130. package/dist/settings-confirmation.js +7 -1
  131. package/dist/settings-confirmation.js.map +1 -1
  132. package/dist/settings-effect.js +19 -7
  133. package/dist/settings-effect.js.map +1 -1
  134. package/dist/settings-http-confirmation.js +1 -1
  135. package/dist/settings-http-confirmation.js.map +1 -1
  136. package/dist/settings-redaction.d.ts +18 -0
  137. package/dist/settings-redaction.js +58 -0
  138. package/dist/settings-redaction.js.map +1 -0
  139. package/dist/setup-form.d.ts +9 -1
  140. package/dist/setup-form.js +25 -1
  141. package/dist/setup-form.js.map +1 -1
  142. package/dist/setup-host.js +13 -3
  143. package/dist/setup-host.js.map +1 -1
  144. package/dist/setup-wizard.js +26 -19
  145. package/dist/setup-wizard.js.map +1 -1
  146. package/dist/systemd-runtime.d.ts +15 -0
  147. package/dist/systemd-runtime.js +41 -0
  148. package/dist/systemd-runtime.js.map +1 -0
  149. package/dist/tmux-control.d.ts +22 -0
  150. package/dist/tmux-control.js +82 -17
  151. package/dist/tmux-control.js.map +1 -1
  152. package/dist/token-env-name.d.ts +37 -0
  153. package/dist/token-env-name.js +93 -0
  154. package/dist/token-env-name.js.map +1 -0
  155. package/dist/tool-permissions.js +5 -3
  156. package/dist/tool-permissions.js.map +1 -1
  157. package/dist/topic-commands.d.ts +7 -16
  158. package/dist/topic-commands.js +45 -83
  159. package/dist/topic-commands.js.map +1 -1
  160. package/dist/transcript-monitor.js +3 -0
  161. package/dist/transcript-monitor.js.map +1 -1
  162. package/dist/transcript-sources.d.ts +68 -0
  163. package/dist/transcript-sources.js +336 -29
  164. package/dist/transcript-sources.js.map +1 -1
  165. package/dist/transcript-turns.d.ts +39 -0
  166. package/dist/transcript-turns.js +58 -0
  167. package/dist/transcript-turns.js.map +1 -0
  168. package/dist/tunnel/cloudflared-install.d.ts +18 -0
  169. package/dist/tunnel/cloudflared-install.js +15 -1
  170. package/dist/tunnel/cloudflared-install.js.map +1 -1
  171. package/dist/turn-reply-guard.d.ts +3 -0
  172. package/dist/turn-reply-guard.js.map +1 -1
  173. package/dist/ui/chat-render.js +32 -6
  174. package/dist/ui/chat-store.js +21 -1
  175. package/dist/ui/chat-strings.js +8 -0
  176. package/dist/ui/chat-thread.js +109 -52
  177. package/dist/ui/first-run.js +64 -0
  178. package/dist/ui/html-attachment.js +26 -0
  179. package/dist/ui/panel-chat.js +13 -5
  180. package/dist/ui/panel-fleet.js +5 -5
  181. package/dist/ui/panel-settings.js +18 -8
  182. package/dist/ui/preview-panel.js +38 -3
  183. package/dist/ui/settings-apply.js +3 -1
  184. package/dist/ui/settings-dialogs.js +95 -25
  185. package/dist/ui/settings-discord.js +130 -0
  186. package/dist/ui/settings-model.js +21 -0
  187. package/dist/ui/settings-strings.js +46 -6
  188. package/dist/ui/settings-token.js +54 -0
  189. package/dist/ui/settings-wizard.js +63 -29
  190. package/dist/ui/shared/app-shell.js +19 -33
  191. package/dist/ui/shared/app-store.js +5 -3
  192. package/dist/ui/shared/app-stream.js +2 -1
  193. package/dist/ui/shared/app.css +34 -2
  194. package/dist/ui/shared/app.js +2 -1
  195. package/dist/ui/shared/instance-nav.js +310 -0
  196. package/dist/ui/shared/panel-view.js +12 -190
  197. package/dist/ui/shared/view-roster-store.js +5 -0
  198. package/dist/ui/shared/view-strings.js +12 -0
  199. package/dist/update-dispatch.d.ts +1 -1
  200. package/dist/update-dispatch.js +1 -1
  201. package/dist/update-install.d.ts +15 -3
  202. package/dist/update-install.js +41 -13
  203. package/dist/update-install.js.map +1 -1
  204. package/dist/update-launch.d.ts +22 -0
  205. package/dist/update-launch.js +106 -0
  206. package/dist/update-launch.js.map +1 -0
  207. package/dist/update-marker.js +3 -8
  208. package/dist/update-marker.js.map +1 -1
  209. package/dist/usage/format-rich.js +7 -1
  210. package/dist/usage/format-rich.js.map +1 -1
  211. package/dist/usage/i18n-keys.d.ts +1 -1
  212. package/dist/usage/i18n-keys.js +1 -1
  213. package/dist/usage/i18n-keys.js.map +1 -1
  214. package/dist/usage/providers.d.ts +4 -0
  215. package/dist/usage/providers.js +2 -1
  216. package/dist/usage/providers.js.map +1 -1
  217. package/dist/usage/usage-api.js +48 -2
  218. package/dist/usage/usage-api.js.map +1 -1
  219. package/dist/view-api.d.ts +40 -0
  220. package/dist/view-api.js +28 -10
  221. package/dist/view-api.js.map +1 -1
  222. package/dist/web-api.d.ts +8 -0
  223. package/dist/web-api.js +50 -16
  224. package/dist/web-api.js.map +1 -1
  225. package/dist/web-auth.d.ts +8 -0
  226. package/dist/web-auth.js +34 -8
  227. package/dist/web-auth.js.map +1 -1
  228. package/dist/web-chat-history.d.ts +21 -0
  229. package/dist/web-chat-history.js +44 -0
  230. package/dist/web-chat-history.js.map +1 -1
  231. package/dist/web-host-guard.d.ts +18 -2
  232. package/dist/web-host-guard.js +30 -5
  233. package/dist/web-host-guard.js.map +1 -1
  234. package/dist/web-session.d.ts +7 -1
  235. package/dist/web-session.js +23 -2
  236. package/dist/web-session.js.map +1 -1
  237. package/launcher/postinstall.cjs +8 -4
  238. package/package.json +1 -1
  239. package/templates/systemd.service.ejs +3 -3
@@ -2,14 +2,16 @@
2
2
  // configuration and, when it finishes, writes the files (a write an admin may have to confirm, #1423) and hands over
3
3
  // to the ordinary Apply: the app's runner (settings-apply.js) posts the apply and watches its job, with the same
4
4
  // progress, deadline and restart recovery as every other change.
5
- import { html, useEffect, useState } from "/assets/app-html.js";
5
+ import { html, useEffect, useRef, useState } from "/assets/app-html.js";
6
6
  import { t } from "/assets/app-i18n.js";
7
7
  import { useLease } from "/assets/app-ctx.js";
8
8
  import { Dialog } from "/assets/ui-dialog.js";
9
9
  import { api } from "./settings-confirm.js";
10
10
  import { startOperation } from "./settings-apply.js";
11
11
  import { BACKENDS, DEFAULT_SCHEMA, impactOf, toYaml } from "./settings-model.js";
12
- import { Select } from "./settings-dialogs.js";
12
+ import { Drawer, Select } from "./settings-dialogs.js";
13
+ import { botHandle, TokenEnvNote, TokenField, verifyBotToken } from "./settings-token.js";
14
+ import { DiscordServerPicker } from "./settings-discord.js";
13
15
 
14
16
  const tn = (k, ...v) => t(`settings.${k}`, ...v);
15
17
  const STEPS = 4;
@@ -18,6 +20,10 @@ const post = (path, body) => api(path, { method: "POST", body: JSON.stringify(bo
18
20
  export function SetupWizard({ ctx, onClose }) {
19
21
  const lease = useLease("wizard");
20
22
  const [w, setW] = useState(null);
23
+ // The credential's revision (#1529 review): a token or platform change moves it, and a Verify, Detect or plan that
24
+ // began before is dropped when it lands — an answer about an old token never names, or plans for, a new one.
25
+ const rev = useRef(0);
26
+ const bump = () => { rev.current++; };
21
27
  const [busy, setBusy] = useState("");
22
28
  const [err, setErr] = useState("");
23
29
  useEffect(() => {
@@ -25,37 +31,51 @@ export function SetupWizard({ ctx, onClose }) {
25
31
  const env = await api("/api/settings/quickstart/environment").catch(() => null);
26
32
  if (!lease.current()) return;
27
33
  const e = (env && env.body) || { backends: [], channels: [], has_fleet: false };
28
- setW({ step: 1, env: e, backend: (e.backends || [])[0] || "claude-code", working_directory: "", instance_name: "agent-1",
29
- platform: "telegram", token: "", token_env: "AGEND_BOT_TOKEN", group_id: "", guild_id: "", general_channel_id: "", admin_user_id: "",
34
+ // #1519 P7: with agents already there, the bot is for one of them by default; a new agent gets a free name.
35
+ const agents = Array.isArray(e.agents) ? e.agents : [];
36
+ const names = new Set(agents.map((a) => a.name));
37
+ let n = 1; while (names.has(`agent-${n}`)) n++;
38
+ setW({ step: 1, env: e, backend: (e.backends || [])[0] || "claude-code", working_directory: "", instance_name: `agent-${n}`,
39
+ agent: agents.length ? "existing" : "new", existing_name: agents.length ? agents[0].name : "",
40
+ platform: "telegram", token: "", group_id: "", guild_id: "", general_channel_id: "", admin_user_id: "",
30
41
  identity: null, guilds: [], plan: null, offset: 0 });
31
42
  })();
32
43
  }, [lease]);
33
44
  if (!w) return html`<${Dialog} title=${tn("wizardTitle")} onClose=${onClose}><p class="note">…</p></${Dialog}>`;
34
- const set = (k) => (v) => setW((x) => ({ ...x, [k]: v }));
45
+ // Every field the plan or a probe is about moves the revision (#1529 review r2): a plan or Detect that began before an
46
+ // edit — target, admin, backend, directory, name — is dropped when it lands, never committed or written over the edit.
47
+ const set = (k) => (v) => { bump(); setW((x) => ({ ...x, [k]: v })); };
48
+ // No token env: the plan generates one (#1519 P1: AGEND_<PLATFORM>_<ID>_TOKEN, unique) and the commit carries the plan's.
49
+ // An existing agent (#1519 P7) is named and nothing else: the server keeps its directory and backend.
35
50
  const input = () => ({
36
- platform: w.platform, token_env: w.token_env, backend: w.backend, working_directory: w.working_directory, instance_name: w.instance_name,
51
+ platform: w.platform,
52
+ ...(w.agent === "existing" ? { instance_name: w.existing_name, existing_agent: true }
53
+ : { backend: w.backend, working_directory: w.working_directory, instance_name: w.instance_name }),
37
54
  group_id: w.group_id || undefined, guild_id: w.guild_id || undefined, general_channel_id: w.general_channel_id || undefined,
38
55
  admin_user_id: w.admin_user_id || undefined,
39
56
  });
40
57
 
41
58
  const verify = async () => {
59
+ const at = rev.current, token = w.token, platform = w.platform;
60
+ const live = () => lease.current() && rev.current === at;
42
61
  setBusy(tn("wizardVerifying")); setErr("");
43
- const res = await post("/api/settings/quickstart/probe", { action: "verify", platform: w.platform, token: w.token });
44
- if (!lease.current()) return;
45
- const identity = (res.body && res.body.identity) || { valid: false };
62
+ const identity = await verifyBotToken(platform, token);
63
+ if (!live()) { if (lease.current()) setBusy(""); return; }
46
64
  let guilds = [];
47
- if (identity.valid && w.platform === "discord") {
48
- const g = await post("/api/settings/quickstart/probe", { action: "guilds", token: w.token });
49
- if (!lease.current()) return;
65
+ if (identity.valid && platform === "discord") {
66
+ const g = await post("/api/settings/quickstart/probe", { action: "guilds", token });
67
+ if (!live()) { if (lease.current()) setBusy(""); return; }
50
68
  guilds = (g.body && g.body.guilds) || [];
51
69
  }
52
70
  setBusy("");
53
- setW((x) => ({ ...x, identity, guilds }));
71
+ setW((x) => (x.token === token && x.platform === platform ? { ...x, identity, guilds } : x));
54
72
  };
55
73
  const detect = async () => {
74
+ const at = rev.current;
56
75
  setBusy(tn("wizardWaitingStart")); setErr("");
57
76
  const res = await post("/api/settings/quickstart/probe", { action: "await-telegram-start", token: w.token, offset: w.offset || 0 });
58
77
  if (!lease.current()) return;
78
+ if (rev.current !== at) { setBusy(""); return; }
59
79
  setBusy("");
60
80
  if (!res.ok) { setErr((res.body && res.body.error) || tn("applyFailed")); return; }
61
81
  if (res.body.found) setW((x) => ({ ...x, offset: res.body.offset, group_id: String(res.body.found.groupId), admin_user_id: String(res.body.found.userId) }));
@@ -63,14 +83,19 @@ export function SetupWizard({ ctx, onClose }) {
63
83
  };
64
84
  const next = async () => {
65
85
  setErr("");
66
- if (w.step === 1) { if (!w.working_directory.startsWith("/")) { setErr(tn("wizardNeedDir")); return; } setW({ ...w, step: 2 }); return; }
86
+ if (w.step === 1) {
87
+ if (w.agent !== "existing" && !w.working_directory.startsWith("/")) { setErr(tn("wizardNeedDir")); return; }
88
+ setW({ ...w, step: 2 }); return;
89
+ }
67
90
  if (w.step === 2) { setW({ ...w, step: 3 }); return; }
68
91
  if (w.step === 3) {
69
92
  if (!w.identity || !w.identity.valid) { setErr(tn("wizardNeedVerify")); return; }
93
+ const at = rev.current;
70
94
  setBusy(tn("wizardPlanLoading"));
71
95
  const res = await post("/api/settings/quickstart/plan", input());
72
96
  if (!lease.current()) return;
73
97
  setBusy("");
98
+ if (rev.current !== at) return;
74
99
  if (!res.ok) { setErr((res.body && res.body.error) || tn("applyFailed")); return; }
75
100
  setW((x) => ({ ...x, plan: res.body, step: 4 }));
76
101
  return;
@@ -84,7 +109,9 @@ export function SetupWizard({ ctx, onClose }) {
84
109
  * commit's body until that write completes, then drops it.
85
110
  */
86
111
  const finish = () => {
87
- const body = { ...input(), token: w.token };
112
+ // The plan's target, all of it (#1529 review): its connection id and its generated token env — the commit refuses
113
+ // either if it was taken since, instead of quietly picking another.
114
+ const body = { ...input(), channel_id: w.plan.channel_id, token_env: w.plan.token_env, token_env_generated: true, token: w.token };
88
115
  // It writes the connection and the first agent: it costs what a connection change does (the server's schema).
89
116
  const handed = startOperation([{ label: tn("wizardTitle"), impact: impactOf((ctx && ctx.schema) || DEFAULT_SCHEMA, "fleet.channels"),
90
117
  request: { method: "POST", url: "/api/settings/quickstart/commit", body, sensitive: true } }]);
@@ -94,28 +121,31 @@ export function SetupWizard({ ctx, onClose }) {
94
121
  };
95
122
 
96
123
  let body;
124
+ const agents = Array.isArray(w.env.agents) ? w.env.agents : [];
97
125
  if (w.step === 1) {
98
- body = html`<div class="field"><label for="wz-be">${tn("wizardBackend")}</label>
126
+ const which = agents.length ? html`<div class="seg-inline" role="group" aria-label=${tn("wizardAgentWhich")}>
127
+ ${["existing", "new"].map((k) => html`<button key=${k} type="button" class=${`btn${w.agent === k ? " btn-primary" : ""}`} aria-pressed=${w.agent === k ? "true" : "false"}
128
+ onClick=${() => set("agent")(k)}>${k === "existing" ? tn("wizardAgentExisting") : tn("wizardAgentNew")}</button>`)}</div>` : null;
129
+ body = w.agent === "existing" ? html`${which}
130
+ <div class="field"><label for="wz-agent">${tn("wizardAgentPick")}</label>
131
+ <${Select} id="wz-agent" value=${w.existing_name} onChange=${set("existing_name")} options=${agents.map((a) => a.name)} />
132
+ <p class="note">${tn("wizardAgentExistingNote")}</p></div>` : html`${which}<div class="field"><label for="wz-be">${tn("wizardBackend")}</label>
99
133
  <${Select} id="wz-be" value=${w.backend} onChange=${set("backend")} options=${w.env.backends && w.env.backends.length ? w.env.backends : BACKENDS} />
100
134
  <p class="note">${w.env.backends && w.env.backends.length ? tn("wizardBackendFound", w.env.backends.join(", ")) : tn("wizardBackendNone")}</p></div>
101
135
  <div class="field"><label for="wz-wd">${tn("workingDir")}</label><input id="wz-wd" type="text" placeholder="/home/you/projects/app" value=${w.working_directory} onInput=${(e) => set("working_directory")(e.target.value.trim())} /></div>
102
- <div class="field"><label for="wz-name">${tn("wizardAgentName")}</label><input id="wz-name" type="text" value=${w.instance_name} onInput=${(e) => set("instance_name")(e.target.value.trim())} /></div>`;
136
+ <div class="field"><label for="wz-name">${agents.length ? tn("wizardAgentNewName") : tn("wizardAgentName")}</label><input id="wz-name" type="text" value=${w.instance_name} onInput=${(e) => set("instance_name")(e.target.value.trim())} /></div>`;
103
137
  } else if (w.step === 2) {
104
138
  body = html`<div class="seg-inline" role="group" aria-label=${tn("wizardPlatform")}>
105
139
  ${["telegram", "discord"].map((p) => html`<button key=${p} type="button" class=${`btn${w.platform === p ? " btn-primary" : ""}`} aria-pressed=${w.platform === p ? "true" : "false"}
106
- onClick=${() => setW({ ...w, platform: p, identity: null })}>${p === "telegram" ? "Telegram" : "Discord"}</button>`)}</div>
140
+ onClick=${() => { bump(); setW((x) => ({ ...x, platform: p, identity: null })); }}>${p === "telegram" ? "Telegram" : "Discord"}</button>`)}</div>
107
141
  <p class="note">${w.platform === "telegram" ? tn("wizardTelegramHint") : tn("wizardDiscordHint")}</p>`;
108
142
  } else if (w.step === 3) {
109
- body = html`<div class="field"><label for="wz-token">${tn("wizardToken")}</label>
110
- <input id="wz-token" type="password" autocomplete="new-password" placeholder="123456:ABC-DEF…" value=${w.token} onInput=${(e) => setW({ ...w, token: e.target.value.trim(), identity: null })} />
111
- <p class="note">${w.platform === "telegram" ? tn("wizardTokenTelegram") : tn("wizardTokenDiscord")}</p></div>
112
- <div class="field"><label for="wz-env">${tn("tokenEnv")}</label><input id="wz-env" type="text" value=${w.token_env} onInput=${(e) => set("token_env")(e.target.value.trim())} /></div>
113
- <div class="dlg-inline-actions"><button type="button" class="btn btn-sm" disabled=${!!busy} onClick=${verify}>${tn("wizardVerify")}</button>
114
- ${w.identity ? html`<span class=${`feedback${w.identity.valid ? "" : " error"}`} role="status">${w.identity.valid ? (w.identity.username || tn("verified")) : (w.identity.reason || tn("verifyFailed"))}</span>` : null}</div>
143
+ body = html`<${TokenField} id="wz-token" platform=${w.platform} value=${w.token} identity=${w.identity} busy=${!!busy}
144
+ onInput=${(v) => { bump(); setW((x) => ({ ...x, token: v, identity: null })); }} onVerify=${verify} />
115
145
  ${w.platform === "discord" ? html`
116
- <div class="field"><label for="wz-guild">${tn("guildIdField")}</label><${Select} id="wz-guild" value=${w.guild_id} onChange=${set("guild_id")}
117
- options=${["", ...w.guilds.map((g) => ({ value: g.id, label: `${g.name} (${g.id})` }))]} /></div>
118
- <div class="field"><label for="wz-gen">${tn("wizardGeneralChannel")}</label><input id="wz-gen" type="text" value=${w.general_channel_id} onInput=${(e) => set("general_channel_id")(e.target.value.trim())} /></div>
146
+ <${DiscordServerPicker} idPrefix="wz" token=${w.identity && w.identity.valid ? w.token : ""} bot=${w.identity && botHandle(w.identity.username)}
147
+ invite=${w.identity && w.identity.invite} portal=${w.identity && w.identity.portal} guilds=${w.guilds} onGuilds=${(g) => setW((x) => ({ ...x, guilds: g }))}
148
+ guild=${w.guild_id} onGuild=${set("guild_id")} channel=${w.general_channel_id} onChannel=${set("general_channel_id")} />
119
149
  <div class="field"><label for="wz-user">${tn("wizardAdminUser")}</label><input id="wz-user" type="text" placeholder=${tn("discordUserPlaceholder")} value=${w.admin_user_id} onInput=${(e) => set("admin_user_id")(e.target.value.trim())} /></div>`
120
150
  : html`
121
151
  <div class="field"><label for="wz-group">${tn("groupIdField")}</label><input id="wz-group" type="text" placeholder="-1001234567890" value=${w.group_id} onInput=${(e) => set("group_id")(e.target.value.trim())} />
@@ -125,8 +155,12 @@ export function SetupWizard({ ctx, onClose }) {
125
155
  } else {
126
156
  body = !w.plan ? html`<p class="note">${tn("wizardPlanLoading")}</p>` : html`
127
157
  <p><strong>${tn("wizardWillWrite")}</strong></p>
128
- <pre class="s-yaml">${toYaml({ channels: [w.plan.channel], instances: { [w.plan.instance.name]: { working_directory: w.plan.instance.working_directory, backend: w.plan.instance.backend } } })}</pre>
129
- <p class="note">${tn("wizardEnvKeys", (w.plan.env_keys || []).join(", "))}</p>
158
+ <pre class="s-yaml">${toYaml({ channels: [w.plan.channel], instances: { [w.plan.instance.name]: { working_directory: w.plan.instance.working_directory, backend: w.plan.instance.backend, channel_id: w.plan.instance.channel_id,
159
+ ...(w.plan.instance.topic && w.plan.instance.topic.to != null ? { topic_id: w.plan.instance.topic.to } : {}) } } })}</pre>
160
+ ${w.plan.instance && w.plan.instance.existing ? html`<p class="note">${tn("wizardAgentExistingNote")}</p>` : null}
161
+ ${w.plan.instance && w.plan.instance.topic ? html`<p class="feedback warning">${w.plan.instance.topic.to == null
162
+ ? tn("wizardTopicNotCarried", w.plan.instance.topic.from) : tn("wizardTopicGeneral")}</p>` : null}
163
+ <${Drawer} title=${tn("advancedSection")}><${TokenEnvNote} name=${w.plan.token_env} /></${Drawer}>
130
164
  ${(w.plan.warnings || []).map((x, i) => html`<p key=${i} class="feedback warning">${x}</p>`)}`;
131
165
  }
132
166
  return html`<${Dialog} title=${tn("wizardTitle")} onClose=${onClose} wide=${true} busy=${busy === tn("wizardCommitting")}
@@ -1,7 +1,7 @@
1
1
  // #1408 §2/§7: the app's frame — the sidebar (a drawer on a phone), the main area with its panel, the bottom tabs on a
2
2
  // phone, the connection line, toasts. Panels render their own header through <PanelHeader>, so every panel's title,
3
3
  // status and ⋯ actions sit in the same place.
4
- import { html, useEffect, useState } from "./app-html.js";
4
+ import { html, useEffect, useLayoutEffect, useRef, useState } from "./app-html.js";
5
5
  import { Icon } from "./ui-icons.js";
6
6
  import { Toasts } from "./ui-toast.js";
7
7
  import { ConfirmHost } from "./ui-confirm.js";
@@ -11,6 +11,7 @@ import { navStore } from "./app-nav.js";
11
11
  import { chatPath, viewPath, settingsPath, routeKey, NEEDS_PATH } from "./app-route.js";
12
12
  import { SessionMenu } from "./app-session.js";
13
13
  import { ErrorState, Skeleton } from "./ui-states.js";
14
+ import { InstanceNav, RosterNav } from "./instance-nav.js";
14
15
 
15
16
  /** Shell state shared with panels: the phone drawer, the collapsed sidebar, extensions a panel module adds. */
16
17
  export const shellStore = createStore({ drawer: false, collapsed: readCollapsed(), footer: [], keyboard: false, dialog: null, side: null });
@@ -25,6 +26,14 @@ export function setSideSection(Component) {
25
26
  return () => { if (shellStore.get().side === entry) shellStore.set({ side: null }); };
26
27
  }
27
28
 
29
+ // The sidebar's instance list (alpha.2, N1): one component on every page. keepActiveInView lives with it.
30
+ export { keepActiveInView } from "./instance-nav.js";
31
+
32
+ /** #1519 P7: "New agent" from anywhere (the sidebar's +, an empty chat): the app registers what opens it. */
33
+ let newInstanceOpener = null;
34
+ export function setNewInstanceOpener(fn) { newInstanceOpener = fn; }
35
+ export function requestNewInstance() { if (newInstanceOpener) newInstanceOpener(); }
36
+
28
37
  /** An app-level dialog (New instance): rendered by the shell until closed, whatever panel is showing. */
29
38
  export function showDialog(Component, props = {}) { shellStore.set({ dialog: { Component, props } }); }
30
39
  export function closeDialog() { shellStore.set({ dialog: null }); }
@@ -90,29 +99,6 @@ export function statusClass(i, exec, awaiting) {
90
99
  }
91
100
  export { statusLabel };
92
101
 
93
- function tooltip(i) {
94
- const src = (s) => (s === "instance" || s === "classic" ? "configured" : s === "fleet-default" ? "fleet default" : s);
95
- const model = i.model ? (i.model_source === "live" || i.model_source === "cli-default" || i.model_source === "unresolved" ? i.model : `${i.model} (${src(i.model_source)})`) : null;
96
- const effort = i.effort ? `effort:${i.effort}${i.effort_source ? ` (${src(i.effort_source)})` : ""}` : null;
97
- const ctx = i.context_pct != null ? `ctx:${Math.round(i.context_pct)}%` : null;
98
- const cost = i.cost > 0 ? `$${i.cost.toFixed(2)}` : null;
99
- return [i.name, i.backend, model, effort, ctx, cost, i.status].filter(Boolean).join(" · ");
100
- }
101
-
102
- function InstanceRow({ i, active, exec, awaiting }) {
103
- const alias = typeof i.display_name === "string" && i.display_name.trim() && i.display_name.trim() !== i.name ? i.display_name.trim() : "";
104
- const cls = statusClass(i, exec, awaiting);
105
- // Two lines: the name (never shortened to make room for a badge, #1408 rough edge 6), then the alias and/or
106
- // "needs you". Everything else is in the tooltip.
107
- return html`<li><a class=${`inst${active ? " active" : ""}`} href=${chatPath(i.name)} title=${tooltip(i)} aria-current=${active ? "page" : undefined}
108
- onClick=${closeDrawer}>
109
- <span class=${`dot ${cls}`} aria-hidden="true"></span>
110
- <span class="inst-text"><span class="inst-name">${i.name}</span>
111
- ${alias || awaiting != null ? html`<span class="inst-sub">${alias ? html`<span class="inst-alias">${alias}</span>` : null}
112
- ${awaiting != null ? html`<span class="badge-await" title=${awaiting || t("app.approxNote")}>${t("app.needsYou")}</span>` : null}</span>` : null}
113
- <span class="sr-only">${statusLabel(i, exec, awaiting)}</span></span></a></li>`;
114
- }
115
-
116
102
  /** Sign in, and come back here afterwards (the sign-in page accepts only the app's own pages). */
117
103
  export function signInHref() {
118
104
  const here = typeof location !== "undefined" ? location.pathname + location.search : "/view";
@@ -122,7 +108,6 @@ export function signInHref() {
122
108
  function Sidebar({ route, onNewInstance, viewOnly }) {
123
109
  const app = useStore(appStore);
124
110
  const shell = useStore(shellStore);
125
- const current = route && route.panel === "chat" ? route.instance : null;
126
111
  const on = (panel) => !!route && route.panel === panel;
127
112
  // A count badge is hidden at 0; its number is also in the link's accessible name.
128
113
  const navLink = (panel, href, icon, label, count = 0) => html`<a class=${`side-row${on(panel) ? " active" : ""}`} href=${href}
@@ -137,7 +122,7 @@ function Sidebar({ route, onNewInstance, viewOnly }) {
137
122
  <button type="button" class="icon-btn side-collapse" onClick=${toggleSidebar} aria-label=${narrow() ? t("app.closeMenu") : t("app.collapse")} title=${narrow() ? t("app.closeMenu") : t("app.collapse")} aria-controls="sidebar"><${Icon} name="sidebar" /></button>
138
123
  </div>
139
124
  <nav class="side-nav" aria-label=${t("app.menu")}>${navLink("view", "/view", "view", t("app.view"))}</nav>
140
- ${shell.side ? html`<${shell.side.Component} />` : html`<div class="side-section"></div>`}
125
+ ${shell.side ? html`<${shell.side.Component} />` : html`<${ViewOnlyNav} route=${route} />`}
141
126
  <div class="side-foot">
142
127
  <${Prefs} />
143
128
  <a class="side-row" href=${signInHref()}><${Icon} name="user" /><span>${t("app.signIn")}</span></a>
@@ -148,19 +133,15 @@ function Sidebar({ route, onNewInstance, viewOnly }) {
148
133
  <div class="side-head">
149
134
  <a class="brand" href="/ui" onClick=${closeDrawer}>${t("app.brand")}</a>
150
135
  <button type="button" class="icon-btn side-collapse" onClick=${toggleSidebar} aria-label=${narrow() ? t("app.closeMenu") : t("app.collapse")} title=${narrow() ? t("app.closeMenu") : t("app.collapse")} aria-controls="sidebar"><${Icon} name="sidebar" /></button>
151
- <button type="button" class="icon-btn" onClick=${onNewInstance} aria-label=${t("app.newInstance")} title=${t("app.newInstance")}><${Icon} name="edit" /></button>
136
+ <button type="button" class="icon-btn" onClick=${onNewInstance} aria-label=${t("app.newInstance")} title=${t("app.newInstance")}><${Icon} name="plus" /></button>
152
137
  </div>
153
138
  <nav class="side-nav" aria-label=${t("app.menu")}>
154
139
  ${navLink("needs", NEEDS_PATH, "inbox", t("app.needsNav"), needsCount)}
155
140
  ${navLink("fleet", "/ui/fleet", "fleet", t("app.fleet"))}
156
141
  ${navLink("view", "/view", "view", t("app.view"))}
157
142
  </nav>
158
- ${shell.side ? html`<${shell.side.Component} />` : html`<div class="side-section" id="instanceList">
159
- <h2 class="side-label">${t("app.instances")}</h2>
160
- ${app.instances.length ? html`<ul class="inst-list">${app.instances.map(i => html`<${InstanceRow} key=${i.name} i=${i} active=${i.name === current}
161
- exec=${app.exec[i.name]} awaiting=${Object.prototype.hasOwnProperty.call(app.awaiting, i.name) ? app.awaiting[i.name] : null} />`)}</ul>`
162
- : html`<p class="side-empty">${t("app.noInstances")}</p>`}
163
- </div>`}
143
+ ${shell.side ? html`<${shell.side.Component} />` : html`<${InstanceNav} route=${route} items=${app.instances} loaded=${app.ready !== false}
144
+ exec=${app.exec} awaiting=${app.awaiting} onPick=${closeDrawer} />`}
164
145
  <div class="side-foot">
165
146
  ${navLink("settings", settingsPath(), "settings", t("app.settings"))}
166
147
  ${shell.footer.map(({ key, Component }) => html`<${Component} key=${key} />`)}
@@ -170,6 +151,11 @@ function Sidebar({ route, onNewInstance, viewOnly }) {
170
151
  </aside>`;
171
152
  }
172
153
 
154
+ /** The anonymous View reader's list: View's roster read (no status frames without a session), the same component. */
155
+ function ViewOnlyNav({ route }) {
156
+ return html`<${RosterNav} route=${route} viewOnly=${true} onPick=${closeDrawer} />`;
157
+ }
158
+
173
159
  function Prefs() {
174
160
  const [theme, setTheme] = useState(typeof AgendTheme !== "undefined" ? AgendTheme.get() : "system");
175
161
  const [l, setL] = useState(lang());
@@ -21,8 +21,9 @@ export function createStore(initial) {
21
21
  /** The app's store. `ready`: a status frame has arrived (before it, an unknown instance may just not be listed yet).
22
22
  * `exec`: each instance's raw execution state (working / idle / stuck / null); `awaiting`: what it
23
23
  * waits on at its terminal (text, "" when unknown), absent when it waits on nothing. `hydration`: a late catch-up
24
- * (app-stream.js catchUp) — "catching" / "retrying" / "failed" / "ok"; `retryHydration` asks for it again. */
25
- export const appStore = createStore({ ready: false, instances: [], uptime: 0, exec: {}, awaiting: {}, connection: "connecting", needs: [],
24
+ * (app-stream.js catchUp) — "catching" / "retrying" / "failed" / "ok"; `retryHydration` asks for it again.
25
+ * `connections`: how many chat connections the fleet has (#1519 P7: 0 is web-only — the first-run card); null until known. */
26
+ export const appStore = createStore({ ready: false, instances: [], uptime: 0, exec: {}, awaiting: {}, connection: "connecting", needs: [], connections: null,
26
27
  hydration: "none", retryHydration: null });
27
28
 
28
29
  /** Fold one status frame (stream or poll) into the store. */
@@ -34,7 +35,8 @@ export function applyStatus(d) {
34
35
  exec[i.name] = (i.execution_state !== undefined ? i.execution_state : i.state) ?? null;
35
36
  if (i.state === "awaiting_input") awaiting[i.name] = typeof i.interaction_summary === "string" ? i.interaction_summary : "";
36
37
  }
37
- appStore.set({ ready: true, instances: d.instances, uptime: d.uptime ?? 0, exec, awaiting });
38
+ appStore.set({ ready: true, instances: d.instances, uptime: d.uptime ?? 0, exec, awaiting,
39
+ connections: typeof d.connections === "number" ? d.connections : null });
38
40
  }
39
41
 
40
42
  /** One "activity" event: an instance started or stopped working. */
@@ -15,7 +15,7 @@
15
15
  // older snapshot. A failed read is retried a few times with a growing delay, and then waits for the person (Retry);
16
16
  // the state is told as "hydration" events. Polls never overlap: a poll asked for while one runs follows it.
17
17
 
18
- const EVENTS = ["status", "message", "delivery", "deliveries", "prompt", "prompts", "prompt_resolved", "activity", "needs"];
18
+ const EVENTS = ["status", "message", "delivery", "deliveries", "prompt", "prompts", "prompt_resolved", "activity", "needs", "reply_buttons"];
19
19
  const MAX_HELD = 1000; // live events held during one catch-up read; more → read again afterwards
20
20
  const RETRY_DELAYS = [1000, 2000, 4000]; // after the first failed catch-up read; then it waits for Retry
21
21
  const POLL_LIMIT_MS = 10000; // a poll that has not answered by then counts as failed
@@ -83,6 +83,7 @@ export function createStream(opts) {
83
83
  if (typeof d.cursor === "string") state.cursor = d.cursor;
84
84
  if (Array.isArray(d.deliveries)) emit("deliveries", d.deliveries);
85
85
  if (Array.isArray(d.prompts)) emit("prompts", d.prompts);
86
+ if (Array.isArray(d.reply_buttons)) for (const u of d.reply_buttons) emit("reply_buttons", u); // #1266
86
87
  if (Array.isArray(d.needs)) emit("needs", { items: d.needs });
87
88
  return true;
88
89
  } catch { return false; /* the next tick, or the catch-up's retry, tries again */ }
@@ -54,7 +54,11 @@ a { color: inherit; text-decoration: none; }
54
54
  .link:hover { text-decoration: underline; }
55
55
 
56
56
  /* ── The frame ── */
57
- .shell { display: flex; height: 100vh; height: 100dvh; overflow: hidden; }
57
+ /* The page itself never scrolls: only its panes do. The shell is the containing block of anything absolutely positioned
58
+ inside it (an .sr-only label), so its overflow: hidden clips it — an absolute element whose containing block is the
59
+ viewport escapes every scroll container and made the whole page scroll (alpha.2: two scrollbars on /ui/chat). */
60
+ .shell { display: flex; height: 100vh; height: 100dvh; overflow: hidden; position: relative; }
61
+ html:has(.shell), html:has(.shell) body { overflow: hidden; }
58
62
  .skip { position: absolute; left: var(--s-2); top: -60px; z-index: 100; background: var(--accent-fill); color: var(--on-accent); padding: var(--s-2) var(--s-3); border-radius: var(--r-sm); }
59
63
  .skip:focus { top: var(--s-2); }
60
64
 
@@ -76,7 +80,7 @@ a { color: inherit; text-decoration: none; }
76
80
  .side-row .meta { color: var(--text-3); font-size: var(--fs-xs); max-width: 40%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
77
81
  .side-row.toggle { cursor: pointer; color: var(--text-2); }
78
82
  .side-row.toggle input { margin: 0; accent-color: var(--accent); }
79
- .side-section { flex: 1; min-height: 0; overflow-y: auto; padding: var(--s-2) var(--s-2); }
83
+ .side-section { flex: 1; min-height: 0; overflow-y: auto; padding: var(--s-2) var(--s-2); position: relative; } /* its .sr-only labels scroll (and clip) with it */
80
84
  /* The sidebar's list scroller: thin and in the theme's colours, not the UA's light bar with arrows. WebKit/Blink take
81
85
  the pseudo-elements; Chromium ignores them once scrollbar-color is set, so the standard properties are Firefox-only. */
82
86
  .side-section::-webkit-scrollbar { width: 10px; }
@@ -159,6 +163,10 @@ a { color: inherit; text-decoration: none; }
159
163
  .empty-title { color: var(--text); font-size: var(--fs-lg); font-weight: var(--fw-semibold); }
160
164
  .empty-hint { font-size: var(--fs-sm); }
161
165
  .empty .btn { margin-top: var(--s-3); }
166
+ /* #1519 P7: the first-run card (a fleet with no chat connection), above an empty chat or a Settings list. */
167
+ .first-run-stack { width: 100%; max-width: var(--col); }
168
+ .first-run h3 { display: flex; align-items: center; gap: var(--s-2); }
169
+ .first-run p { color: var(--text-2); font-size: var(--fs-sm); }
162
170
  .error-state { display: flex; flex-direction: column; align-items: center; gap: var(--s-3); padding: var(--s-8) var(--s-4); color: var(--danger); text-align: center; }
163
171
  .error-state p { color: var(--text); }
164
172
  .skeleton { display: flex; flex-direction: column; gap: var(--s-3); padding: var(--s-6) var(--s-5); max-width: var(--col); margin: 0 auto; width: 100%; opacity: 0; animation: appear 0s linear 300ms forwards; }
@@ -319,6 +327,7 @@ a { color: inherit; text-decoration: none; }
319
327
  .pv-newer { display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; padding: var(--s-2) var(--s-3); border-radius: var(--r-sm);
320
328
  background: var(--surface); font-size: var(--fs-xs); }
321
329
  .pv-newer > span { flex: 1; min-width: 0; }
330
+ .pv-newer > .pv-newer-note { flex-basis: 100%; color: var(--text-3); }
322
331
  .pv-panel-body { flex: 1; min-height: 0; display: flex; flex-direction: column; padding: 0 var(--s-3) var(--s-3); }
323
332
  .pv-panel-body .pv-holder { flex: 1; min-height: 0; margin: 0; display: flex; flex-direction: column; }
324
333
  .pv-panel-body.show-code .pv-holder { display: none; }
@@ -337,6 +346,12 @@ a { color: inherit; text-decoration: none; }
337
346
  @container (max-width: 560px) {
338
347
  .pv-panel-bar :not(.pv-panel-run) > .lbl { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
339
348
  }
349
+ /* #1266: an agent reply's buttons — choices under the text; one answers for everyone. */
350
+ .rb-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); margin-top: var(--s-2); }
351
+ .rb-btn { max-width: 100%; white-space: normal; text-align: left; }
352
+ .rb-btn.chosen, .rb-btn.chosen:disabled { border-color: var(--ok); color: var(--text); background: color-mix(in srgb, var(--ok) 14%, transparent); font-weight: var(--fw-semibold); opacity: 1; }
353
+ .rb-btn:disabled:not(.chosen) { opacity: 0.55; }
354
+ .rb-note { font-size: var(--fs-xs); color: var(--text-3); }
340
355
  .pv-menu { position: relative; }
341
356
  .pv-menu > summary { list-style: none; cursor: pointer; padding: 2px var(--s-2); border-radius: var(--r-sm); color: var(--text-3); }
342
357
  .pv-menu > summary::-webkit-details-marker { display: none; }
@@ -484,6 +499,19 @@ a { color: inherit; text-decoration: none; }
484
499
  .v-filter-box input::placeholder { color: var(--text-3); }
485
500
  .v-filter-clear { width: 28px; height: 28px; }
486
501
  .v-filter .note { padding: var(--s-1) var(--s-1) 0; font-variant-numeric: tabular-nums; }
502
+ /* alpha.2 (N1): status and CLI chips under the filter box — each a <details> with checkboxes. */
503
+ .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; }
504
+ .nav-facet { font-size: var(--fs-xs); color: var(--text-2); }
505
+ .nav-facet > summary { cursor: pointer; list-style: none; padding: 2px var(--s-2); border-radius: var(--r-sm); }
506
+ .nav-facet > summary::-webkit-details-marker { display: none; }
507
+ .nav-facet > summary::after { content: " ▾"; color: var(--text-3); }
508
+ .nav-facet[open] > summary::after { content: " ▴"; }
509
+ .nav-facet > summary:hover { background: var(--hover); }
510
+ .nav-facet > summary:focus-visible { outline: 2px solid var(--accent); }
511
+ .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; }
512
+ .nav-facet-list { display: flex; flex-direction: column; gap: 2px; padding: var(--s-1) var(--s-2); }
513
+ .nav-chip { display: flex; align-items: center; gap: var(--s-2); cursor: pointer; color: var(--text); }
514
+ .nav-facet-reset { font-size: var(--fs-xs); }
487
515
 
488
516
  .p-view .panel-actions .btn { gap: var(--s-1); }
489
517
  .v-term { flex: 1; min-height: 0; overflow: auto; scrollbar-gutter: stable; padding: 5px 6px; background: #000000; position: relative; }
@@ -587,6 +615,10 @@ a { color: inherit; text-decoration: none; }
587
615
  .chip-x:hover { color: var(--text); background: var(--surface); }
588
616
  .dlg-impact { margin-right: auto; align-self: center; }
589
617
  .dlg-inline-actions { display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; }
618
+ /* #1519 P1: the bot-token field — the input and its show/hide button on one row. */
619
+ .token-row { display: flex; align-items: center; gap: var(--s-1); }
620
+ .token-row input { flex: 1; min-width: 0; }
621
+ .feedback.ok { color: var(--ok, var(--text)); }
590
622
  .seg-inline { display: inline-flex; gap: var(--s-1); }
591
623
  .btn.on { background: var(--surface-2); color: var(--text); font-weight: var(--fw-semibold); }
592
624
  .card h4 { font-size: var(--fs-sm); font-weight: var(--fw-semibold); color: var(--text-2); }
@@ -6,7 +6,7 @@
6
6
  // reached only by the dynamic imports below, in "full" mode. A test walks the static imports over real HTTP and asserts
7
7
  // none of them names /ui/.
8
8
  import { html, render } from "./app-html.js";
9
- import { Shell, handleKey, showDialog, closeDrawer, retryable } from "./app-shell.js";
9
+ import { Shell, handleKey, showDialog, closeDrawer, retryable, setNewInstanceOpener } from "./app-shell.js";
10
10
  import { startRouter, navStore, navigate } from "./app-nav.js";
11
11
  import { createStream } from "./app-stream.js";
12
12
  import { appStore, applyStatus, applyActivity } from "./app-store.js";
@@ -72,6 +72,7 @@ if (mode === "full") {
72
72
  try { m = await loadFleet(); } catch { toast(t("app.loadFailed"), false); return; }
73
73
  showDialog(m.CreateInstanceDialog);
74
74
  };
75
+ setNewInstanceOpener(onNewInstance);
75
76
  } else appStore.set({ connection: "none" });
76
77
 
77
78
  startRouter(window);