@songsid/agend 2.2.0-alpha.1 → 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 (317) hide show
  1. package/README.md +6 -4
  2. package/README.zh-TW.md +6 -4
  3. package/dist/agent-cli.js +6 -1
  4. package/dist/agent-cli.js.map +1 -1
  5. package/dist/agent-endpoint.d.ts +4 -1
  6. package/dist/agent-endpoint.js +82 -14
  7. package/dist/agent-endpoint.js.map +1 -1
  8. package/dist/agent-token-header.d.ts +13 -0
  9. package/dist/agent-token-header.js +16 -0
  10. package/dist/agent-token-header.js.map +1 -0
  11. package/dist/atomic-write.d.ts +21 -0
  12. package/dist/atomic-write.js +65 -0
  13. package/dist/atomic-write.js.map +1 -0
  14. package/dist/auth-api.js +2 -0
  15. package/dist/auth-api.js.map +1 -1
  16. package/dist/backend/antigravity.js +4 -3
  17. package/dist/backend/antigravity.js.map +1 -1
  18. package/dist/backend/codex.d.ts +7 -0
  19. package/dist/backend/codex.js +7 -0
  20. package/dist/backend/codex.js.map +1 -1
  21. package/dist/backend/grok.js +1 -1
  22. package/dist/backend/grok.js.map +1 -1
  23. package/dist/backend/kiro.d.ts +7 -0
  24. package/dist/backend/kiro.js +82 -33
  25. package/dist/backend/kiro.js.map +1 -1
  26. package/dist/backend/muse.d.ts +7 -0
  27. package/dist/backend/muse.js +8 -1
  28. package/dist/backend/muse.js.map +1 -1
  29. package/dist/backend/types.d.ts +12 -0
  30. package/dist/backend/types.js.map +1 -1
  31. package/dist/binary-probe.d.ts +80 -0
  32. package/dist/binary-probe.js +182 -0
  33. package/dist/binary-probe.js.map +1 -0
  34. package/dist/cache-analysis.d.ts +92 -0
  35. package/dist/cache-analysis.js +147 -0
  36. package/dist/cache-analysis.js.map +1 -0
  37. package/dist/cache-ledger.d.ts +108 -0
  38. package/dist/cache-ledger.js +230 -0
  39. package/dist/cache-ledger.js.map +1 -0
  40. package/dist/cache-prices.d.ts +32 -0
  41. package/dist/cache-prices.js +73 -0
  42. package/dist/cache-prices.js.map +1 -0
  43. package/dist/cache-scan.d.ts +39 -0
  44. package/dist/cache-scan.js +115 -0
  45. package/dist/cache-scan.js.map +1 -0
  46. package/dist/cache-service.d.ts +131 -0
  47. package/dist/cache-service.js +452 -0
  48. package/dist/cache-service.js.map +1 -0
  49. package/dist/channel/adapters/discord.d.ts +5 -1
  50. package/dist/channel/adapters/discord.js +56 -11
  51. package/dist/channel/adapters/discord.js.map +1 -1
  52. package/dist/channel/adapters/telegram.d.ts +38 -1
  53. package/dist/channel/adapters/telegram.js +82 -0
  54. package/dist/channel/adapters/telegram.js.map +1 -1
  55. package/dist/channel/mcp-tools.js +1 -1
  56. package/dist/channel/mcp-tools.js.map +1 -1
  57. package/dist/channel/tool-router.d.ts +8 -1
  58. package/dist/channel/tool-router.js +4 -2
  59. package/dist/channel/tool-router.js.map +1 -1
  60. package/dist/channel/types.d.ts +21 -0
  61. package/dist/cli-entry.d.ts +19 -0
  62. package/dist/cli-entry.js +36 -0
  63. package/dist/cli-entry.js.map +1 -0
  64. package/dist/cli.js +376 -54
  65. package/dist/cli.js.map +1 -1
  66. package/dist/connection-secrets.d.ts +5 -0
  67. package/dist/connection-secrets.js.map +1 -1
  68. package/dist/cpu-profile.js +50 -0
  69. package/dist/cpu-profile.js.map +1 -1
  70. package/dist/daemon.d.ts +18 -0
  71. package/dist/daemon.js +256 -63
  72. package/dist/daemon.js.map +1 -1
  73. package/dist/delivery-outbox.d.ts +1 -0
  74. package/dist/delivery-outbox.js +12 -0
  75. package/dist/delivery-outbox.js.map +1 -1
  76. package/dist/discord-permissions.d.ts +33 -0
  77. package/dist/discord-permissions.js +42 -0
  78. package/dist/discord-permissions.js.map +1 -0
  79. package/dist/env-file.d.ts +11 -0
  80. package/dist/env-file.js +27 -0
  81. package/dist/env-file.js.map +1 -0
  82. package/dist/event-log.d.ts +4 -1
  83. package/dist/event-log.js +6 -3
  84. package/dist/event-log.js.map +1 -1
  85. package/dist/fleet-context.d.ts +5 -0
  86. package/dist/fleet-context.js.map +1 -1
  87. package/dist/fleet-control-audit.d.ts +5 -0
  88. package/dist/fleet-control-audit.js +7 -0
  89. package/dist/fleet-control-audit.js.map +1 -1
  90. package/dist/fleet-level-config.d.ts +8 -1
  91. package/dist/fleet-level-config.js +53 -0
  92. package/dist/fleet-level-config.js.map +1 -1
  93. package/dist/fleet-manager.d.ts +122 -7
  94. package/dist/fleet-manager.js +871 -152
  95. package/dist/fleet-manager.js.map +1 -1
  96. package/dist/fleet-stop-budget.d.ts +22 -0
  97. package/dist/fleet-stop-budget.js +95 -0
  98. package/dist/fleet-stop-budget.js.map +1 -0
  99. package/dist/full-restart.js +2 -3
  100. package/dist/full-restart.js.map +1 -1
  101. package/dist/general-knowledge/skills/persona-emoji/SKILL.md +3 -1
  102. package/dist/install-lock.d.ts +49 -0
  103. package/dist/install-lock.js +200 -0
  104. package/dist/install-lock.js.map +1 -0
  105. package/dist/instance-config-impact.js +3 -2
  106. package/dist/instance-config-impact.js.map +1 -1
  107. package/dist/kiro-transcript-lane.d.ts +8 -4
  108. package/dist/kiro-transcript-lane.js +66 -44
  109. package/dist/kiro-transcript-lane.js.map +1 -1
  110. package/dist/locale.js +96 -0
  111. package/dist/locale.js.map +1 -1
  112. package/dist/next-steps.d.ts +5 -0
  113. package/dist/next-steps.js +26 -0
  114. package/dist/next-steps.js.map +1 -0
  115. package/dist/outbound-handlers.js +8 -0
  116. package/dist/outbound-handlers.js.map +1 -1
  117. package/dist/outbound-schemas.d.ts +4 -0
  118. package/dist/outbound-schemas.js +7 -2
  119. package/dist/outbound-schemas.js.map +1 -1
  120. package/dist/outbox-open-error.d.ts +7 -0
  121. package/dist/outbox-open-error.js +15 -0
  122. package/dist/outbox-open-error.js.map +1 -0
  123. package/dist/package-preimage.d.ts +26 -0
  124. package/dist/package-preimage.js +96 -0
  125. package/dist/package-preimage.js.map +1 -0
  126. package/dist/pane-context-cache.d.ts +25 -0
  127. package/dist/pane-context-cache.js +73 -0
  128. package/dist/pane-context-cache.js.map +1 -0
  129. package/dist/paths.d.ts +0 -7
  130. package/dist/paths.js +8 -2
  131. package/dist/paths.js.map +1 -1
  132. package/dist/provider-probe.d.ts +15 -0
  133. package/dist/provider-probe.js +20 -0
  134. package/dist/provider-probe.js.map +1 -1
  135. package/dist/public-link-progress.d.ts +73 -0
  136. package/dist/public-link-progress.js +190 -0
  137. package/dist/public-link-progress.js.map +1 -0
  138. package/dist/public-web-gateway.js +2 -2
  139. package/dist/public-web-gateway.js.map +1 -1
  140. package/dist/public-web-link.d.ts +6 -1
  141. package/dist/public-web-link.js +87 -15
  142. package/dist/public-web-link.js.map +1 -1
  143. package/dist/quickstart-api.d.ts +93 -20
  144. package/dist/quickstart-api.js +194 -62
  145. package/dist/quickstart-api.js.map +1 -1
  146. package/dist/quickstart.d.ts +15 -0
  147. package/dist/quickstart.js +110 -33
  148. package/dist/quickstart.js.map +1 -1
  149. package/dist/reply-buttons-controller.d.ts +98 -0
  150. package/dist/reply-buttons-controller.js +174 -0
  151. package/dist/reply-buttons-controller.js.map +1 -0
  152. package/dist/reply-buttons.d.ts +113 -0
  153. package/dist/reply-buttons.js +208 -0
  154. package/dist/reply-buttons.js.map +1 -0
  155. package/dist/reply-dedup.js +3 -1
  156. package/dist/reply-dedup.js.map +1 -1
  157. package/dist/restart-guard.d.ts +53 -0
  158. package/dist/restart-guard.js +114 -0
  159. package/dist/restart-guard.js.map +1 -0
  160. package/dist/scheduler/db.js +6 -2
  161. package/dist/scheduler/db.js.map +1 -1
  162. package/dist/service-activation.d.ts +62 -7
  163. package/dist/service-activation.js +237 -55
  164. package/dist/service-activation.js.map +1 -1
  165. package/dist/service-installer.d.ts +56 -1
  166. package/dist/service-installer.js +152 -12
  167. package/dist/service-installer.js.map +1 -1
  168. package/dist/service-plan.d.ts +65 -0
  169. package/dist/service-plan.js +108 -0
  170. package/dist/service-plan.js.map +1 -0
  171. package/dist/settings-api.js +92 -10
  172. package/dist/settings-api.js.map +1 -1
  173. package/dist/settings-authority.d.ts +11 -0
  174. package/dist/settings-authority.js +128 -0
  175. package/dist/settings-authority.js.map +1 -0
  176. package/dist/settings-change.d.ts +5 -0
  177. package/dist/settings-change.js +37 -8
  178. package/dist/settings-change.js.map +1 -1
  179. package/dist/settings-confirmation.d.ts +3 -0
  180. package/dist/settings-confirmation.js +7 -1
  181. package/dist/settings-confirmation.js.map +1 -1
  182. package/dist/settings-effect.js +19 -7
  183. package/dist/settings-effect.js.map +1 -1
  184. package/dist/settings-http-confirmation.js +1 -1
  185. package/dist/settings-http-confirmation.js.map +1 -1
  186. package/dist/settings-redaction.d.ts +18 -0
  187. package/dist/settings-redaction.js +58 -0
  188. package/dist/settings-redaction.js.map +1 -0
  189. package/dist/setup-form.d.ts +9 -1
  190. package/dist/setup-form.js +25 -1
  191. package/dist/setup-form.js.map +1 -1
  192. package/dist/setup-host.js +15 -6
  193. package/dist/setup-host.js.map +1 -1
  194. package/dist/setup-wizard.js +27 -19
  195. package/dist/setup-wizard.js.map +1 -1
  196. package/dist/sqlite-open-errors.d.ts +12 -0
  197. package/dist/sqlite-open-errors.js +13 -0
  198. package/dist/sqlite-open-errors.js.map +1 -0
  199. package/dist/systemd-runtime.d.ts +15 -0
  200. package/dist/systemd-runtime.js +41 -0
  201. package/dist/systemd-runtime.js.map +1 -0
  202. package/dist/tmux-control.d.ts +44 -0
  203. package/dist/tmux-control.js +170 -21
  204. package/dist/tmux-control.js.map +1 -1
  205. package/dist/token-env-name.d.ts +37 -0
  206. package/dist/token-env-name.js +93 -0
  207. package/dist/token-env-name.js.map +1 -0
  208. package/dist/tool-permissions.js +5 -3
  209. package/dist/tool-permissions.js.map +1 -1
  210. package/dist/topic-commands.d.ts +7 -16
  211. package/dist/topic-commands.js +56 -85
  212. package/dist/topic-commands.js.map +1 -1
  213. package/dist/transcript-monitor.js +3 -0
  214. package/dist/transcript-monitor.js.map +1 -1
  215. package/dist/transcript-sources.d.ts +68 -0
  216. package/dist/transcript-sources.js +336 -29
  217. package/dist/transcript-sources.js.map +1 -1
  218. package/dist/transcript-turns.d.ts +39 -0
  219. package/dist/transcript-turns.js +58 -0
  220. package/dist/transcript-turns.js.map +1 -0
  221. package/dist/tunnel/cloudflared-install.d.ts +18 -0
  222. package/dist/tunnel/cloudflared-install.js +15 -1
  223. package/dist/tunnel/cloudflared-install.js.map +1 -1
  224. package/dist/turn-reply-guard.d.ts +3 -0
  225. package/dist/turn-reply-guard.js.map +1 -1
  226. package/dist/ui/chat-commands.js +43 -0
  227. package/dist/ui/chat-render.js +34 -8
  228. package/dist/ui/chat-store.js +51 -2
  229. package/dist/ui/chat-strings.js +30 -2
  230. package/dist/ui/chat-thread.js +141 -47
  231. package/dist/ui/first-run.js +64 -0
  232. package/dist/ui/fleet-cache.js +168 -0
  233. package/dist/ui/html-attachment.js +26 -0
  234. package/dist/ui/panel-chat.js +117 -23
  235. package/dist/ui/panel-fleet.js +31 -17
  236. package/dist/ui/panel-settings.js +29 -15
  237. package/dist/ui/preview-panel.js +237 -0
  238. package/dist/ui/preview.js +7 -3
  239. package/dist/ui/settings-apply.js +3 -1
  240. package/dist/ui/settings-dialogs.js +95 -25
  241. package/dist/ui/settings-discord.js +130 -0
  242. package/dist/ui/settings-model.js +21 -0
  243. package/dist/ui/settings-strings.js +50 -8
  244. package/dist/ui/settings-token.js +54 -0
  245. package/dist/ui/settings-wizard.js +63 -29
  246. package/dist/ui/shared/app-route.js +1 -1
  247. package/dist/ui/shared/app-shell.js +25 -37
  248. package/dist/ui/shared/app-store.js +5 -3
  249. package/dist/ui/shared/app-stream.js +2 -1
  250. package/dist/ui/shared/app.css +181 -5
  251. package/dist/ui/shared/app.js +2 -1
  252. package/dist/ui/shared/instance-nav.js +310 -0
  253. package/dist/ui/shared/panel-view.js +12 -190
  254. package/dist/ui/shared/signin.js +13 -1
  255. package/dist/ui/shared/ui-icons.js +2 -0
  256. package/dist/ui/shared/view-roster-store.js +5 -0
  257. package/dist/ui/shared/view-strings.js +12 -0
  258. package/dist/ui/signin.html +2 -1
  259. package/dist/update-dispatch.d.ts +28 -0
  260. package/dist/update-dispatch.js +73 -0
  261. package/dist/update-dispatch.js.map +1 -0
  262. package/dist/update-install.d.ts +78 -8
  263. package/dist/update-install.js +158 -30
  264. package/dist/update-install.js.map +1 -1
  265. package/dist/update-launch.d.ts +22 -0
  266. package/dist/update-launch.js +106 -0
  267. package/dist/update-launch.js.map +1 -0
  268. package/dist/update-marker.js +3 -8
  269. package/dist/update-marker.js.map +1 -1
  270. package/dist/usage/format-rich.js +7 -1
  271. package/dist/usage/format-rich.js.map +1 -1
  272. package/dist/usage/i18n-keys.d.ts +1 -1
  273. package/dist/usage/i18n-keys.js +1 -1
  274. package/dist/usage/i18n-keys.js.map +1 -1
  275. package/dist/usage/providers.d.ts +4 -0
  276. package/dist/usage/providers.js +2 -1
  277. package/dist/usage/providers.js.map +1 -1
  278. package/dist/usage/usage-api.js +48 -2
  279. package/dist/usage/usage-api.js.map +1 -1
  280. package/dist/view-api.d.ts +40 -0
  281. package/dist/view-api.js +28 -10
  282. package/dist/view-api.js.map +1 -1
  283. package/dist/web-api.d.ts +22 -0
  284. package/dist/web-api.js +115 -16
  285. package/dist/web-api.js.map +1 -1
  286. package/dist/web-auth.d.ts +8 -0
  287. package/dist/web-auth.js +34 -8
  288. package/dist/web-auth.js.map +1 -1
  289. package/dist/web-chat-history.d.ts +21 -0
  290. package/dist/web-chat-history.js +44 -0
  291. package/dist/web-chat-history.js.map +1 -1
  292. package/dist/web-commands.d.ts +77 -0
  293. package/dist/web-commands.js +81 -0
  294. package/dist/web-commands.js.map +1 -0
  295. package/dist/web-host-guard.d.ts +18 -2
  296. package/dist/web-host-guard.js +30 -5
  297. package/dist/web-host-guard.js.map +1 -1
  298. package/dist/web-session.d.ts +7 -1
  299. package/dist/web-session.js +23 -2
  300. package/dist/web-session.js.map +1 -1
  301. package/dist/web-shell-routes.d.ts +1 -1
  302. package/dist/web-shell-routes.js +1 -1
  303. package/dist/web-shell-routes.js.map +1 -1
  304. package/launcher/agend +110 -0
  305. package/launcher/agend-agent +110 -0
  306. package/launcher/agend-agent.cjs +4 -0
  307. package/launcher/agend.cjs +4 -0
  308. package/launcher/install-admission.cjs +105 -0
  309. package/launcher/launch.cjs +64 -0
  310. package/launcher/postinstall.cjs +118 -0
  311. package/launcher/run-in-process.cjs +13 -0
  312. package/launcher/runtime-platform.cjs +86 -0
  313. package/launcher/runtime-select.cjs +217 -0
  314. package/package.json +12 -4
  315. package/scripts/preinstall-guard.cjs +40 -21
  316. package/templates/launchd.plist.ejs +2 -2
  317. package/templates/systemd.service.ejs +4 -4
