fauna-api 0.0.0-stage → 1.0.679

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 (171) hide show
  1. package/dist/brands.d.ts +2 -0
  2. package/dist/brands.js +2 -0
  3. package/dist/index.d.ts +64 -0
  4. package/dist/index.js +121 -0
  5. package/dist/src/ant/chat.d.ts +130 -0
  6. package/dist/src/ant/chat.js +467 -0
  7. package/dist/src/ant/format.d.ts +40 -0
  8. package/dist/src/ant/format.js +143 -0
  9. package/dist/src/ant/graph.d.ts +130 -0
  10. package/dist/src/ant/graph.js +449 -0
  11. package/dist/src/ant/index.d.ts +16 -0
  12. package/dist/src/ant/index.js +61 -0
  13. package/dist/src/ant/persona.d.ts +151 -0
  14. package/dist/src/ant/persona.js +136 -0
  15. package/dist/src/ant/timeline.d.ts +283 -0
  16. package/dist/src/ant/timeline.js +1025 -0
  17. package/dist/src/authentification.d.ts +45 -0
  18. package/dist/src/authentification.js +194 -0
  19. package/dist/src/billing.d.ts +135 -0
  20. package/dist/src/billing.js +128 -0
  21. package/dist/src/calendar.d.ts +83 -0
  22. package/dist/src/calendar.js +133 -0
  23. package/dist/src/chats.d.ts +113 -0
  24. package/dist/src/chats.js +100 -0
  25. package/dist/src/cicada/contacts.d.ts +31 -0
  26. package/dist/src/cicada/contacts.js +28 -0
  27. package/dist/src/cicada/conversations.d.ts +73 -0
  28. package/dist/src/cicada/conversations.js +236 -0
  29. package/dist/src/cicada/inboxes.d.ts +48 -0
  30. package/dist/src/cicada/inboxes.js +82 -0
  31. package/dist/src/cicada/index.d.ts +16 -0
  32. package/dist/src/cicada/index.js +61 -0
  33. package/dist/src/cicada/phone.d.ts +48 -0
  34. package/dist/src/cicada/phone.js +306 -0
  35. package/dist/src/drive.d.ts +80 -0
  36. package/dist/src/drive.js +472 -0
  37. package/dist/src/extensions/index.d.ts +4 -0
  38. package/dist/src/extensions/index.js +5 -0
  39. package/dist/src/extensions/web/index.d.ts +6 -0
  40. package/dist/src/extensions/web/index.js +7 -0
  41. package/dist/src/extensions/web/session.d.ts +108 -0
  42. package/dist/src/extensions/web/session.js +470 -0
  43. package/dist/src/extensions/web/tools.d.ts +13 -0
  44. package/dist/src/extensions/web/tools.js +72 -0
  45. package/dist/src/friends.d.ts +39 -0
  46. package/dist/src/friends.js +119 -0
  47. package/dist/src/groups.d.ts +82 -0
  48. package/dist/src/groups.js +269 -0
  49. package/dist/src/inbox.d.ts +11 -0
  50. package/dist/src/inbox.js +35 -0
  51. package/dist/src/integrations.d.ts +52 -0
  52. package/dist/src/integrations.js +205 -0
  53. package/dist/src/magpie/explorer.d.ts +14 -0
  54. package/dist/src/magpie/explorer.js +36 -0
  55. package/dist/src/magpie/index.d.ts +16 -0
  56. package/dist/src/magpie/index.js +61 -0
  57. package/dist/src/magpie/portfolio.d.ts +52 -0
  58. package/dist/src/magpie/portfolio.js +179 -0
  59. package/dist/src/magpie/quotes.d.ts +35 -0
  60. package/dist/src/magpie/quotes.js +47 -0
  61. package/dist/src/magpie/room.d.ts +61 -0
  62. package/dist/src/magpie/room.js +238 -0
  63. package/dist/src/maps.d.ts +159 -0
  64. package/dist/src/maps.js +171 -0
  65. package/dist/src/marmot/browser.d.ts +139 -0
  66. package/dist/src/marmot/browser.js +389 -0
  67. package/dist/src/marmot/index.d.ts +20 -0
  68. package/dist/src/marmot/index.js +65 -0
  69. package/dist/src/marmot/media.d.ts +56 -0
  70. package/dist/src/marmot/media.js +265 -0
  71. package/dist/src/marmot/research.d.ts +337 -0
  72. package/dist/src/marmot/research.js +1138 -0
  73. package/dist/src/marmot/scraper.d.ts +59 -0
  74. package/dist/src/marmot/scraper.js +94 -0
  75. package/dist/src/marmot/web.d.ts +402 -0
  76. package/dist/src/marmot/web.js +2586 -0
  77. package/dist/src/marmot/workspace.d.ts +81 -0
  78. package/dist/src/marmot/workspace.js +263 -0
  79. package/dist/src/parrot/agent.d.ts +53 -0
  80. package/dist/src/parrot/agent.js +93 -0
  81. package/dist/src/parrot/chat.d.ts +84 -0
  82. package/dist/src/parrot/chat.js +237 -0
  83. package/dist/src/parrot/format.d.ts +128 -0
  84. package/dist/src/parrot/format.js +639 -0
  85. package/dist/src/parrot/generative.d.ts +261 -0
  86. package/dist/src/parrot/generative.js +185 -0
  87. package/dist/src/parrot/index.d.ts +22 -0
  88. package/dist/src/parrot/index.js +67 -0
  89. package/dist/src/parrot/room.d.ts +225 -0
  90. package/dist/src/parrot/room.js +466 -0
  91. package/dist/src/parrot/routing.d.ts +90 -0
  92. package/dist/src/parrot/routing.js +78 -0
  93. package/dist/src/parrot/share.d.ts +126 -0
  94. package/dist/src/parrot/share.js +261 -0
  95. package/dist/src/parrot/socket.d.ts +41 -0
  96. package/dist/src/parrot/socket.js +478 -0
  97. package/dist/src/payments.d.ts +73 -0
  98. package/dist/src/payments.js +107 -0
  99. package/dist/src/private/cards.css +75 -0
  100. package/dist/src/rooms.d.ts +76 -0
  101. package/dist/src/rooms.js +168 -0
  102. package/dist/src/shared/schema.d.ts +24 -0
  103. package/dist/src/shared/schema.js +1 -0
  104. package/dist/src/shared/server.d.ts +65 -0
  105. package/dist/src/shared/server.js +302 -0
  106. package/dist/src/termite/environment.d.ts +110 -0
  107. package/dist/src/termite/environment.js +263 -0
  108. package/dist/src/termite/index.d.ts +14 -0
  109. package/dist/src/termite/index.js +59 -0
  110. package/dist/src/termite/os.d.ts +326 -0
  111. package/dist/src/termite/os.js +572 -0
  112. package/dist/src/termite/versioning.d.ts +94 -0
  113. package/dist/src/termite/versioning.js +217 -0
  114. package/dist/src/tickets.d.ts +43 -0
  115. package/dist/src/tickets.js +138 -0
  116. package/dist/src/travel.d.ts +85 -0
  117. package/dist/src/travel.js +44 -0
  118. package/dist/src/ui/authentification.d.ts +24 -0
  119. package/dist/src/ui/authentification.js +177 -0
  120. package/dist/src/ui/brands.css +11 -0
  121. package/dist/src/ui/brands.d.ts +7 -0
  122. package/dist/src/ui/brands.js +14 -0
  123. package/dist/src/ui/browser.d.ts +74 -0
  124. package/dist/src/ui/browser.js +470 -0
  125. package/dist/src/ui/chat.d.ts +183 -0
  126. package/dist/src/ui/chat.js +2379 -0
  127. package/dist/src/ui/chatPanel.d.ts +109 -0
  128. package/dist/src/ui/chatPanel.js +768 -0
  129. package/dist/src/ui/chatSequence.d.ts +36 -0
  130. package/dist/src/ui/chatSequence.js +42 -0
  131. package/dist/src/ui/crown.d.ts +19 -0
  132. package/dist/src/ui/crown.js +199 -0
  133. package/dist/src/ui/cursor.d.ts +73 -0
  134. package/dist/src/ui/cursor.js +175 -0
  135. package/dist/src/ui/device.d.ts +33 -0
  136. package/dist/src/ui/device.js +49 -0
  137. package/dist/src/ui/drive.d.ts +69 -0
  138. package/dist/src/ui/drive.js +1243 -0
  139. package/dist/src/ui/generative.d.ts +39 -0
  140. package/dist/src/ui/generative.js +313 -0
  141. package/dist/src/ui/hover.d.ts +6 -0
  142. package/dist/src/ui/hover.js +210 -0
  143. package/dist/src/ui/index.d.ts +42 -0
  144. package/dist/src/ui/index.js +87 -0
  145. package/dist/src/ui/integrations.d.ts +13 -0
  146. package/dist/src/ui/integrations.js +175 -0
  147. package/dist/src/ui/maps.d.ts +87 -0
  148. package/dist/src/ui/maps.js +547 -0
  149. package/dist/src/ui/roster.d.ts +32 -0
  150. package/dist/src/ui/roster.js +163 -0
  151. package/dist/src/ui/talk.d.ts +12 -0
  152. package/dist/src/ui/talk.js +560 -0
  153. package/dist/src/ui/tickets.d.ts +13 -0
  154. package/dist/src/ui/tickets.js +966 -0
  155. package/dist/src/ui/toast.d.ts +7 -0
  156. package/dist/src/ui/toast.js +258 -0
  157. package/dist/src/ui/tokenText.d.ts +18 -0
  158. package/dist/src/ui/tokenText.js +96 -0
  159. package/dist/src/ui/tokenized.d.ts +42 -0
  160. package/dist/src/ui/tokenized.js +622 -0
  161. package/dist/src/ui/ui.css +4763 -0
  162. package/dist/src/ui/view.d.ts +23 -0
  163. package/dist/src/ui/view.js +584 -0
  164. package/dist/src/view.d.ts +113 -0
  165. package/dist/src/view.js +386 -0
  166. package/dist/src/webhooks.d.ts +66 -0
  167. package/dist/src/webhooks.js +78 -0
  168. package/dist/ui.d.ts +1 -0
  169. package/dist/ui.js +1 -0
  170. package/package.json +87 -4
  171. package/README.md +0 -3
