zidane-ui 9.1.13 → 9.1.16

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 (290) hide show
  1. package/dist/adapters/index.d.ts +2 -2
  2. package/dist/adapters/index.js +2 -2
  3. package/dist/adapters/live.d.ts +29 -3
  4. package/dist/adapters/live.js +9 -1
  5. package/dist/adapters/server-composer-host.js +17 -2
  6. package/dist/adapters/server-extensions-host.d.ts +0 -1
  7. package/dist/adapters/server-extensions-host.js +4 -15
  8. package/dist/adapters/server-git-host.js +7 -16
  9. package/dist/adapters/server-host.js +181 -43
  10. package/dist/adapters/server-machines-host.d.ts +2 -0
  11. package/dist/adapters/server-machines-host.js +22 -23
  12. package/dist/adapters/server-onboarding-host.d.ts +1 -1
  13. package/dist/adapters/server-onboarding-host.js +3 -4
  14. package/dist/adapters/server-session-host.js +22 -3
  15. package/dist/adapters/server-settings-accounts.js +2 -2
  16. package/dist/adapters/server-settings-host.js +2 -2
  17. package/dist/adapters/server-settings-performance.js +7 -9
  18. package/dist/adapters/server-settings-sections.js +13 -14
  19. package/dist/adapters/server-sidebar-host.js +2 -2
  20. package/dist/adapters/sse-host.js +68 -3
  21. package/dist/adapters/sse-protocol.d.ts +12 -2
  22. package/dist/adapters/sse-protocol.js +4 -2
  23. package/dist/app/AppHostProvider.js +4 -3
  24. package/dist/app/ModalProvider.d.ts +13 -3
  25. package/dist/app/ModalProvider.js +103 -24
  26. package/dist/app/NoticeStack.d.ts +33 -0
  27. package/dist/app/NoticeStack.js +90 -0
  28. package/dist/app/index.d.ts +3 -2
  29. package/dist/app/index.js +3 -2
  30. package/dist/chat/SessionFilterMenu.d.ts +1 -1
  31. package/dist/chat/SessionFilterMenu.js +2 -1
  32. package/dist/chat/composer/Composer.js +10 -8
  33. package/dist/chat/composer/ModelChainList.js +17 -8
  34. package/dist/chat/composer/ModelChainTooltip.js +5 -2
  35. package/dist/chat/composer/use-draft-target.d.ts +20 -1
  36. package/dist/chat/composer/use-draft-target.js +121 -5
  37. package/dist/chat/composer/use-new-session-defaults.d.ts +5 -1
  38. package/dist/chat/composer/use-new-session-defaults.js +17 -4
  39. package/dist/chat/headless/chip-labels.d.ts +1 -0
  40. package/dist/chat/headless/chip-labels.js +1 -0
  41. package/dist/chat/headless/composer/labels-editor.d.ts +0 -2
  42. package/dist/chat/headless/composer/labels-editor.js +0 -2
  43. package/dist/chat/headless/draft-target.d.ts +21 -1
  44. package/dist/chat/headless/fixture-host.js +17 -2
  45. package/dist/chat/headless/host.d.ts +3 -3
  46. package/dist/chat/headless/index.d.ts +9 -9
  47. package/dist/chat/headless/index.js +2 -2
  48. package/dist/chat/headless/labels.d.ts +13 -6
  49. package/dist/chat/headless/labels.js +10 -9
  50. package/dist/chat/headless/session-list.d.ts +20 -8
  51. package/dist/chat/headless/session-list.js +1 -0
  52. package/dist/chat/headless/sidebar/directory.d.ts +56 -14
  53. package/dist/chat/headless/sidebar/directory.js +149 -70
  54. package/dist/chat/headless/sidebar/index.d.ts +3 -3
  55. package/dist/chat/headless/sidebar/index.js +2 -2
  56. package/dist/chat/headless/sidebar/labels.d.ts +15 -8
  57. package/dist/chat/headless/sidebar/types.d.ts +3 -1
  58. package/dist/chat/headless/sidebar/view.d.ts +13 -5
  59. package/dist/chat/headless/sidebar/view.js +31 -11
  60. package/dist/chat/headless/streams/actions.d.ts +5 -3
  61. package/dist/chat/headless/streams/actions.js +3 -2
  62. package/dist/chat/headless/streams/index.d.ts +3 -3
  63. package/dist/chat/headless/streams/merge.d.ts +6 -10
  64. package/dist/chat/headless/streams/merge.js +7 -21
  65. package/dist/chat/headless/streams/session-streams.d.ts +29 -4
  66. package/dist/chat/headless/streams/session-streams.js +86 -0
  67. package/dist/chat/headless/streams/store.d.ts +162 -16
  68. package/dist/chat/headless/streams/store.js +334 -38
  69. package/dist/chat/headless/streams/types.d.ts +10 -7
  70. package/dist/chat/headless/transcript-labels.d.ts +1 -0
  71. package/dist/chat/interactions/PlanCard.js +10 -3
  72. package/dist/chat/interactions/QuestionForm.js +7 -3
  73. package/dist/chat/interactions/card-ui.d.ts +5 -3
  74. package/dist/chat/interactions/card-ui.js +9 -7
  75. package/dist/chat/rich-composer/RichComposer.js +1 -7
  76. package/dist/chat/rich-composer/composer-extensions.d.ts +0 -2
  77. package/dist/chat/rich-composer/composer-extensions.js +2 -2
  78. package/dist/chat/rich-composer/use-composer-editor.d.ts +0 -2
  79. package/dist/chat/rich-composer/use-composer-editor.js +1 -2
  80. package/dist/chat/sidebar/SessionSidebar.js +5 -5
  81. package/dist/chat/sidebar/SidebarFilterMenu.js +3 -1
  82. package/dist/chat/sidebar/SidebarFrame.d.ts +4 -0
  83. package/dist/chat/sidebar/SidebarFrame.js +27 -9
  84. package/dist/chat/sidebar/SidebarGroupHeader.js +1 -1
  85. package/dist/chat/sidebar/SidebarHostProvider.d.ts +8 -5
  86. package/dist/chat/sidebar/SidebarHostProvider.js +13 -9
  87. package/dist/chat/sidebar/SidebarToggleButton.js +7 -3
  88. package/dist/chat/sidebar/index.d.ts +2 -2
  89. package/dist/chat/sidebar/session-menu.js +4 -4
  90. package/dist/chat/sidebar/useSaveViewAsSpace.js +2 -2
  91. package/dist/chat/testing/contract.js +2 -2
  92. package/dist/chat/transcript/AuthFailureCard.js +11 -8
  93. package/dist/chat/transcript/BadgeMenu.js +13 -2
  94. package/dist/chat/transcript/MermaidDiagram.js +7 -3
  95. package/dist/chat/transcript/fold.d.ts +4 -0
  96. package/dist/chat/transcript/fold.js +53 -22
  97. package/dist/chat/transcript/script-input.d.ts +1 -1
  98. package/dist/chat/transcript/shell-block.js +7 -3
  99. package/dist/chat/transcript/useStickToBottom.js +12 -0
  100. package/dist/components/BrowserSignIn.js +23 -21
  101. package/dist/components/Form.d.ts +6 -4
  102. package/dist/components/Form.js +16 -8
  103. package/dist/components/LoadError.d.ts +4 -2
  104. package/dist/components/LoadError.js +13 -9
  105. package/dist/components/Notice.d.ts +29 -0
  106. package/dist/components/Notice.js +63 -0
  107. package/dist/components/PanelEmpty.d.ts +1 -1
  108. package/dist/components/PanelEmpty.js +4 -2
  109. package/dist/components/Restricted.d.ts +1 -1
  110. package/dist/components/palette/PaletteList.js +8 -1
  111. package/dist/components/palette/PalettePanel.js +14 -6
  112. package/dist/core.css +24 -5
  113. package/dist/dist/{index-Bq87OcGy.d.ts → index-BZM8tu4L.d.ts} +28 -3
  114. package/dist/dist/index.d.ts +2 -2
  115. package/dist/dist/{timeout-B5pO5I5a.d.ts → timeout-Cyx3VGPz.d.ts} +1 -1
  116. package/dist/extensions/ExtensionSettingsPage.js +1 -4
  117. package/dist/extensions/ExtensionSettingsSection.js +4 -3
  118. package/dist/extensions/ExtensionSettingsSidebar.js +2 -6
  119. package/dist/extensions/SettingsWireForm.d.ts +1 -7
  120. package/dist/extensions/SettingsWireForm.js +7 -5
  121. package/dist/extensions/headless/fixture-host.d.ts +1 -5
  122. package/dist/extensions/headless/fixture-host.js +1 -4
  123. package/dist/extensions/headless/host.d.ts +1 -11
  124. package/dist/extensions/headless/index.d.ts +3 -3
  125. package/dist/extensions/headless/index.js +2 -2
  126. package/dist/extensions/headless/select.d.ts +1 -11
  127. package/dist/extensions/headless/select.js +1 -18
  128. package/dist/files/EditorColumn.js +12 -1
  129. package/dist/files/TreeColumn.js +13 -20
  130. package/dist/files/useOpenFiles.js +2 -0
  131. package/dist/git/BranchMenu.js +5 -3
  132. package/dist/git/ImportRepoDialog.js +18 -9
  133. package/dist/git/MoveSessionDialog.js +15 -7
  134. package/dist/git/RemoteFolderPicker.js +34 -16
  135. package/dist/git/SyncChip.js +17 -17
  136. package/dist/git/WherePicker.d.ts +16 -1
  137. package/dist/git/WherePicker.js +80 -36
  138. package/dist/git/headless/environment.d.ts +33 -3
  139. package/dist/git/headless/environment.js +33 -5
  140. package/dist/git/headless/index.d.ts +2 -2
  141. package/dist/git/headless/index.js +2 -2
  142. package/dist/git/headless/labels.d.ts +15 -2
  143. package/dist/git/headless/labels.js +17 -4
  144. package/dist/git/useEnvironmentChoices.d.ts +3 -1
  145. package/dist/git/useEnvironmentChoices.js +6 -3
  146. package/dist/index.d.ts +2 -1
  147. package/dist/index.js +2 -1
  148. package/dist/labels.d.ts +5 -0
  149. package/dist/labels.js +5 -0
  150. package/dist/machines/AddMachineDialog.d.ts +34 -0
  151. package/dist/machines/AddMachineDialog.js +464 -0
  152. package/dist/machines/Banners.d.ts +33 -4
  153. package/dist/machines/Banners.js +64 -3
  154. package/dist/machines/DaemonMenu.js +27 -4
  155. package/dist/machines/HostCapabilities.d.ts +1 -3
  156. package/dist/machines/HostCapabilities.js +24 -45
  157. package/dist/machines/HostSyncPanel.js +123 -129
  158. package/dist/machines/HostsSection.d.ts +4 -1
  159. package/dist/machines/HostsSection.js +30 -7
  160. package/dist/machines/MachinesExplainer.d.ts +17 -0
  161. package/dist/machines/MachinesExplainer.js +71 -0
  162. package/dist/machines/MachinesProvider.d.ts +14 -2
  163. package/dist/machines/MachinesProvider.js +35 -1
  164. package/dist/machines/ServerAddedCard.d.ts +31 -0
  165. package/dist/machines/ServerAddedCard.js +72 -0
  166. package/dist/machines/ServerCard.js +91 -215
  167. package/dist/machines/ServerMachinesPanel.d.ts +2 -2
  168. package/dist/machines/ServerMachinesPanel.js +184 -323
  169. package/dist/machines/ServerUpdatePanel.js +51 -40
  170. package/dist/machines/ShareThisComputerForm.d.ts +19 -0
  171. package/dist/machines/ShareThisComputerForm.js +133 -0
  172. package/dist/machines/TokensPanel.d.ts +5 -3
  173. package/dist/machines/TokensPanel.js +98 -78
  174. package/dist/machines/headless/fixture-host.d.ts +19 -3
  175. package/dist/machines/headless/fixture-host.js +42 -5
  176. package/dist/machines/headless/fixtures.js +4 -6
  177. package/dist/machines/headless/host.d.ts +40 -19
  178. package/dist/machines/headless/index.d.ts +3 -3
  179. package/dist/machines/headless/index.js +2 -2
  180. package/dist/machines/headless/labels.d.ts +63 -20
  181. package/dist/machines/headless/labels.js +88 -25
  182. package/dist/machines/headless/messages.js +8 -0
  183. package/dist/machines/headless/stores.d.ts +9 -2
  184. package/dist/machines/headless/stores.js +28 -1
  185. package/dist/machines/index.d.ts +7 -3
  186. package/dist/machines/index.js +8 -4
  187. package/dist/machines/testing/contract.d.ts +15 -3
  188. package/dist/machines/testing/contract.js +68 -14
  189. package/dist/motion/grow.d.ts +8 -1
  190. package/dist/motion/grow.js +10 -4
  191. package/dist/onboarding/demos/index.js +1 -1
  192. package/dist/onboarding/headless/labels.d.ts +13 -0
  193. package/dist/onboarding/headless/labels.js +8 -0
  194. package/dist/onboarding/index.js +3 -3
  195. package/dist/onboarding/sources/SourceBrowser.js +11 -7
  196. package/dist/onboarding/steps/ConnectorsStep.js +1 -1
  197. package/dist/onboarding/steps/ImportStep.js +4 -1
  198. package/dist/onboarding/steps/ReadyStep.js +9 -2
  199. package/dist/onboarding/steps/SkillsStep.js +1 -1
  200. package/dist/onboarding/steps/connect/KeyForm.js +6 -2
  201. package/dist/onboarding/steps/connect/ProviderPanel.js +6 -2
  202. package/dist/onboarding/steps/connect/ProviderReady.js +25 -20
  203. package/dist/onboarding/steps/tasks.js +17 -14
  204. package/dist/server/sse-handler.js +141 -19
  205. package/dist/session/ArtifactsPanel.js +39 -17
  206. package/dist/session/PlanTracker.js +12 -7
  207. package/dist/session/ProjectHome.js +13 -11
  208. package/dist/session/RemindersPanel.js +34 -16
  209. package/dist/session/RunningWorkDialog.js +6 -2
  210. package/dist/session/SecretsPanel.js +29 -10
  211. package/dist/session/SessionDetailsDialog.d.ts +1 -1
  212. package/dist/session/SessionDetailsDialog.js +18 -10
  213. package/dist/session/SideChat.js +10 -7
  214. package/dist/session/TurnDetailsDialog.js +19 -5
  215. package/dist/session/UsagePanel.js +10 -4
  216. package/dist/session/headless/host.d.ts +2 -0
  217. package/dist/session/headless/labels.d.ts +19 -1
  218. package/dist/session/headless/labels.js +15 -1
  219. package/dist/session/headless/queue.js +2 -1
  220. package/dist/session/headless/reminder-labels.d.ts +4 -0
  221. package/dist/session/headless/reminder-labels.js +4 -0
  222. package/dist/settings/SettingsRestricted.d.ts +1 -1
  223. package/dist/settings/SettingsRestricted.js +4 -4
  224. package/dist/settings/SettingsShell.d.ts +7 -0
  225. package/dist/settings/SettingsShell.js +6 -1
  226. package/dist/settings/headless/fixture-performance.d.ts +1 -1
  227. package/dist/settings/headless/host.d.ts +2 -2
  228. package/dist/settings/headless/labels.d.ts +3 -3
  229. package/dist/settings/headless/labels.js +3 -3
  230. package/dist/settings/headless/performance.d.ts +3 -4
  231. package/dist/settings/headless/search.d.ts +1 -1
  232. package/dist/settings/index.d.ts +2 -1
  233. package/dist/settings/index.js +2 -1
  234. package/dist/settings/sections/editor/EditorSection.js +7 -4
  235. package/dist/settings/sections/editor/labels.d.ts +1 -0
  236. package/dist/settings/sections/editor/labels.js +1 -0
  237. package/dist/settings/sections/environments/EnvironmentsSection.js +19 -12
  238. package/dist/settings/sections/environments/labels.d.ts +1 -0
  239. package/dist/settings/sections/environments/labels.js +2 -1
  240. package/dist/settings/sections/extensions/ExtensionConfigRows.d.ts +1 -3
  241. package/dist/settings/sections/extensions/ExtensionConfigRows.js +20 -19
  242. package/dist/settings/sections/extensions/ExtensionOptionsSection.js +2 -1
  243. package/dist/settings/sections/extensions/options-labels.d.ts +2 -1
  244. package/dist/settings/sections/extensions/options-labels.js +2 -1
  245. package/dist/settings/sections/index.d.ts +2 -1
  246. package/dist/settings/sections/index.js +2 -1
  247. package/dist/settings/sections/logs/LogsSection.d.ts +2 -2
  248. package/dist/settings/sections/logs/LogsSection.js +2 -2
  249. package/dist/settings/sections/mcp/McpSection.d.ts +1 -1
  250. package/dist/settings/sections/mcp/McpSection.js +12 -6
  251. package/dist/settings/sections/mcp/McpServerRow.js +20 -13
  252. package/dist/settings/sections/mcp/labels.d.ts +8 -2
  253. package/dist/settings/sections/mcp/labels.js +8 -2
  254. package/dist/settings/sections/models/ModelRows.js +1 -1
  255. package/dist/settings/sections/models/NewChatPlaceRow.d.ts +26 -0
  256. package/dist/settings/sections/models/NewChatPlaceRow.js +78 -0
  257. package/dist/settings/sections/models/SpacesDefaultsSection.js +15 -3
  258. package/dist/settings/sections/models/labels.d.ts +7 -0
  259. package/dist/settings/sections/models/labels.js +7 -0
  260. package/dist/settings/sections/models/model-test.js +7 -4
  261. package/dist/settings/sections/performance/PerformanceSection.js +16 -6
  262. package/dist/settings/sections/performance/labels.d.ts +1 -1
  263. package/dist/settings/sections/performance/labels.js +1 -1
  264. package/dist/settings/sections/providers/ProvidersSection.d.ts +1 -1
  265. package/dist/settings/sections/providers/ProvidersSection.js +14 -7
  266. package/dist/settings/sections/providers/ServerProvidersSection.d.ts +1 -1
  267. package/dist/settings/sections/providers/ServerProvidersSection.js +11 -5
  268. package/dist/settings/sections/providers/labels.d.ts +2 -2
  269. package/dist/settings/sections/providers/labels.js +2 -2
  270. package/dist/settings/sections/skills/SkillList.js +1 -1
  271. package/dist/settings/sections/skills/SkillsSection.js +1 -1
  272. package/dist/settings/testing/performance.contract.js +8 -12
  273. package/dist/styles.css +32 -3
  274. package/dist/terminal/HostTerminalSurface.js +15 -5
  275. package/dist/terminal/TerminalPanel.js +30 -17
  276. package/dist/terminal/TerminalSurface.js +11 -4
  277. package/dist/terminal/headless/labels.d.ts +5 -0
  278. package/dist/terminal/headless/labels.js +5 -0
  279. package/dist/terminal/headless/store.d.ts +1 -1
  280. package/dist/terminal/headless/store.js +6 -3
  281. package/dist/workspace/LayoutBudget.d.ts +5 -1
  282. package/dist/workspace/LayoutBudget.js +14 -1
  283. package/dist/workspace/PanelContentView.d.ts +1 -1
  284. package/dist/workspace/PanelContentView.js +17 -6
  285. package/dist/workspace/WorkspaceProvider.js +3 -2
  286. package/dist/workspace/headless/labels.d.ts +1 -0
  287. package/dist/workspace/headless/labels.js +1 -0
  288. package/package.json +3 -3
  289. package/src/styles/motion.css +8 -0
  290. package/src/styles/primitives.css +7 -0