@@ -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 { PanelHeader, onPanelKey, setTitle, addFooterItem, statusClass, statusLabel, openDrawer } from "/assets/app-shell.js";
13
+ import { PanelHeader, onPanelKey, setTitle, addFooterItem, statusClass, statusLabel, openDrawer, requestNewInstance } from "/assets/app-shell.js";
14
14
  import { navigate } from "/assets/app-nav.js";
15
15
  import { Menu } from "/assets/ui-menu.js";
16
16
  import { Dialog } from "/assets/ui-dialog.js";
@@ -21,8 +21,11 @@ import { confirmDialog } from "/assets/ui-confirm.js";
21
21
  import "./chat-strings.js";
22
22
  import { confirmedWrite } from "./settings-confirm.js";
23
23
  import { createChatStore } from "./chat-store.js";
24
- import { createThread } from "./chat-thread.js";
24
+ import { createThread, msgKey } from "./chat-thread.js";
25
+ import { PreviewPanel, openPanel, panelStore, shownKey } from "./preview-panel.js";
26
+ import { needsArgument, paletteFor, parseCommandLine } from "./chat-commands.js";
25
27
  import { installTour, refreshTourSpot, startTour } from "./chat-tour.js";
28
+ import { FirstRunCard } from "./first-run.js";
26
29
 
