fauna-api 1.0.759 → 1.0.771

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.
@@ -4,6 +4,7 @@ import { Room, Presence, Authorization } from "../shared/server.js";
4
4
  import { ChatTarget, AppendResult, Attachment } from "./share.js";
5
5
  import type { Repository } from "../termite/versioning.js";
6
6
  import type { Group } from "../groups.js";
7
+ import type { Target } from "../view.js";
7
8
  export { Attachment } from "./share.js";
8
9
  /**
9
10
  * One durable message in a room transcript. Parts keep their file refs as
@@ -80,8 +81,8 @@ export type RoomHooks = Partial<{
80
81
  onRenamed(room: ChatRoom, name: string): void;
81
82
  /** The chat's repository was attached, swapped or detached. */
82
83
  onRepository(room: ChatRoom): void;
83
- /** The reply pointed everyone to a file (Parrot's `open`): show it. */
84
- onOpenFile(room: ChatRoom, attachment: Attachment): void;
84
+ /** The reply pointed everyone to a file (Parrot's `open`): show it. A chat attachment, or a file in the chat's repository, live. */
85
+ onOpenFile(room: ChatRoom, file: Target): void;
85
86
  /** The chat was deleted/closed by its owner (or the host disconnected). */
86
87
  onClosed(room: ChatRoom): void;
87
88
  /** The roster changed (someone joined/left or updated presence/status). */
@@ -2,6 +2,7 @@ import { Role } from "./generative.js";
2
2
  import { Room, Presence, connect } from "../shared/server.js";
3
3
  import { Parrot } from "./index.js";
4
4
  import { appendRoomMessage, steerRoom, unqueueRoomMessage, pageMessages, setRoomRepository, listRoomAttachments, Attachment, } from "./share.js";
5
+ import { File as FaunaFile } from "../drive.js";
5
6
  export { Attachment } from "./share.js";
6
7
  /**
7
8
  * A live, domain-agnostic chat room. `ChatRoom extends Room` and owns everything
@@ -412,9 +413,19 @@ export class ChatRoom extends Room {
412
413
  return true;
413
414
  }
414
415
  case "chat-open-file": {
415
- const raw = packet.data?.attachment;
416
- if (raw)
417
- this.hooks.onOpenFile?.(this, Attachment.fromJSON(raw, this.getAuthorization()));
416
+ const attachment = packet.data?.attachment;
417
+ const repository = packet.data?.repository;
418
+ if (attachment) {
419
+ this.hooks.onOpenFile?.(this, Attachment.fromJSON(attachment, this.getAuthorization()));
420
+ }
421
+ else if (repository) {
422
+ this.hooks.onOpenFile?.(this, FaunaFile.fromJSON(repository, {
423
+ authorization: this.getAuthorization(),
424
+ repository: Number(repository.id),
425
+ path: String(repository.path),
426
+ ...(repository.group ? { group: String(repository.group) } : {}),
427
+ }));
428
+ }
418
429
  return true;
419
430
  }
420
431
  case "chat-tool-call": {
@@ -5,8 +5,10 @@ export type Payment = {
5
5
  id: string;
6
6
  status: PaymentStatus;
7
7
  amount: number;
8
- /** Fauna's fee, taken from what the payee receives. */
8
+ /** The whole fee, taken from what the payee receives: the card cost, Fauna's cut and the app's fee. */
9
9
  fee: number;
10
+ /** The app's part of `fee`, paid to the account that owns the API key that asked for the payment. */
11
+ appFee: number;
10
12
  refunded: number;
11
13
  currency: string;
12
14
  /** The payee's email. */
