@songsid/agend 2.1.12-beta.9 → 2.2.0-alpha.1

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 (358) hide show
  1. package/README.md +2 -2
  2. package/README.zh-TW.md +2 -2
  3. package/dist/auth/one-time-code.d.ts +19 -0
  4. package/dist/auth/one-time-code.js +49 -0
  5. package/dist/auth/one-time-code.js.map +1 -0
  6. package/dist/auth-api.d.ts +52 -0
  7. package/dist/auth-api.js +346 -0
  8. package/dist/auth-api.js.map +1 -0
  9. package/dist/backend/claude-code.d.ts +4 -0
  10. package/dist/backend/claude-code.js +97 -0
  11. package/dist/backend/claude-code.js.map +1 -1
  12. package/dist/backend/codex.js +59 -30
  13. package/dist/backend/codex.js.map +1 -1
  14. package/dist/backend/kiro-agent.d.ts +66 -0
  15. package/dist/backend/kiro-agent.js +302 -0
  16. package/dist/backend/kiro-agent.js.map +1 -0
  17. package/dist/backend/kiro-identity.d.ts +80 -0
  18. package/dist/backend/kiro-identity.js +537 -0
  19. package/dist/backend/kiro-identity.js.map +1 -0
  20. package/dist/backend/kiro-v2-store-worker.d.ts +1 -0
  21. package/dist/backend/kiro-v2-store-worker.js +87 -0
  22. package/dist/backend/kiro-v2-store-worker.js.map +1 -0
  23. package/dist/backend/kiro-v2-store.d.ts +54 -0
  24. package/dist/backend/kiro-v2-store.js +131 -0
  25. package/dist/backend/kiro-v2-store.js.map +1 -0
  26. package/dist/backend/kiro-v3-identity.d.ts +5 -0
  27. package/dist/backend/kiro-v3-identity.js +22 -0
  28. package/dist/backend/kiro-v3-identity.js.map +1 -1
  29. package/dist/backend/kiro.d.ts +83 -7
  30. package/dist/backend/kiro.js +418 -91
  31. package/dist/backend/kiro.js.map +1 -1
  32. package/dist/backend/types.d.ts +45 -3
  33. package/dist/backend/types.js.map +1 -1
  34. package/dist/channel/adapters/discord.d.ts +1 -0
  35. package/dist/channel/adapters/discord.js +29 -10
  36. package/dist/channel/adapters/discord.js.map +1 -1
  37. package/dist/channel/adapters/telegram.d.ts +1 -0
  38. package/dist/channel/adapters/telegram.js +13 -1
  39. package/dist/channel/adapters/telegram.js.map +1 -1
  40. package/dist/channel/types.d.ts +9 -1
  41. package/dist/classic-channel-manager.d.ts +18 -10
  42. package/dist/classic-channel-manager.js +45 -18
  43. package/dist/classic-channel-manager.js.map +1 -1
  44. package/dist/cli.js +225 -127
  45. package/dist/cli.js.map +1 -1
  46. package/dist/command-table.d.ts +5 -3
  47. package/dist/command-table.js +29 -27
  48. package/dist/command-table.js.map +1 -1
  49. package/dist/config-validator.js +46 -0
  50. package/dist/config-validator.js.map +1 -1
  51. package/dist/config.js +12 -0
  52. package/dist/config.js.map +1 -1
  53. package/dist/connection-secrets.d.ts +6 -0
  54. package/dist/connection-secrets.js +4 -0
  55. package/dist/connection-secrets.js.map +1 -1
  56. package/dist/daemon.d.ts +89 -4
  57. package/dist/daemon.js +661 -77
  58. package/dist/daemon.js.map +1 -1
  59. package/dist/darwin-memory.d.ts +6 -1
  60. package/dist/darwin-memory.js +17 -6
  61. package/dist/darwin-memory.js.map +1 -1
  62. package/dist/delivery-consumed-watch.d.ts +85 -0
  63. package/dist/delivery-consumed-watch.js +244 -0
  64. package/dist/delivery-consumed-watch.js.map +1 -0
  65. package/dist/delivery-outbox.d.ts +61 -0
  66. package/dist/delivery-outbox.js +120 -2
  67. package/dist/delivery-outbox.js.map +1 -1
  68. package/dist/delivery-reconciliation.d.ts +14 -5
  69. package/dist/delivery-reconciliation.js +27 -4
  70. package/dist/delivery-reconciliation.js.map +1 -1
  71. package/dist/delivery-reconciliation.test.js +35 -1
  72. package/dist/delivery-reconciliation.test.js.map +1 -1
  73. package/dist/event-loop-watch.d.ts +28 -0
  74. package/dist/event-loop-watch.js +75 -3
  75. package/dist/event-loop-watch.js.map +1 -1
  76. package/dist/fleet-context.d.ts +22 -2
  77. package/dist/fleet-level-config.d.ts +2 -2
  78. package/dist/fleet-level-config.js +6 -1
  79. package/dist/fleet-level-config.js.map +1 -1
  80. package/dist/fleet-manager.d.ts +293 -12
  81. package/dist/fleet-manager.js +2364 -449
  82. package/dist/fleet-manager.js.map +1 -1
  83. package/dist/host-memory.d.ts +4 -0
  84. package/dist/host-memory.js.map +1 -1
  85. package/dist/instance-config-impact.js +5 -0
  86. package/dist/instance-config-impact.js.map +1 -1
  87. package/dist/instance-lifecycle.d.ts +10 -5
  88. package/dist/instance-lifecycle.js +162 -53
  89. package/dist/instance-lifecycle.js.map +1 -1
  90. package/dist/locale.js +150 -16
  91. package/dist/locale.js.map +1 -1
  92. package/dist/login-controller.d.ts +2 -0
  93. package/dist/login-controller.js +17 -5
  94. package/dist/login-controller.js.map +1 -1
  95. package/dist/memory-pressure.d.ts +7 -8
  96. package/dist/memory-pressure.js +76 -15
  97. package/dist/memory-pressure.js.map +1 -1
  98. package/dist/monotonic-budget.d.ts +2 -0
  99. package/dist/monotonic-budget.js +28 -0
  100. package/dist/monotonic-budget.js.map +1 -0
  101. package/dist/needs-you-hub.d.ts +114 -0
  102. package/dist/needs-you-hub.js +370 -0
  103. package/dist/needs-you-hub.js.map +1 -0
  104. package/dist/needs-you-live.d.ts +116 -0
  105. package/dist/needs-you-live.js +228 -0
  106. package/dist/needs-you-live.js.map +1 -0
  107. package/dist/needs-you.d.ts +132 -0
  108. package/dist/needs-you.js +164 -0
  109. package/dist/needs-you.js.map +1 -0
  110. package/dist/node-version-guard.d.ts +30 -0
  111. package/dist/node-version-guard.js +50 -0
  112. package/dist/node-version-guard.js.map +1 -0
  113. package/dist/operator-control-paths.d.ts +4 -0
  114. package/dist/operator-control-paths.js +24 -0
  115. package/dist/operator-control-paths.js.map +1 -0
  116. package/dist/outbound-handlers.d.ts +2 -0
  117. package/dist/outbound-handlers.js +9 -2
  118. package/dist/outbound-handlers.js.map +1 -1
  119. package/dist/outbound-schemas.d.ts +5 -5
  120. package/dist/profile-control.js +4 -20
  121. package/dist/profile-control.js.map +1 -1
  122. package/dist/public-web-gateway.d.ts +18 -0
  123. package/dist/public-web-gateway.js +141 -0
  124. package/dist/public-web-gateway.js.map +1 -0
  125. package/dist/public-web-link.d.ts +53 -0
  126. package/dist/public-web-link.js +175 -0
  127. package/dist/public-web-link.js.map +1 -0
  128. package/dist/quickstart-api.d.ts +8 -1
  129. package/dist/quickstart-api.js +86 -48
  130. package/dist/quickstart-api.js.map +1 -1
  131. package/dist/quickstart.d.ts +1 -0
  132. package/dist/quickstart.js +6 -2
  133. package/dist/quickstart.js.map +1 -1
  134. package/dist/scheduler/db.d.ts +7 -1
  135. package/dist/scheduler/db.js +38 -0
  136. package/dist/scheduler/db.js.map +1 -1
  137. package/dist/scheduler/index.d.ts +1 -1
  138. package/dist/scheduler/retry-label.d.ts +11 -0
  139. package/dist/scheduler/retry-label.js +24 -0
  140. package/dist/scheduler/retry-label.js.map +1 -0
  141. package/dist/scheduler/scheduler.d.ts +55 -2
  142. package/dist/scheduler/scheduler.js +220 -8
  143. package/dist/scheduler/scheduler.js.map +1 -1
  144. package/dist/scheduler/types.d.ts +22 -0
  145. package/dist/scheduler/types.js.map +1 -1
  146. package/dist/secret-store.d.ts +5 -0
  147. package/dist/secret-store.js +20 -0
  148. package/dist/secret-store.js.map +1 -1
  149. package/dist/service-activation.d.ts +110 -0
  150. package/dist/service-activation.js +302 -0
  151. package/dist/service-activation.js.map +1 -0
  152. package/dist/service-installer.d.ts +7 -0
  153. package/dist/service-installer.js +7 -4
  154. package/dist/service-installer.js.map +1 -1
  155. package/dist/service-restart-selection.d.ts +6 -5
  156. package/dist/service-restart-selection.js +6 -5
  157. package/dist/service-restart-selection.js.map +1 -1
  158. package/dist/settings-api.d.ts +10 -2
  159. package/dist/settings-api.js +175 -72
  160. package/dist/settings-api.js.map +1 -1
  161. package/dist/settings-baseline.d.ts +20 -0
  162. package/dist/settings-baseline.js +104 -0
  163. package/dist/settings-baseline.js.map +1 -0
  164. package/dist/settings-change.d.ts +21 -0
  165. package/dist/settings-change.js +133 -0
  166. package/dist/settings-change.js.map +1 -0
  167. package/dist/settings-confirmation.d.ts +105 -0
  168. package/dist/settings-confirmation.js +232 -0
  169. package/dist/settings-confirmation.js.map +1 -0
  170. package/dist/settings-control.d.ts +41 -0
  171. package/dist/settings-control.js +294 -0
  172. package/dist/settings-control.js.map +1 -0
  173. package/dist/settings-effect.d.ts +24 -0
  174. package/dist/settings-effect.js +169 -0
  175. package/dist/settings-effect.js.map +1 -0
  176. package/dist/settings-http-confirmation.d.ts +29 -0
  177. package/dist/settings-http-confirmation.js +123 -0
  178. package/dist/settings-http-confirmation.js.map +1 -0
  179. package/dist/settings-instance-patch.d.ts +4 -0
  180. package/dist/settings-instance-patch.js +26 -0
  181. package/dist/settings-instance-patch.js.map +1 -0
  182. package/dist/settings-request-capability.d.ts +16 -0
  183. package/dist/settings-request-capability.js +32 -0
  184. package/dist/settings-request-capability.js.map +1 -0
  185. package/dist/settings-transaction.d.ts +65 -0
  186. package/dist/settings-transaction.js +223 -0
  187. package/dist/settings-transaction.js.map +1 -0
  188. package/dist/setup-auth.d.ts +10 -18
  189. package/dist/setup-auth.js +14 -38
  190. package/dist/setup-auth.js.map +1 -1
  191. package/dist/setup-form.d.ts +1 -1
  192. package/dist/setup-form.js +75 -18
  193. package/dist/setup-form.js.map +1 -1
  194. package/dist/setup-guide.d.ts +13 -0
  195. package/dist/setup-guide.js +25 -0
  196. package/dist/setup-guide.js.map +1 -0
  197. package/dist/setup-host.d.ts +16 -0
  198. package/dist/setup-host.js +197 -32
  199. package/dist/setup-host.js.map +1 -1
  200. package/dist/setup-wizard.js +3 -0
  201. package/dist/setup-wizard.js.map +1 -1
  202. package/dist/slash-authz.d.ts +17 -3
  203. package/dist/slash-authz.js +5 -0
  204. package/dist/slash-authz.js.map +1 -1
  205. package/dist/spawn-gate.js +3 -3
  206. package/dist/spawn-gate.js.map +1 -1
  207. package/dist/statusline-watcher.d.ts +5 -0
  208. package/dist/statusline-watcher.js +2 -0
  209. package/dist/statusline-watcher.js.map +1 -1
  210. package/dist/steer-capability.d.ts +6 -0
  211. package/dist/steer-capability.js +8 -0
  212. package/dist/steer-capability.js.map +1 -1
  213. package/dist/sync-work-attribution.d.ts +14 -0
  214. package/dist/sync-work-attribution.js +43 -8
  215. package/dist/sync-work-attribution.js.map +1 -1
  216. package/dist/tips.js +10 -5
  217. package/dist/tips.js.map +1 -1
  218. package/dist/tmux-control.d.ts +35 -5
  219. package/dist/tmux-control.js +291 -57
  220. package/dist/tmux-control.js.map +1 -1
  221. package/dist/tmux-manager.d.ts +20 -6
  222. package/dist/tmux-manager.js +90 -47
  223. package/dist/tmux-manager.js.map +1 -1
  224. package/dist/tmux-read.d.ts +59 -0
  225. package/dist/tmux-read.js +213 -0
  226. package/dist/tmux-read.js.map +1 -0
  227. package/dist/topic-commands.d.ts +21 -10
  228. package/dist/topic-commands.js +191 -58
  229. package/dist/topic-commands.js.map +1 -1
  230. package/dist/transcript-delta-reader.js +2 -1
  231. package/dist/transcript-delta-reader.js.map +1 -1
  232. package/dist/tunnel/cloudflared-install.d.ts +2 -0
  233. package/dist/tunnel/cloudflared-install.js +1 -1
  234. package/dist/tunnel/cloudflared-install.js.map +1 -1
  235. package/dist/tunnel/purpose-lane.d.ts +14 -0
  236. package/dist/tunnel/purpose-lane.js +43 -0
  237. package/dist/tunnel/purpose-lane.js.map +1 -0
  238. package/dist/turn-reply-guard.d.ts +2 -0
  239. package/dist/turn-reply-guard.js +10 -1
  240. package/dist/turn-reply-guard.js.map +1 -1
  241. package/dist/types.d.ts +33 -0
  242. package/dist/ui/app.html +18 -0
  243. package/dist/ui/chat-render.js +478 -0
  244. package/dist/ui/chat-store.js +289 -0
  245. package/dist/ui/chat-strings.js +69 -0
  246. package/dist/ui/chat-thread.js +209 -0
  247. package/dist/ui/chat-tour.js +116 -0
  248. package/dist/ui/fleet-org.js +142 -0
  249. package/dist/ui/panel-chat.js +418 -0
  250. package/dist/ui/panel-fleet.js +413 -0
  251. package/dist/ui/panel-needs.js +202 -0
  252. package/dist/ui/panel-settings.js +750 -0
  253. package/dist/ui/preview.js +266 -0
  254. package/dist/ui/settings-apply.js +239 -0
  255. package/dist/ui/settings-confirm.js +186 -0
  256. package/dist/ui/settings-dialogs.js +546 -0
  257. package/dist/ui/settings-model.js +277 -0
  258. package/dist/ui/settings-strings.js +652 -0
  259. package/dist/ui/settings-wizard.js +140 -0
  260. package/dist/ui/shared/agend-auth.js +93 -0
  261. package/dist/ui/shared/app-ctx.js +80 -0
  262. package/dist/ui/shared/app-html.js +9 -0
  263. package/dist/ui/shared/app-i18n.js +95 -0
  264. package/dist/ui/shared/app-nav.js +73 -0
  265. package/dist/ui/shared/app-needs.js +114 -0
  266. package/dist/ui/shared/app-route.js +73 -0
  267. package/dist/ui/shared/app-session.js +66 -0
  268. package/dist/ui/shared/app-shell.js +344 -0
  269. package/dist/ui/shared/app-store.js +51 -0
  270. package/dist/ui/shared/app-stream.js +201 -0
  271. package/dist/ui/shared/app.css +676 -0
  272. package/dist/ui/shared/app.js +88 -0
  273. package/dist/ui/shared/fonts/OFL.txt +92 -0
  274. package/dist/ui/shared/fonts/README.md +21 -0
  275. package/dist/ui/shared/fonts/inter.woff2 +0 -0
  276. package/dist/ui/shared/htm.module.js +1 -0
  277. package/dist/ui/shared/panel-view.js +655 -0
  278. package/dist/ui/shared/preact-hooks.module.js +2 -0
  279. package/dist/ui/shared/preact.module.js +2 -0
  280. package/dist/ui/shared/signin.js +130 -0
  281. package/dist/ui/shared/theme.js +29 -0
  282. package/dist/ui/shared/tokens.css +153 -0
  283. package/dist/ui/shared/ui-confirm.js +43 -0
  284. package/dist/ui/shared/ui-dialog.js +45 -0
  285. package/dist/ui/shared/ui-icons.js +61 -0
  286. package/dist/ui/shared/ui-menu.js +39 -0
  287. package/dist/ui/shared/ui-states.js +20 -0
  288. package/dist/ui/shared/ui-toast.js +18 -0
  289. package/dist/ui/shared/vendor/htm.LICENSE +202 -0
  290. package/dist/ui/shared/vendor/preact.LICENSE +21 -0
  291. package/dist/ui/shared/vendor/vendor.json +39 -0
  292. package/dist/ui/shared/view-strings.js +309 -0
  293. package/dist/ui/signin.html +51 -0
  294. package/dist/update-check.d.ts +6 -2
  295. package/dist/update-check.js +12 -3
  296. package/dist/update-check.js.map +1 -1
  297. package/dist/update-install.d.ts +71 -0
  298. package/dist/update-install.js +162 -0
  299. package/dist/update-install.js.map +1 -0
  300. package/dist/upgrade-notices.d.ts +17 -0
  301. package/dist/upgrade-notices.js +93 -0
  302. package/dist/upgrade-notices.js.map +1 -0
  303. package/dist/usage/statusline-usage.d.ts +2 -0
  304. package/dist/usage/statusline-usage.js +14 -10
  305. package/dist/usage/statusline-usage.js.map +1 -1
  306. package/dist/view-api.d.ts +6 -1
  307. package/dist/view-api.js +62 -51
  308. package/dist/view-api.js.map +1 -1
  309. package/dist/web-api.d.ts +71 -14
  310. package/dist/web-api.js +492 -77
  311. package/dist/web-api.js.map +1 -1
  312. package/dist/web-auth.d.ts +71 -27
  313. package/dist/web-auth.js +169 -69
  314. package/dist/web-auth.js.map +1 -1
  315. package/dist/web-body.d.ts +3 -0
  316. package/dist/web-body.js +55 -0
  317. package/dist/web-body.js.map +1 -0
  318. package/dist/web-channel-echo.d.ts +9 -0
  319. package/dist/web-channel-echo.js +55 -0
  320. package/dist/web-channel-echo.js.map +1 -0
  321. package/dist/web-chat-history.d.ts +125 -0
  322. package/dist/web-chat-history.js +184 -0
  323. package/dist/web-chat-history.js.map +1 -0
  324. package/dist/web-continuation.d.ts +7 -0
  325. package/dist/web-continuation.js +22 -0
  326. package/dist/web-continuation.js.map +1 -0
  327. package/dist/web-host-guard.d.ts +34 -0
  328. package/dist/web-host-guard.js +57 -1
  329. package/dist/web-host-guard.js.map +1 -1
  330. package/dist/web-login.d.ts +72 -0
  331. package/dist/web-login.js +132 -0
  332. package/dist/web-login.js.map +1 -0
  333. package/dist/web-org.d.ts +55 -0
  334. package/dist/web-org.js +67 -0
  335. package/dist/web-org.js.map +1 -0
  336. package/dist/web-preview.d.ts +77 -0
  337. package/dist/web-preview.js +240 -0
  338. package/dist/web-preview.js.map +1 -0
  339. package/dist/web-request-context.d.ts +10 -0
  340. package/dist/web-request-context.js +13 -0
  341. package/dist/web-request-context.js.map +1 -0
  342. package/dist/web-session.d.ts +167 -0
  343. package/dist/web-session.js +389 -0
  344. package/dist/web-session.js.map +1 -0
  345. package/dist/web-shell-routes.d.ts +59 -0
  346. package/dist/web-shell-routes.js +96 -0
  347. package/dist/web-shell-routes.js.map +1 -0
  348. package/dist/web-upload.d.ts +183 -0
  349. package/dist/web-upload.js +468 -0
  350. package/dist/web-upload.js.map +1 -0
  351. package/dist/website-tips-renderer.d.ts +2 -2
  352. package/dist/website-tips-renderer.js +3 -3
  353. package/dist/website-tips-renderer.js.map +1 -1
  354. package/package.json +8 -5
  355. package/scripts/preinstall-guard.cjs +36 -0
  356. package/dist/ui/dashboard.html +0 -774
  357. package/dist/ui/settings.html +0 -2165
  358. package/dist/ui/view.html +0 -1139