27
30
  const R = () => globalThis.AgendChatRender;
28
31
  const P = () => globalThis.AgendPreview;
@@ -56,7 +59,7 @@ export async function setPreviewOptIn(on) {
56
59
  export function boot({ stream, boot: bootData, deps = {} }) {
57
60
  if (store) return store;
58
61
  store = createChatStore({
59
- fetch: (...a) => fetch(...a), toast, announce, t, setTimeout: (fn, ms) => setTimeout(fn, ms), ...deps,
62
+ fetch: (...a) => fetch(...a), toast, announce, t, setTimeout: (fn, ms) => setTimeout(fn, ms), confirm: confirmDialog, ...deps,
60
63
  });
61
64
  store.attach(stream);
62
65
  // #1306: what the server said about previews for this load. The page-wide message and storage listeners this adds
@@ -97,9 +100,9 @@ async function copyText(text) {
97
100
  return ok;
98
101
  }
99
102
  /** The HTML as a file: a blob of the source as application/octet-stream, saved as reply.html — never opened. */
100
- function downloadHtml(code) {
103
+ function downloadHtml(code, name) {
101
104
  const url = URL.createObjectURL(new Blob([code], { type: "application/octet-stream" }));
102
- const a = document.createElement("a"); a.href = url; a.download = "reply.html"; a.className = "offscreen";
105
+ const a = document.createElement("a"); a.href = url; a.download = typeof name === "string" && name.trim() ? name.trim() : "reply.html"; a.className = "offscreen";
103
106
  document.body.appendChild(a); a.click(); a.remove();
104
107
  URL.revokeObjectURL(url);
105
108
  }
@@ -125,9 +128,14 @@ export function ChatPanel({ route, navKey }) {
125
128
  }, [name, !!inst]);
126
129
  useEffect(() => { setTitle(name || t("app.chat")); }, [name, navKey]);
127
130
  if (!name) {
131
+ // #1519 P7: a web-only fleet starts here with the first-run card; with no agent at all, the way to make one.
132
+ const none = app.ready && !app.instances.length;
128
133
  return html`<div class="panel p-chat"><${PanelHeader} title=${t("app.chat")} />
129
- <div class="panel-body center"><${Empty} icon="chat" title=${t("chat.pick")} hint=${t("chat.pickHint")}
130
- action=${html`<button type="button" class="btn only-narrow" onClick=${openDrawer}>${t("chat.backToList")}</button>`} /></div></div>`;
134
+ <div class="panel-body center"><div class="first-run-stack"><${FirstRunCard} />
135
+ ${none ? html`<${Empty} icon="bot" title=${t("app.noInstances")} hint=${t("app.noInstancesHint")}
136
+ action=${html`<button type="button" class="btn btn-primary" onClick=${requestNewInstance}><${Icon} name="plus" size=${16} />${t("app.newInstance")}</button>`} />`
137
+ : html`<${Empty} icon="chat" title=${t("chat.pick")} hint=${t("chat.pickHint")}
138
+ action=${html`<button type="button" class="btn only-narrow" onClick=${openDrawer}>${t("chat.backToList")}</button>`} />`}</div></div></div>`;
131
139
  }
132
140
  if (!app.ready) return html`<div class="panel p-chat"><${PanelHeader} title=${name} /><div class="panel-body"><${Skeleton} lines=${5} /></div></div>`;
133
141
  if (!inst) return html`<${NotFound} name=${name} />`;
@@ -146,7 +154,7 @@ function NotFound({ name }) {
146
154
  function ChatView({ name, inst, lease, exec, awaiting }) {
147
155
  const [dialog, setDialog] = useState(null); // "details" | "delete" | null
148
156
  const [wrap, setWrap] = useState(codeWrap);
149
- const view = useRef(null);
157
+ const view = useRef(null), split = useRef(null), thread = useRef(null);
150
158
  useEffect(() => { wrapListeners.add(setWrap); return () => wrapListeners.delete(setWrap); }, []);
151
159
  // First visit to this instance's chat on this page: its history (a navigation — it counts as use). Back again: nothing.
152
160
  useEffect(() => { store.openHistory(name, lease); }, [name]);
@@ -189,11 +197,21 @@ function ChatView({ name, inst, lease, exec, awaiting }) {
189
197
  { key: "delete", label: t("chat.delete"), icon: "trash", danger: true, onSelect: () => setDialog("delete") },
190
198
  ];
191
199
  const cls = statusClass(inst, exec, awaiting);
192
- const sub = html`<span class="status"><span class=${`dot ${cls}`} aria-hidden="true"></span>${statusLabel(inst, exec, awaiting)}</span>`;
200
+ // #1269 quick actions: the model and effort it runs, each a way to /model or /effort (the same pick list).
201
+ const pick = (command) => store.runCommand(name, command, "", { alive: () => lease.current() });
202
+ const sub = html`<span class="status"><span class=${`dot ${cls}`} aria-hidden="true"></span>${statusLabel(inst, exec, awaiting)}</span>
203
+ ${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}
204
+ ${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}`;
193
205
  return html`<div class=${`panel p-chat${wrap ? " wrap-code" : ""}`} ref=${view}>
194
206
  <${PanelHeader} title=${name} sub=${sub}><${Menu} items=${items} label=${t("app.more")} /></${PanelHeader}>
195
- <${Thread} name=${name} />
196
- <${Dock} name=${name} lease=${lease} exec=${exec} awaiting=${awaiting} />
207
+ <div class="chat-split" ref=${split}>
208
+ <div class="chat-main">
209
+ <${Thread} name=${name} th=${thread} />
210
+ <${Dock} name=${name} inst=${inst} lease=${lease} exec=${exec} awaiting=${awaiting} />
211
+ </div>
212
+ <${PreviewPanel} name=${name} split=${split} msgs=${() => store.state.msgs[name] || []} subscribe=${(fn) => store.subscribe(fn)}
213
+ keyOf=${msgKey} reveal=${(k) => thread.current && thread.current.reveal(k)} download=${downloadHtml} />
214
+ </div>
197
215
  <div class="drop-overlay" aria-hidden="true"><div class="drop-card"><${Icon} name="attach" size=${32} /><span>${t("chat.dropHere")}</span></div></div>
198
216
  ${dialog === "details" ? html`<${DetailsDialog} name=${name} onClose=${() => setDialog(null)} />` : null}
199
217
  ${dialog === "delete" ? html`<${DeleteDialog} name=${name} onClose=${() => setDialog(null)} />` : null}
@@ -201,14 +219,17 @@ function ChatView({ name, inst, lease, exec, awaiting }) {
201
219
  }
202
220
 
203
221
  /** The thread: Preact owns the elements, the keyed renderer owns what is inside the list (chat-thread.js). */
204
- function Thread({ name }) {
205
- const scroller = useRef(null), list = useRef(null), th = useRef(null);
222
+ function Thread({ name, th }) {
223
+ const scroller = useRef(null), list = useRef(null);
206
224
  const [jump, setJump] = useState({ show: false, unseen: 0 });
207
225
  const [empty, setEmpty] = useState(false);
208
226
  useLayoutEffect(() => {
209
227
  const thread = createThread(list.current, scroller.current, {
210
228
  t, tf: t, isUser: (x) => store.isUser(x, name), onJump: setJump, onEmpty: setEmpty,
211
229
  setPreviewOptIn, copyText, download: downloadHtml, toggleWrap,
230
+ clickReplyButton: (id, index) => store.clickReplyButton(name, id, index), // #1266
231
+ replyButtonBusy: (id) => store.state.rbBusy.has(id),
232
+ panel: { shown: shownKey, open: (spec, o) => openPanel(spec, o) }, // #1481
212
233
  });
213
234
  th.current = thread;
214
235
  thread.render(store.state.msgs[name] || [], { restore: store.state.scrollMemo[name] ?? null });
@@ -218,12 +239,13 @@ function Thread({ name }) {
218
239
  else if (kind === "sent") thread.jumpLatest();
219
240
  });
220
241
  const offPv = P() ? P().onChange(() => thread.refreshCards()) : () => {};
242
+ const offPanel = panelStore.subscribe(() => thread.refreshCards()); // a card shows whether the panel has its block
221
243
  const onScroll = () => { store.state.scrollMemo[name] = thread.onScroll(); };
222
244
  const sc = scroller.current, ls = list.current;
223
245
  sc.addEventListener("scroll", onScroll, { passive: true });
224
246
  ls.addEventListener("click", thread.onClick);
225
247
  return () => {
226
- off(); offPv();
248
+ off(); offPv(); offPanel();
227
249
  sc.removeEventListener("scroll", onScroll);
228
250
  ls.removeEventListener("click", thread.onClick);
229
251
  thread.dispose(); // the whole thread goes: every preview in it stops first
@@ -239,20 +261,51 @@ function Thread({ name }) {
239
261
  }
240
262
 
241
263
  /** Everything under the thread: prompts, the working line, a failed send, files waiting, and the composer. */
242
- function Dock({ name, lease, exec, awaiting }) {
264
+ function Dock({ name, inst, lease, exec, awaiting }) {
243
265
  useChatTick(name, ["state"]);
244
266
  const s = store.state;
245
267
  return html`<div class="dock"><div class="dock-col">
246
268
  <${Prompts} name=${name} />
247
269
  <${WorkBar} name=${name} lease=${lease} exec=${exec} awaiting=${awaiting} />
270
+ <${CommandCard} name=${name} lease=${lease} />
271
+ <${QuickActions} name=${name} inst=${inst} lease=${lease} />
248
272
  ${s.failedSends[name] ? html`<div class="failed-send" role="alert"><span>${t("chat.notSent")}</span><span class="txt">${s.failedSends[name]}</span>
249
273
  <button type="button" class="btn btn-primary btn-sm" onClick=${() => store.putBack(name)}>${t("chat.putBack")}</button>
250
274
  <button type="button" class="btn btn-sm" onClick=${() => store.discardFailed(name)}>${t("chat.discard")}</button></div>` : null}
251
- <${Composer} name=${name} busy=${R().isBusy(exec)} />
275
+ <${Composer} name=${name} lease=${lease} busy=${R().isBusy(exec)} />
252
276
  <p class="hint">${t("chat.composerHint")}</p>
253
277
  </div></div>`;
254
278
  }
255
279
 
280
+ /** #1269: what the last chat command answered — its text, why it was refused, or a list to choose from. */
281
+ function CommandCard({ name, lease }) {
282
+ const c = store.state.commands[name];
283
+ if (!c) return null;
284
+ const run = (args) => store.runCommand(name, c.command, args, { alive: () => lease.current() });
285
+ return html`<div class=${`cmd-card${c.error ? " bad" : ""}`} role=${c.error ? "alert" : "status"}>
286
+ <div class="cmd-head"><span class="cmd-name mono">/${c.command}</span>
287
+ ${c.busy ? html`<span class="wait">${t("chat.cmdRunning", c.command)}</span>` : html`<button type="button" class="icon-btn" title=${t("chat.cmdDismiss")}
288
+ aria-label=${t("chat.cmdDismiss")} onClick=${() => store.dismissCommand(name)}><${Icon} name="close" size=${14} /></button>`}</div>
289
+ ${c.error ? html`<p class="cmd-text">${c.error}</p>` : null}
290
+ ${c.text ? html`<pre class="cmd-text">${c.text}</pre>` : null}
291
+ ${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"
292
+ 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}
293
+ </div>`;
294
+ }
295
+
296
+ /** #1269 quick actions: Compact and Clear… once the instance's context is 70% used (from the status it already has). */
297
+ function QuickActions({ name, inst, lease }) {
298
+ const pct = inst && typeof inst.context_pct === "number" ? Math.round(inst.context_pct) : null;
299
+ if (pct == null || pct < 70) return null;
300
+ const busy = !!(store.state.commands[name] && store.state.commands[name].busy);
301
+ const run = (command) => store.runCommand(name, command, "", { alive: () => lease.current() });
302
+ return html`<div class="quick-actions" role="group" aria-label=${t("chat.chipContext", pct)}>
303
+ <span class="qa-ctx">${t("chat.chipContext", pct)}</span>
304
+ <button type="button" class="btn btn-sm" disabled=${busy} onClick=${() => run("compact")}>${t("chat.chipCompact")}</button>
305
+ <button type="button" class="btn btn-sm" disabled=${busy} onClick=${() => run("clear")}>${t("chat.chipClear")}</button>
306
+ </div>`;
307
+ }
308
+
256
309
  function Prompts({ name }) {
257
310
  const list = store.promptsFor(name);
258
311
  if (!list.length) return null;
@@ -281,20 +334,53 @@ function WorkBar({ name, lease, exec, awaiting }) {
281
334
  : html`<span class="elapsed" title=${t("chat.elapsedTitle")} aria-hidden="true">${since == null ? "" : R().formatElapsed(performance.now() - since)}</span>`}</div>`;
282
335
  }
283
336
 
284
- function Composer({ name, busy }) {
337
+ function Composer({ name, lease, busy }) {
285
338
  const s = store.state;
286
339
  const input = useRef(null), fileIn = useRef(null);
287
340
  const [, tick] = useState(0);
341
+ const [sel, setSel] = useState(0);
342
+ const [closedFor, setClosedFor] = useState(null); // the draft the palette was closed on (Esc); it opens again on change
288
343
  const value = s.drafts[name] || "";
289
344
  const files = s.pendingFiles[name] || [];
290
345
  const content = !!value.trim() || files.length > 0;
346
+ // A file waiting: what goes is a message with its file, never a command (no palette, no command line).
347
+ const palette = files.length || closedFor === value ? null : paletteFor(value);
348
+ const pick = palette && palette.length ? palette[Math.min(sel, palette.length - 1)] : null;
291
349
  // Grow with the text, up to a limit: set through the CSSOM, never a style attribute (#1300).
292
350
  useLayoutEffect(() => { const el = input.current; if (!el) return; el.style.height = "auto"; el.style.height = `${Math.min(el.scrollHeight, 240)}px`; }, [value]);
293
351
  useEffect(() => { if (input.current) input.current.focus(); }, [name]);
294
352
  useEffect(() => { refreshTourSpot(); });
295
- const send = () => store.send(name);
296
- const onKeyDown = (e) => { if (R().composerKey(e) === "send") { e.preventDefault(); send(); } };
297
- const onInput = (e) => { store.setDraft(name, e.target.value); tick(n => n + 1); };
353
+ const setValue = (v) => { store.setDraft(name, v); setSel(0); tick(n => n + 1); };
354
+ // #1269: a command line runs the command (POST /ui/command); anything else — "/" included — is sent as before.
355
+ // The draft is consumed only by a command that starts: one still out keeps the new line in the composer (#1476 review).
356
+ const runLine = (cmd) => {
357
+ if (!store.commandFree(name)) return;
358
+ setValue("");
359
+ store.runCommand(name, cmd.command, cmd.args, { alive: () => lease.current() });
360
+ };
361
+ const complete = (c) => setValue(`/${c.name}${c.arg ? " " : ""}`);
362
+ const submit = () => {
363
+ if (pick) {
364
+ if (value === `/${pick.name}` && !needsArgument(pick.name)) runLine({ command: pick.name, args: "" });
365
+ else complete(pick);
366
+ return;
367
+ }
368
+ const cmd = files.length ? null : parseCommandLine(value);
369
+ if (cmd && needsArgument(cmd.command) && !cmd.args) { setValue(`/${cmd.command} `); return; }
370
+ if (cmd) { runLine(cmd); return; }
371
+ store.send(name);
372
+ };
373
+ const onKeyDown = (e) => {
374
+ if (palette && palette.length && (e.key === "ArrowDown" || e.key === "ArrowUp")) {
375
+ e.preventDefault();
376
+ setSel((n) => (Math.min(n, palette.length - 1) + (e.key === "ArrowDown" ? 1 : palette.length - 1)) % palette.length);
377
+ return;
378
+ }
379
+ if (pick && e.key === "Tab" && !e.shiftKey) { e.preventDefault(); complete(pick); return; }
380
+ if (palette && e.key === "Escape" && !e.isComposing) { e.preventDefault(); setClosedFor(value); return; }
381
+ if (R().composerKey(e) === "send") { e.preventDefault(); submit(); }
382
+ };
383
+ const onInput = (e) => { setValue(e.target.value); };
298
384
  // Files in a paste are attached; a very long text paste goes as a text file — only once it is really attached.
299
385
  const onPaste = (e) => {
300
386
  const list = e.clipboardData && e.clipboardData.files;
@@ -302,18 +388,26 @@ function Composer({ name, busy }) {
302
388
  if (list && list.length) { store.addFiles(name, list); if (!text) e.preventDefault(); return; }
303
389
  if (R().isLongPaste(text) && store.attachPastedText(name, text)) e.preventDefault();
304
390
  };
391
+ const optId = (c) => `cmd-${c.name}`;
305
392
  return html`<div class="composer-box">
393
+ ${palette ? html`<ul class="cmd-palette" id="cmdPalette" role="listbox" aria-label=${t("chat.cmdCommands")}>
394
+ ${palette.length ? palette.map((c) => html`<li key=${c.name} id=${optId(c)} role="option" aria-selected=${c === pick ? "true" : "false"}
395
+ class=${c === pick ? "on" : ""} onMouseDown=${(e) => { e.preventDefault(); complete(c); }}>
396
+ <span class="cmd-name mono">/${c.name}${c.arg ? html` <span class="cmd-arg">${c.arg}</span>` : null}</span><span class="cmd-desc">${t(`chat.cmd_${c.name}`)}</span></li>`)
397
+ : html`<li class="cmd-none" role="option" aria-selected="false">${t("chat.cmdNotCommand")}</li>`}
398
+ </ul>` : null}
306
399
  ${files.length ? html`<div class="pending-files">${files.map((f, i) => html`<${FileChip} key=${`${f.name}-${i}`} f=${f} i=${i} name=${name} />`)}</div>` : null}
307
400
  <div class="composer">
308
401
  <button id="attachBtn" type="button" class="icon-btn" title=${t("chat.attachTitle")} aria-label=${t("chat.attachTitle")} onClick=${() => fileIn.current && fileIn.current.click()}><${Icon} name="attach" /></button>
309
402
  <input ref=${fileIn} type="file" multiple hidden accept="image/png,image/jpeg,image/gif,image/webp,application/pdf,text/*,.md,.csv,.json,.log,.yaml,.yml"
310
403
  onChange=${(e) => { store.addFiles(name, e.target.files); e.target.value = ""; }} />
311
404
  <textarea id="msgIn" ref=${input} rows="1" value=${value} placeholder=${t("chat.composerPlaceholder", name)} aria-label=${t("chat.composerPlaceholder", name)}
312
- autocomplete="off" onInput=${onInput} onKeyDown=${onKeyDown} onPaste=${onPaste}></textarea>
405
+ autocomplete="off" aria-controls=${palette ? "cmdPalette" : undefined} aria-expanded=${palette ? "true" : "false"}
406
+ aria-activedescendant=${pick ? optId(pick) : undefined} onInput=${onInput} onKeyDown=${onKeyDown} onPaste=${onPaste}></textarea>
313
407
  <button id="stopBtn" type="button" class="btn btn-stop" hidden=${!busy} title=${t("chat.stopReplyTitle")}
314
- disabled=${!!s.cancelling[name]} onClick=${() => store.cancelReply(name)}><${Icon} name="stop" size=${14} />${t("chat.stopReply")}</button>
408
+ disabled=${!!s.cancelling[name]} onClick=${() => store.cancelReply(name)}><${Icon} name="stop" size=${14} /><span class="stop-label">${t("chat.stopReply")}</span></button>
315
409
  <button id="sendBtn" type="button" class="btn btn-primary btn-send" hidden=${busy && !content} disabled=${!content || !!s.sending[name]}
316
- aria-label=${t("chat.send")} title=${t("chat.send")} onClick=${send}><${Icon} name="send" size=${16} /><span class="send-label">${t("chat.send")}</span></button>
410
+ aria-label=${t("chat.send")} title=${t("chat.send")} onClick=${submit}><${Icon} name="send" size=${16} /><span class="send-label">${t("chat.send")}</span></button>
317
411
  </div></div>`;
318
412
  }
319
413
 
@@ -18,13 +18,14 @@ import { toast } from "/assets/ui-toast.js";
18
18
  import { confirmDialog } from "/assets/ui-confirm.js";
19
19
  import { confirmedWrite } from "./settings-confirm.js";
20
20
  import { OrgChart } from "./fleet-org.js";
21
+ import { CacheReport } from "./fleet-cache.js";
21
22
 
22
23
  register("fleet", {
23
24
  en: {
24
- title: "Fleet", tasks: "Tasks", schedules: "Schedules", teams: "Teams", org: "Org chart", config: "Config",
25
+ title: "Fleet", tasks: "Tasks", schedules: "Schedules", teams: "Teams", org: "Org chart", cache: "Cache", config: "Config",
25
26
  newTask: "New task", newSchedule: "New schedule", newTeam: "New team",
26
27
  noTasks: "No tasks yet", noTasksHint: "Create one to coordinate fleet work.", noSchedules: "No schedules yet", noSchedulesHint: "Automate recurring work with cron.",
27
- noTeams: "No teams yet", noTeamsHint: "Group instances for coordinated work.", countTasks: "{0} tasks", countSchedules: "{0} schedules", countTeams: "{0} teams",
28
+ noTeams: "No teams yet", noTeamsHint: "Group instances for coordinated work.", countTasks: "{0} tasks", countSchedules: "{0} schedules", countTeams: "{0} teams", countTask: "1 task", countSchedule: "1 schedule", countTeam: "1 team",
28
29
  claim: "Claim", done: "Done", delete: "Delete", on: "on", off: "off", cancel: "Cancel", create: "Create", creating: "Creating…", save: "Save changes",
29
30
  taskTitle: "Title", taskDesc: "Description", priority: "Priority", assignee: "Assignee", unassigned: "Unassigned",
30
31
  pNormal: "Normal", pLow: "Low", pHigh: "High", pUrgent: "Urgent",
@@ -35,16 +36,16 @@ register("fleet", {
35
36
  channel: "Channel", type: "Type", tokenEnv: "Bot token env", groupId: "Group ID", show: "Show", hide: "Hide", access: "Access control", mode: "Mode", locked: "Locked", open: "Open",
36
37
  allowedUsers: "Allowed users", allowedUsersNote: "Comma-separated user IDs", defaults: "Defaults", backend: "Backend", roots: "Project roots", addRoot: "Add root", removeRoot: "Remove",
37
38
  saveNote: "Saving rewrites fleet.yaml (its comments are lost).",
38
- newInstance: "New instance", directory: "Directory", directoryHint: "Optional — a workspace is created if empty", topic: "Topic name", topicHint: "From the directory name",
39
- topicRequired: "Topic name is required when Directory is empty", description: "Description", descriptionHint: "What this instance does", model: "Model", modelHint: "e.g. sonnet, opus, gpt-5",
39
+ newInstance: "New instance", directory: "Directory", directoryHint: "Optional — a workspace is created if empty", topic: "Name", topicHint: "From the directory name",
40
+ topicRequired: "Name is required when Directory is empty", description: "Description", descriptionHint: "What this instance does", model: "Model", modelHint: "e.g. sonnet, opus, gpt-5",
40
41
  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",
41
42
  fleetDefault: "Use the fleet default", notInstalled: "(not installed)", deprecated: "(deprecated)", instanceCreated: "Instance created",
42
43
  },
43
44
  "zh-TW": {
44
- title: "Fleet", tasks: "Tasks", schedules: "排程", teams: "Teams", org: "組織圖", config: "設定",
45
+ title: "Fleet", tasks: "Tasks", schedules: "排程", teams: "Teams", org: "組織圖", cache: "快取", config: "設定",
45
46
  newTask: "新增 task", newSchedule: "新增排程", newTeam: "新增 team",
46
47
  noTasks: "還沒有 task", noTasksHint: "建立一個來協調 fleet 的工作。", noSchedules: "還沒有排程", noSchedulesHint: "用 cron 自動執行例行工作。",
47
- noTeams: "還沒有 team", noTeamsHint: "把 instance 分組一起工作。", countTasks: "{0} 個 task", countSchedules: "{0} 個排程", countTeams: "{0} 個 team",
48
+ noTeams: "還沒有 team", noTeamsHint: "把 instance 分組一起工作。", countTasks: "{0} 個 task", countSchedules: "{0} 個排程", countTeams: "{0} 個 team", countTask: "1 個 task", countSchedule: "1 個排程", countTeam: "1 個 team",
48
49
  claim: "認領", done: "完成", delete: "刪除", on: "開", off: "關", cancel: "取消", create: "建立", creating: "建立中…", save: "儲存變更",
49
50
  taskTitle: "標題", taskDesc: "說明", priority: "優先順序", assignee: "負責人", unassigned: "未指派",
50
51
  pNormal: "一般", pLow: "低", pHigh: "高", pUrgent: "緊急",
@@ -55,14 +56,14 @@ register("fleet", {
55
56
  channel: "頻道", type: "類型", tokenEnv: "Bot token 環境變數", groupId: "Group ID", show: "顯示", hide: "隱藏", access: "存取控制", mode: "模式", locked: "鎖定", open: "開放",
56
57
  allowedUsers: "允許的使用者", allowedUsersNote: "以逗號分隔的使用者 ID", defaults: "預設值", backend: "Backend", roots: "專案根目錄", addRoot: "新增根目錄", removeRoot: "移除",
57
58
  saveNote: "儲存會改寫 fleet.yaml(其中的註解會遺失)。",
58
- newInstance: "新增 instance", directory: "目錄", directoryHint: "可留空——留空會自動建立工作區", topic: "Topic 名稱", topicHint: "預設取自目錄名稱",
59
- topicRequired: "目錄留空時必須填寫 Topic 名稱", description: "說明", descriptionHint: "這個 instance 做什麼", model: "模型", modelHint: "例如 sonnet、opus、gpt-5",
59
+ newInstance: "新增 instance", directory: "目錄", directoryHint: "可留空——留空會自動建立工作區", topic: "名稱", topicHint: "預設取自目錄名稱",
60
+ topicRequired: "目錄留空時必須填寫名稱", description: "說明", descriptionHint: "這個 instance 做什麼", model: "模型", modelHint: "例如 sonnet、opus、gpt-5",
60
61
  branch: "分支(git worktree)", branchHint: "例如 feature-x", tags: "標籤", tagsHint: "以逗號分隔,例如 dev, review", systemPrompt: "System prompt", systemPromptHint: "這個 instance 的自訂指示",
61
62
  fleetDefault: "使用 fleet 預設", notInstalled: "(未安裝)", deprecated: "(已淘汰)", instanceCreated: "Instance 已建立",
62
63
  },
63
64
  });
64
65
 
65
- const ICONS = { tasks: "tasks", schedules: "clock", teams: "team", org: "org", config: "sliders" };
66
+ const ICONS = { tasks: "tasks", schedules: "clock", teams: "team", org: "org", cache: "chart", config: "sliders" };
66
67
  const BACKENDS = ["claude-code", "codex", "opencode", "kiro-cli", "antigravity", "grok", "muse"];
67
68
 
68
69
  async function api(method, path, body, lease) {
@@ -73,7 +74,7 @@ async function api(method, path, body, lease) {
73
74
  }
74
75
 
75
76
  /**
76
- * Load `path` under the lease: `null` while the first load runs, `{ error }` on failure. A refresh (a new `version`,
77
+ * Load `path` under the lease: `null` while the first load runs, `{ error }` on failure (no answer, or an HTTP error). A refresh (a new `version`,
77
78
  * after Claim or Create) keeps showing what is there until the new list arrives — nothing below it is torn down, so
78
79
  * an open dialog and its draft survive (#1425 review). Only the latest request's answer is kept.
79
80
  */
@@ -86,7 +87,10 @@ function useLoad(lease, path, version) {
86
87
  if (shownFor.current !== lease) { shownFor.current = lease; setState(null); }
87
88
  (async () => {
88
89
  try {
89
- const d = await api("GET", path, null, lease);
90
+ // A read the server refused (a 4xx/5xx with an { error } body) is a failure, not an empty list (alpha.2 sweep).
91
+ const r = await lease.fetch(path, { method: "GET", headers: { "Content-Type": "application/json" } });
92
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
93
+ const d = await r.json();
90
94
  if (lease.current() && mine === seq.current) setState({ data: d });
91
95
  } catch { if (lease.current() && mine === seq.current) setState({ error: true }); }
92
96
  })();
@@ -98,12 +102,12 @@ export function FleetPanel({ route, navKey }) {
98
102
  const lease = useLease(navKey);
99
103
  const tab = route.tab;
100
104
  useEffect(() => { setTitle(`${t("fleet.title")} · ${t(`fleet.${tab}`)}`); }, [tab, navKey]);
101
- const Body = { tasks: Tasks, schedules: Schedules, teams: Teams, org: Org, config: Config }[tab] || Tasks;
105
+ const Body = { tasks: Tasks, schedules: Schedules, teams: Teams, org: Org, cache: Cache, config: Config }[tab] || Tasks;
102
106
  return html`<div class="panel p-fleet">
103
107
  <${PanelHeader} title=${t("fleet.title")} />
104
108
  <nav class="seg" aria-label=${t("fleet.title")}>${FLEET_TABS.map(k => html`<a key=${k} href=${fleetPath(k)} class=${`seg-item${k === tab ? " active" : ""}`}
105
109
  aria-current=${k === tab ? "page" : undefined}><${Icon} name=${ICONS[k]} size=${16} /><span>${t(`fleet.${k}`)}</span></a>`)}</nav>
106
- <div class="panel-body"><div class=${`col${tab === "org" ? " col-wide" : ""}`}><${Body} lease=${lease} /></div></div>
110
+ <div class="panel-body"><div class=${`col${tab === "org" || tab === "cache" ? " col-wide" : ""}`}><${Body} lease=${lease} /></div></div>
107
111
  </div>`;
108
112
  }
109
113
 
@@ -126,7 +130,7 @@ function Tasks({ lease }) {
126
130
  }
127
131
  return html`<${Loaded} state=${state}>${(d) => {
128
132
  const tasks = d.tasks || [];
129
- return html`<${ListHead} count=${t("fleet.countTasks", tasks.length)} label=${t("fleet.newTask")} action=${{ onClick: () => setCreating(true) }} />
133
+ return html`<${ListHead} count=${tasks.length === 1 ? t("fleet.countTask") : t("fleet.countTasks", tasks.length)} label=${t("fleet.newTask")} action=${{ onClick: () => setCreating(true) }} />
130
134
  ${tasks.length ? html`<ul class="rows">${tasks.map(x => html`<li key=${x.id} class="row-item">
131
135
  <span class=${`pill ${x.status}`}>${x.status}</span><span class="grow">${x.title}</span>
132
136
  ${x.assignee ? html`<a class="link" href=${chatPath(x.assignee)}>${x.assignee}</a>` : null}
@@ -150,7 +154,7 @@ function Schedules({ lease }) {
150
154
  }
151
155
  return html`<${Loaded} state=${state}>${(d) => {
152
156
  const list = d.schedules || [];
153
- return html`<${ListHead} count=${t("fleet.countSchedules", list.length)} label=${t("fleet.newSchedule")} action=${{ onClick: () => setCreating(true) }} />
157
+ return html`<${ListHead} count=${list.length === 1 ? t("fleet.countSchedule") : t("fleet.countSchedules", list.length)} label=${t("fleet.newSchedule")} action=${{ onClick: () => setCreating(true) }} />
154
158
  ${list.length ? html`<ul class="rows">${list.map(x => html`<li key=${x.id} class="row-item">
155
159
  <span class=${`pill ${x.enabled ? "open" : "done"}`}>${x.enabled ? t("fleet.on") : t("fleet.off")}</span>
156
160
  <span class="grow">${x.label || cronDesc(x.cron)}</span><span class="mono muted" title=${cronDesc(x.cron)}>${x.cron}</span>
@@ -174,7 +178,7 @@ function Teams({ lease }) {
174
178
  }
175
179
  return html`<${Loaded} state=${state}>${(d) => {
176
180
  const entries = Object.entries(d.teams || {});
177
- return html`<${ListHead} count=${t("fleet.countTeams", entries.length)} label=${t("fleet.newTeam")} action=${{ onClick: () => setCreating(true) }} />
181
+ return html`<${ListHead} count=${entries.length === 1 ? t("fleet.countTeam") : t("fleet.countTeams", entries.length)} label=${t("fleet.newTeam")} action=${{ onClick: () => setCreating(true) }} />
178
182
  ${entries.length ? html`<ul class="rows">${entries.map(([name, team]) => html`<li key=${name} class="row-item">
179
183
  <span class="grow strong">${name}</span><span class="muted">${(team.members || []).join(", ")}</span>
180
184
  <button type="button" class="btn btn-ghost btn-sm danger" onClick=${() => del(name)}>${t("fleet.delete")}</button></li>`)}</ul>`
@@ -191,6 +195,16 @@ function Org({ lease }) {
191
195
  return html`<${Loaded} state=${state}>${(org) => html`<${OrgChart} org=${org} app=${app} />`}</${Loaded}>`;
192
196
  }
193
197
 
198
+ /** #1468: the prompt-cache analysis — one read per opening and per window chosen; Refresh reads again (fleet-cache.js). */
199
+ function Cache({ lease }) {
200
+ const [win, setWin] = useState("7d");
201
+ const [version, setVersion] = useState(0);
202
+ const state = useLoad(lease, `/ui/cache?window=${win}`, version);
203
+ return html`<${Loaded} state=${state}>${(report) => (report && Array.isArray(report.instances)
204
+ ? html`<${CacheReport} report=${report} win=${win} setWin=${setWin} refresh=${() => setVersion((v) => v + 1)} />`
205
+ : html`<${ErrorState} onRetry=${renavigate} />`)}</${Loaded}>`;
206
+ }
207
+
194
208
  function Secret({ id, value, onInput, label }) {
195
209
  const [shown, setShown] = useState(false);
196
210
  return html`<div class="field-row"><input id=${id} type=${shown ? "text" : "password"} value=${value} onInput=${onInput} autocomplete="off" />
@@ -345,7 +359,7 @@ function CreateTeam({ onClose, onDone }) {
345
359
  </${FormDialog}>`;
346
360
  }
347
361
 
348
- /** "New instance": the sidebar's ✎ opens it from anywhere (#1408 step 3 merges it with Settings' create flow). */
362
+ /** "New instance": the sidebar's + opens it from anywhere (#1408 step 3 merges it with Settings' create flow). */
349
363
  export function CreateInstanceDialog({ onClose }) {
350
364
  const lease = useLease("create-instance");
351
365
  const [backends, setBackends] = useState(null);
@@ -12,7 +12,7 @@ import { html, useEffect, useMemo, useRef, useState } from "/assets/app-html.js"
12
12
  import { t } from "/assets/app-i18n.js";
13
13
  import { appStore, useStore } from "/assets/app-store.js";
14
14
  import { useLease } from "/assets/app-ctx.js";
15
- import { PanelHeader, setTitle } from "/assets/app-shell.js";
15
+ import { PanelHeader, PendingChanges, setTitle } from "/assets/app-shell.js";
16
16
  import { navigate, setLeaveGuard } from "/assets/app-nav.js";
17
17
  import { settingsPath, SETTINGS_SECTIONS } from "/assets/app-route.js";
18
18
  import { Dialog } from "/assets/ui-dialog.js";
@@ -25,7 +25,7 @@ import { api, attach, confirmedWrite, newKey } from "./settings-confirm.js";
25
25
  import { dismissOperation, operationActive, restartFleet, startOperation, takeLeftover } from "./settings-apply.js";
26
26
  import {
27
27
  ACCESS_MODES, AGENT_MODES, DEFAULT_SCHEMA, LOG_LEVELS, TOOL_PROGRESS, TOOL_SETS, VISIBILITY_MODES, batchImpact,
28
- changedFields, channelId, channelsOf, chLabel, effectiveSummary, fleetModel, fromYaml, fullModelRequests, groupAgents,
28
+ changedFields, channelId, channelsOf, chLabel, connectionState, effectiveSummary, fleetModel, fromYaml, fullModelRequests, groupAgents,
29
29
  hasOwn, impactOf, localeOptions, defaultsImpact, requestImpact, nonNegative, positive, replyGuardSupported, shortName, STATUS_ORDER, toYaml,
30
30
  visibilityDefault, worstImpact,
31
31
  } from "./settings-model.js";
@@ -35,6 +35,7 @@ import {
35
35
  } from "./settings-dialogs.js";
36
36
  import { SetupWizard } from "./settings-wizard.js";
37
37
  import { CreateInstanceDialog } from "./panel-fleet.js";
38
+ import { FirstRunCard, takeWizardRequest } from "./first-run.js";
38
39
 
39
40
  const tn = (k, ...v) => t(`settings.${k}`, ...v);
40
41
  const ICONS = { agents: "bot", bots: "plug", classic: "room", general: "sliders", advanced: "code" };
@@ -66,7 +67,8 @@ async function loadAll(lease) {
66
67
  const connections = await read(lease, "/api/settings/connections");
67
68
  if (!lease.current()) return null;
68
69
  out.connections = connections.ok && Array.isArray(connections.body) ? connections.body : [];
69
- const secrets = await read(lease, "/api/settings/provider-secrets");
70
+ // Only where the fleet offers them (the schema says so): otherwise the route is a 404 and there is nothing to show.
71
+ const secrets = out.schema && out.schema.provider_secrets === false ? { ok: false } : await read(lease, "/api/settings/provider-secrets");
70
72
  if (!lease.current()) return null;
71
73
  out.providerOk = secrets.ok;
72
74
  out.providerSecrets = secrets.ok && Array.isArray(secrets.body) ? secrets.body : [];
@@ -196,6 +198,8 @@ export function SettingsPanel({ route, navKey }) {
196
198
  };
197
199
 
198
200
  const ctx = data && !data.error ? makeCtx(data, setData, stage, unstage, reload, reloadLive, () => mount.current()) : null;
201
+ // #1519 P7: "Connect a chat app" on the first-run card elsewhere came here for the wizard: it opens once the data is in.
202
+ useEffect(() => { if (ctx && takeWizardRequest()) setDialog({ kind: "wizard", key: navKey }); }, [!!ctx, navKey]);
199
203
  const sectionBody = !data ? html`<${Skeleton} lines=${6} />`
200
204
  : data.error ? html`<${ErrorState} message=${tn("configLoadFailed")} onRetry=${reload} />`
201
205
  : section === "agents" ? html`<${Agents} ctx=${ctx} search=${search} openDialog=${(d) => setDialog({ ...d, key: navKey })} />`
@@ -218,6 +222,8 @@ export function SettingsPanel({ route, navKey }) {
218
222
  ${searchable ? html`<div class="s-toolbar"><label class="s-search"><${Icon} name="search" size=${14} /><span class="sr-only">${tn("searchAll")}</span>
219
223
  <input type="search" value=${search} placeholder=${tn("searchAll")} onInput=${(e) => setSearch(e.target.value)} /></label></div>` : null}
220
224
  ${op ? html`<${OperationCard} op=${op} schema=${data && data.schema} />` : null}
225
+ <${PendingChanges} inline=${true} />
226
+ ${ctx && (section === "agents" || section === "bots") ? html`<${FirstRunCard} onConnect=${() => setDialog({ kind: "wizard", key: navKey })} />` : null}
221
227
  ${sectionBody}
222
228
  </div></div>
223
229
  ${staged.size ? html`<${PendingBar} staged=${staged} busy=${opBusy(op)} onApply=${apply} onDiscard=${discard} />` : null}
@@ -329,7 +335,7 @@ function Agents({ ctx, search, openDialog }) {
329
335
  setBusy((b) => new Map(b).set(name, action));
330
336
  try { await work(); } finally { setBusy((b) => { const n = new Map(b); n.delete(name); return n; }); }
331
337
  };
332
- const head = html`<div class="list-head"><span>${tn("countAgents", Object.keys(insts).length)}</span>
338
+ const head = html`<div class="list-head"><span>${Object.keys(insts).length === 1 ? tn("countAgent") : tn("countAgents", Object.keys(insts).length)}</span>
333
339
  <button type="button" class="btn btn-primary" onClick=${() => openDialog({ kind: "create" })}><${Icon} name="plus" size=${16} />${tn("newAgent")}</button></div>`;
334
340
  if (!Object.keys(insts).length) return html`${head}<${Empty} icon="bot" title=${tn("noAgents")} />`;
335
341
  if (!groups.length) return html`${head}<${Empty} icon="search" title=${tn("noAgentMatch", search.trim())} />`;
@@ -350,6 +356,7 @@ function Agents({ ctx, search, openDialog }) {
350
356
  <span class=${dotClass(st)} title=${tn(STATUS_KEY[st] || "stStopped")} aria-hidden="true"></span>
351
357
  <span class="s-name" title=${name}>${shortName(name, inst)}</span>
352
358
  <span class="s-meta" title=${sum.backend}>${sum.model}</span>
359
+ <span class="s-br" aria-hidden="true"></span>
353
360
  <span class="tag">${tn("effortTag", sum.effort)}</span>
354
361
  ${ch ? html`<span class=${`tag${ch.includes("persona") ? " persona" : ""}`}>${ch}</span>` : null}
355
362
  <span class="sr-only">${tn(STATUS_KEY[st] || "stStopped")}</span>
@@ -370,12 +377,17 @@ function Bots({ ctx, search, openDialog }) {
370
377
  const all = channelsOf(ctx.fleet);
371
378
  const needle = search.trim().toLowerCase();
372
379
  const rows = all.map((ch, i) => ({ ch, i })).filter(({ ch }) => !needle || [ch.id, ch.type, ch.group_id, ch.bot_token_env].some((v) => String(v ?? "").toLowerCase().includes(needle)));
373
- const head = html`<div class="list-head"><span>${tn("countBots", all.length)}</span>
380
+ const head = html`<div class="list-head"><span>${all.length === 1 ? tn("countBot") : tn("countBots", all.length)}</span>
374
381
  <button type="button" class="btn btn-primary" onClick=${() => openDialog({ kind: "newBot" })}><${Icon} name="plus" size=${16} />${tn("newBot")}</button></div>`;
375
382
  return html`${head}
376
383
  ${!rows.length ? html`<${Empty} icon="plug" title=${needle ? tn("noAgentMatch", search.trim()) : tn("noBots")} />` : html`<div class="s-list">${rows.map(({ ch, i }) => {
377
384
  const users = (ch.access && ch.access.allowed_users) || [];
378
- const token = (ctx.connections.find((c, j) => channelId(c, j) === channelId(ch, i)) || {}).token_present;
385
+ const meta = ctx.connections.find((c, j) => channelId(c, j) === channelId(ch, i)) || {};
386
+ const token = meta.token_present;
387
+ // #1519 P6: this connection's own state (not the fleet's "Connected") — a stopped one says so; a known problem says
388
+ // what to do (P3: Message Content intent; a rejected token).
389
+ const state = connectionState(meta, ctx.fleetUp);
390
+ const stateText = tn(state.key, state.bot);
379
391
  // #1408 step 5: one line on a desktop — who it is, its facts in one clipped line (all of them in its tooltip),
380
392
  // then the cluster; on a phone the facts go under it. It no longer wraps fact by fact.
381
393
  const facts = [ch.bot_token_env || tn("noTokenEnv"),
@@ -383,13 +395,13 @@ function Bots({ ctx, search, openDialog }) {
383
395
  tn("accessTag", (ch.access && ch.access.mode) || "locked"),
384
396
  users.length ? `${users.slice(0, 3).join(", ")}${users.length > 3 ? ` +${users.length - 3}` : ""}` : tn("noAllowedUsers")].filter(Boolean).join(" · ");
385
397
  return html`<div key=${channelId(ch, i)} class="s-row s-conn">
386
- <span class="s-ident"><span class=${`dot ${ctx.fleetUp ? "ok" : "bad"}`} title=${ctx.fleetUp ? tn("connected") : tn("problem")} aria-hidden="true"></span>
398
+ <span class="s-ident"><span class=${`dot ${state.cls}`} title=${stateText} aria-hidden="true"></span>
387
399
  <span class="s-name">${ch.type === "telegram" ? "Telegram" : "Discord"}</span>
388
400
  <span class=${`tag${i === 0 ? "" : " persona"}`}>${ch.id || chLabel(i)}</span></span>
389
401
  <span class="s-facts" title=${facts}>${facts}</span>
390
402
  <span class="s-actions">
391
403
  <span class=${`tag${token ? "" : " warn"}`}>${token ? tn("tokenConfigured") : tn("tokenMissing")}</span>
392
- <span class=${`s-state ${ctx.fleetUp ? "ok" : "bad"}`}>${ctx.fleetUp ? tn("connected") : tn("problem")}</span>
404
+ <span class=${`s-state ${state.cls}`} title=${stateText}>${stateText}</span>
393
405
  <button type="button" class="btn btn-sm" onClick=${() => openDialog({ kind: "bot", id: channelId(ch, i) })}>${tn("settingsButton")}</button></span>
394
406
  </div>`;
395
407
  })}</div>`}
@@ -464,7 +476,7 @@ function Classic({ ctx, search, openDialog }) {
464
476
  .sort((a, b) => ((STATUS_ORDER[ctx.classicRuntime(a.instanceName).status] ?? 9) - (STATUS_ORDER[ctx.classicRuntime(b.instanceName).status] ?? 9))
465
477
  || String(a.name || "").localeCompare(String(b.name || "")));
466
478
  if (!list.length) return html`<${Empty} icon="search" title=${tn("noAgentMatch", search.trim())} />`;
467
- return html`<div class="list-head"><span>${tn("countRooms", rooms.length)}</span></div><div class="s-list">${list.map((c) => {
479
+ return html`<div class="list-head"><span>${rooms.length === 1 ? tn("countRoom") : tn("countRooms", rooms.length)}</span></div><div class="s-list">${list.map((c) => {
468
480
  const rt = ctx.classicRuntime(c.instanceName);
469
481
  const sum = effectiveSummary(ctx.live[c.instanceName] || {}, c, ctx.fleet.defaults || {}, (ctx.classic && ctx.classic.defaults) || {});
470
482
  const adapter = c.adapterId || "discord";
@@ -472,6 +484,7 @@ function Classic({ ctx, search, openDialog }) {
472
484
  <span class=${dotClass(rt.execution)} title=${tn(`exec_${rt.execution}`)} aria-hidden="true"></span>
473
485
  <span class="s-name" title=${c.instanceName || ""}>${shortName(c.name || c.instanceName || "?", { display_name: c.display_name })}</span>
474
486
  <span class="s-meta" title=${sum.backend}>${sum.model}</span>
487
+ <span class="s-br" aria-hidden="true"></span>
475
488
  <span class="tag">${tn("effortTag", sum.effort)}</span>
476
489
  <span class=${`tag${adapter !== "discord" ? " persona" : ""}`}>${adapter}</span>
477
490
  ${c.collab ? html`<span class="tag">collab</span>` : null}
@@ -666,10 +679,10 @@ function Developer({ ctx }) {
666
679
  <div class="seg-inline" role="group" aria-label=${tn("format")}>
667
680
  ${["yaml", "json"].map((k) => html`<button key=${k} type="button" class=${`btn btn-sm${fmt === k ? " on" : ""}`} aria-pressed=${fmt === k ? "true" : "false"} onClick=${() => setFmt(k)}>${k.toUpperCase()}</button>`)}</div>
668
681
  <button type="button" class="btn btn-sm" onClick=${() => setEditing(!editing)}>${editing ? tn("viewMode") : tn("editMode")}</button>
669
- <button type="button" class="btn btn-sm" onClick=${copy}><${Icon} name="copy" size=${14} />${tn("copy")}</button>
670
- <button type="button" class="btn btn-sm" onClick=${download}><${Icon} name="download" size=${14} />fleet.yaml</button>
682
+ <button type="button" class="btn btn-sm" title=${tn("copy")} onClick=${copy}><${Icon} name="copy" size=${14} /><span class="lbl">${tn("copy")}</span></button>
683
+ <button type="button" class="btn btn-sm" title=${tn("devDownload")} onClick=${download}><${Icon} name="download" size=${14} /><span class="lbl">${tn("devDownload")}</span></button>
671
684
  </div>
672
- ${editing ? html`<textarea class="s-yaml" aria-label=${tn("devEditor")} spellcheck="false" value=${draft} onInput=${(e) => setDraft(e.target.value)}></textarea>
685
+ ${editing ? html`<textarea class="s-yaml" aria-label=${tn("devEditor")} spellcheck="false" wrap="off" value=${draft} onInput=${(e) => setDraft(e.target.value)}></textarea>
673
686
  <div class="save-row"><button type="button" class="btn btn-primary" onClick=${save}>${tn("devApply")}</button><span class="note">${tn("devApplyHint")}</span></div>`
674
687
  : html`<pre class="s-yaml">${text}</pre>`}
675
688
  ${msg ? html`<p class=${`feedback${msg.error ? " error" : ""}`} role="status">${msg.error || msg.text}</p>` : null}
@@ -695,7 +708,8 @@ function OperationCard({ op, schema }) {
695
708
  const phaseBusy = ["writing", "posting", "watching"].includes(op.phase);
696
709
  const job = op.job;
697
710
  const rows = (job && job.targets) || [];
698
- const fleetRestarting = job && job.status === "running" && rows.some((r) => r.target === "fleet" && r.status === "running");
711
+ // A fleet row of kind "hot" is connections starting without a restart (#1519 P6), not AgEnD restarting.
712
+ const fleetRestarting = job && job.status === "running" && rows.some((r) => r.target === "fleet" && r.status === "running" && r.kind !== "hot");
699
713
  const needsRestart = rows.some((r) => r.status === "restart-required");
700
714
  const failedRows = rows.some((r) => r.status === "failed");
701
715
  const mismatch = schema && schema.fleet_signature_mismatch;
@@ -728,11 +742,11 @@ function OperationCard({ op, schema }) {
728
742
  ${op.steps.length ? html`<ul class="s-op-steps">${op.steps.map((s, i) => html`<li key=${i} class=${`s-step ${s.status}`}>
729
743
  <span class="s-step-icon" aria-hidden="true">${icon(s.status) ? html`<${Icon} name=${icon(s.status)} size=${14} />` : "·"}</span>
730
744
  <span class="grow">${s.label}</span>
731
- <span class="note">${s.status === "waiting" ? tn("stepWaiting") : s.status === "skipped" ? tn("stepSkipped") : s.status === "failed" ? (s.error || tn("applyFailed")) : impactText(s.impact)}</span></li>`)}</ul>` : null}
745
+ <span class="note">${s.status === "waiting" ? tn("stepWaiting") : s.status === "skipped" ? tn("stepSkipped") : s.status === "failed" ? (s.error || tn("applyFailed")) : s.note || impactText(s.impact)}</span></li>`)}</ul>` : null}
732
746
  ${rows.length ? html`<ul class="s-op-steps">${rows.map((r) => html`<li key=${r.target} class=${`s-step ${r.status}`}>
733
747
  <span class="s-step-icon" aria-hidden="true">${r.status === "done" ? html`<${Icon} name="check" size=${14} />` : r.status === "failed" ? html`<${Icon} name="alert" size=${14} />` : r.status === "restart-required" ? html`<${Icon} name="restart" size=${14} />` : "·"}</span>
734
748
  <span class="grow">${r.target === "fleet" ? "AgEnD" : shortName(r.target, {})}</span>
735
- <span class="note">${r.target === "fleet" && r.status === "running" ? tn("restartFleetTitle") : r.status === "restart-required" ? tn("applyRestartNeeded")
749
+ <span class="note">${r.target === "fleet" && r.status === "running" ? tn(r.kind === "hot" ? "connStartingNow" : "restartFleetTitle") : r.status === "restart-required" ? tn("applyRestartNeeded")
736
750
  : r.settled_by === "no-change" ? tn("applyNoChange") : r.settled_by === "fleet-restart" ? tn("viaRestart") : r.kind === "hot" ? tn("impactNow") : tn("impactAgent")}</span>
737
751
  ${r.error ? html`<span class="feedback error">${r.error}</span>` : null}</li>`)}</ul>` : null}
738
752
  ${job && job.error ? html`<p class="feedback error">${job.error}</p>` : null}