@@ -0,0 +1,464 @@
1
+ import { useLabels } from "../labels.js";
2
+ import { IconButton } from "../components/IconButton.js";
3
+ import { BTN_PRIMARY, BTN_SECONDARY } from "../components/button.js";
4
+ import { CopyButton } from "../components/CopyButton.js";
5
+ import { Notice } from "../components/Notice.js";
6
+ import { Segmented } from "../components/Segmented.js";
7
+ import { FIELD_LABEL_CLASS, INPUT_CLASS, MetaChip, OpLine } from "../components/Form.js";
8
+ import { SegmentSpinner } from "../components/SegmentSpinner.js";
9
+ import { useMinuteTick } from "../hooks/useMinuteTick.js";
10
+ import { constant, useReadable } from "../chat/headless/store.js";
11
+ import { machinesError } from "./headless/host.js";
12
+ import { useMachinesBridge, useMachinesHost, useMachinesLabels, usePermitted } from "./MachinesProvider.js";
13
+ import { useOptionalModal } from "../app/ModalProvider.js";
14
+ import { ShareThisComputerForm } from "./ShareThisComputerForm.js";
15
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
16
+ import { useEffect, useMemo, useState } from "react";
17
+ import { isValidMachineName, machineInstallCommand, machineSetupCommand } from "zidane/chat/pure";
18
+ //#region src/machines/AddMachineDialog.tsx
19
+ /**
20
+ * Add a machine to a server, in one dialog that follows the machine until
21
+ * it joins:
22
+ *
23
+ * - **Another computer** (the host's `machines.invite`): an optional name,
24
+ * then the command to run there, with the time the code has left. The
25
+ * dialog then waits on the server's machine list and turns into "<name>
26
+ * joined <server>" with Start a chat on it (the host's `newChat`). An
27
+ * ended code greys out and offers a new one.
28
+ * - **This computer** (the host's `links.shareStart`): share this computer
29
+ * with the server, in folders you pick.
30
+ *
31
+ * `useAddMachine` opens it above whatever is open (the composer's where
32
+ * menu, Settings).
33
+ */
34
+ const TITLE_ID = "add-machine-title";
35
+ const NO_LIST = constant({
36
+ status: "idle",
37
+ machines: [],
38
+ error: null
39
+ });
40
+ const NO_HOSTS = constant({
41
+ enabled: true,
42
+ hosts: []
43
+ });
44
+ /** An address that other computers usually cannot reach: loopback, a private network, a `.local` name. */
45
+ function isLocalAddress(url) {
46
+ let host;
47
+ try {
48
+ host = new URL(url).hostname.replace(/^\[|\]$/g, "");
49
+ } catch {
50
+ return false;
51
+ }
52
+ return host === "localhost" || host === "::1" || host.endsWith(".local") || /^127\./.test(host) || /^10\./.test(host) || /^192\.168\./.test(host) || /^172\.(1[6-9]|2\d|3[01])\./.test(host);
53
+ }
54
+ /** Minutes a code has left, rounded up; 0 once it ended. */
55
+ function minutesLeft(expiresAt) {
56
+ return Math.max(0, Math.ceil((expiresAt - Date.now()) / 6e4));
57
+ }
58
+ /** The machine that joined: one the server lists that it did not have when the code was made. */
59
+ function joinedMachine(join, machines) {
60
+ if (join.step !== "code" || !join.known) return void 0;
61
+ const known = join.known;
62
+ return machines.find((machine) => !known.has(machine.id));
63
+ }
64
+ /**
65
+ * Renders the Add a machine dialog's panel (for a modal layer: {@link useAddMachine} opens it).
66
+ *
67
+ * @param props - The server, the first tab and what closes it.
68
+ */
69
+ function AddMachineDialog({ hostId, tab: initialTab, onClose, className, style }) {
70
+ const labels = useMachinesLabels();
71
+ const kitLabels = useLabels();
72
+ const { machines, links, hosts, newChat } = useMachinesHost();
73
+ const invite = usePermitted("machines.invite") ? machines?.invite : void 0;
74
+ const canShare = Boolean(links?.shareStart);
75
+ const host = useReadable(hosts ?? NO_HOSTS).hosts.find((h) => h.id === hostId) ?? {
76
+ id: hostId,
77
+ kind: "server",
78
+ name: hostId,
79
+ status: "connected"
80
+ };
81
+ const list = machines ? machines.list(hostId) : null;
82
+ const listState = useReadable(list ? list.state : NO_LIST);
83
+ const [tab, setTab] = useState(initialTab ?? (invite ? "another" : "this"));
84
+ const [forThisComputer, setForThisComputer] = useState(false);
85
+ const [name, setName] = useState("");
86
+ const [join, setJoin] = useState({ step: "name" });
87
+ const [busy, setBusy] = useState(false);
88
+ const [error, setError] = useState(null);
89
+ const trimmed = name.trim();
90
+ const nameInvalid = trimmed !== "" && !isValidMachineName(trimmed);
91
+ if (join.step === "code" && !join.known && listState.status === "ready") setJoin({
92
+ ...join,
93
+ known: new Set(listState.machines.map((m) => m.id))
94
+ });
95
+ const joined = joinedMachine(join, listState.machines);
96
+ const [heard, setHeard] = useState(null);
97
+ const pairingId = join.step === "code" ? join.invite.code.replace(/[^0-9A-Z]/gi, "").slice(0, 4).toUpperCase() : null;
98
+ const onJoin = machines?.onJoin;
99
+ useEffect(() => {
100
+ if (!pairingId || !onJoin) return void 0;
101
+ return onJoin(hostId, (event) => {
102
+ if (event.pairingId.toUpperCase() === pairingId) setHeard(event);
103
+ });
104
+ }, [
105
+ pairingId,
106
+ onJoin,
107
+ hostId
108
+ ]);
109
+ const [address, setAddress] = useState(null);
110
+ const givenAddress = join.step === "code" ? join.invite.join.slice(0, join.invite.join.lastIndexOf("#")) : "";
111
+ const joinAddress = address ?? givenAddress;
112
+ const local = isLocalAddress(givenAddress);
113
+ useMinuteTick();
114
+ const expiresAt = join.step === "code" ? join.invite.expiresAt : null;
115
+ const [endedAt, setEndedAt] = useState(null);
116
+ const expired = expiresAt !== null && endedAt === expiresAt;
117
+ useEffect(() => {
118
+ if (expiresAt === null) return void 0;
119
+ const timer = setTimeout(() => setEndedAt(expiresAt), Math.max(0, expiresAt - Date.now()));
120
+ return () => clearTimeout(timer);
121
+ }, [expiresAt]);
122
+ const create = async () => {
123
+ if (!invite || nameInvalid) return;
124
+ setBusy(true);
125
+ setError(null);
126
+ try {
127
+ const made = await invite(hostId, trimmed ? { name: trimmed } : {});
128
+ const known = listState.status === "ready" ? new Set(listState.machines.map((m) => m.id)) : null;
129
+ setJoin({
130
+ step: "code",
131
+ invite: made,
132
+ known
133
+ });
134
+ setHeard(null);
135
+ list?.refresh();
136
+ } catch (err) {
137
+ setError(labels.machinesError(machinesError("invite", err)));
138
+ } finally {
139
+ setBusy(false);
140
+ }
141
+ };
142
+ const startChat = (machine) => {
143
+ newChat?.(hostId, machine.name);
144
+ onClose();
145
+ };
146
+ const joinString = join.step === "code" ? `${joinAddress}#${join.invite.code}` : "";
147
+ const command = joinString ? machineInstallCommand(joinString) : "";
148
+ const setupCommand = joinString ? machineSetupCommand(joinString) : "";
149
+ const refused = heard?.outcome === "refused" ? heard : null;
150
+ return /* @__PURE__ */ jsxs("div", {
151
+ className: `zg-modal-panel zg-float w-[520px] max-w-[92vw] flex flex-col overflow-hidden${className ? ` ${className}` : ""}`,
152
+ style,
153
+ onMouseDown: (event) => event.stopPropagation(),
154
+ "data-testid": "add-machine-dialog",
155
+ children: [
156
+ /* @__PURE__ */ jsxs("header", {
157
+ className: "flex items-start gap-2 px-5 py-3.5 border-b border-[var(--zg-hairline)]",
158
+ children: [/* @__PURE__ */ jsxs("div", {
159
+ className: "flex-1 min-w-0",
160
+ children: [/* @__PURE__ */ jsx("h2", {
161
+ id: TITLE_ID,
162
+ className: "m-0 text-sm font-semibold text-[var(--zg-text)]",
163
+ children: labels.machinesInviteTitle(host.name)
164
+ }), /* @__PURE__ */ jsx("p", {
165
+ className: "m-0 mt-1 text-xs text-[var(--zg-text-muted)]",
166
+ children: labels.machinesExplain
167
+ })]
168
+ }), /* @__PURE__ */ jsx(IconButton, {
169
+ icon: "i-ph-x",
170
+ label: kitLabels.close,
171
+ size: "lg",
172
+ onClick: onClose,
173
+ className: "-mr-2"
174
+ })]
175
+ }),
176
+ /* @__PURE__ */ jsxs("div", {
177
+ className: "px-5 py-4 space-y-4 overflow-y-auto",
178
+ children: [
179
+ invite && canShare && !joined && /* @__PURE__ */ jsx(Segmented, {
180
+ value: tab,
181
+ onChange: setTab,
182
+ ariaLabel: labels.machinesAddWhich,
183
+ options: [{
184
+ value: "another",
185
+ label: labels.machinesAddAnother,
186
+ icon: "i-ph-desktop-tower"
187
+ }, {
188
+ value: "this",
189
+ label: labels.machinesAddThis,
190
+ icon: "i-ph-laptop"
191
+ }]
192
+ }),
193
+ tab === "this" && canShare ? /* @__PURE__ */ jsxs("div", {
194
+ className: "space-y-3",
195
+ children: [
196
+ /* @__PURE__ */ jsx("p", {
197
+ className: "m-0 text-xs text-[var(--zg-text-muted)]",
198
+ children: labels.machinesAddThisHint(host.name)
199
+ }),
200
+ /* @__PURE__ */ jsx(ShareThisComputerForm, {
201
+ host,
202
+ onShared: onClose,
203
+ onCancel: onClose
204
+ }),
205
+ invite && /* @__PURE__ */ jsxs("div", {
206
+ className: "zg-well rounded-zg-md p-3 flex items-center gap-3",
207
+ children: [/* @__PURE__ */ jsxs("div", {
208
+ className: "flex-1 min-w-0",
209
+ children: [/* @__PURE__ */ jsx("div", {
210
+ className: "text-xs font-medium text-[var(--zg-text)]",
211
+ children: labels.machinesAddKeepOn
212
+ }), /* @__PURE__ */ jsx("div", {
213
+ className: "text-xs text-[var(--zg-text-muted)]",
214
+ children: labels.machinesAddKeepOnHint
215
+ })]
216
+ }), /* @__PURE__ */ jsx("button", {
217
+ type: "button",
218
+ className: BTN_SECONDARY,
219
+ onClick: () => {
220
+ setForThisComputer(true);
221
+ setTab("another");
222
+ },
223
+ children: labels.machinesAddKeepOnOpen
224
+ })]
225
+ })
226
+ ]
227
+ }) : joined ? /* @__PURE__ */ jsxs("div", {
228
+ className: "flex items-start gap-3",
229
+ role: "status",
230
+ "data-testid": "add-machine-joined",
231
+ children: [/* @__PURE__ */ jsx("span", {
232
+ "aria-hidden": true,
233
+ className: "shrink-0 inline-flex items-center justify-center size-9 rounded-full bg-zg-success/15 text-zg-success text-base",
234
+ children: /* @__PURE__ */ jsx("span", { className: "i-ph-check-bold" })
235
+ }), /* @__PURE__ */ jsxs("div", {
236
+ className: "min-w-0 space-y-1",
237
+ children: [
238
+ /* @__PURE__ */ jsx("div", {
239
+ className: "text-sm font-medium text-[var(--zg-text)]",
240
+ children: labels.machinesAddJoined(joined.name, host.name)
241
+ }),
242
+ /* @__PURE__ */ jsx("div", {
243
+ className: "text-xs text-[var(--zg-text-muted)]",
244
+ children: labels.machinesAddJoinedHint
245
+ }),
246
+ (joined.capabilities?.gpu?.length ?? 0) > 0 && /* @__PURE__ */ jsx("div", {
247
+ className: "flex flex-wrap gap-1 pt-0.5",
248
+ children: joined.capabilities.gpu.map((gpu) => /* @__PURE__ */ jsx(MetaChip, {
249
+ tone: "info",
250
+ children: gpu
251
+ }, gpu))
252
+ })
253
+ ]
254
+ })]
255
+ }) : join.step === "code" ? /* @__PURE__ */ jsxs("div", {
256
+ className: "space-y-3",
257
+ children: [
258
+ /* @__PURE__ */ jsxs("div", {
259
+ className: "flex items-center gap-2",
260
+ children: [/* @__PURE__ */ jsx("span", {
261
+ className: `${FIELD_LABEL_CLASS} flex-1`,
262
+ children: labels.machinesInviteCommand
263
+ }), !expired && /* @__PURE__ */ jsx(MetaChip, {
264
+ tone: "muted",
265
+ children: labels.machinesAddTimeLeft(minutesLeft(join.invite.expiresAt))
266
+ })]
267
+ }),
268
+ /* @__PURE__ */ jsxs("div", {
269
+ className: `zg-well rounded-zg-md flex items-start gap-2 p-2.5 min-w-0${expired ? " opacity-50" : ""}`,
270
+ "aria-disabled": expired,
271
+ children: [/* @__PURE__ */ jsx("code", {
272
+ className: "flex-1 min-w-0 break-all font-mono text-xs text-[var(--zg-text)] select-all",
273
+ children: command
274
+ }), !expired && /* @__PURE__ */ jsx(CopyButton, {
275
+ text: command,
276
+ label: labels.machinesInviteCopy
277
+ })]
278
+ }),
279
+ /* @__PURE__ */ jsx("p", {
280
+ className: "m-0 text-xs text-[var(--zg-text-muted)]",
281
+ children: forThisComputer ? labels.machinesAddThisTerminalHint : labels.machinesInviteHint
282
+ }),
283
+ !expired && /* @__PURE__ */ jsxs("div", {
284
+ className: "flex items-center gap-2 min-w-0 text-xs text-[var(--zg-text-muted)]",
285
+ children: [
286
+ /* @__PURE__ */ jsx("span", {
287
+ className: "shrink-0",
288
+ children: labels.machinesAddInstalled
289
+ }),
290
+ /* @__PURE__ */ jsx("code", {
291
+ className: "flex-1 min-w-0 truncate font-mono",
292
+ title: setupCommand,
293
+ children: setupCommand
294
+ }),
295
+ /* @__PURE__ */ jsx(CopyButton, {
296
+ text: setupCommand,
297
+ label: labels.machinesAddInstalledCopy
298
+ })
299
+ ]
300
+ }),
301
+ local && /* @__PURE__ */ jsxs("div", {
302
+ className: "space-y-1.5",
303
+ children: [
304
+ /* @__PURE__ */ jsx(Notice, {
305
+ tone: "warning",
306
+ compact: true,
307
+ children: labels.machinesAddLocalAddress
308
+ }),
309
+ /* @__PURE__ */ jsx("label", {
310
+ className: "block text-xs text-[var(--zg-text-muted)] mb-1",
311
+ htmlFor: `add-machine-address-${hostId}`,
312
+ children: labels.machinesAddAddress
313
+ }),
314
+ /* @__PURE__ */ jsx("input", {
315
+ id: `add-machine-address-${hostId}`,
316
+ className: "zg-well w-full h-8 px-2.5 text-zg-ui placeholder:text-[var(--zg-text-subtle)] disabled:opacity-zg-disabled",
317
+ value: joinAddress,
318
+ spellCheck: false,
319
+ onChange: (event) => setAddress(event.target.value.trim())
320
+ })
321
+ ]
322
+ }),
323
+ refused ? /* @__PURE__ */ jsx(Notice, {
324
+ tone: "error",
325
+ ...refused.detail ? { detail: refused.detail } : {},
326
+ action: /* @__PURE__ */ jsx("button", {
327
+ type: "button",
328
+ className: BTN_SECONDARY,
329
+ disabled: busy,
330
+ onClick: () => void create(),
331
+ children: labels.machinesAddNewCode
332
+ }),
333
+ children: labels.machinesAddRefused(refused.reason ?? "other", refused.name)
334
+ }) : expired ? /* @__PURE__ */ jsx(Notice, {
335
+ tone: "warning",
336
+ compact: true,
337
+ action: /* @__PURE__ */ jsx("button", {
338
+ type: "button",
339
+ className: BTN_SECONDARY,
340
+ disabled: busy,
341
+ onClick: () => void create(),
342
+ children: labels.machinesAddNewCode
343
+ }),
344
+ children: labels.machinesAddExpired
345
+ }) : /* @__PURE__ */ jsxs("div", {
346
+ className: "flex items-center gap-2 text-xs text-[var(--zg-text-muted)]",
347
+ "data-testid": "add-machine-waiting",
348
+ children: [/* @__PURE__ */ jsx(SegmentSpinner, {
349
+ size: "xs",
350
+ tone: "muted",
351
+ label: heard?.outcome === "paired" ? labels.machinesAddPaired(heard.name) : labels.machinesAddWaiting
352
+ }), /* @__PURE__ */ jsx("span", { children: heard?.outcome === "paired" ? labels.machinesAddPaired(heard.name) : labels.machinesAddWaiting })]
353
+ })
354
+ ]
355
+ }) : /* @__PURE__ */ jsxs("form", {
356
+ id: "add-machine-form",
357
+ className: "space-y-2",
358
+ "aria-label": labels.machinesInviteForm,
359
+ onSubmit: (event) => {
360
+ event.preventDefault();
361
+ create();
362
+ },
363
+ children: [
364
+ /* @__PURE__ */ jsx("label", {
365
+ className: FIELD_LABEL_CLASS,
366
+ htmlFor: `add-machine-name-${hostId}`,
367
+ children: labels.machinesInviteName
368
+ }),
369
+ /* @__PURE__ */ jsx("input", {
370
+ id: `add-machine-name-${hostId}`,
371
+ className: INPUT_CLASS,
372
+ placeholder: labels.machinesInviteNamePlaceholder,
373
+ value: name,
374
+ maxLength: 64,
375
+ "aria-invalid": nameInvalid,
376
+ ...nameInvalid ? { "aria-describedby": `add-machine-rule-${hostId}` } : {},
377
+ onChange: (event) => setName(event.target.value)
378
+ }),
379
+ nameInvalid && /* @__PURE__ */ jsx("div", {
380
+ id: `add-machine-rule-${hostId}`,
381
+ children: /* @__PURE__ */ jsx(Notice, {
382
+ tone: "error",
383
+ compact: true,
384
+ children: labels.machinesInviteNameRule
385
+ })
386
+ })
387
+ ]
388
+ }),
389
+ busy && /* @__PURE__ */ jsx(OpLine, {
390
+ kind: "busy",
391
+ children: labels.machinesInviteCreating
392
+ }),
393
+ error && !busy && /* @__PURE__ */ jsx(OpLine, {
394
+ kind: "error",
395
+ children: error
396
+ })
397
+ ]
398
+ }),
399
+ !(tab === "this" && canShare) && /* @__PURE__ */ jsx("footer", {
400
+ className: "flex items-center justify-end gap-2 px-5 py-3 bg-[var(--zg-tray)]",
401
+ children: joined ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
402
+ type: "button",
403
+ className: BTN_SECONDARY,
404
+ onClick: onClose,
405
+ children: labels.machinesInviteDone
406
+ }), newChat && /* @__PURE__ */ jsx("button", {
407
+ type: "button",
408
+ className: BTN_PRIMARY,
409
+ onClick: () => startChat(joined),
410
+ children: labels.machinesAddStartChat
411
+ })] }) : join.step === "code" ? /* @__PURE__ */ jsx("button", {
412
+ type: "button",
413
+ className: BTN_SECONDARY,
414
+ onClick: onClose,
415
+ children: labels.machinesInviteDone
416
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
417
+ type: "button",
418
+ className: BTN_SECONDARY,
419
+ onClick: onClose,
420
+ children: labels.machinesCancel
421
+ }), /* @__PURE__ */ jsx("button", {
422
+ type: "submit",
423
+ form: "add-machine-form",
424
+ className: BTN_PRIMARY,
425
+ disabled: busy || nameInvalid || !invite,
426
+ children: labels.machinesInviteCreate
427
+ })] })
428
+ })
429
+ ]
430
+ });
431
+ }
432
+ /**
433
+ * Opens Add a machine above whatever is open (the app's modal layer). `undefined` when there is nothing to add
434
+ * with (the host makes no join codes and does not share this computer) or no modal layer: hide the control.
435
+ */
436
+ function useAddMachine() {
437
+ const { machines, links } = useMachinesHost();
438
+ const canInvite = usePermitted("machines.invite");
439
+ const modal = useOptionalModal();
440
+ const bridge = useMachinesBridge();
441
+ const available = Boolean(modal) && Boolean(machines?.invite && canInvite || links?.shareStart);
442
+ const openLayer = modal?.openLayer;
443
+ const closeLayer = modal?.closeLayer;
444
+ return useMemo(() => {
445
+ if (!available || !openLayer || !closeLayer) return void 0;
446
+ return (hostId, options) => {
447
+ openLayer(bridge(/* @__PURE__ */ jsx(AddMachineDialog, {
448
+ hostId,
449
+ ...options?.tab ? { tab: options.tab } : {},
450
+ onClose: closeLayer
451
+ })), {
452
+ onDismiss: () => {},
453
+ labelledBy: TITLE_ID
454
+ });
455
+ };
456
+ }, [
457
+ available,
458
+ openLayer,
459
+ closeLayer,
460
+ bridge
461
+ ]);
462
+ }
463
+ //#endregion
464
+ export { AddMachineDialog, useAddMachine };
@@ -1,4 +1,6 @@
1
1
  import { HostSummary } from "./headless/host.js";