@@ -0,0 +1,418 @@
1
+ // #1408 step 1: the chat panel. Behind the session gate (/ui/js/), loaded by the app with a dynamic import in "full"
2
+ // mode only. boot() runs once per page (the chat store must hear the stream from the first frame); <ChatPanel> mounts
3
+ // for /ui and /ui/chat/<name>.
4
+ //
5
+ // The look follows the design (§2): the conversation in a centred column, the person's messages as bubbles on the
6
+ // right, the agent's at full width, and one rounded composer pinned below with attach, Stop reply and Send inside it.
7
+ // The instance's own actions (details, start, stop, restart, delete) are in the header's ⋯ menu, and only the ones
8
+ // that apply to its state are offered (#1408 rough edges 4 and 7).
9
+ import { html, useEffect, useLayoutEffect, useRef, useState } from "/assets/app-html.js";
10
+ import { t } from "/assets/app-i18n.js";
11
+ import { appStore, useStore } from "/assets/app-store.js";
12
+ import { useLease } from "/assets/app-ctx.js";
13
+ import { PanelHeader, onPanelKey, setTitle, addFooterItem, statusClass, statusLabel, openDrawer } from "/assets/app-shell.js";
14
+ import { navigate } from "/assets/app-nav.js";
15
+ import { Menu } from "/assets/ui-menu.js";
16
+ import { Dialog } from "/assets/ui-dialog.js";
17
+ import { Empty, ErrorState, Skeleton } from "/assets/ui-states.js";
18
+ import { Icon } from "/assets/ui-icons.js";
19
+ import { toast } from "/assets/ui-toast.js";
20
+ import { confirmDialog } from "/assets/ui-confirm.js";
21
+ import "./chat-strings.js";
22
+ import { confirmedWrite } from "./settings-confirm.js";
23
+ import { createChatStore } from "./chat-store.js";
24
+ import { createThread } from "./chat-thread.js";
25
+ import { installTour, refreshTourSpot, startTour } from "./chat-tour.js";
26
+
27
+ const R = () => globalThis.AgendChatRender;
28
+ const P = () => globalThis.AgendPreview;
29
+
30
+ /** The page's chat store (boot creates it). Exported for the tests. */
31
+ export let store = null;
32
+
33
+ // ── Announcements: one polite, visually hidden line for the coarse events only (a reply arrived, the agent started,
34
+ // finished, or waits on you). Messages themselves are not read out as they arrive; the list is a log to browse.
35
+ let announceTimer = null;
36
+ function announce(text) {
37
+ let el = document.getElementById("announcer");
38
+ if (!el) { el = document.createElement("div"); el.id = "announcer"; el.className = "sr-only"; el.setAttribute("role", "status"); el.setAttribute("aria-live", "polite"); el.setAttribute("aria-atomic", "true"); document.body.appendChild(el); }
39
+ if (!text) return;
40
+ el.textContent = "";
41
+ clearTimeout(announceTimer);
42
+ announceTimer = setTimeout(() => { el.textContent = text; }, 60);
43
+ }
44
+
45
+ /**
46
+ * This device's preview opt-in: asks once (the app's dialog) and says what it means; turning it off stops every
47
+ * running preview. Resolves to the choice in force.
48
+ */
49
+ export async function setPreviewOptIn(on) {
50
+ if (on && !P().optedIn() && !(await confirmDialog({ message: t("chat.pvConfirm"), confirmLabel: t("chat.pvAllowButton") }))) on = false;
51
+ P().setOptIn(on);
52
+ return P().optedIn();
53
+ }
54
+
55
+ /** Once per page, in "full" mode: the store on the app's stream, previews (#1306), the sidebar's chat controls. */
56
+ export function boot({ stream, boot: bootData, deps = {} }) {
57
+ if (store) return store;
58
+ store = createChatStore({
59
+ fetch: (...a) => fetch(...a), toast, announce, t, setTimeout: (fn, ms) => setTimeout(fn, ms), ...deps,
60
+ });
61
+ store.attach(stream);
62
+ // #1306: what the server said about previews for this load. The page-wide message and storage listeners this adds
63
+ // belong to the app, once (they are never per mount).
64
+ if (P() && bootData) P().init(bootData);
65
+ addFooterItem("previews", PreviewOptIn);
66
+ addFooterItem("tour", TourButton);
67
+ installTour();
68
+ return store;
69
+ }
70
+
71
+ function PreviewOptIn() {
72
+ const [on, setOn] = useState(P() ? P().optedIn() : false);
73
+ useEffect(() => (P() ? P().onChange(() => setOn(P().optedIn())) : undefined), []);
74
+ return html`<label class="side-row toggle"><input type="checkbox" checked=${on} onChange=${(e) => { const want = e.target.checked; setPreviewOptIn(want).then(setOn); }} />
75
+ <span>${t("chat.previews")}</span></label>`;
76
+ }
77
+ function TourButton() {
78
+ return html`<button type="button" id="tourBtn" class="side-row" title=${t("chat.tourBtnTitle")} onClick=${startTour}><${Icon} name="info" /><span>${t("chat.tour")}</span></button>`;
79
+ }
80
+
81
+ // ── Code wrap: this browser's choice (agend_code_wrap). On a phone, code wraps whatever it is (app.css).
82
+ const stored = (k) => { try { return localStorage.getItem(k); } catch { return null; } };
83
+ let codeWrap = stored("agend_code_wrap") === "1";
84
+ const wrapListeners = new Set();
85
+ function toggleWrap() {
86
+ codeWrap = !codeWrap;
87
+ try { localStorage.setItem("agend_code_wrap", codeWrap ? "1" : "0"); } catch { /* this page only */ }
88
+ for (const fn of wrapListeners) fn(codeWrap);
89
+ }
90
+
91
+ async function copyText(text) {
92
+ try { if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(text); return true; } } catch { /* fall back */ }
93
+ const ta = document.createElement("textarea"); ta.value = text; ta.setAttribute("readonly", ""); ta.className = "offscreen";
94
+ document.body.appendChild(ta); ta.select();
95
+ let ok = false; try { ok = document.execCommand("copy"); } catch { ok = false; }
96
+ ta.remove();
97
+ return ok;
98
+ }
99
+ /** The HTML as a file: a blob of the source as application/octet-stream, saved as reply.html — never opened. */
100
+ function downloadHtml(code) {
101
+ 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";
103
+ document.body.appendChild(a); a.click(); a.remove();
104
+ URL.revokeObjectURL(url);
105
+ }
106
+
107
+ /** Re-render when the store changes for `name` (or for everyone). `kind` "msgs" is the thread's alone. */
108
+ function useChatTick(name, kinds) {
109
+ const [, tick] = useState(0);
110
+ useEffect(() => store.subscribe((instance, kind) => {
111
+ if ((instance === null || instance === name) && (!kinds || kinds.includes(kind || "state"))) tick(n => n + 1);
112
+ }), [name]);
113
+ }
114
+
115
+ // ── The panel ──
116
+
117
+ export function ChatPanel({ route, navKey }) {
118
+ const lease = useLease(navKey);
119
+ const app = useStore(appStore);
120
+ const name = route.instance;
121
+ const inst = name ? app.instances.find(i => i.name === name) : null;
122
+ useEffect(() => {
123
+ store.setCurrent(name && inst ? name : null);
124
+ return () => store.setCurrent(null);
125
+ }, [name, !!inst]);
126
+ useEffect(() => { setTitle(name || t("app.chat")); }, [name, navKey]);
127
+ if (!name) {
128
+ 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>`;
131
+ }
132
+ if (!app.ready) return html`<div class="panel p-chat"><${PanelHeader} title=${name} /><div class="panel-body"><${Skeleton} lines=${5} /></div></div>`;
133
+ if (!inst) return html`<${NotFound} name=${name} />`;
134
+ return html`<${ChatView} key=${name} name=${name} inst=${inst} lease=${lease} exec=${app.exec[name]}
135
+ awaiting=${Object.prototype.hasOwnProperty.call(app.awaiting, name) ? app.awaiting[name] : null} />`;
136
+ }
137
+
138
+ function NotFound({ name }) {
139
+ // A remembered chat that no longer exists is forgotten, so /ui does not come back here.
140
+ useEffect(() => { try { if (localStorage.getItem("agend_last_instance") === name) localStorage.removeItem("agend_last_instance"); } catch { /* none */ } }, [name]);
141
+ return html`<div class="panel p-chat"><${PanelHeader} title=${name} />
142
+ <div class="panel-body center"><${Empty} icon="alert" title=${t("chat.notFound", name)} hint=${t("chat.notFoundHint")}
143
+ action=${html`<button type="button" class="btn only-narrow" onClick=${openDrawer}>${t("chat.backToList")}</button>`} /></div></div>`;
144
+ }
145
+
146
+ function ChatView({ name, inst, lease, exec, awaiting }) {
147
+ const [dialog, setDialog] = useState(null); // "details" | "delete" | null
148
+ const [wrap, setWrap] = useState(codeWrap);
149
+ const view = useRef(null);
150
+ useEffect(() => { wrapListeners.add(setWrap); return () => wrapListeners.delete(setWrap); }, []);
151
+ // First visit to this instance's chat on this page: its history (a navigation — it counts as use). Back again: nothing.
152
+ useEffect(() => { store.openHistory(name, lease); }, [name]);
153
+ useEffect(() => { try { localStorage.setItem("agend_last_instance", name); } catch { /* none */ } }, [name]);
154
+ // Files dragged over the chat show where they go; a drop attaches them (counted, so moving over messages does not flicker).
155
+ useEffect(() => {
156
+ const el = view.current; if (!el) return undefined;
157
+ let depth = 0;
158
+ const hasFiles = (e) => !!(e.dataTransfer && [...e.dataTransfer.types].includes("Files"));
159
+ lease.on(el, "dragenter", (e) => { if (hasFiles(e)) { depth++; el.classList.add("dragging"); } });
160
+ lease.on(el, "dragover", (e) => { if (hasFiles(e)) e.preventDefault(); });
161
+ lease.on(el, "dragleave", (e) => { if (hasFiles(e) && --depth <= 0) { depth = 0; el.classList.remove("dragging"); } });
162
+ lease.on(el, "drop", (e) => { depth = 0; el.classList.remove("dragging"); if (e.dataTransfer && e.dataTransfer.files.length) { e.preventDefault(); store.addFiles(name, e.dataTransfer.files); } });
163
+ return undefined;
164
+ }, [lease]);
165
+ // Esc stops the agent's reply (as Esc in its terminal would): from the chat, while it works, and only when nothing
166
+ // else that Esc would close is open (a dialog, a menu, the Session menu).
167
+ useEffect(() => lease.hold(onPanelKey((e) => {
168
+ if (e.key !== "Escape" || e.isComposing || e.defaultPrevented) return;
169
+ if (!R().isBusy(store.state.exec[name]) || store.state.stopping[name] || store.state.cancelling[name]) return;
170
+ if (document.querySelector("dialog[open], .menu-list, .pop")) return;
171
+ const active = document.activeElement;
172
+ if (active && active !== document.body && !(view.current && view.current.contains(active))) return;
173
+ e.preventDefault();
174
+ store.cancelReply(name);
175
+ })), [lease]);
176
+ const running = inst.status === "running";
177
+ const action = async (verb) => {
178
+ let r;
179
+ try { const res = await fetch(`/ui/${verb}/${encodeURIComponent(name)}`, { method: "POST", headers: { "Content-Type": "application/json" } }); r = await res.json(); }
180
+ catch (err) { r = { error: err && err.message ? err.message : t("chat.disconnected") }; }
181
+ if (r && r.error) toast(r.error, false);
182
+ else toast(t(verb === "start" ? "chat.started" : verb === "stop" ? "chat.stoppedInst" : "chat.restarted", name));
183
+ };
184
+ const items = [
185
+ { key: "details", label: t("chat.details"), icon: "info", onSelect: () => setDialog("details") },
186
+ running ? null : { key: "start", label: t("chat.start"), icon: "play", onSelect: () => action("start") },
187
+ running ? { key: "restart", label: t("chat.restart"), icon: "restart", onSelect: () => action("restart") } : null,
188
+ running ? { key: "stop", label: t("chat.stopInstance"), icon: "stop", onSelect: () => action("stop") } : null,
189
+ { key: "delete", label: t("chat.delete"), icon: "trash", danger: true, onSelect: () => setDialog("delete") },
190
+ ];
191
+ 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>`;
193
+ return html`<div class=${`panel p-chat${wrap ? " wrap-code" : ""}`} ref=${view}>
194
+ <${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} />
197
+ <div class="drop-overlay" aria-hidden="true"><div class="drop-card"><${Icon} name="attach" size=${32} /><span>${t("chat.dropHere")}</span></div></div>
198
+ ${dialog === "details" ? html`<${DetailsDialog} name=${name} onClose=${() => setDialog(null)} />` : null}
199
+ ${dialog === "delete" ? html`<${DeleteDialog} name=${name} onClose=${() => setDialog(null)} />` : null}
200
+ </div>`;
201
+ }
202
+
203
+ /** 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);
206
+ const [jump, setJump] = useState({ show: false, unseen: 0 });
207
+ const [empty, setEmpty] = useState(false);
208
+ useLayoutEffect(() => {
209
+ const thread = createThread(list.current, scroller.current, {
210
+ t, tf: t, isUser: (x) => store.isUser(x, name), onJump: setJump, onEmpty: setEmpty,
211
+ setPreviewOptIn, copyText, download: downloadHtml, toggleWrap,
212
+ });
213
+ th.current = thread;
214
+ thread.render(store.state.msgs[name] || [], { restore: store.state.scrollMemo[name] ?? null });
215
+ const off = store.subscribe((instance, kind) => {
216
+ if (instance !== name) return;
217
+ if (kind === "msgs") thread.render(store.state.msgs[name] || []);
218
+ else if (kind === "sent") thread.jumpLatest();
219
+ });
220
+ const offPv = P() ? P().onChange(() => thread.refreshCards()) : () => {};
221
+ const onScroll = () => { store.state.scrollMemo[name] = thread.onScroll(); };
222
+ const sc = scroller.current, ls = list.current;
223
+ sc.addEventListener("scroll", onScroll, { passive: true });
224
+ ls.addEventListener("click", thread.onClick);
225
+ return () => {
226
+ off(); offPv();
227
+ sc.removeEventListener("scroll", onScroll);
228
+ ls.removeEventListener("click", thread.onClick);
229
+ thread.dispose(); // the whole thread goes: every preview in it stops first
230
+ th.current = null;
231
+ };
232
+ }, [name]);
233
+ return html`<div class="scroller" ref=${scroller}>
234
+ <div class="thread" ref=${list} role="log" aria-live="off" aria-label=${t("chat.conversationWith", name)}></div>
235
+ ${empty ? html`<${Empty} icon="chat" title=${t("chat.noMessages")} hint=${t("chat.sendFirst")} />` : null}
236
+ ${jump.show ? html`<button type="button" class="jump-latest" onClick=${() => th.current && th.current.jumpLatest()}>
237
+ <${Icon} name="down" size=${16} />${jump.unseen > 0 ? t("chat.newBelow", jump.unseen) : t("chat.jumpLatest")}</button>` : null}
238
+ </div>`;
239
+ }
240
+
241
+ /** Everything under the thread: prompts, the working line, a failed send, files waiting, and the composer. */
242
+ function Dock({ name, lease, exec, awaiting }) {
243
+ useChatTick(name, ["state"]);
244
+ const s = store.state;
245
+ return html`<div class="dock"><div class="dock-col">
246
+ <${Prompts} name=${name} />
247
+ <${WorkBar} name=${name} lease=${lease} exec=${exec} awaiting=${awaiting} />
248
+ ${s.failedSends[name] ? html`<div class="failed-send" role="alert"><span>${t("chat.notSent")}</span><span class="txt">${s.failedSends[name]}</span>
249
+ <button type="button" class="btn btn-primary btn-sm" onClick=${() => store.putBack(name)}>${t("chat.putBack")}</button>
250
+ <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)} />
252
+ <p class="hint">${t("chat.composerHint")}</p>
253
+ </div></div>`;
254
+ }
255
+
256
+ function Prompts({ name }) {
257
+ const list = store.promptsFor(name);
258
+ if (!list.length) return null;
259
+ return html`<div class="prompts" aria-live="polite">${list.map(p => html`<div key=${p.nonce} class=${`prompt${p.resolved ? " done" : ""}`} role="group" aria-label=${p.text}>
260
+ <div class="txt">${p.resolved ? (p.outcome || t("chat.promptGone")) : p.text}</div>
261
+ ${p.resolved ? null : html`<div class="acts">${p.actions.map(a => html`<button key=${a.id} type="button" class="btn" disabled=${!!p.busy} onClick=${() => store.answerPrompt(p, a.id)}>${a.label}</button>`)}
262
+ ${p.busy ? html`<span class="wait">${t("chat.promptAnswering")}</span>` : null}</div>`}</div>`)}</div>`;
263
+ }
264
+
265
+ /** "<name> is working… 0:42", "looks stuck", "Stopping…", or what it waits on at its terminal. Text only. */
266
+ function WorkBar({ name, lease, exec, awaiting }) {
267
+ const s = store.state;
268
+ const busy = R().isBusy(exec) ? exec : "";
269
+ const kind = awaiting != null ? "awaiting" : s.stopping[name] && busy ? "stopping" : busy;
270
+ const [, tick] = useState(0);
271
+ useEffect(() => {
272
+ if (!kind || kind === "awaiting") return undefined;
273
+ const h = lease.interval(() => tick(n => n + 1), 1000); // ticks only while it shows, cleared with the lease
274
+ return () => lease.clear(h);
275
+ }, [kind, lease]);
276
+ if (!kind) return null;
277
+ const label = t(kind === "stuck" ? "chat.stuck" : kind === "stopping" ? "chat.stopping" : kind === "awaiting" ? "chat.awaiting" : "chat.working", name);
278
+ const since = s.workingSince[name];
279
+ return html`<div class=${`work-bar ${kind}`}><span class="pulse" aria-hidden="true"></span><span class="lbl">${label}</span>
280
+ ${kind === "awaiting" ? html`<span class="note" title=${t("chat.approxNote")}>${awaiting || t("chat.approxNote")}</span>`
281
+ : html`<span class="elapsed" title=${t("chat.elapsedTitle")} aria-hidden="true">${since == null ? "" : R().formatElapsed(performance.now() - since)}</span>`}</div>`;
282
+ }
283
+
284
+ function Composer({ name, busy }) {
285
+ const s = store.state;
286
+ const input = useRef(null), fileIn = useRef(null);
287
+ const [, tick] = useState(0);
288
+ const value = s.drafts[name] || "";
289
+ const files = s.pendingFiles[name] || [];
290
+ const content = !!value.trim() || files.length > 0;
291
+ // Grow with the text, up to a limit: set through the CSSOM, never a style attribute (#1300).
292
+ useLayoutEffect(() => { const el = input.current; if (!el) return; el.style.height = "auto"; el.style.height = `${Math.min(el.scrollHeight, 240)}px`; }, [value]);
293
+ useEffect(() => { if (input.current) input.current.focus(); }, [name]);
294
+ 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); };
298
+ // Files in a paste are attached; a very long text paste goes as a text file — only once it is really attached.
299
+ const onPaste = (e) => {
300
+ const list = e.clipboardData && e.clipboardData.files;
301
+ const text = e.clipboardData ? e.clipboardData.getData("text") : "";
302
+ if (list && list.length) { store.addFiles(name, list); if (!text) e.preventDefault(); return; }
303
+ if (R().isLongPaste(text) && store.attachPastedText(name, text)) e.preventDefault();
304
+ };
305
+ return html`<div class="composer-box">
306
+ ${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
+ <div class="composer">
308
+ <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
+ <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
+ onChange=${(e) => { store.addFiles(name, e.target.files); e.target.value = ""; }} />
311
+ <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>
313
+ <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>
315
+ <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>
317
+ </div></div>`;
318
+ }
319
+
320
+ function FileChip({ f, i, name }) {
321
+ const img = useRef(null);
322
+ const isImage = /^image\//.test(f.type) && typeof URL.createObjectURL === "function";
323
+ useEffect(() => {
324
+ if (!isImage || !img.current) return undefined;
325
+ const url = URL.createObjectURL(f);
326
+ img.current.src = url;
327
+ return () => URL.revokeObjectURL(url);
328
+ }, [f]);
329
+ return html`<span class="file-chip">
330
+ ${isImage ? html`<img ref=${img} alt="" />` : html`<span class="ic"><${Icon} name="file" size=${16} /></span>`}
331
+ <span class="nm">${f.name}</span><span class="sz">${R().formatSize(f.size)}</span>
332
+ ${store.isPasted(f) ? html`<button type="button" class="as-text" onClick=${() => store.fileBackAsText(name, i)}>${t("chat.asText")}</button>` : null}
333
+ <button type="button" class="chip-x" title=${t("chat.removeFile")} aria-label=${`${t("chat.removeFile")} ${f.name}`} onClick=${() => store.removeFile(name, i)}><${Icon} name="close" size=${14} /></button></span>`;
334
+ }
335
+
336
+ // ── Dialogs ──
337
+
338
+ function DetailsDialog({ name, onClose }) {
339
+ const lease = useLease(`details:${name}`);
340
+ const [d, setD] = useState(null);
341
+ const [failed, setFailed] = useState(false);
342
+ const body = useRef(null);
343
+ useEffect(() => {
344
+ (async () => {
345
+ try {
346
+ const r = await lease.fetch(`/ui/instance/${encodeURIComponent(name)}`, { headers: { "Content-Type": "application/json" } });
347
+ if (!lease.current()) return;
348
+ const j = await r.json();
349
+ if (!lease.current()) return;
350
+ setD(j);
351
+ } catch { if (lease.current()) setFailed(true); }
352
+ })();
353
+ }, [lease]);
354
+ // Bars are sized after they are drawn, through the CSSOM (never a style attribute).
355
+ useLayoutEffect(() => { if (body.current) for (const f of body.current.querySelectorAll(".progress-fill[data-pct]")) f.style.width = `${f.dataset.pct}%`; });
356
+ const src = (s) => (s === "instance" ? ` ${t("chat.configured")}` : s === "fleet-default" ? ` ${t("chat.fleetDefault")}` : "");
357
+ const bar = (pct, level) => html`<span class="progress-bar"><span class=${`progress-fill${level ? ` ${level}` : ""}`} data-pct=${Math.max(0, Math.min(Number(pct) || 0, 100))}></span></span>`;
358
+ const row = (k, v) => html`<div class="kv"><span class="k">${k}</span><span class="v">${v}</span></div>`;
359
+ let content;
360
+ if (failed) content = html`<${ErrorState} message=${t("chat.loadFailed")} />`;
361
+ else if (!d) content = html`<${Skeleton} lines=${6} />`;
362
+ else {
363
+ const sl = d.statusline || {}, cost = sl.cost?.total_cost_usd ?? 0, ctx = d.context_pct;
364
+ const rl5 = sl.rate_limits?.five_hour?.used_percentage ?? 0, rl7 = sl.rate_limits?.seven_day?.used_percentage ?? 0;
365
+ content = html`<section class="card"><h3>${t("chat.dInstance")}</h3>
366
+ ${row(t("chat.dName"), d.name)}${row(t("chat.dStatus"), d.status)}${row(t("chat.dDisplay"), d.display_name || "--")}
367
+ ${row(t("chat.dDescription"), d.description || "--")}${row(t("chat.dDirectory"), html`<span class="mono">${d.working_directory}</span>`)}</section>
368
+ <section class="card"><h3>${t("chat.dRuntime")}</h3>
369
+ ${row(t("chat.dBackend"), d.backend || "--")}
370
+ ${row(t("chat.dModel"), `${d.model || sl.model?.display_name || "--"}${d.model_source === "live" ? "" : src(d.model_source)}`)}
371
+ ${d.effort ? row(t("chat.dEffort"), `${d.effort}${src(d.effort_source)}`) : null}
372
+ ${row(t("chat.dCost"), `$${cost.toFixed(2)} ${t("chat.dCostNote")}`)}
373
+ ${row(t("chat.dContext"), html`<span class="pct">${ctx != null ? `${Math.round(ctx)}%` : "--"}</span>${ctx != null ? bar(ctx, ctx > 80 ? "warn" : "") : null}`)}
374
+ ${row(t("chat.dRate5"), html`<span class="pct">${Math.round(rl5)}%</span>${bar(rl5, rl5 > 90 ? "error" : rl5 > 70 ? "warn" : "")}`)}
375
+ ${row(t("chat.dRate7"), html`<span class="pct">${Math.round(rl7)}%</span>${bar(rl7, rl7 > 90 ? "error" : rl7 > 70 ? "warn" : "")}`)}</section>
376
+ ${d.recent_activity?.length ? html`<section class="card"><h3>${t("chat.dActivity")}</h3>${d.recent_activity.map((a, i) => html`<div key=${i} class="activity">
377
+ <span class="mono muted">${a.timestamp ? new Date(`${a.timestamp.replace(" ", "T")}Z`).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : ""}</span>
378
+ <span class="ev">${a.event}</span> ${a.summary || ""}</div>`)}</section>` : null}`;
379
+ }
380
+ return html`<${Dialog} title=${t("chat.details")} onClose=${onClose} wide=${true}><div ref=${body}>${content}</div></${Dialog}>`;
381
+ }
382
+
383
+ function DeleteDialog({ name, onClose }) {
384
+ // The dialog's own lease: it ends when the dialog goes (closed, or the chat left for another page). The delete
385
+ // may still finish on the server, and says so, but a dialog that is gone never closes or navigates the page the
386
+ // person is on now (#1425 review).
387
+ const lease = useLease(`delete:${name}`);
388
+ const [typed, setTyped] = useState("");
389
+ const [busy, setBusy] = useState(false);
390
+ const want = `delete ${name}`;
391
+ async function go() {
392
+ if (typed !== want || busy) return;
393
+ setBusy(true);
394
+ let r, handedOver = false;
395
+ // Deleting an instance may need a fleet admin's confirmation (#1423): then the dialog goes at once and the shell
396
+ // follows the request. From then on the dialog is done: the decision is said in a toast, and it never closes or
397
+ // navigates anything (the person may be anywhere by then).
398
+ try {
399
+ const res = await confirmedWrite(`/ui/instances/${encodeURIComponent(name)}/delete`, { method: "POST", body: { confirm: typed }, label: t("chat.deleteTitle", name),
400
+ onPending: () => { handedOver = true; toast(t("app.pendingSent")); if (lease.current()) { setBusy(false); onClose(); } } });
401
+ r = res.ok ? res.body || {} : { error: (res.body && res.body.error) || `HTTP ${res.status}` };
402
+ } catch (err) { r = { error: err && err.message ? err.message : t("chat.disconnected") }; }
403
+ if (r && r.error) { toast(r.error, false); if (!handedOver && lease.current()) setBusy(false); return; }
404
+ toast(t("chat.instanceDeleted", name));
405
+ if (handedOver || !lease.current()) return;
406
+ setBusy(false);
407
+ onClose();
408
+ navigate("/ui");
409
+ }
410
+ return html`<${Dialog} title=${t("chat.deleteTitle", name)} onClose=${onClose} busy=${busy}
411
+ actions=${html`<button type="button" class="btn" onClick=${onClose} disabled=${busy}>${t("chat.cancel")}</button>
412
+ <button type="button" class="btn btn-danger" disabled=${typed !== want || busy} onClick=${go}>${t("chat.deleteGo")}</button>`}>
413
+ <p>${t("chat.deleteHelp", name)}</p>
414
+ <label class="field"><span class="sr-only">${t("chat.deleteHelp", name)}</span>
415
+ <input type="text" value=${typed} autocomplete="off" spellcheck="false" placeholder=${want} onInput=${(e) => setTyped(e.target.value)}
416
+ onKeyDown=${(e) => { if (e.key === "Enter") go(); }} /></label>
417
+ </${Dialog}>`;
418
+ }