@@ -28,6 +30,12 @@ export type PaymentRequest = {
28
30
  currency?: string;
29
31
  description?: string;
30
32
  reference?: string;
33
+ /**
34
+ * The app's own fee, a percent of the amount (at most 30), paid to the account that owns
35
+ * the API key once the payment succeeds. Only an API key can ask for one, and its account
36
+ * needs payouts set up.
37
+ */
38
+ appFee?: number;
31
39
  /** Where Checkout sends the payer back, with ?payment=paid or ?payment=cancelled. */
32
40
  returnUrl?: string;
33
41
  cancelUrl?: string;
@@ -39,10 +47,11 @@ export type Payouts = {
39
47
  payoutsEnabled: boolean;
40
48
  detailsSubmitted: boolean;
41
49
  };
42
- /** What a payment of `amount` costs: Fauna's fee and what the payee receives. */
50
+ /** What a payment of `amount` costs: the whole fee, the app's part of it and what the payee receives. */
43
51
  export type Quote = {
44
52
  amount: number;
45
53
  fee: number;
54
+ appFee: number;
46
55
  payout: number;
47
56
  };
48
57
  /** Start or continue setting up payouts. Open the returned URL; Stripe sends you back with ?payouts=connected. */
@@ -56,7 +65,10 @@ export declare function payouts(credentials: Credentials): Promise<Payouts>;
56
65
  export declare function dashboard(credentials: Credentials): Promise<{
57
66
  url: string;
58
67
  }>;
59
- export declare function quote(authorization: Credentials | ApiKey, amount: number): Promise<Quote>;
68
+ /** What a payment of `amount` would cost, with the app's fee (`appFee`, a percent) when an API key asks for one. */
69
+ export declare function quote(authorization: Credentials | ApiKey, amount: number, options?: {
70
+ appFee?: number;
71
+ }): Promise<Quote>;
60
72
  /**
61
73
  * Ask for a payment to `payee`. Send the payer to the returned `url`. With an API
62
74
  * key, that key's webhooks hear `payment.succeeded`, `payment.expired`,
@@ -69,5 +81,8 @@ export declare function get(authorization: Credentials | ApiKey, id: string): Pr
69
81
  export declare function list(authorization: Credentials | ApiKey, query?: {
70
82
  reference?: string;
71
83
  }): Promise<Payment[]>;
72
- /** Refund a completed payment in full, Fauna's fee included. Whoever made it, or its payee, can. */
84
+ /**
85
+ * Refund a completed payment in full. The payee gets back the whole fee except Stripe's own
86
+ * fee on the charge, the app's fee included. Whoever made it, or its payee, can.
87
+ */
73
88
  export declare function refund(authorization: Credentials | ApiKey, id: string): Promise<Payment>;
@@ -39,8 +39,10 @@ export async function dashboard(credentials) {
39
39
  throw new Error(await response.text());
40
40
  return await response.json();
41
41
  }
42
- export async function quote(authorization, amount) {
43
- const response = await fetch(`${Fauna.getURL()}/payments/quote?amount=${encodeURIComponent(String(amount))}`, {
42
+ /** What a payment of `amount` would cost, with the app's fee (`appFee`, a percent) when an API key asks for one. */
43
+ export async function quote(authorization, amount, options = {}) {
44
+ const appFee = options.appFee !== undefined ? `&appFee=${encodeURIComponent(String(options.appFee))}` : '';
45
+ const response = await fetch(`${Fauna.getURL()}/payments/quote?amount=${encodeURIComponent(String(amount))}${appFee}`, {
44
46
  method: "GET",
45
47
  headers: {
46
48
  "Authorization": 'email' in authorization ? `Token ${authorization.token}` : `ApiKey ${authorization.id}`
@@ -93,7 +95,10 @@ export async function list(authorization, query = {}) {
93
95
  throw new Error(await response.text());
94
96
  return (await response.json()).map(hydrate);
95
97
  }
96
- /** Refund a completed payment in full, Fauna's fee included. Whoever made it, or its payee, can. */
98
+ /**
99
+ * Refund a completed payment in full. The payee gets back the whole fee except Stripe's own
100
+ * fee on the charge, the app's fee included. Whoever made it, or its payee, can.
101
+ */
97
102
  export async function refund(authorization, id) {
98
103
  const response = await fetch(`${Fauna.getURL()}/payments/${encodeURIComponent(id)}/refund`, {
99
104
  method: "POST",
@@ -103,12 +103,18 @@ export type ProcessTool = {
103
103
  pid: number;
104
104
  signal?: 'TERM' | 'KILL';
105
105
  };
106
+ /** How the machine reaches the internet: `private` only from a residential address, billed per
107
+ * GB to whoever pays for the machine, or `public` from its own. */
108
+ export type InternetTool = {
109
+ kind: 'internet';
110
+ mode: 'private' | 'public';
111
+ };
106
112
  /** Pointer and keyboard input on the desktop. No executeJavascript, which would run on the
107
113
  * desktop page itself, and no search or exit, which belong to a web page's agent. */
108
114
  export type DesktopTool = Exclude<BrowserTool, {
109
115
  kind: 'executeJavascript' | 'search' | 'exit';
110
116
  }>;
111
- export type MachineTool = DesktopTool | WindowTool | FileTool | ProcessTool;
117
+ export type MachineTool = DesktopTool | WindowTool | FileTool | ProcessTool | InternetTool;
112
118
  /** Metadata for one desktop window. Its pixels live in the desktop HTML. */
113
119
  export type Window = {
114
120
  /** Same id as the element's data-window and the window tools. */
@@ -153,7 +159,7 @@ export type MachineRoomHooks = BrowserRoomHooks<DesktopTool> & Partial<{
153
159
  /** A Files window's folder or view changed. */
154
160
  onFiles(room: MachineRoom, window: number, files: FilesState): void;
155
161
  onStatus(room: MachineRoom): void;
156
- /** Private turned on or off, or couldn't stay on (`privateReason` says why). */
162
+ /** Private turned on or off, paused while it reconnects (`privateReason` says why), or came back. */
157
163
  onPrivate(room: MachineRoom): void;
158
164
  /** An agent holding the machine took a step. */
159
165
  onAgentStep(room: MachineRoom, step: AgentStep): void;
@@ -180,7 +186,7 @@ export type AgentStep = {
180
186
  };
181
187
  };
182
188
  /**
183
- * Every machine tool for a model: desktop input, windows, files, and exec.
189
+ * Every machine tool for a model: desktop input, windows, files, processes, the internet switch, and exec.
184
190
  * `invoke` runs a tool; `exec` runs a shell command and returns its result as text.
185
191
  */
186
192
  export declare function parrotTools(invoke: (tool: MachineTool) => Promise<string>, exec: (command: string, timeoutMs?: number) => Promise<string>): Tool[];
@@ -239,9 +245,11 @@ export declare class MachineRoom extends PageRoom<DesktopTool> {
239
245
  error: string;
240
246
  /** The repository's name, so a guest who joined by code knows what it is. */
241
247
  name: string;
242
- /** Its connections go out from a residential address, billed per GB to whoever pays for the machine. */
248
+ /** Its connections go out only from a residential address, billed per GB to whoever pays for
249
+ * the machine. Stays true while private is paused. */
243
250
  private: boolean;
244
- /** Why private is off when it couldn't turn on or stay on, like a payer out of balance. */
251
+ /** Why private is paused while it reconnects, like a payer out of balance. Its internet is
252
+ * cut until then. Empty while private carries traffic, or is off. */
245
253
  privateReason: string;
246
254
  private machineHooks;
247
255
  private calls;
@@ -292,7 +300,7 @@ export declare class MachineRoom extends PageRoom<DesktopTool> {
292
300
  group?: Group;
293
301
  }): Promise<void>;
294
302
  /** Turn private on or off for a repository's live machine, for its owner or group members.
295
- * Resolves to the machine's private state and, when it couldn't turn on, why. */
303
+ * Resolves to the machine's private state and, while it's paused, why. */
296
304
  static setPrivate(authorization: Credentials | ApiKey, target: {
297
305
  repository: number;
298
306
  }, on: boolean, opts?: {
@@ -46,7 +46,7 @@ const CALL_MS = 60000;
46
46
  const EXEC_MS = 120000;
47
47
  const STOP_MS = 10 * 60000;
48
48
  /**
49
- * Every machine tool for a model: desktop input, windows, files, and exec.
49
+ * Every machine tool for a model: desktop input, windows, files, processes, the internet switch, and exec.
50
50
  * `invoke` runs a tool; `exec` runs a shell command and returns its result as text.
51
51
  */
52
52
  export function parrotTools(invoke, exec) {
@@ -156,6 +156,9 @@ export function parrotTools(invoke, exec) {
156
156
  pid: { type: "NUMBER", description: "Process id from processes" },
157
157
  signal: { type: "STRING", enum: ["TERM", "KILL"], description: "TERM (default) or KILL" },
158
158
  }, ["pid"], (p) => ({ kind: 'kill', pid: Number(p.pid), ...(p.signal === 'KILL' || p.signal === 'TERM' ? { signal: p.signal } : {}) }));
159
+ add("internet", "Choose how the machine reaches the internet. private sends every connection out from a residential address, billed per GB; public uses the machine's own address. While private reconnects the internet is paused and connections are refused; switch to public if the work can't wait.", {
160
+ mode: { type: "STRING", enum: ["private", "public"], description: "private or public" },
161
+ }, ["mode"], (p) => ({ kind: 'internet', mode: p.mode === 'public' ? 'public' : 'private' }));
159
162
  tools.push({
160
163
  function: {
161
164
  name: "exec",
@@ -295,14 +298,16 @@ export class MachineRoom extends PageRoom {
295
298
  writable: true,
296
299
  value: ''
297
300
  });
298
- /** Its connections go out from a residential address, billed per GB to whoever pays for the machine. */
301
+ /** Its connections go out only from a residential address, billed per GB to whoever pays for
302
+ * the machine. Stays true while private is paused. */
299
303
  Object.defineProperty(this, "private", {
300
304
  enumerable: true,
301
305
  configurable: true,
302
306
  writable: true,
303
307
  value: false
304
308
  });
305
- /** Why private is off when it couldn't turn on or stay on, like a payer out of balance. */
309
+ /** Why private is paused while it reconnects, like a payer out of balance. Its internet is
310
+ * cut until then. Empty while private carries traffic, or is off. */
306
311
  Object.defineProperty(this, "privateReason", {
307
312
  enumerable: true,
308
313
  configurable: true,
@@ -512,7 +517,7 @@ export class MachineRoom extends PageRoom {
512
517
  throw new Error(await response.text());
513
518
  }
514
519
  /** Turn private on or off for a repository's live machine, for its owner or group members.
515
- * Resolves to the machine's private state and, when it couldn't turn on, why. */
520
+ * Resolves to the machine's private state and, while it's paused, why. */
516
521
  static async setPrivate(authorization, target, on, opts) {
517
522
  const response = await fetch(`${Termite.getURL()}/termite/repositories/${target.repository}/machine/private`, {
518
523
  method: "POST",
@@ -245,11 +245,27 @@ const MIRRORED = [
245
245
  ];
246
246
  const SELECTION = "rgba(51, 144, 255, 0.38)";
247
247
  const BLINK_MS = 1060;
248
+ /** The field showing the host's caret, in the replay's document or a frame inside it, with
249
+ * where that frame's viewport sits in the replay and the outermost frame it is in. */
250
+ function caretField(doc) {
251
+ const field = doc.querySelector("[data-fauna-caret]");
252
+ if (field)
253
+ return { field, left: 0, top: 0, frame: null };
254
+ for (const frame of Array.from(doc.querySelectorAll("iframe"))) {
255
+ const inner = frame.contentDocument ? caretField(frame.contentDocument) : null;
256
+ if (!inner)
257
+ continue;
258
+ const box = frame.getBoundingClientRect();
259
+ return { field: inner.field, left: inner.left + box.left + frame.clientLeft, top: inner.top + box.top + frame.clientTop, frame };
260
+ }
261
+ return null;
262
+ }
248
263
  /**
249
264
  * The host's caret, or its selection, drawn over the focused text field of a replay. The
250
265
  * copy is never focused, so it has neither; the recorder streams them as
251
266
  * `data-fauna-caret="start,end,scrollLeft,scrollTop"`. The marks sit in a layer at the end
252
- * of the replay's document, outside every subtree the page mutates.
267
+ * of the replay's document, outside every subtree the page mutates. A field inside a frame
268
+ * is measured in the frame's own document, with its fonts, and its marks moved into place.
253
269
  */
254
270
  function caretPainter(getDoc) {
255
271
  let layer = null;
@@ -271,18 +287,20 @@ function caretPainter(getDoc) {
271
287
  doc.documentElement.appendChild(layer);
272
288
  painted = "";
273
289
  }
274
- const field = doc.querySelector("[data-fauna-caret]");
290
+ const found = caretField(doc);
291
+ const field = found?.field;
292
+ const local = field?.ownerDocument;
275
293
  const caret = field?.getAttribute("data-fauna-caret") ?? "";
276
294
  const rect = field?.getBoundingClientRect();
277
- if (!field || !caret || !rect?.width || !rect.height)
295
+ if (!found || !field || !local?.defaultView || !caret || !rect?.width || !rect.height)
278
296
  return clear();
279
297
  const [start, end, scrollLeft, scrollTop] = caret.split(",").map((n) => Number(n) || 0);
280
- const style = doc.defaultView.getComputedStyle(field);
298
+ const style = local.defaultView.getComputedStyle(field);
281
299
  const textarea = field.tagName === "TEXTAREA";
282
300
  const value = field.value;
283
301
  const from = Math.max(0, Math.min(start, value.length));
284
302
  const to = Math.max(from, Math.min(end, value.length));
285
- const mirror = doc.createElement("div");
303
+ const mirror = local.createElement("div");
286
304
  mirror.style.cssText = "all:initial;position:fixed;pointer-events:none;visibility:hidden;overflow:hidden";
287
305
  for (const name of MIRRORED)
288
306
  mirror.style.setProperty(name, style.getPropertyValue(name));
@@ -303,10 +321,13 @@ function caretPainter(getDoc) {
303
321
  // An input centers its one line in the box.
304
322
  if (!textarea)
305
323
  mirror.style.lineHeight = `${inner.bottom - inner.top - px("padding-top") - px("padding-bottom")}px`;
306
- const mark = doc.createElement("span");
324
+ const mark = local.createElement("span");
307
325
  mark.textContent = from === to ? "\u200b" : value.slice(from, to);
308
326
  mirror.append(value.slice(0, from), mark, value.slice(to) + (textarea ? "\u200b" : ""));
309
- layer.appendChild(mirror);
327
+ if (local === doc)
328
+ layer.appendChild(mirror);
329
+ else
330
+ local.documentElement.appendChild(mirror);
310
331
  mirror.scrollLeft = scrollLeft;
311
332
  mirror.scrollTop = scrollTop;
312
333
  const boxes = Array.from(mark.getClientRects());
@@ -322,11 +343,15 @@ function caretPainter(getDoc) {
322
343
  const bottom = Math.min(box.top + (box.height + height) / 2, inner.bottom);
323
344
  if (right <= left || bottom <= top || left < inner.left - 1 || left > inner.right)
324
345
  continue;
325
- // A window over the field covers its caret too.
326
- const under = doc.elementFromPoint(Math.min(left + 1, right), (top + bottom) / 2);
346
+ // A window over the field covers its caret too, in its frame or over the frame.
347
+ const x = Math.min(left + 1, right);
348
+ const y = (top + bottom) / 2;
349
+ const under = local.elementFromPoint(x, y);
327
350
  if (under && under !== field && !field.contains(under))
328
351
  continue;
329
- shapes.push({ left, top, width: right - left, height: bottom - top });
352
+ if (found.frame && doc.elementFromPoint(x + found.left, y + found.top) !== found.frame)
353
+ continue;
354
+ shapes.push({ left: left + found.left, top: top + found.top, width: right - left, height: bottom - top });
330
355
  }
331
356
  // Unchanged marks stay, so the caret keeps blinking through unrelated changes.
332
357
  const key = `${color}|${shapes.map((s) => `${s.left},${s.top},${s.width},${s.height}`).join(";")}`;
@@ -1727,10 +1727,10 @@ class ChatEngine {
1727
1727
  this.roomStream.renderer.feedText(delta);
1728
1728
  autoScroll(this.refs.messagesEl);
1729
1729
  },
1730
- onOpenFile: (_r, attachment) => {
1730
+ onOpenFile: (_r, file) => {
1731
1731
  if (liveEnded || this.destroyed)
1732
1732
  return;
1733
- this.refs.panel?.point(attachment);
1733
+ this.refs.panel?.point(file);
1734
1734
  },
1735
1735
  onThinking: (_r, thinking) => {
1736
1736
  if (liveEnded)
@@ -6,13 +6,13 @@ import type { Attachment } from "../parrot/share.js";
6
6
  export type PanelBus = {
7
7
  /** A person opened a file (a card, or a row): a tab for them only. */
8
8
  open(attachment: Attachment): void;
9
- /** The reply pointed everyone to a file: open the panel and focus its tab. */
10
- point(attachment: Attachment): void;
9
+ /** The reply pointed everyone to a file, an attachment or one in the chat's repository: open the panel and focus its tab. */
10
+ point(file: Fauna.View.Target): void;
11
11
  /** New files landed in the chat. */
12
12
  added(): void;
13
13
  listen(handlers: Partial<{
14
14
  open(a: Attachment): void;
15
- point(a: Attachment): void;
15
+ point(f: Fauna.View.Target): void;
16
16
  added(): void;
17
17
  }>): () => void;
18
18
  };
@@ -23,9 +23,15 @@ export function createPanelBus() {
23
23
  export function attachmentOf(room, part, message) {
24
24
  return Fauna.Parrot.Share.Attachment.fromJSON({ ...part, chat: room.chatId ?? "", ...(message !== undefined ? { message } : {}) }, room.getAuthorization());
25
25
  }
26
- function keyOf(attachment) {
27
- const auth = attachment.file.authorization;
28
- return attachment.ref || auth.accessCode || attachment.file.name;
26
+ function fileOf(target) {
27
+ return 'ref' in target && 'chat' in target ? target.file : target;
28
+ }
29
+ function keyOf(target) {
30
+ const file = fileOf(target);
31
+ const auth = file.authorization;
32
+ if (auth.repository)
33
+ return `repository:${auth.repository}:${auth.path}`;
34
+ return ('ref' in target && target.ref) || auth.accessCode || file.name;
29
35
  }
30
36
  function iconOf(contentType, name) {
31
37
  if (contentType.startsWith("image/"))
@@ -253,9 +259,9 @@ export function ChatPanel({ room, bus, open, onOpenChange }) {
253
259
  load();
254
260
  }
255
261
  }, [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: [] }]);
262
+ const openTab = useCallback((target, everyone) => {
263
+ const key = keyOf(target);
264
+ setTabs((current) => current.some((t) => t.key === key) ? current : [...current, { key, target, people: [] }]);
259
265
  setActive(key);
260
266
  slideBack();
261
267
  if (everyone || !open)
@@ -263,7 +269,7 @@ export function ChatPanel({ room, bus, open, onOpenChange }) {
263
269
  }, [open, onOpenChange, slideBack]);
264
270
  useEffect(() => bus.listen({
265
271
  open: (a) => openTab(a, false),
266
- point: (a) => openTab(a, true),
272
+ point: (f) => openTab(f, true),
267
273
  added: () => {
268
274
  if (!loaded.current)
269
275
  return;
@@ -342,7 +348,10 @@ export function ChatPanel({ room, bus, open, onOpenChange }) {
342
348
  };
343
349
  if (!open)
344
350
  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
351
+ 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) => {
352
+ const file = fileOf(tab.target);
353
+ return (_jsxs("div", { className: `chat-panel-tab${active === tab.key ? " chat-panel-tab-on" : ""}`, onClick: () => setActive(tab.key), title: file.name, children: [_jsx("span", { className: "material-symbols-outlined", children: iconOf(file.contentType, file.name) }), _jsx("span", { className: "chat-panel-tab-name", children: 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 ${file.name}`, onClick: (e) => { e.stopPropagation(); closeTab(tab.key); }, children: "close" })] }, tab.key));
354
+ }), _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
355
  ? _jsx("div", { className: "chat-panel-empty", children: failed ? "Couldn't load the attachments." : "Loading\u2026" })
347
356
  : attachments.length === 0
348
357
  ? _jsx("div", { className: "chat-panel-empty", children: "Files sent in this chat, and files Parrot opens, show up here." })
@@ -350,7 +359,7 @@ export function ChatPanel({ room, bus, open, onOpenChange }) {
350
359
  const key = keyOf(a);
351
360
  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
361
  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)))] })] })] }));
362
+ }) }), tabs.map((tab) => (_jsx("div", { className: "chat-panel-view", style: active === tab.key ? undefined : { display: "none" }, children: _jsx(View, { target: tab.target, onPeople: (people) => setPeople(tab.key, people), onSaved: () => refreshPreview({ key: tab.key, file: fileOf(tab.target) }) }) }, tab.key)))] })] })] }));
354
363
  }
355
364
  function reducedMotion() {
356
365
  return typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true;
@@ -729,6 +738,11 @@ export function chatToolLabel(call) {
729
738
  case "kill":
730
739
  out = { icon: "cancel", label: pick(`Stopping process ${args.pid}`, `Stopped process ${args.pid}`, `Couldn't stop process ${args.pid}`) };
731
740
  break;
741
+ case "internet": {
742
+ const mode = args.mode === "public" ? "public" : "private";
743
+ out = { icon: mode === "private" ? "shield_lock" : "public", label: pick(`Switching the internet to ${mode}`, `Switched the internet to ${mode}`, `Couldn't switch the internet to ${mode}`) };
744
+ break;
745
+ }
732
746
  case "open": {
733
747
  const name = path ? baseName(path) : String(args.attachment ?? "the file");
734
748
  out = { icon: "open_in_new", label: pick(`Opening ${name}`, `Opened ${name}`, `Couldn't open ${name}`) };
@@ -8,7 +8,10 @@ import type { Attachment } from "./parrot/share.js";
8
8
  * too large to edit) is looked at together.
9
9
  */
10
10
  export type Kind = 'text' | 'table' | 'image' | 'view';
11
- /** What you may do in a file room: `read` for share links, `write` for owners, group members and chat members. */
11
+ /**
12
+ * What you may do in a file room: `read` for share links, `write` for owners, group members and
13
+ * chat members. A repository file is `write` only while the machine's agent user may write it.
14
+ */
12
15
  export type Role = 'read' | 'write';
13
16
  /** Text and tables over this size open as `view`. */
14
17
  export declare const EDIT_MAX_BYTES: number;
@@ -49,7 +52,7 @@ export declare function contentOf(kind: Kind, doc: Y.Doc): Uint8Array;
49
52
  /** Base64 for document updates on the wire. */
50
53
  export declare function toBase64(bytes: Uint8Array): string;
51
54
  export declare function fromBase64(text: string): Uint8Array;
52
- /** A file to open live: a Drive file (by path or share link) or a chat attachment. */
55
+ /** A file to open live: a Drive file (by path or share link), a file in a repository's /repository, or a chat attachment. */
53
56
  export type Target = DriveFile | Attachment;
54
57
  export type Hooks = Partial<{
55
58
  /** Someone joined or left, or moved their pointer or selection. */
@@ -104,8 +107,9 @@ export declare class ViewRoom extends Room {
104
107
  disconnect(): void;
105
108
  }
106
109
  /**
107
- * Join the live room for a file: a Drive file (by path or share link) or a chat
108
- * attachment. Owners, group members and chat members can edit; share links look.
110
+ * Join the live room for a file: a Drive file (by path or share link), a file in a
111
+ * repository's /repository, or a chat attachment. Owners, group members and chat members
112
+ * can edit, repository files only where the machine's agent user could; share links look.
109
113
  */
110
114
  export declare function connect(target: Target, hooks?: Hooks): Promise<{
111
115
  room: ViewRoom;
package/dist/src/view.js CHANGED
@@ -360,13 +360,13 @@ function authorizationFor(target) {
360
360
  const auth = target.authorization;
361
361
  if ('accessCode' in auth)
362
362
  return { authorization: auth.authorization, accessCode: auth.accessCode };
363
- if ('repository' in auth)
364
- throw new Error('Repository files open in Termite.');
365
- return { authorization: auth.authorization, server: `fauna:file:${auth.path}`, ...(auth.group ? { group: auth.group } : {}) };
363
+ const server = 'repository' in auth ? `fauna:file:repository:${auth.repository}/${auth.path.replace(/^\/+/, '')}` : `fauna:file:${auth.path}`;
364
+ return { authorization: auth.authorization, server, ...(auth.group ? { group: auth.group } : {}) };
366
365
  }
367
366
  /**
368
- * Join the live room for a file: a Drive file (by path or share link) or a chat
369
- * attachment. Owners, group members and chat members can edit; share links look.
367
+ * Join the live room for a file: a Drive file (by path or share link), a file in a
368
+ * repository's /repository, or a chat attachment. Owners, group members and chat members
369
+ * can edit, repository files only where the machine's agent user could; share links look.
370
370
  */
371
371
  export async function connect(target, hooks = {}) {
372
372
  let created;
@@ -1,8 +1,9 @@
1
1
  import { ApiKey, Credentials, Profile } from "./authentification.js";
2
2
  import { GroupRef } from "./groups.js";
3
3
  /** Event types a webhook can listen for: `chat.message` for groups and chats,
4
- * `group.joined` and `group.left` for groups, payment events for API keys. */
5
- export type EventType = 'chat.message' | 'group.joined' | 'group.left' | 'payment.succeeded' | 'payment.expired' | 'payment.refunded' | 'payment.disputed';
4
+ * `group.joined` and `group.left` for groups, payment events for API keys.
5
+ * `calendar.event.started` goes to a calendar event's own `webhook`. */
6
+ export type EventType = 'chat.message' | 'group.joined' | 'group.left' | 'payment.succeeded' | 'payment.expired' | 'payment.refunded' | 'payment.disputed' | 'calendar.event.started';
6
7
  /** Who a webhook belongs to: a group, one chat, or the API key making the call. */
7
8
  export type WebhookOwner = {
8
9
  group: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fauna-api",
3
- "version": "1.0.759",
3
+ "version": "1.0.771",
4
4
  "description": "The Fauna SDK: accounts, Drive, groups, calendar, chats, payments and every Fauna app (Parrot, Marmot, Magpie, Cicada, Ant, Termite), plus React UI.",
5
5
  "homepage": "https://docs.faunakit.com",
6
6
  "repository": {