fauna-api 1.0.780 → 1.0.781

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.
@@ -113,7 +113,7 @@ export declare class PageRoom<T extends BrowserTool = BrowserTool> extends Room
113
113
  parrotTools(): Tool[];
114
114
  handleEvent(packet: any): boolean;
115
115
  }
116
- /** A live web page in a Marmot (or Ant) browser: every browser tool. */
116
+ /** A live web page in Marmot's browser: every browser tool. */
117
117
  export declare class BrowserRoom extends PageRoom<BrowserTool> {
118
118
  static connect(authorization: Credentials | ApiKey, target: {
119
119
  reference: string;
@@ -125,13 +125,11 @@ export declare class BrowserRoom extends PageRoom<BrowserTool> {
125
125
  room: BrowserRoom;
126
126
  self: Presence;
127
127
  }>;
128
- /** `endpoint` is where the page's room lives: Marmot's by default, or e.g. Ant's /timeline/browser/live. */
129
128
  static join(authorization: Credentials | ApiKey, target: {
130
129
  accessCode: string;
131
130
  }, opts?: {
132
131
  tools?: BrowserTool["kind"][];
133
132
  hooks?: BrowserRoomHooks;
134
- endpoint?: string;
135
133
  }): Promise<{
136
134
  room: BrowserRoom;
137
135
  self: Presence;
@@ -337,12 +337,11 @@ export class PageRoom extends Room {
337
337
  }
338
338
  }
339
339
  }
340
- /** A live web page in a Marmot (or Ant) browser: every browser tool. */
340
+ /** A live web page in Marmot's browser: every browser tool. */
341
341
  export class BrowserRoom extends PageRoom {
342
342
  static async connect(authorization, target, opts) {
343
343
  return openBrowser(authorization, { server: target.reference, group: opts?.group?.id }, opts);
344
344
  }
345
- /** `endpoint` is where the page's room lives: Marmot's by default, or e.g. Ant's /timeline/browser/live. */
346
345
  static async join(authorization, target, opts) {
347
346
  return openBrowser(authorization, { accessCode: target.accessCode }, opts);
348
347
  }
@@ -352,7 +351,7 @@ async function openBrowser(authorization, target, opts) {
352
351
  const auth = "accessCode" in target
353
352
  ? { authorization, accessCode: target.accessCode }
354
353
  : { authorization, server: target.server, ...(target.group ? { group: target.group } : {}) };
355
- const endpoint = opts?.endpoint ?? `${Marmot.getURL()}/browser/live`;
354
+ const endpoint = `${Marmot.getURL()}/browser/live`;
356
355
  let room;
357
356
  const notify = () => { if (room)
358
357
  room.emitPresence(); };
@@ -192,18 +192,18 @@ export function desktopTool(repository) {
192
192
  if (repository) {
193
193
  return {
194
194
  name: "use_desktop",
195
- description: `Do something on the repository's desktop that needs a screen, like using a website. A desktop agent does it and reports back. ${DESKTOP_CAPTURE}`,
195
+ description: `Do something on the repository's desktop that needs a screen, like using an app on the machine or working across its windows. A desktop agent does it and reports back. For a website alone, use_browser is quicker. ${DESKTOP_CAPTURE}`,
196
196
  parameters: { type: "OBJECT", properties, required: ["goal"] },
197
197
  };
198
198
  }
199
199
  return {
200
200
  name: "use_desktop",
201
- description: `Do something on a computer that needs a screen, like using a website. This chat has no computer yet, so the person is asked first to make a repository for it: a Linux machine with a desktop. If they agree, a desktop agent does it and reports back; if they don't, do that request without a computer. ${DESKTOP_CAPTURE}`,
201
+ description: `Do something on a computer that needs a screen, like running an app or working across windows. This chat has no computer yet, so the person is asked first to make a repository for it: a Linux machine with a desktop. If they agree, a desktop agent does it and reports back; if they don't, do that request without a computer. For a website alone, use use_browser, which needs no computer. ${DESKTOP_CAPTURE}`,
202
202
  parameters: {
203
203
  type: "OBJECT",
204
204
  properties: {
205
205
  ...properties,
206
- name: { type: "STRING", description: "What to call the repository this makes: two to four words about the task, like eBay Switch search" },
206
+ name: { type: "STRING", description: "What to call the repository this makes: two to four words about the task, like Invoice spreadsheet cleanup" },
207
207
  },
208
208
  required: ["goal", "name"],
209
209
  },
@@ -48,6 +48,8 @@ export type BrowserMountOptions<R extends PageRoom<any> = PageRoom> = {
48
48
  /** The page's real pointer moved or changed shape. The browser takes the room's
49
49
  * `onCursor` hook, so follow the pointer here. */
50
50
  onCursor?: (pointer: HtmlPointer) => void;
51
+ /** The page closed. The browser takes the room's `onClosed` hook, so hear it here. */
52
+ onClosed?: () => void;
51
53
  };
52
54
  export declare function cursorSeed(options: {
53
55
  seed?: string;
@@ -466,7 +466,10 @@ export function createBrowser(container, options) {
466
466
  room.watch({
467
467
  onCursor: (_r, pointer) => applyCursor(pointer),
468
468
  onTool: (_r, tool) => applyTool(tool),
469
- onClosed: () => { badge.style.display = "none"; },
469
+ onClosed: () => {
470
+ badge.style.display = "none";
471
+ options.onClosed?.();
472
+ },
470
473
  web: {
471
474
  onBootstrap: (_web, meta) => { void applyBootstrap(meta.html, meta.viewport); },
472
475
  onMutation: (_web, mutation) => {
@@ -7,6 +7,7 @@ import { Fauna } from "fauna-api";
7
7
  import { mimeFromFileName } from "../marmot/media.js";
8
8
  import { createTokenizedInput, parseRichText, toPlainText, hasTokens } from "./tokenized.js";
9
9
  import { roomPartsToChatFields } from "./chatSequence.js";
10
+ import { SessionPopout } from "./chatSession.js";
10
11
  import { ChatPanel, askForComputer, attachmentOf, buildAttachmentCard, chatToolLabel, createPanelBus, execOutput, mountRepositoryControl, openViewModal, paintPreview, roomHas, showPreview } from "./chatPanel.js";
11
12
  export { RepositoryControl, askForComputer, pickedRepository } from "./chatPanel.js";
12
13
  import { createTooltip } from "./hover.js";
@@ -105,8 +106,12 @@ function renderTool(call) {
105
106
  }
106
107
  return toolChip(call, 'build', status === 'error' ? `${name} failed` : name);
107
108
  }
108
- function renderToolCallElement(call, override) {
109
- return override?.(call) ?? renderTool(call);
109
+ /** A tool call's element; one that opened a live room pops it out when its chip is clicked. */
110
+ function renderToolCallElement(call, override, sessions) {
111
+ const el = override?.(call) ?? renderTool(call);
112
+ if (call.session && call.id && sessions)
113
+ sessions.decorate(el, call.id, call.session);
114
+ return el;
110
115
  }
111
116
  function buildFileElement(file, iconMap, cards) {
112
117
  const code = typeof file.part?.accessCode === 'string' ? file.part.accessCode : '';
@@ -277,7 +282,7 @@ function appendTextBlock(el, text, message, onTokenClick) {
277
282
  });
278
283
  el.appendChild(textEl);
279
284
  }
280
- async function buildMessageElement(message, iconMap, modelIcons, renderToolCall, mode, selfUserId, onTokenClick, fileCards) {
285
+ async function buildMessageElement(message, iconMap, modelIcons, renderToolCall, mode, selfUserId, onTokenClick, fileCards, sessions) {
281
286
  const incoming = isIncomingAuthor(message, chatMode(mode), selfUserId);
282
287
  const el = document.createElement('div');
283
288
  el.className = (message.role === 'user' && !incoming) ? 'chat-message-user' : 'chat-message-model';
@@ -288,7 +293,7 @@ async function buildMessageElement(message, iconMap, modelIcons, renderToolCall,
288
293
  if (block.type === 'text')
289
294
  appendTextBlock(el, block.text, message, onTokenClick);
290
295
  else if (block.type === 'tool')
291
- el.appendChild(renderToolCallElement(block.call, renderToolCall));
296
+ el.appendChild(renderToolCallElement(block.call, renderToolCall, sessions));
292
297
  else
293
298
  el.appendChild(buildFileElement(block, iconMap, fileCards));
294
299
  }
@@ -298,7 +303,7 @@ async function buildMessageElement(message, iconMap, modelIcons, renderToolCall,
298
303
  appendTextBlock(el, message.text, message, onTokenClick);
299
304
  if (message.toolCalls) {
300
305
  for (const call of message.toolCalls) {
301
- el.appendChild(renderToolCallElement(call, renderToolCall));
306
+ el.appendChild(renderToolCallElement(call, renderToolCall, sessions));
302
307
  }
303
308
  }
304
309
  if (message.files) {
@@ -332,7 +337,7 @@ function autoScroll(messagesEl, stick = nearBottom(messagesEl)) {
332
337
  messagesEl.scrollTop = messagesEl.scrollHeight;
333
338
  }
334
339
  }
335
- function createStreamRenderer(modelEl, messagesEl, renderToolCall) {
340
+ function createStreamRenderer(modelEl, messagesEl, renderToolCall, sessions) {
336
341
  // Text is rendered in *segments* — each run of text between tool calls
337
342
  // gets its own .chat-message-text div. This way tool chips persist in the
338
343
  // DOM between segments, and the visual paragraph break between pre- and
@@ -427,7 +432,7 @@ function createStreamRenderer(modelEl, messagesEl, renderToolCall) {
427
432
  Object.assign(stored, call, { id });
428
433
  if (call.status === 'error')
429
434
  console.error('[chat] tool failed', call.name, call.result ?? '');
430
- const el = renderToolCallElement(view, renderToolCall);
435
+ const el = renderToolCallElement(view, renderToolCall, sessions);
431
436
  existing.replaceWith(el);
432
437
  toolElements.set(id, el);
433
438
  continue;
@@ -444,7 +449,7 @@ function createStreamRenderer(modelEl, messagesEl, renderToolCall) {
444
449
  }
445
450
  const stored = { ...call, id };
446
451
  toolCalls.push(stored);
447
- const chip = renderToolCallElement(view, renderToolCall);
452
+ const chip = renderToolCallElement(view, renderToolCall, sessions);
448
453
  toolElements.set(id, chip);
449
454
  modelEl.appendChild(chip);
450
455
  gsap.from(chip, { opacity: 0, x: -10, duration: 0.3, ease: 'power2.out' });
@@ -749,6 +754,13 @@ class ChatEngine {
749
754
  openViewModal(attachment);
750
755
  }
751
756
  });
757
+ /** The live rooms tools open, popped out of their chips. */
758
+ Object.defineProperty(this, "sessions", {
759
+ enumerable: true,
760
+ configurable: true,
761
+ writable: true,
762
+ value: void 0
763
+ });
752
764
  // ── Send button ──
753
765
  Object.defineProperty(this, "sendTooltip", {
754
766
  enumerable: true,
@@ -756,6 +768,7 @@ class ChatEngine {
756
768
  writable: true,
757
769
  value: null
758
770
  });
771
+ this.sessions = new SessionPopout(refs.root, () => chatAuthorization(this.options));
759
772
  const locked = lockedModels(options.models);
760
773
  const start = options.model && (!locked || locked.includes(options.model)) ? options.model : Fauna.Parrot.Generative.Text.AUTO;
761
774
  this.currentModel = locked?.length === 1 ? locked[0] : start;
@@ -784,7 +797,7 @@ class ChatEngine {
784
797
  const gen = this.generation;
785
798
  for (const message of options.initialHistory) {
786
799
  this.history.push(message);
787
- buildMessageElement(message, options.acceptedTypes, this.modelIcons, options.renderToolCall, options.mode, options.room?.self.profile.email, options.onTokenClick, this.fileCards).then((el) => {
800
+ buildMessageElement(message, options.acceptedTypes, this.modelIcons, options.renderToolCall, options.mode, options.room?.self.profile.email, options.onTokenClick, this.fileCards, this.sessions).then((el) => {
788
801
  if (this.destroyed || this.generation !== gen)
789
802
  return;
790
803
  refs.messagesEl.appendChild(el);
@@ -1222,7 +1235,7 @@ class ChatEngine {
1222
1235
  this.refs.messagesEl.appendChild(modelEl);
1223
1236
  gsap.from(modelEl, { duration: 0.5, y: 100, opacity: 0, ease: 'power2.inOut' });
1224
1237
  gsap.to(this.refs.messagesEl, { duration: 0.5, scrollTop: this.refs.messagesEl.scrollHeight, ease: 'power2.inOut' });
1225
- const renderer = createStreamRenderer(modelEl, this.refs.messagesEl, this.options.renderToolCall);
1238
+ const renderer = createStreamRenderer(modelEl, this.refs.messagesEl, this.options.renderToolCall, this.sessions);
1226
1239
  const isStale = () => this.destroyed || this.generation !== gen;
1227
1240
  const usedModel = this.currentModel;
1228
1241
  try {
@@ -1338,7 +1351,7 @@ class ChatEngine {
1338
1351
  gsap.from(el, { duration: 0.4, y: 40, opacity: 0, ease: 'power2.out' });
1339
1352
  }
1340
1353
  else {
1341
- buildMessageElement(chat, this.options.acceptedTypes, this.modelIcons, this.options.renderToolCall, this.options.mode, this.options.room?.self.profile.email, this.options.onTokenClick, this.fileCards).then((built) => {
1354
+ buildMessageElement(chat, this.options.acceptedTypes, this.modelIcons, this.options.renderToolCall, this.options.mode, this.options.room?.self.profile.email, this.options.onTokenClick, this.fileCards, this.sessions).then((built) => {
1342
1355
  if (this.destroyed || this.generation !== gen)
1343
1356
  return;
1344
1357
  if (this.paintedMessage(messageId))
@@ -1718,7 +1731,7 @@ class ChatEngine {
1718
1731
  stampModelAvatar(el, this.currentModel, this.modelIcons);
1719
1732
  this.refs.messagesEl.appendChild(el);
1720
1733
  gsap.from(el, { duration: 0.4, y: 40, opacity: 0, ease: 'power2.out' });
1721
- this.roomStream = { turn, el, renderer: createStreamRenderer(el, this.refs.messagesEl, this.options.renderToolCall) };
1734
+ this.roomStream = { turn, el, renderer: createStreamRenderer(el, this.refs.messagesEl, this.options.renderToolCall, this.sessions) };
1722
1735
  }
1723
1736
  this.roomStream.renderer.syncToolCalls(this.toolsWithProgress(tools));
1724
1737
  if (files?.length)
@@ -2035,7 +2048,7 @@ class ChatEngine {
2035
2048
  model: role === 'model' ? this.currentModel : undefined,
2036
2049
  };
2037
2050
  this.history.push(message);
2038
- buildMessageElement(message, this.options.acceptedTypes, this.modelIcons, this.options.renderToolCall, this.options.mode, this.options.room?.self.profile.email, this.options.onTokenClick, this.fileCards).then((el) => {
2051
+ buildMessageElement(message, this.options.acceptedTypes, this.modelIcons, this.options.renderToolCall, this.options.mode, this.options.room?.self.profile.email, this.options.onTokenClick, this.fileCards, this.sessions).then((el) => {
2039
2052
  if (message.model)
2040
2053
  el.dataset.model = message.model;
2041
2054
  this.refs.messagesEl.appendChild(el);
@@ -2139,6 +2152,7 @@ class ChatEngine {
2139
2152
  }
2140
2153
  destroy() {
2141
2154
  this.destroyed = true;
2155
+ this.sessions.destroy();
2142
2156
  this.generation++;
2143
2157
  this.generating = false;
2144
2158
  this.steerTarget = null;
@@ -697,6 +697,15 @@ function short(text, max = 60) {
697
697
  function baseName(path) {
698
698
  return path.split("/").filter(Boolean).pop() ?? path;
699
699
  }
700
+ /** A page's site, like example.com, for a chip. */
701
+ function siteOf(url) {
702
+ try {
703
+ return new URL(url).hostname || short(url);
704
+ }
705
+ catch {
706
+ return short(url) || "the page";
707
+ }
708
+ }
700
709
  /** The icon and words for a chat's file and repository tools, or null for other tools. */
701
710
  export function chatToolLabel(call) {
702
711
  const args = call.args ?? {};
@@ -753,6 +762,11 @@ export function chatToolLabel(call) {
753
762
  out = { icon: "save", label: pick(`Saving ${name}`, `Saved ${name} to the repository`, `Couldn't save ${name}`) };
754
763
  break;
755
764
  }
765
+ case "use_browser": {
766
+ const site = siteOf(String(args.url ?? ""));
767
+ out = { icon: "travel_explore", label: pick(`Browsing ${site}`, `Browsed ${site}`, `Couldn't open ${site}`) };
768
+ break;
769
+ }
756
770
  case "use_desktop":
757
771
  if (done && call.result === Fauna.Termite.OS.DESKTOP_DECLINED)
758
772
  out = { icon: "desktop_access_disabled", label: "Went without a computer" };
@@ -0,0 +1,26 @@
1
+ import { Fauna } from "fauna-api";
2
+ import type { ToolSession } from "./chatSequence.js";
3
+ type Authorization = Fauna.Authentification.Credentials | Fauna.Authentification.ApiKey;
4
+ /** What a session's panel is called: the site a browser is on, or the desktop. */
5
+ export declare function sessionTitle(session: ToolSession): string;
6
+ /**
7
+ * Pops the live room a tool opened (a browser page, a machine's desktop) out of its chip,
8
+ * inside the chat. One at a time; clicking the chip again, the close button or Escape puts it
9
+ * back. Chips are replaced as their tool's status changes, so each is found again by its tool id.
10
+ */
11
+ export declare class SessionPopout {
12
+ private root;
13
+ private authorization;
14
+ private open;
15
+ constructor(root: HTMLElement, authorization: () => Authorization | undefined);
16
+ /** Makes a tool's chip a button that pops out its room. */
17
+ decorate(element: HTMLElement, id: string, session: ToolSession): void;
18
+ private chip;
19
+ private mark;
20
+ toggle(id: string, session: ToolSession, chip: HTMLElement): void;
21
+ private show;
22
+ /** Puts the open panel back into its chip and leaves its room. */
23
+ close(): void;
24
+ destroy(): void;
25
+ }
26
+ export {};
@@ -0,0 +1,241 @@
1
+ import gsap from "gsap";
2
+ import { Fauna } from "fauna-api";
3
+ import { createBrowser } from "./browser.js";
4
+ /** What a desktop draws at before its first frame says. */
5
+ const DESKTOP = { width: 1440, height: 900 };
6
+ const OPEN_SECONDS = 0.36;
7
+ const CLOSE_SECONDS = 0.26;
8
+ function reducedMotion() {
9
+ return typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true;
10
+ }
11
+ function host(url) {
12
+ try {
13
+ return new URL(url ?? "").hostname || "the page";
14
+ }
15
+ catch {
16
+ return "the page";
17
+ }
18
+ }
19
+ /** What a session's panel is called: the site a browser is on, or the desktop. */
20
+ export function sessionTitle(session) {
21
+ return session.kind === "desktop" ? "Desktop" : host(session.url);
22
+ }
23
+ /** The transform that puts `panel` where `chip` is, for growing out of it and back. */
24
+ function fromChip(panel, chip) {
25
+ const from = chip.getBoundingClientRect();
26
+ const to = panel.getBoundingClientRect();
27
+ return {
28
+ x: from.left - to.left,
29
+ y: from.top - to.top,
30
+ scaleX: from.width / Math.max(to.width, 1),
31
+ scaleY: from.height / Math.max(to.height, 1),
32
+ opacity: 0,
33
+ transformOrigin: "0 0",
34
+ };
35
+ }
36
+ /**
37
+ * Pops the live room a tool opened (a browser page, a machine's desktop) out of its chip,
38
+ * inside the chat. One at a time; clicking the chip again, the close button or Escape puts it
39
+ * back. Chips are replaced as their tool's status changes, so each is found again by its tool id.
40
+ */
41
+ export class SessionPopout {
42
+ constructor(root, authorization) {
43
+ Object.defineProperty(this, "root", {
44
+ enumerable: true,
45
+ configurable: true,
46
+ writable: true,
47
+ value: root
48
+ });
49
+ Object.defineProperty(this, "authorization", {
50
+ enumerable: true,
51
+ configurable: true,
52
+ writable: true,
53
+ value: authorization
54
+ });
55
+ Object.defineProperty(this, "open", {
56
+ enumerable: true,
57
+ configurable: true,
58
+ writable: true,
59
+ value: null
60
+ });
61
+ }
62
+ /** Makes a tool's chip a button that pops out its room. */
63
+ decorate(element, id, session) {
64
+ const chip = element.classList.contains("chat-tool-chip")
65
+ ? element
66
+ : element.querySelector(".chat-tool-chip");
67
+ if (!chip)
68
+ return;
69
+ const title = sessionTitle(session);
70
+ chip.classList.add("chat-tool-session");
71
+ chip.dataset.toolId = id;
72
+ chip.tabIndex = 0;
73
+ chip.setAttribute("role", "button");
74
+ chip.setAttribute("aria-label", `Watch ${title}`);
75
+ chip.setAttribute("aria-expanded", String(this.open?.id === id));
76
+ const icon = document.createElement("span");
77
+ icon.className = "material-symbols-outlined chat-tool-session-icon";
78
+ icon.textContent = "open_in_full";
79
+ chip.appendChild(icon);
80
+ const toggle = (event) => {
81
+ event.preventDefault();
82
+ event.stopPropagation();
83
+ this.toggle(id, session, chip);
84
+ };
85
+ chip.addEventListener("click", toggle);
86
+ chip.addEventListener("keydown", (event) => {
87
+ if (event.key === "Enter" || event.key === " ")
88
+ toggle(event);
89
+ });
90
+ }
91
+ chip(id) {
92
+ return this.root.querySelector(`.chat-tool-session[data-tool-id="${CSS.escape(id)}"]`);
93
+ }
94
+ mark(id, expanded) {
95
+ this.chip(id)?.setAttribute("aria-expanded", String(expanded));
96
+ }
97
+ toggle(id, session, chip) {
98
+ const same = this.open?.id === id;
99
+ this.close();
100
+ if (!same)
101
+ this.show(id, session, chip);
102
+ }
103
+ show(id, session, chip) {
104
+ const panel = document.createElement("div");
105
+ panel.className = "chat-session";
106
+ panel.setAttribute("role", "dialog");
107
+ panel.setAttribute("aria-label", sessionTitle(session));
108
+ const bar = document.createElement("div");
109
+ bar.className = "chat-session-bar";
110
+ const icon = document.createElement("span");
111
+ icon.className = "material-symbols-outlined";
112
+ icon.textContent = session.kind === "desktop" ? "desktop_windows" : "travel_explore";
113
+ const heading = document.createElement("span");
114
+ heading.className = "chat-session-title";
115
+ heading.textContent = sessionTitle(session);
116
+ const close = document.createElement("button");
117
+ close.type = "button";
118
+ close.className = "chat-session-close";
119
+ close.setAttribute("aria-label", "Close");
120
+ close.innerHTML = '<span class="material-symbols-outlined">close_fullscreen</span>';
121
+ bar.append(icon, heading, close);
122
+ const area = document.createElement("div");
123
+ area.className = "chat-session-area";
124
+ const stage = document.createElement("div");
125
+ stage.className = session.kind === "desktop" ? "chat-session-stage chat-session-desktop" : "chat-session-stage";
126
+ area.appendChild(stage);
127
+ panel.append(bar, area);
128
+ this.root.appendChild(panel);
129
+ let room = null;
130
+ let browser = null;
131
+ let left = false;
132
+ const fit = () => {
133
+ if (session.kind !== "desktop")
134
+ return;
135
+ const size = room?.web.getBootstrap()?.viewport ?? DESKTOP;
136
+ const { width, height } = area.getBoundingClientRect();
137
+ const scale = Math.min(width / size.width, height / size.height) || 1;
138
+ stage.style.width = `${size.width}px`;
139
+ stage.style.height = `${size.height}px`;
140
+ stage.style.transform = `scale(${scale})`;
141
+ };
142
+ const sized = new ResizeObserver(fit);
143
+ sized.observe(area);
144
+ const ended = () => {
145
+ browser?.destroy();
146
+ browser = null;
147
+ stage.className = "chat-session-ended";
148
+ stage.removeAttribute("style");
149
+ stage.textContent = "This session has ended.";
150
+ };
151
+ const onKey = (event) => {
152
+ if (event.key === "Escape")
153
+ this.close();
154
+ };
155
+ document.addEventListener("keydown", onKey);
156
+ close.addEventListener("click", () => this.close());
157
+ this.open = {
158
+ id,
159
+ panel,
160
+ leave: () => {
161
+ left = true;
162
+ sized.disconnect();
163
+ document.removeEventListener("keydown", onKey);
164
+ browser?.destroy();
165
+ try {
166
+ room?.disconnect();
167
+ }
168
+ catch { /* already gone */ }
169
+ },
170
+ };
171
+ this.mark(id, true);
172
+ if (reducedMotion()) {
173
+ gsap.fromTo(panel, { opacity: 0 }, { opacity: 1, duration: 0.15 });
174
+ }
175
+ else {
176
+ gsap.fromTo(panel, fromChip(panel, chip), {
177
+ x: 0, y: 0, scaleX: 1, scaleY: 1, opacity: 1,
178
+ duration: OPEN_SECONDS, ease: "power3.out", clearProps: "transform",
179
+ });
180
+ }
181
+ const authorization = this.authorization();
182
+ if (!authorization) {
183
+ ended();
184
+ return;
185
+ }
186
+ void (async () => {
187
+ try {
188
+ if (session.kind === "desktop") {
189
+ const joined = await Fauna.Termite.OS.MachineRoom.connect(authorization, { reference: session.reference });
190
+ if (left) {
191
+ joined.room.disconnect();
192
+ return;
193
+ }
194
+ room = joined.room;
195
+ browser = createBrowser(stage, { room: joined.room, pagePixels: true, onFrame: fit, onClosed: ended });
196
+ }
197
+ else {
198
+ const joined = await Fauna.Marmot.Browser.BrowserRoom.join(authorization, { accessCode: session.code ?? "" });
199
+ if (left) {
200
+ joined.room.disconnect();
201
+ return;
202
+ }
203
+ room = joined.room;
204
+ browser = createBrowser(stage, { room: joined.room, url: session.url, onClosed: ended });
205
+ }
206
+ fit();
207
+ }
208
+ catch {
209
+ if (!left)
210
+ ended();
211
+ }
212
+ })();
213
+ }
214
+ /** Puts the open panel back into its chip and leaves its room. */
215
+ close() {
216
+ const open = this.open;
217
+ if (!open)
218
+ return;
219
+ this.open = null;
220
+ open.leave();
221
+ this.mark(open.id, false);
222
+ const chip = this.chip(open.id);
223
+ const remove = () => open.panel.remove();
224
+ if (reducedMotion() || !chip) {
225
+ gsap.to(open.panel, { opacity: 0, duration: 0.15, onComplete: remove });
226
+ return;
227
+ }
228
+ gsap.to(open.panel, {
229
+ ...fromChip(open.panel, chip),
230
+ duration: CLOSE_SECONDS,
231
+ ease: "power2.in",
232
+ onComplete: remove,
233
+ });
234
+ }
235
+ destroy() {
236
+ const open = this.open;
237
+ this.open = null;
238
+ open?.leave();
239
+ open?.panel.remove();
240
+ }
241
+ }
@@ -1305,6 +1305,100 @@ button.chat-message-file {
1305
1305
  background: #fff7f6;
1306
1306
  }
1307
1307
 
1308
+ /* A chip whose tool opened a live room: clicking it pops the browser or desktop out. */
1309
+ .chat-tool-session {
1310
+ cursor: pointer;
1311
+ transition: border-color 0.15s ease, color 0.15s ease;
1312
+ }
1313
+ .chat-tool-session:hover,
1314
+ .chat-tool-session:focus-visible,
1315
+ .chat-tool-session[aria-expanded="true"] {
1316
+ border-color: var(--chat-accent, #888);
1317
+ color: var(--chat-text, #1a1a1a);
1318
+ outline: none;
1319
+ }
1320
+ .chat-tool-chip .chat-tool-session-icon {
1321
+ font-size: 13px;
1322
+ opacity: 0.7;
1323
+ }
1324
+
1325
+ .chat-session {
1326
+ position: absolute;
1327
+ inset: 16px 16px 96px;
1328
+ z-index: 30;
1329
+ display: flex;
1330
+ flex-direction: column;
1331
+ max-width: 960px;
1332
+ margin: 0 auto;
1333
+ border-radius: 16px;
1334
+ border: 1px solid var(--chat-border, #e0e0e0);
1335
+ overflow: hidden;
1336
+ background: var(--chat-input-bg, #fff);
1337
+ color: var(--chat-text, #1a1a1a);
1338
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06), 0 12px 32px rgba(0, 0, 0, 0.16), 0 32px 64px rgba(0, 0, 0, 0.1);
1339
+ }
1340
+ .chat-session-bar {
1341
+ display: flex;
1342
+ align-items: center;
1343
+ gap: 8px;
1344
+ padding: 8px 10px 8px 14px;
1345
+ border-bottom: 1px solid var(--chat-border, #e0e0e0);
1346
+ font-size: 13px;
1347
+ }
1348
+ .chat-session-bar > .material-symbols-outlined {
1349
+ font-size: 16px;
1350
+ color: var(--chat-accent, #888);
1351
+ }
1352
+ .chat-session-title {
1353
+ flex: 1;
1354
+ min-width: 0;
1355
+ overflow: hidden;
1356
+ text-overflow: ellipsis;
1357
+ white-space: nowrap;
1358
+ font-weight: 600;
1359
+ }
1360
+ .chat-session-close {
1361
+ display: inline-flex;
1362
+ padding: 4px;
1363
+ border: none;
1364
+ border-radius: 8px;
1365
+ background: none;
1366
+ color: var(--chat-accent, #888);
1367
+ cursor: pointer;
1368
+ }
1369
+ .chat-session-close:hover {
1370
+ background: var(--chat-secondary, #f5f5f5);
1371
+ color: var(--chat-text, #1a1a1a);
1372
+ }
1373
+ .chat-session-close .material-symbols-outlined {
1374
+ font-size: 18px;
1375
+ }
1376
+ .chat-session-area {
1377
+ position: relative;
1378
+ flex: 1;
1379
+ min-height: 0;
1380
+ overflow: hidden;
1381
+ background: var(--chat-secondary, #f5f5f5);
1382
+ }
1383
+ .chat-session-stage {
1384
+ position: absolute;
1385
+ inset: 0;
1386
+ }
1387
+ .chat-session-desktop {
1388
+ inset: auto;
1389
+ top: 0;
1390
+ left: 0;
1391
+ transform-origin: 0 0;
1392
+ }
1393
+ .chat-session-ended {
1394
+ display: flex;
1395
+ align-items: center;
1396
+ justify-content: center;
1397
+ height: 100%;
1398
+ font-size: 13px;
1399
+ color: var(--chat-accent, #888);
1400
+ }
1401
+
1308
1402
  .chat-tool-web {
1309
1403
  align-self: flex-start;
1310
1404
  max-width: 100%;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fauna-api",
3
- "version": "1.0.780",
3
+ "version": "1.0.781",
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": {