@@ -0,0 +1,768 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
3
+ import { createRoot } from "react-dom/client";
4
+ import gsap from "gsap";
5
+ import { Fauna } from "../../index.js";
6
+ import { View, createView } from "./view.js";
7
+ export function createPanelBus() {
8
+ const listeners = new Set();
9
+ return {
10
+ open: (a) => { for (const l of listeners)
11
+ l.open?.(a); },
12
+ point: (a) => { for (const l of listeners)
13
+ l.point?.(a); },
14
+ added: () => { for (const l of listeners)
15
+ l.added?.(); },
16
+ listen(handlers) {
17
+ listeners.add(handlers);
18
+ return () => { listeners.delete(handlers); };
19
+ },
20
+ };
21
+ }
22
+ /** A file part from a message as an attachment of this chat. */
23
+ export function attachmentOf(room, part, message) {
24
+ return Fauna.Parrot.Share.Attachment.fromJSON({ ...part, chat: room.chatId ?? "", ...(message !== undefined ? { message } : {}) }, room.getAuthorization());
25
+ }
26
+ function keyOf(attachment) {
27
+ const auth = attachment.file.authorization;
28
+ return attachment.ref || auth.accessCode || attachment.file.name;
29
+ }
30
+ function iconOf(contentType, name) {
31
+ if (contentType.startsWith("image/"))
32
+ return "image";
33
+ if (contentType.startsWith("video/"))
34
+ return "movie";
35
+ if (contentType.startsWith("audio/"))
36
+ return "audio_file";
37
+ if (contentType === "application/pdf")
38
+ return "picture_as_pdf";
39
+ if (contentType === "text/csv" || /\.csv$/i.test(name))
40
+ return "table_chart";
41
+ if (Fauna.View.isText(contentType, name))
42
+ return "description";
43
+ return "draft";
44
+ }
45
+ function when(at) {
46
+ const seconds = Math.round((Date.now() - at.getTime()) / 1000);
47
+ if (seconds < 60)
48
+ return "just now";
49
+ if (seconds < 3600)
50
+ return `${Math.floor(seconds / 60)} min ago`;
51
+ if (seconds < 86400)
52
+ return `${Math.floor(seconds / 3600)} h ago`;
53
+ return at.toLocaleDateString(undefined, { month: "short", day: "numeric" });
54
+ }
55
+ function personName(profile) {
56
+ return profile?.displayName || profile?.email?.split("@")[0] || "Someone";
57
+ }
58
+ /** One object URL per file for the session; null when the file has no preview. */
59
+ const previewUrls = new Map();
60
+ const previewListeners = new Map();
61
+ const previewStarts = new WeakMap();
62
+ const nearScreen = typeof IntersectionObserver === "undefined" ? null : new IntersectionObserver((entries) => {
63
+ for (const entry of entries) {
64
+ if (!entry.isIntersecting)
65
+ continue;
66
+ nearScreen.unobserve(entry.target);
67
+ previewStarts.get(entry.target)?.();
68
+ previewStarts.delete(entry.target);
69
+ }
70
+ }, { rootMargin: "200px" });
71
+ function loadPreview(file, revalidate) {
72
+ return file.preview(revalidate).then((bytes) => bytes ? URL.createObjectURL(new Blob([new Uint8Array(bytes)], { type: "image/webp" })) : null, () => null);
73
+ }
74
+ /**
75
+ * Tell `onUrl` the file's preview once `target` nears the screen, and again whenever
76
+ * it's refreshed. Returns a function that stops listening.
77
+ */
78
+ export function watchPreview(target, source, onUrl) {
79
+ const { key, file } = source;
80
+ let listeners = previewListeners.get(key);
81
+ if (!listeners)
82
+ previewListeners.set(key, listeners = new Set());
83
+ listeners.add(onUrl);
84
+ const start = () => {
85
+ let url = previewUrls.get(key);
86
+ if (!url)
87
+ previewUrls.set(key, url = loadPreview(file, false));
88
+ void url.then((u) => { if (listeners.has(onUrl))
89
+ onUrl(u); });
90
+ };
91
+ if (previewUrls.has(key) || !nearScreen)
92
+ start();
93
+ else {
94
+ previewStarts.set(target, start);
95
+ nearScreen.observe(target);
96
+ }
97
+ return () => {
98
+ listeners.delete(onUrl);
99
+ nearScreen?.unobserve(target);
100
+ previewStarts.delete(target);
101
+ };
102
+ }
103
+ /** The file changed: fetch its preview again and show it everywhere it's shown. */
104
+ export function refreshPreview(source) {
105
+ const old = previewUrls.get(source.key);
106
+ const url = loadPreview(source.file, true);
107
+ previewUrls.set(source.key, url);
108
+ void url.then((u) => {
109
+ for (const onUrl of [...(previewListeners.get(source.key) ?? [])])
110
+ onUrl(u);
111
+ void old?.then((o) => { if (o && o !== u)
112
+ URL.revokeObjectURL(o); });
113
+ });
114
+ }
115
+ /** Show `url` in a holder that keeps its type icon underneath for when there's none. */
116
+ export function paintPreview(holder, url) {
117
+ let img = holder.querySelector(":scope > img");
118
+ if (!url) {
119
+ img?.remove();
120
+ holder.classList.remove("chat-file-thumb-on");
121
+ return;
122
+ }
123
+ if (!img) {
124
+ img = document.createElement("img");
125
+ img.alt = "";
126
+ img.draggable = false;
127
+ holder.appendChild(img);
128
+ }
129
+ img.src = url;
130
+ holder.classList.add("chat-file-thumb-on");
131
+ }
132
+ /** A file's preview in a holder built outside React; it stops listening once the holder leaves the page. */
133
+ export function showPreview(holder, source) {
134
+ let shown = false;
135
+ const stop = watchPreview(holder, source, (url) => {
136
+ if (shown && !holder.isConnected)
137
+ return stop();
138
+ shown = true;
139
+ paintPreview(holder, url);
140
+ });
141
+ }
142
+ /** A file card for a message: its preview when the file has one, else its type icon. Click opens it. */
143
+ export function buildAttachmentCard(name, contentType, onOpen, iconMap, preview) {
144
+ const card = document.createElement(onOpen ? "button" : "div");
145
+ card.className = "chat-message-file";
146
+ if (onOpen) {
147
+ ;
148
+ card.type = "button";
149
+ card.classList.add("chat-message-file-open");
150
+ card.title = `Open ${name}`;
151
+ card.addEventListener("click", (e) => {
152
+ e.stopPropagation();
153
+ onOpen();
154
+ });
155
+ }
156
+ const thumb = document.createElement("span");
157
+ thumb.className = "chat-file-thumb chat-message-file-thumb";
158
+ if (contentType.startsWith("video/"))
159
+ thumb.classList.add("chat-file-thumb-video");
160
+ const icon = document.createElement("span");
161
+ icon.className = "material-symbols-outlined chat-message-file-preview";
162
+ icon.textContent = iconMap?.[contentType] ?? iconOf(contentType, name);
163
+ thumb.appendChild(icon);
164
+ card.appendChild(thumb);
165
+ if (preview)
166
+ showPreview(thumb, preview);
167
+ const label = document.createElement("span");
168
+ label.className = "chat-message-file-label";
169
+ label.textContent = name;
170
+ card.appendChild(label);
171
+ return card;
172
+ }
173
+ /** A file's view in a popup, for hosts without a side panel. */
174
+ export function openViewModal(target) {
175
+ const veil = document.createElement("div");
176
+ veil.className = "chat-view-modal";
177
+ const box = document.createElement("div");
178
+ box.className = "chat-view-modal-box";
179
+ const close = document.createElement("button");
180
+ close.type = "button";
181
+ close.className = "material-symbols-outlined chat-view-modal-close";
182
+ close.title = "Close";
183
+ close.textContent = "close";
184
+ const body = document.createElement("div");
185
+ body.className = "chat-view-modal-body";
186
+ box.append(close, body);
187
+ veil.appendChild(box);
188
+ document.body.appendChild(veil);
189
+ const view = createView(body, target);
190
+ const shut = () => {
191
+ view.destroy();
192
+ veil.remove();
193
+ document.removeEventListener("keydown", onKey);
194
+ };
195
+ const onKey = (e) => { if (e.key === "Escape")
196
+ shut(); };
197
+ close.onclick = shut;
198
+ veil.addEventListener("click", (e) => { if (e.target === veil)
199
+ shut(); });
200
+ document.addEventListener("keydown", onKey);
201
+ }
202
+ // ─── The side panel ──────────────────────────────────────────────────────────
203
+ /** A row's small preview, with the type icon until (or unless) the file has one. */
204
+ function RowThumb({ attachment }) {
205
+ const holder = useRef(null);
206
+ const [url, setUrl] = useState(null);
207
+ const key = keyOf(attachment);
208
+ useEffect(() => {
209
+ if (!holder.current)
210
+ return;
211
+ return watchPreview(holder.current, { key, file: attachment.file }, setUrl);
212
+ }, [key, attachment.file]);
213
+ return (_jsxs("span", { ref: holder, className: `chat-file-thumb chat-panel-row-thumb${url ? " chat-file-thumb-on" : ""}`, children: [_jsx("span", { className: "material-symbols-outlined chat-panel-row-icon", children: iconOf(attachment.file.contentType, attachment.file.name) }), url && _jsx("img", { src: url, alt: "", draggable: false })] }));
214
+ }
215
+ const WIDTH_KEY = "fauna-chat-panel-width";
216
+ const MIN_WIDTH = 280;
217
+ /** The chat keeps at least this much beside the panel; narrower, the panel covers it. */
218
+ const CHAT_MIN_WIDTH = 400;
219
+ /** The panel slides in and out like the apps' left navigation. */
220
+ const SLIDE = { duration: 0.5, ease: "power2.out" };
221
+ export function ChatPanel({ room, bus, open, onOpenChange }) {
222
+ const [attachments, setAttachments] = useState(null);
223
+ const [failed, setFailed] = useState(false);
224
+ const [tabs, setTabs] = useState([]);
225
+ const [active, setActive] = useState("attachments");
226
+ const [selected, setSelected] = useState(null);
227
+ const [width, setWidth] = useState(() => Number(localStorage.getItem(WIDTH_KEY)) || 440);
228
+ const [rootWidth, setRootWidth] = useState(0);
229
+ const panel = useRef(null);
230
+ const loaded = useRef(false);
231
+ const refresh = useRef(null);
232
+ const entered = useRef(false);
233
+ const closing = useRef(null);
234
+ const layout = useRef({ cover: false, shown: 0 });
235
+ /** Slide back open from wherever a close got to. */
236
+ const slideBack = useCallback(() => {
237
+ const el = panel.current;
238
+ if (!el || !closing.current)
239
+ return;
240
+ closing.current.kill();
241
+ closing.current = null;
242
+ gsap.to(el, layout.current.cover ? { xPercent: 0, ...SLIDE } : { width: layout.current.shown, ...SLIDE });
243
+ }, []);
244
+ const load = useCallback(() => {
245
+ room.attachments().then((list) => {
246
+ setAttachments(list);
247
+ setFailed(false);
248
+ }, () => setFailed(true));
249
+ }, [room]);
250
+ useEffect(() => {
251
+ if (open && !loaded.current) {
252
+ loaded.current = true;
253
+ load();
254
+ }
255
+ }, [open, load]);
256
+ const openTab = useCallback((attachment, everyone) => {
257
+ const key = keyOf(attachment);
258
+ setTabs((current) => current.some((t) => t.key === key) ? current : [...current, { key, attachment, people: [] }]);
259
+ setActive(key);
260
+ slideBack();
261
+ if (everyone || !open)
262
+ onOpenChange(true);
263
+ }, [open, onOpenChange, slideBack]);
264
+ useEffect(() => bus.listen({
265
+ open: (a) => openTab(a, false),
266
+ point: (a) => openTab(a, true),
267
+ added: () => {
268
+ if (!loaded.current)
269
+ return;
270
+ if (refresh.current)
271
+ clearTimeout(refresh.current);
272
+ refresh.current = setTimeout(load, 400);
273
+ },
274
+ }), [bus, openTab, load]);
275
+ const closeTab = (key) => {
276
+ setTabs((current) => current.filter((t) => t.key !== key));
277
+ if (active === key)
278
+ setActive("attachments");
279
+ };
280
+ const setPeople = useCallback((key, people) => {
281
+ setTabs((current) => current.map((t) => t.key === key ? { ...t, people } : t));
282
+ }, []);
283
+ useLayoutEffect(() => {
284
+ const root = panel.current?.parentElement;
285
+ if (!open || !root)
286
+ return;
287
+ const observer = new ResizeObserver(() => setRootWidth(root.clientWidth));
288
+ observer.observe(root);
289
+ setRootWidth(root.clientWidth);
290
+ return () => observer.disconnect();
291
+ }, [open]);
292
+ const cover = rootWidth > 0 && rootWidth < MIN_WIDTH + CHAT_MIN_WIDTH;
293
+ const shown = rootWidth > 0 ? Math.max(MIN_WIDTH, Math.min(width, rootWidth - CHAT_MIN_WIDTH)) : width;
294
+ layout.current = { cover, shown };
295
+ // Slide in once the room for it is measured, so it lands at its real width.
296
+ useLayoutEffect(() => {
297
+ if (!open) {
298
+ entered.current = false;
299
+ return;
300
+ }
301
+ const el = panel.current;
302
+ if (!el || rootWidth === 0 || entered.current)
303
+ return;
304
+ entered.current = true;
305
+ if (cover)
306
+ gsap.fromTo(el, { xPercent: 100 }, { xPercent: 0, ...SLIDE });
307
+ else
308
+ gsap.fromTo(el, { width: 0 }, { width: shown, ...SLIDE });
309
+ // eslint-disable-next-line react-hooks/exhaustive-deps
310
+ }, [open, rootWidth]);
311
+ const hide = () => {
312
+ const el = panel.current;
313
+ if (!el)
314
+ return onOpenChange(false);
315
+ gsap.killTweensOf(el);
316
+ closing.current = gsap.to(el, {
317
+ ...(layout.current.cover ? { xPercent: 100 } : { width: 0 }),
318
+ ...SLIDE,
319
+ onComplete: () => {
320
+ closing.current = null;
321
+ onOpenChange(false);
322
+ },
323
+ });
324
+ };
325
+ const startResize = (e) => {
326
+ e.preventDefault();
327
+ if (panel.current)
328
+ gsap.killTweensOf(panel.current);
329
+ const startX = e.clientX;
330
+ const startWidth = shown;
331
+ const move = (ev) => {
332
+ const most = (panel.current?.parentElement?.clientWidth ?? window.innerWidth) - CHAT_MIN_WIDTH;
333
+ setWidth(Math.max(MIN_WIDTH, Math.min(most, startWidth + (startX - ev.clientX))));
334
+ };
335
+ const up = () => {
336
+ window.removeEventListener("pointermove", move);
337
+ window.removeEventListener("pointerup", up);
338
+ setWidth((w) => { localStorage.setItem(WIDTH_KEY, String(Math.round(w))); return w; });
339
+ };
340
+ window.addEventListener("pointermove", move);
341
+ window.addEventListener("pointerup", up);
342
+ };
343
+ if (!open)
344
+ return null;
345
+ return (_jsxs("div", { className: `chat-panel${cover ? " chat-panel-cover" : ""}`, ref: panel, style: cover ? undefined : { width: shown }, children: [!cover && _jsx("div", { className: "chat-panel-handle", onPointerDown: startResize, title: "Drag to resize" }), _jsxs("div", { className: "chat-panel-inner", style: cover ? undefined : { width: shown }, children: [_jsxs("div", { className: "chat-panel-tabs", children: [_jsxs("button", { className: `chat-panel-tab${active === "attachments" ? " chat-panel-tab-on" : ""}`, onClick: () => setActive("attachments"), children: [_jsx("span", { className: "material-symbols-outlined", children: "attach_file" }), _jsx("span", { className: "chat-panel-tab-name", children: "Attachments" })] }), tabs.map((tab) => (_jsxs("div", { className: `chat-panel-tab${active === tab.key ? " chat-panel-tab-on" : ""}`, onClick: () => setActive(tab.key), title: tab.attachment.file.name, children: [_jsx("span", { className: "material-symbols-outlined", children: iconOf(tab.attachment.file.contentType, tab.attachment.file.name) }), _jsx("span", { className: "chat-panel-tab-name", children: tab.attachment.file.name }), tab.people.length > 0 && _jsx("span", { className: "chat-panel-tab-people", title: tab.people.map((p) => personName(p.profile)).join(", "), children: tab.people.length }), _jsx("button", { className: "material-symbols-outlined chat-panel-tab-close", title: "Close", "aria-label": `Close ${tab.attachment.file.name}`, onClick: (e) => { e.stopPropagation(); closeTab(tab.key); }, children: "close" })] }, tab.key))), _jsx("span", { className: "chat-panel-tabs-gap" }), _jsx("button", { className: "material-symbols-outlined chat-panel-hide", title: "Hide", "aria-label": "Hide attachments", onClick: hide, children: "right_panel_close" })] }), _jsxs("div", { className: "chat-panel-body", children: [_jsx("div", { className: "chat-panel-list", style: active === "attachments" ? undefined : { display: "none" }, children: attachments === null
346
+ ? _jsx("div", { className: "chat-panel-empty", children: failed ? "Couldn't load the attachments." : "Loading\u2026" })
347
+ : attachments.length === 0
348
+ ? _jsx("div", { className: "chat-panel-empty", children: "Files sent in this chat, and files Parrot opens, show up here." })
349
+ : attachments.map((a) => {
350
+ const key = keyOf(a);
351
+ return (_jsxs("div", { className: `chat-panel-row${selected === key ? " chat-panel-row-on" : ""}`, tabIndex: 0, onClick: () => setSelected(key), onDoubleClick: () => openTab(a, false), onKeyDown: (e) => { if (e.key === "Enter")
352
+ openTab(a, false); }, children: [_jsx(RowThumb, { attachment: a }), _jsxs("div", { className: "chat-panel-row-text", children: [_jsx("div", { className: "chat-panel-row-name", children: a.file.name }), _jsxs("div", { className: "chat-panel-row-meta", children: [a.author ? personName(a.author) : "Parrot", ", ", when(a.createdAt)] })] })] }, key));
353
+ }) }), tabs.map((tab) => (_jsx("div", { className: "chat-panel-view", style: active === tab.key ? undefined : { display: "none" }, children: _jsx(View, { target: tab.attachment, onPeople: (people) => setPeople(tab.key, people), onSaved: () => refreshPreview({ key: tab.key, file: tab.attachment.file }) }) }, tab.key)))] })] })] }));
354
+ }
355
+ function reducedMotion() {
356
+ return typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true;
357
+ }
358
+ /** A popup coming in: a short rise and scale from `origin`, then its `rows` one after another. */
359
+ function popIn(el, origin, rows) {
360
+ if (reducedMotion())
361
+ return;
362
+ gsap.fromTo(el, { autoAlpha: 0, y: 8, scale: 0.96, transformOrigin: origin }, { autoAlpha: 1, y: 0, scale: 1, duration: 0.28, ease: "back.out(1.7)" });
363
+ const items = el.querySelectorAll(rows);
364
+ if (items.length) {
365
+ gsap.fromTo(items, { autoAlpha: 0, y: 6 }, { autoAlpha: 1, y: 0, duration: 0.24, stagger: 0.035, delay: 0.05, ease: "power2.out", clearProps: "opacity,visibility,transform" });
366
+ }
367
+ }
368
+ /** A dialog coming in over its veil. */
369
+ function popInDialog(veil, box) {
370
+ if (reducedMotion())
371
+ return;
372
+ gsap.fromTo(veil, { autoAlpha: 0 }, { autoAlpha: 1, duration: 0.22, ease: "power1.out" });
373
+ popIn(box, "50% 60%", ":scope > *");
374
+ }
375
+ /** A popup going away, then `done`. With `inner`, `el` is a veil that fades while `inner` sinks. */
376
+ function popOut(el, done, inner) {
377
+ if (reducedMotion()) {
378
+ done();
379
+ return;
380
+ }
381
+ if (inner) {
382
+ gsap.killTweensOf([el, inner]);
383
+ gsap.to(inner, { y: 8, scale: 0.97, duration: 0.18, ease: "power2.in" });
384
+ gsap.to(el, { autoAlpha: 0, duration: 0.18, ease: "power2.in", onComplete: done });
385
+ return;
386
+ }
387
+ gsap.killTweensOf(el);
388
+ gsap.to(el, { autoAlpha: 0, y: 6, scale: 0.97, duration: 0.16, ease: "power2.in", onComplete: done });
389
+ }
390
+ /** A holder that only keeps its pick, for a form that saves it later (a new timeline's repository). */
391
+ export function pickedRepository(authorization, group) {
392
+ const listeners = new Set();
393
+ const holder = {
394
+ picked: null,
395
+ get repository() { return holder.picked?.id ?? null; },
396
+ group,
397
+ getAuthorization: () => authorization,
398
+ async setRepository(repository) {
399
+ holder.picked = repository;
400
+ for (const listener of listeners)
401
+ listener(repository?.id ?? null);
402
+ },
403
+ watchRepository(listener) {
404
+ listeners.add(listener);
405
+ return () => { listeners.delete(listener); };
406
+ },
407
+ };
408
+ return holder;
409
+ }
410
+ /** Resolves once the room has `repository` (an app passes it on), or after a while either way. */
411
+ export function roomHas(room, repository, signal, ms = 15000) {
412
+ if (room.repository === repository)
413
+ return Promise.resolve();
414
+ return new Promise((resolve) => {
415
+ let stop = () => { };
416
+ const finish = () => { clearTimeout(timer); stop(); resolve(); };
417
+ const timer = setTimeout(finish, ms);
418
+ stop = room.watchRepository((current) => { if (current === repository)
419
+ finish(); });
420
+ signal?.addEventListener("abort", finish, { once: true });
421
+ });
422
+ }
423
+ function statusLabel(repository) {
424
+ const status = repository?.machine?.status;
425
+ if (status === "RUNNING")
426
+ return "Running";
427
+ if (status === "BOOTING")
428
+ return "Starting";
429
+ if (status === "STOPPING")
430
+ return "Saving";
431
+ return "Stopped";
432
+ }
433
+ /**
434
+ * The chat's repository in the message box: a button to attach one (pick or make
435
+ * one of the chat owner's), and once attached a chip with its name and whether its
436
+ * machine is running, with Open in Termite and Detach. While a reply runs it
437
+ * rests, faded and out of reach: the reply finishes on the repository it started with.
438
+ */
439
+ export function RepositoryControl({ holder, replying = false, onOpenRepository, onChange, suggestedName = "", placement = "up" }) {
440
+ const [id, setId] = useState(holder.repository);
441
+ const [info, setInfo] = useState(null);
442
+ const [menu, setMenu] = useState(false);
443
+ const [list, setList] = useState(null);
444
+ const [name, setName] = useState(suggestedName);
445
+ const [busy, setBusy] = useState(false);
446
+ const [error, setError] = useState("");
447
+ const box = useRef(null);
448
+ const menuRef = useRef(null);
449
+ const closing = useRef(false);
450
+ const shaking = useRef(null);
451
+ const group = holder.group;
452
+ const auth = holder.getAuthorization();
453
+ useEffect(() => { setName((current) => current || suggestedName); }, [suggestedName]);
454
+ const closeMenu = useCallback(() => {
455
+ if (!menuRef.current || closing.current)
456
+ return;
457
+ closing.current = true;
458
+ popOut(menuRef.current, () => {
459
+ closing.current = false;
460
+ setMenu(false);
461
+ setList(null);
462
+ });
463
+ }, []);
464
+ const toggleMenu = () => { if (menu)
465
+ closeMenu();
466
+ else
467
+ setMenu(true); };
468
+ useLayoutEffect(() => {
469
+ if (menu && menuRef.current)
470
+ popIn(menuRef.current, placement === "down" ? "100% 0%" : "100% 100%", ".chat-repository-current, .chat-repository-heading, .chat-shimmer, .chat-repository-option, .chat-repository-new");
471
+ }, [menu, placement]);
472
+ // Rows that arrive after the menu opened follow one after another too.
473
+ useLayoutEffect(() => {
474
+ if (!menu || !list || !menuRef.current || reducedMotion())
475
+ return;
476
+ const rows = menuRef.current.querySelectorAll(".chat-repository-option, .chat-repository-empty");
477
+ if (rows.length)
478
+ gsap.fromTo(rows, { autoAlpha: 0, y: 4 }, { autoAlpha: 1, y: 0, duration: 0.2, stagger: 0.03, ease: "power2.out", clearProps: "opacity,visibility,transform" });
479
+ }, [list, menu]);
480
+ useEffect(() => {
481
+ const el = box.current;
482
+ if (!el)
483
+ return;
484
+ el.style.pointerEvents = replying ? "none" : "";
485
+ if (replying)
486
+ closeMenu();
487
+ gsap.to(el, { opacity: replying ? 0.4 : 1, duration: reducedMotion() ? 0 : 0.3, ease: "power2.out" });
488
+ }, [replying, closeMenu]);
489
+ /** The box icon shakes on hover, like the paperclip turns and send tilts. */
490
+ const hover = (e, on) => {
491
+ const target = e.currentTarget;
492
+ gsap.to(target, { duration: 0.3, color: on ? "var(--chat-text, #1a1a1a)" : "var(--chat-accent, #888)", ease: "power2.inOut" });
493
+ const icon = target.classList.contains("material-symbols-outlined") ? target : target.querySelector(".material-symbols-outlined");
494
+ if (!on || !icon || reducedMotion())
495
+ return;
496
+ shaking.current?.kill();
497
+ const shake = gsap.timeline();
498
+ for (const rotation of [-14, 12, -8, 5, 0])
499
+ shake.to(icon, { rotation, duration: 0.09, ease: "sine.inOut", transformOrigin: "50% 60%" });
500
+ shaking.current = shake;
501
+ };
502
+ const onChangeRef = useRef(onChange);
503
+ onChangeRef.current = onChange;
504
+ useEffect(() => {
505
+ setId(holder.repository);
506
+ return holder.watchRepository((repository) => { setId(repository); onChangeRef.current?.(repository); });
507
+ }, [holder]);
508
+ useEffect(() => {
509
+ if (id === null) {
510
+ setInfo(null);
511
+ return;
512
+ }
513
+ let active = true;
514
+ const refresh = () => Fauna.Termite.Versioning.get(auth, id, group ? { group } : undefined)
515
+ .then((repository) => { if (active)
516
+ setInfo(repository); }, () => { if (active)
517
+ setInfo(null); });
518
+ void refresh();
519
+ const timer = setInterval(refresh, 15000);
520
+ return () => { active = false; clearInterval(timer); };
521
+ }, [id, auth, group]);
522
+ useEffect(() => {
523
+ if (!menu)
524
+ return;
525
+ setError("");
526
+ let active = true;
527
+ Fauna.Termite.Versioning.list(auth, group ? { group } : undefined).then((repositories) => { if (active)
528
+ setList(repositories); }, (err) => { if (active)
529
+ setError(err.message); });
530
+ const onDown = (e) => { if (!box.current?.contains(e.target))
531
+ closeMenu(); };
532
+ const onKey = (e) => { if (e.key === "Escape")
533
+ closeMenu(); };
534
+ document.addEventListener("mousedown", onDown);
535
+ document.addEventListener("keydown", onKey);
536
+ return () => {
537
+ active = false;
538
+ document.removeEventListener("mousedown", onDown);
539
+ document.removeEventListener("keydown", onKey);
540
+ };
541
+ }, [menu, auth, group, closeMenu]);
542
+ const attach = async (repository) => {
543
+ setBusy(true);
544
+ setError("");
545
+ try {
546
+ await holder.setRepository(repository);
547
+ setId(repository?.id ?? null);
548
+ closeMenu();
549
+ }
550
+ catch (err) {
551
+ setError(err?.message ?? "Couldn't change the repository.");
552
+ }
553
+ finally {
554
+ setBusy(false);
555
+ }
556
+ };
557
+ const create = async () => {
558
+ const trimmed = name.trim();
559
+ if (!trimmed)
560
+ return;
561
+ setBusy(true);
562
+ setError("");
563
+ try {
564
+ const repository = await Fauna.Termite.Versioning.create(auth, { name: trimmed, ...(group ? { group } : {}) });
565
+ setName("");
566
+ await attach(repository);
567
+ }
568
+ catch (err) {
569
+ setError(err?.message ?? "Couldn't make the repository.");
570
+ setBusy(false);
571
+ }
572
+ };
573
+ const status = statusLabel(info);
574
+ const others = list?.filter((r) => r.id !== id) ?? null;
575
+ return (_jsxs("div", { className: placement === "down" ? "chat-repository chat-repository-down" : "chat-repository", ref: box, children: [id === null
576
+ ? _jsx("span", { className: "material-symbols-outlined chat-repository-button", title: "Attach a repository", onClick: toggleMenu, onMouseEnter: (e) => hover(e, true), onMouseLeave: (e) => hover(e, false), children: "deployed_code" })
577
+ : (_jsxs("button", { type: "button", className: "chat-repository-chip", title: `${info?.name ?? "Repository"}, ${status}`, onClick: toggleMenu, onMouseEnter: (e) => hover(e, true), onMouseLeave: (e) => hover(e, false), children: [_jsx("span", { className: "material-symbols-outlined", children: "deployed_code" }), _jsx("span", { className: "chat-repository-name", children: info?.name ?? "Repository" }), _jsx("span", { className: `chat-repository-dot chat-repository-${status.toLowerCase()}` })] })), menu && (_jsxs("div", { className: "chat-repository-menu", ref: menuRef, children: [id !== null && (_jsxs("div", { className: "chat-repository-current", children: [_jsxs("div", { className: "chat-repository-current-row", children: [_jsx("span", { className: "material-symbols-outlined", children: "deployed_code" }), _jsxs("div", { className: "chat-repository-current-text", children: [_jsx("div", { className: "chat-repository-current-name", children: info?.name ?? "Repository" }), _jsxs("div", { className: "chat-repository-current-status", children: [_jsx("span", { className: `chat-repository-dot chat-repository-${status.toLowerCase()}` }), status] })] })] }), _jsxs("div", { className: "chat-repository-actions", children: [onOpenRepository && _jsx("button", { type: "button", onClick: () => { onOpenRepository(id); closeMenu(); }, children: "Open in Termite" }), _jsx("button", { type: "button", disabled: busy, onClick: () => void attach(null), children: "Detach" })] })] })), _jsx("div", { className: "chat-repository-heading", children: id === null ? "Work in a repository" : "Switch to another" }), _jsxs("div", { className: "chat-repository-list", children: [others === null && !error && _jsxs(_Fragment, { children: [_jsx("div", { className: "chat-shimmer" }), _jsx("div", { className: "chat-shimmer" })] }), others?.map((r) => (_jsxs("button", { type: "button", className: "chat-repository-option", disabled: busy, onClick: () => void attach(r), children: [_jsx("span", { className: "material-symbols-outlined", children: "deployed_code" }), _jsx("span", { className: "chat-repository-option-name", children: r.name || `Repository ${r.id}` }), _jsx("span", { className: `chat-repository-dot chat-repository-${statusLabel(r).toLowerCase()}`, title: statusLabel(r) })] }, r.id))), others?.length === 0 && (_jsx("div", { className: "chat-repository-empty", children: id !== null ? "No other repositories yet." : group ? "This group has no repositories yet." : "You have no repositories yet." }))] }), _jsxs("form", { className: "chat-repository-new", onSubmit: (e) => { e.preventDefault(); void create(); }, children: [_jsx("input", { value: name, placeholder: "New repository name", onChange: (e) => setName(e.target.value), disabled: busy }), _jsx("button", { type: "submit", disabled: busy || !name.trim(), children: "Create" })] }), error && _jsx("div", { className: "chat-repository-error", children: error })] }))] }));
578
+ }
579
+ /** The repository control in a node the chat engine owns; `setReplying` rests it while a reply runs. */
580
+ export function mountRepositoryControl(container, holder, onOpenRepository, onChange) {
581
+ const root = createRoot(container);
582
+ let current = null;
583
+ const render = (replying) => {
584
+ if (replying === current)
585
+ return;
586
+ current = replying;
587
+ root.render(_jsx(RepositoryControl, { holder: holder, replying: replying, onOpenRepository: onOpenRepository, onChange: onChange }));
588
+ };
589
+ render(false);
590
+ return { setReplying: render, unmount: () => root.unmount() };
591
+ }
592
+ // ─── Asking for a computer ───────────────────────────────────────────────────
593
+ let prices = null;
594
+ function termitePricing() {
595
+ prices ?? (prices = Fauna.Termite.OS.pricing().catch((err) => { prices = null; throw err; }));
596
+ return prices;
597
+ }
598
+ const dollars = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", minimumFractionDigits: 2, maximumFractionDigits: 3 });
599
+ function ComputerConsent({ holder, suggested, asker, signal, onDone }) {
600
+ const [name, setName] = useState(suggested.trim().slice(0, 60) || `${asker} computer`);
601
+ const [pricing, setPricing] = useState(null);
602
+ const [busy, setBusy] = useState(false);
603
+ const [error, setError] = useState("");
604
+ const veil = useRef(null);
605
+ const box = useRef(null);
606
+ const input = useRef(null);
607
+ const closing = useRef(false);
608
+ const group = holder.group;
609
+ const close = useCallback((repository) => {
610
+ if (closing.current)
611
+ return;
612
+ closing.current = true;
613
+ if (veil.current)
614
+ popOut(veil.current, () => onDone(repository), box.current);
615
+ else
616
+ onDone(repository);
617
+ }, [onDone]);
618
+ useLayoutEffect(() => {
619
+ if (veil.current && box.current)
620
+ popInDialog(veil.current, box.current);
621
+ input.current?.focus();
622
+ input.current?.select();
623
+ }, []);
624
+ useEffect(() => {
625
+ let active = true;
626
+ termitePricing().then((p) => { if (active)
627
+ setPricing(p); }, () => { if (active)
628
+ setPricing("none"); });
629
+ return () => { active = false; };
630
+ }, []);
631
+ useEffect(() => {
632
+ const onKey = (e) => { if (e.key === "Escape" && !busy)
633
+ close(null); };
634
+ const onAbort = () => close(null);
635
+ document.addEventListener("keydown", onKey);
636
+ signal?.addEventListener("abort", onAbort, { once: true });
637
+ return () => {
638
+ document.removeEventListener("keydown", onKey);
639
+ signal?.removeEventListener("abort", onAbort);
640
+ };
641
+ }, [busy, close, signal]);
642
+ const create = async () => {
643
+ const trimmed = name.trim();
644
+ if (!trimmed || busy)
645
+ return;
646
+ setBusy(true);
647
+ setError("");
648
+ try {
649
+ const repository = await Fauna.Termite.Versioning.create(holder.getAuthorization(), { name: trimmed, ...(group ? { group } : {}) });
650
+ await holder.setRepository(repository);
651
+ close(repository);
652
+ }
653
+ catch (err) {
654
+ setError(err?.message ?? "Couldn't make the repository.");
655
+ setBusy(false);
656
+ }
657
+ };
658
+ return (_jsx("div", { className: "chat-computer-veil", ref: veil, onMouseDown: (e) => { if (e.target === veil.current && !busy)
659
+ close(null); }, children: _jsxs("form", { className: "chat-computer", ref: box, role: "dialog", "aria-modal": "true", "aria-labelledby": "chat-computer-title", onSubmit: (e) => { e.preventDefault(); void create(); }, children: [_jsx("div", { className: "chat-computer-icon", children: _jsx("span", { className: "material-symbols-outlined", children: "desktop_windows" }) }), _jsx("h2", { className: "chat-computer-title", id: "chat-computer-title", children: "Use a computer?" }), _jsxs("p", { className: "chat-computer-text", children: [asker, " wants a computer for this. It needs a repository: a Linux machine with a desktop, a browser and its own files, yours to keep in Termite."] }), pricing !== "none" && (_jsxs("div", { className: "chat-computer-prices", children: [pricing === null
660
+ ? _jsxs(_Fragment, { children: [_jsx("div", { className: "chat-shimmer" }), _jsx("div", { className: "chat-shimmer" })] })
661
+ : _jsxs(_Fragment, { children: [_jsxs("div", { className: "chat-computer-price", children: [_jsx("span", { className: "material-symbols-outlined", children: "bolt" }), _jsxs("span", { children: [_jsx("b", { children: dollars.format(pricing.machinePerHour) }), " an hour while it runs. It stops on its own once it's done."] })] }), _jsxs("div", { className: "chat-computer-price", children: [_jsx("span", { className: "material-symbols-outlined", children: "folder" }), _jsxs("span", { children: [_jsx("b", { children: dollars.format(pricing.storagePerGbMonth) }), " per GB of files a month."] })] })] }), _jsx("div", { className: "chat-computer-payer", children: group ? "The group pays for it from its credits." : "It runs on your credits." })] })), _jsx("label", { className: "chat-computer-label", htmlFor: "chat-computer-name", children: "Name" }), _jsx("input", { id: "chat-computer-name", className: "chat-computer-name", ref: input, value: name, maxLength: 60, disabled: busy, onChange: (e) => setName(e.target.value) }), error && _jsx("div", { className: "chat-computer-error", children: error }), _jsxs("div", { className: "chat-computer-buttons", children: [_jsx("button", { type: "button", className: "chat-computer-no", disabled: busy, onClick: () => close(null), children: "Not now" }), _jsx("button", { type: "submit", className: "chat-computer-yes", disabled: busy || !name.trim(), children: busy ? "Creating" : "Create" })] })] }) }));
662
+ }
663
+ /**
664
+ * Ask the person to make a repository so `asker` (Parrot, an Ant persona) can use
665
+ * a computer, named `suggested` to start. Resolves with the new repository, attached
666
+ * to `holder`, or null when they say no (or `signal` aborts first).
667
+ */
668
+ export function askForComputer(holder, suggested, signal, asker = "Parrot") {
669
+ if (signal?.aborted)
670
+ return Promise.resolve(null);
671
+ return new Promise((resolve) => {
672
+ const node = document.createElement("div");
673
+ document.body.appendChild(node);
674
+ const root = createRoot(node);
675
+ const done = (repository) => {
676
+ root.unmount();
677
+ node.remove();
678
+ resolve(repository);
679
+ };
680
+ root.render(_jsx(ComputerConsent, { holder: holder, suggested: suggested, asker: asker, signal: signal, onDone: done }));
681
+ });
682
+ }
683
+ // ─── Tool chips for files and repositories ───────────────────────────────────
684
+ function short(text, max = 60) {
685
+ const line = text.replace(/\s+/g, " ").trim();
686
+ return line.length > max ? `${line.slice(0, max - 1)}\u2026` : line;
687
+ }
688
+ function baseName(path) {
689
+ return path.split("/").filter(Boolean).pop() ?? path;
690
+ }
691
+ /** The icon and words for a chat's file and repository tools, or null for other tools. */
692
+ export function chatToolLabel(call) {
693
+ const args = call.args ?? {};
694
+ const path = String(args.path ?? "").trim();
695
+ const done = call.status === "success";
696
+ const failed = call.status === "error";
697
+ const pick = (running, success, error) => failed ? error : done ? success : running;
698
+ let out = null;
699
+ switch (call.name) {
700
+ case "write":
701
+ out = { icon: "edit_document", label: pick(`Writing ${path}`, `Wrote ${path}`, `Couldn't write ${path}`) };
702
+ break;
703
+ case "replace":
704
+ case "insert":
705
+ case "remove":
706
+ out = { icon: "edit_note", label: pick(`Editing ${path}`, `Edited ${path}`, `Couldn't edit ${path}`) };
707
+ break;
708
+ case "read":
709
+ out = { icon: "description", label: pick(`Reading ${path}`, `Read ${path}`, `Couldn't read ${path}`) };
710
+ break;
711
+ case "list": {
712
+ const target = String(args.pattern ?? "") || path || "the repository";
713
+ out = { icon: "folder_open", label: pick(`Looking through ${target}`, `Looked through ${target}`, `Couldn't look through ${target}`) };
714
+ break;
715
+ }
716
+ case "grep": {
717
+ const pattern = short(String(args.pattern ?? ""), 40);
718
+ out = { icon: "search", label: pick(`Searching for ${pattern}`, `Searched for ${pattern}`, `Search for ${pattern} failed`) };
719
+ break;
720
+ }
721
+ case "exec": {
722
+ const command = short(String(args.command ?? ""));
723
+ out = { icon: "terminal", label: pick(`Running ${command}`, `Ran ${command}`, `${command} failed`) };
724
+ break;
725
+ }
726
+ case "processes":
727
+ out = { icon: "monitoring", label: pick("Checking processes", "Checked processes", "Couldn't check processes") };
728
+ break;
729
+ case "kill":
730
+ out = { icon: "cancel", label: pick(`Stopping process ${args.pid}`, `Stopped process ${args.pid}`, `Couldn't stop process ${args.pid}`) };
731
+ break;
732
+ case "open": {
733
+ const name = path ? baseName(path) : String(args.attachment ?? "the file");
734
+ out = { icon: "open_in_new", label: pick(`Opening ${name}`, `Opened ${name}`, `Couldn't open ${name}`) };
735
+ break;
736
+ }
737
+ case "save": {
738
+ const name = String(args.attachment ?? "the file");
739
+ out = { icon: "save", label: pick(`Saving ${name}`, `Saved ${name} to the repository`, `Couldn't save ${name}`) };
740
+ break;
741
+ }
742
+ case "use_desktop":
743
+ if (done && call.result === Fauna.Termite.OS.DESKTOP_DECLINED)
744
+ out = { icon: "desktop_access_disabled", label: "Went without a computer" };
745
+ else if (!done && !failed && args.name !== undefined)
746
+ out = { icon: "desktop_windows", label: "Asking to use a computer" };
747
+ else
748
+ out = { icon: "desktop_windows", label: pick("Using the desktop", "Used the desktop", "The desktop couldn't finish") };
749
+ break;
750
+ }
751
+ if (out && call.status === "running" && typeof call.progress === "string" && call.progress)
752
+ out = { ...out, label: call.progress };
753
+ return out;
754
+ }
755
+ /** What an exec printed, from its result, for the chip's details. */
756
+ export function execOutput(result) {
757
+ if (!result)
758
+ return "";
759
+ try {
760
+ const parsed = JSON.parse(result);
761
+ const parts = [parsed.stdout, parsed.stderr].filter((s) => typeof s === "string" && s.trim());
762
+ const body = parts.join("\n").trim();
763
+ return `${body}${typeof parsed.code === "number" && parsed.code !== 0 ? `${body ? "\n" : ""}Exit code ${parsed.code}` : ""}`;
764
+ }
765
+ catch {
766
+ return result;
767
+ }
768
+ }