2
+ import { NoticeTone } from "../components/Notice.js";
3
+ import { DraftPlaceIssue } from "../chat/headless/draft-target.js";
2
4
  import { CSSProperties } from "react";
3
5
  //#region src/machines/Banners.d.ts
4
6
  /** Props for {@link ServerOfflineBanner}. */
@@ -21,8 +23,12 @@ interface ServerOfflineBannerProps {
21
23
  declare function ServerOfflineBanner({ host, name, className, style }: ServerOfflineBannerProps): import("react").JSX.Element;
22
24
  /** Props for {@link ComposerNotice}. */
23
25
  interface ComposerNoticeProps {
24
- /** What happened. */
26
+ /** What happened, worded for people. */
25
27
  text: string;
28
+ /** A warning or an error: a {@link Notice} of that tone. Default: a muted line. */
29
+ tone?: NoticeTone;
30
+ /** The raw words behind it (an exception's message): folded under Details. Needs `tone`. */
31
+ detail?: string;
26
32
  /** Called when the user dismisses it. */
27
33
  onDismiss: () => void;
28
34
  /** The root's `data-testid`. Default `composer-notice`. */
@@ -35,9 +41,32 @@ interface ComposerNoticeProps {
35
41
  /**
36
42
  * A dismissable notice above the composer: why a new chat could not be created (its server went offline, …).
37
43
  *
38
- * @param props - The text, the dismiss callback, a test id, `className` and `style`.
44
+ * @param props - The text, its tone and detail, the dismiss callback, a test id, `className` and `style`.
39
45
  */
40
- declare function ComposerNotice({ text, onDismiss, testId, className, style }: ComposerNoticeProps): import("react").JSX.Element;
46
+ declare function ComposerNotice({ text, tone, detail, onDismiss, testId, className, style }: ComposerNoticeProps): import("react").JSX.Element;
47
+ /** Props for {@link DraftPlaceNotice}. */
48
+ interface DraftPlaceNoticeProps {
49
+ /** Why the draft cannot use the default place as it is. */
50
+ issue?: DraftPlaceIssue | null;
51
+ /** The draft's server, when the app cannot reach it now. A chat there waits for it. */
52
+ offlineServer?: string;
53
+ /** Moves the draft to this computer (an offline server). Absent: no button. */
54
+ onUseThisComputer?: () => void;
55
+ /** Opens Add a server (an unsaved server). Absent: no button. */
56
+ onAddServer?: (hostName: string, hostUrl?: string) => void;
57
+ /** Hides the issue. */
58
+ onDismiss: () => void;
59
+ /** Extra classes on the root. */
60
+ className?: string;
61
+ }
62
+ /**
63
+ * The new chat's place, when it cannot be what the default says: the default names a server this device has not
64
+ * saved (Add it), a machine that left the server, a folder that is gone, or its server is offline (use this
65
+ * computer instead). Nothing when all is well.
66
+ *
67
+ * @param props - The issue, the offline server, the actions and `className`.
68
+ */
69
+ declare function DraftPlaceNotice({ issue, offlineServer, onUseThisComputer, onAddServer, onDismiss, className }: DraftPlaceNoticeProps): import("react").JSX.Element | null;
41
70
  /** Props for {@link HostRemovedBanner}. */
42
71
  interface HostRemovedBannerProps {
43
72
  /** What was removed (`"<server> was removed"`). */
@@ -72,4 +101,4 @@ interface MachineWaitingBannerProps {
72
101
  */
73
102
  declare function MachineWaitingBanner({ machine, className, style }: MachineWaitingBannerProps): import("react").JSX.Element | null;
74
103
  //#endregion
75
- export { ComposerNotice, ComposerNoticeProps, HostRemovedBanner, HostRemovedBannerProps, MachineWaitingBanner, MachineWaitingBannerProps, ServerOfflineBanner, ServerOfflineBannerProps };
104
+ export { ComposerNotice, ComposerNoticeProps, DraftPlaceNotice, DraftPlaceNoticeProps, HostRemovedBanner, HostRemovedBannerProps, MachineWaitingBanner, MachineWaitingBannerProps, ServerOfflineBanner, ServerOfflineBannerProps };
@@ -1,3 +1,4 @@
1
+ import { Notice } from "../components/Notice.js";
1
2
  import { Tray } from "../components/Surface.js";
2
3
  import { useMachinesHost, useMachinesLabels, useOnlineMachines } from "./MachinesProvider.js";
3
4
  import { hostStatusText } from "./headless/messages.js";
@@ -34,10 +35,26 @@ function ServerOfflineBanner({ host, name, className = "", style }) {
34
35
  /**
35
36
  * A dismissable notice above the composer: why a new chat could not be created (its server went offline, …).
36
37
  *
37
- * @param props - The text, the dismiss callback, a test id, `className` and `style`.
38
+ * @param props - The text, its tone and detail, the dismiss callback, a test id, `className` and `style`.
38
39
  */
39
- function ComposerNotice({ text, onDismiss, testId, className = "", style }) {
40
+ function ComposerNotice({ text, tone, detail, onDismiss, testId, className = "", style }) {
40
41
  const labels = useMachinesLabels();
42
+ if (tone) return /* @__PURE__ */ jsx("div", {
43
+ className: className || void 0,
44
+ style,
45
+ "data-testid": testId ?? "composer-notice",
46
+ children: /* @__PURE__ */ jsx(Notice, {
47
+ tone,
48
+ ...detail ? { detail } : {},
49
+ action: /* @__PURE__ */ jsx("button", {
50
+ type: "button",
51
+ onClick: onDismiss,
52
+ className: NOTICE_BUTTON_CLASS,
53
+ children: labels.machinesDismiss
54
+ }),
55
+ children: text
56
+ })
57
+ });
41
58
  return /* @__PURE__ */ jsxs(Tray, {
42
59
  radius: "xl",
43
60
  className: `flex items-center justify-between gap-3 pl-4 pr-2 py-2 ${className}`.trimEnd(),
@@ -56,6 +73,50 @@ function ComposerNotice({ text, onDismiss, testId, className = "", style }) {
56
73
  });
57
74
  }
58
75
  /**
76
+ * The new chat's place, when it cannot be what the default says: the default names a server this device has not
77
+ * saved (Add it), a machine that left the server, a folder that is gone, or its server is offline (use this
78
+ * computer instead). Nothing when all is well.
79
+ *
80
+ * @param props - The issue, the offline server, the actions and `className`.
81
+ */
82
+ function DraftPlaceNotice({ issue, offlineServer, onUseThisComputer, onAddServer, onDismiss, className = "" }) {
83
+ const labels = useMachinesLabels();
84
+ const text = offlineServer ? labels.machinesDraftServerOffline(offlineServer) : issue?.kind === "unsaved" ? labels.machinesPlaceUnsaved(issue.hostName) : issue?.kind === "machine-gone" ? labels.machinesPlaceMachineGone(issue.machine, issue.hostName) : issue?.kind === "folder-gone" ? labels.machinesPlaceFolderGone(issue.folder) : null;
85
+ if (!text) return null;
86
+ const action = offlineServer ? onUseThisComputer && /* @__PURE__ */ jsx("button", {
87
+ type: "button",
88
+ onClick: onUseThisComputer,
89
+ className: NOTICE_BUTTON_CLASS,
90
+ children: labels.machinesUseThisComputer
91
+ }) : issue?.kind === "unsaved" && onAddServer && /* @__PURE__ */ jsx("button", {
92
+ type: "button",
93
+ onClick: () => onAddServer(issue.hostName, issue.hostUrl),
94
+ className: NOTICE_BUTTON_CLASS,
95
+ children: labels.machinesPlaceAddServer(issue.hostName)
96
+ });
97
+ return /* @__PURE__ */ jsxs(Tray, {
98
+ radius: "xl",
99
+ className: `flex items-center justify-between gap-3 pl-4 pr-2 py-2 ${className}`.trimEnd(),
100
+ "data-testid": "draft-place-notice",
101
+ role: "status",
102
+ children: [/* @__PURE__ */ jsxs("span", {
103
+ className: "flex items-center gap-2 text-zg-ui text-[var(--zg-text-muted)]",
104
+ children: [/* @__PURE__ */ jsx("span", {
105
+ "aria-hidden": true,
106
+ className: offlineServer ? "i-ph-cloud-slash shrink-0" : "i-ph-signpost shrink-0"
107
+ }), text]
108
+ }), /* @__PURE__ */ jsxs("span", {
109
+ className: "flex items-center gap-1 shrink-0",
110
+ children: [action, !offlineServer && /* @__PURE__ */ jsx("button", {
111
+ type: "button",
112
+ onClick: onDismiss,
113
+ className: NOTICE_BUTTON_CLASS,
114
+ children: labels.machinesDismiss
115
+ })]
116
+ })]
117
+ });
118
+ }
119
+ /**
59
120
  * The chat that was open closed because its server was removed.
60
121
  *
61
122
  * @param props - The notice, the dismiss callback, `className` and `style`.
@@ -96,4 +157,4 @@ function MachineWaitingBanner({ machine, className = "", style }) {
96
157
  });
97
158
  }
98
159
  //#endregion
99
- export { ComposerNotice, HostRemovedBanner, MachineWaitingBanner, ServerOfflineBanner };
160
+ export { ComposerNotice, DraftPlaceNotice, HostRemovedBanner, MachineWaitingBanner, ServerOfflineBanner };
@@ -1,4 +1,6 @@
1
+ import { Notice } from "../components/Notice.js";
1
2
  import { MetaChip } from "../components/Form.js";
3
+ import { errorMessage } from "../components/report-error.js";
2
4
  import { FloatingSurface } from "../components/floating/FloatingSurface.js";
3
5
  import { useReadable } from "../chat/headless/store.js";
4
6
  import { useMachinesConfirm, useMachinesHost, useMachinesLabels } from "./MachinesProvider.js";
@@ -76,6 +78,8 @@ function DaemonMenuBody({ daemon, running: runningCount = 0, attention = 0, sess
76
78
  const [open, setOpen] = useState(false);
77
79
  const [status, setStatus] = useState(null);
78
80
  const [notice, setNotice] = useState(null);
81
+ /** What the last action's failure said: worded by a label, the raw words under Details. */
82
+ const [failure, setFailure] = useState(null);
79
83
  const [busy, setBusy] = useState(false);
80
84
  const wrapperRef = useRef(null);
81
85
  const menuRef = useRef(null);
@@ -112,17 +116,26 @@ function DaemonMenuBody({ daemon, running: runningCount = 0, attention = 0, sess
112
116
  refresh
113
117
  ]);
114
118
  useEffect(() => {
115
- if (!notice || busy) return void 0;
116
- const timer = setTimeout(() => setNotice(null), 4e3);
119
+ if (!notice && failure === null || busy) return void 0;
120
+ const timer = setTimeout(() => {
121
+ setNotice(null);
122
+ setFailure(null);
123
+ }, 4e3);
117
124
  return () => clearTimeout(timer);
118
- }, [notice, busy]);
125
+ }, [
126
+ notice,
127
+ failure,
128
+ busy
129
+ ]);
119
130
  const run = useCallback(async (pending, action) => {
120
131
  setBusy(true);
132
+ setFailure(null);
121
133
  setNotice(pending);
122
134
  try {
123
135
  setNotice(await action());
124
136
  } catch (err) {
125
- setNotice(err?.message ?? String(err));
137
+ setNotice(null);
138
+ setFailure(errorMessage(err));
126
139
  } finally {
127
140
  setBusy(false);
128
141
  refresh();
@@ -161,6 +174,7 @@ function DaemonMenuBody({ daemon, running: runningCount = 0, attention = 0, sess
161
174
  title: label,
162
175
  onClick: () => {
163
176
  setNotice(null);
177
+ setFailure(null);
164
178
  setOpen((v) => !v);
165
179
  }
166
180
  }), /* @__PURE__ */ jsx(FloatingSurface, {
@@ -307,6 +321,15 @@ function DaemonMenuBody({ daemon, running: runningCount = 0, attention = 0, sess
307
321
  role: "status",
308
322
  className: "px-3 pt-1.5 pb-1 text-zg-meta leading-4 text-[var(--zg-text-muted)]",
309
323
  children: notice
324
+ }),
325
+ failure !== null && /* @__PURE__ */ jsx("div", {
326
+ className: "px-3 pt-1.5 pb-1",
327
+ children: /* @__PURE__ */ jsx(Notice, {
328
+ tone: "error",
329
+ compact: true,
330
+ detail: failure,
331
+ children: labels.machinesDaemonActionFailed
332
+ })
310
333
  })
311
334
  ]
312
335
  })