@ai-matrx/chat 0.7.27 → 0.7.29

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.
@@ -0,0 +1,612 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+ import { Bot, Link2, Pencil, Pin, Plus, RefreshCw, Settings2 } from "lucide-react";
5
+ import { TextInputDialog } from "@ai-matrx/design-system";
6
+ import { useAppDispatch, useAppSelector } from "../../store/hooks.js";
7
+ import { useMandateSet } from "../../mandates/useMandateSet.js";
8
+ import { fetchConversationHistory } from "../../agents/redux/conversation-history/thunks.js";
9
+ import { renameConversation } from "../../agents/redux/conversation-list/conversation-row-actions.thunks.js";
10
+ import { ChatRoomClient } from "../../agents/components/chat/ChatRoomClient.js";
11
+ import { ChatRoomSkeleton } from "../../agents/components/chat/ChatRoomSkeleton.js";
12
+ import { useRouter } from "../../host/navigation.js";
13
+ import { toast } from "../../host/notify.js";
14
+ import { selectAuthReady, selectUserId } from "../../host/identity.js";
15
+ import { supabase } from "../../host/db.js";
16
+ import { MessengerShell } from "../MessengerShell.js";
17
+ import {
18
+ EMPTY_PERSON_STATE,
19
+ MESSENGER_STARTER_MANDATE_KEYS,
20
+ agentRowId,
21
+ mergeRoster,
22
+ orderRows,
23
+ parseRowId,
24
+ relativeDate,
25
+ threadRowId,
26
+ threadTitle
27
+ } from "./roster.js";
28
+ import { readMessengerPersonState, writeAgentSeat, writeThreadPin } from "./roster-store.js";
29
+ import { buildAgentRowMenu, buildThreadMenu, buildTitleMenu } from "./menus.js";
30
+ import { AgentDetailsPanel } from "./AgentDetailsPanel.js";
31
+ import { MessengerManageAgents } from "./MessengerManageAgents.js";
32
+ const PAGE_SIZE = 20;
33
+ const scopeIdOf = (agentId) => `messenger-agent:${agentId}`;
34
+ const SPACE_ALL = "all";
35
+ const SPACE_PINNED = "pinned";
36
+ const SPACE_MANAGE = "manage";
37
+ const STARTER_LABEL = "Starter";
38
+ const DEFAULT_HREFS = {
39
+ agent: (id) => `/agents/${id}`,
40
+ editAgent: (id) => `/agents/${id}/build`,
41
+ conversation: (id) => `/chat/${id}`
42
+ };
43
+ function defaultLinkTo(location) {
44
+ const here = globalThis.location;
45
+ const base = here ? `${here.origin}${here.pathname}` : "";
46
+ const params = new URLSearchParams();
47
+ if (location.agentId) params.set("agent", location.agentId);
48
+ if (location.conversationId) params.set("thread", location.conversationId);
49
+ const query = params.toString();
50
+ return query ? `${base}?${query}` : base;
51
+ }
52
+ function copyText(text, what) {
53
+ const clipboard = globalThis.navigator?.clipboard;
54
+ if (!clipboard) {
55
+ toast.error(`Could not copy the ${what}`);
56
+ return;
57
+ }
58
+ clipboard.writeText(text).then(
59
+ () => toast.success(`Copied the ${what}`),
60
+ () => toast.error(`Could not copy the ${what}`)
61
+ );
62
+ }
63
+ function useAgentInfo(agentIds) {
64
+ const [info, setInfo] = useState(/* @__PURE__ */ new Map());
65
+ const key = [...agentIds].sort().join(",");
66
+ useEffect(() => {
67
+ if (key === "") return void 0;
68
+ let cancelled = false;
69
+ void (async () => {
70
+ const { data, error } = await supabase.schema("agent").from("definition").select("id, name, description, model_id").in("id", key.split(","));
71
+ if (cancelled) return;
72
+ if (error) {
73
+ console.error("[messenger/agents] agent names could not be read:", error.message);
74
+ return;
75
+ }
76
+ const next = /* @__PURE__ */ new Map();
77
+ for (const row of data ?? []) {
78
+ next.set(row.id, { name: row.name ?? "Agent", description: row.description ?? null, modelId: row.model_id ?? null });
79
+ }
80
+ setInfo(next);
81
+ })();
82
+ return () => {
83
+ cancelled = true;
84
+ };
85
+ }, [key]);
86
+ return info;
87
+ }
88
+ function useAgentsMessengerSource(options = {}) {
89
+ const {
90
+ title = "Agents",
91
+ profile,
92
+ initialLocation,
93
+ onLocationChange,
94
+ linkTo = defaultLinkTo,
95
+ renderSeatRoom
96
+ } = options;
97
+ const hrefs = { ...DEFAULT_HREFS, ...options.hrefs };
98
+ const dispatch = useAppDispatch();
99
+ const router = useRouter();
100
+ const authReady = useAppSelector(selectAuthReady);
101
+ const userId = useAppSelector(selectUserId);
102
+ const seatStates = useMandateSet(MESSENGER_STARTER_MANDATE_KEYS, { optionalKeys: MESSENGER_STARTER_MANDATE_KEYS });
103
+ const seats = useMemo(
104
+ () => MESSENGER_STARTER_MANDATE_KEYS.map((mandateKey) => ({ mandateKey, agentId: seatStates[mandateKey]?.mandate?.agentId ?? null })),
105
+ [seatStates]
106
+ );
107
+ const seatsLoading = MESSENGER_STARTER_MANDATE_KEYS.some((key) => seatStates[key]?.loading !== false);
108
+ const [person, setPerson] = useState(EMPTY_PERSON_STATE);
109
+ const [personStatus, setPersonStatus] = useState("loading");
110
+ const [personError, setPersonError] = useState(null);
111
+ const [pending, setPending] = useState(/* @__PURE__ */ new Set());
112
+ const reload = useCallback(async () => {
113
+ try {
114
+ setPerson(await readMessengerPersonState());
115
+ setPersonStatus("ready");
116
+ setPersonError(null);
117
+ } catch (error) {
118
+ setPersonStatus("failed");
119
+ setPersonError(error instanceof Error ? error.message : String(error));
120
+ }
121
+ }, []);
122
+ useEffect(() => {
123
+ if (!authReady || !userId) return;
124
+ void reload();
125
+ }, [authReady, userId, reload]);
126
+ const mutate = useCallback(
127
+ async (key, write, failure) => {
128
+ setPending((prev) => new Set(prev).add(key));
129
+ try {
130
+ await write();
131
+ await reload();
132
+ } catch (error) {
133
+ toast.error(`${failure}: ${error instanceof Error ? error.message : String(error)}`);
134
+ } finally {
135
+ setPending((prev) => {
136
+ const next = new Set(prev);
137
+ next.delete(key);
138
+ return next;
139
+ });
140
+ }
141
+ },
142
+ [reload]
143
+ );
144
+ const roster = useMemo(() => mergeRoster(seats, person), [seats, person]);
145
+ const rosterById = useMemo(() => new Map(roster.agents.map((a) => [a.agentId, a])), [roster.agents]);
146
+ const knownIds = useMemo(() => {
147
+ const ids = new Set(roster.agents.map((a) => a.agentId));
148
+ for (const seat of roster.removedStarters) if (seat.agentId) ids.add(seat.agentId);
149
+ for (const pin of person.threads.values()) ids.add(pin.agentId);
150
+ return [...ids];
151
+ }, [roster, person.threads]);
152
+ const info = useAgentInfo(knownIds);
153
+ const nameOf = useCallback((agentId) => info.get(agentId)?.name ?? "Agent", [info]);
154
+ const threadAgentIds = useMemo(() => {
155
+ const ids = new Set(roster.agents.map((a) => a.agentId));
156
+ for (const pin of person.threads.values()) ids.add(pin.agentId);
157
+ return [...ids];
158
+ }, [roster.agents, person.threads]);
159
+ const loadThreads = useCallback(
160
+ (agentId, replace = true) => dispatch(fetchConversationHistory({ scopeId: scopeIdOf(agentId), agentIds: [agentId], pageSize: PAGE_SIZE, replace })),
161
+ [dispatch]
162
+ );
163
+ const threadKey = threadAgentIds.join(",");
164
+ useEffect(() => {
165
+ if (!authReady || !userId || threadKey === "") return;
166
+ for (const agentId of threadKey.split(",")) void loadThreads(agentId);
167
+ }, [authReady, userId, threadKey, loadThreads]);
168
+ const scopes = useAppSelector((state) => state.conversationHistory.scopes);
169
+ const threadsByAgent = useMemo(() => {
170
+ const map = /* @__PURE__ */ new Map();
171
+ for (const agentId of threadAgentIds) {
172
+ const items = scopes[scopeIdOf(agentId)]?.items ?? [];
173
+ map.set(
174
+ agentId,
175
+ items.map((item) => ({ conversationId: item.conversationId, title: item.title, updatedAt: item.updatedAt }))
176
+ );
177
+ }
178
+ return map;
179
+ }, [scopes, threadAgentIds]);
180
+ const scopeOf = (agentId) => scopes[scopeIdOf(agentId)];
181
+ const [activeRow, setActiveRow] = useState(
182
+ () => initialLocation?.agentId ? agentRowId(initialLocation.agentId) : null
183
+ );
184
+ const [choices, setChoices] = useState(
185
+ () => initialLocation?.agentId && initialLocation.conversationId ? { [initialLocation.agentId]: { conversationId: initialLocation.conversationId, nonce: 0 } } : {}
186
+ );
187
+ const [space, setSpace] = useState(SPACE_ALL);
188
+ const [manageOpen, setManageOpen] = useState(false);
189
+ const [renaming, setRenaming] = useState(null);
190
+ const currentThreadOf = useCallback(
191
+ (agentId) => {
192
+ const choice = choices[agentId];
193
+ if (choice) return choice.conversationId;
194
+ const scope = scopes[scopeIdOf(agentId)];
195
+ if (!scope || scope.status === "idle" || scope.status === "loading" && scope.items.length === 0) return void 0;
196
+ return scope.items[0]?.conversationId ?? null;
197
+ },
198
+ [choices, scopes]
199
+ );
200
+ const pinnedThreadIds = useMemo(() => new Set(person.threads.keys()), [person.threads]);
201
+ const openAgentThread = useCallback(
202
+ (agentId, conversationId) => {
203
+ setManageOpen(false);
204
+ if (conversationId && pinnedThreadIds.has(conversationId)) {
205
+ setActiveRow(threadRowId(conversationId));
206
+ return;
207
+ }
208
+ setChoices((prev) => ({ ...prev, [agentId]: { conversationId, nonce: prev[agentId]?.nonce ?? 0 } }));
209
+ setActiveRow(agentRowId(agentId));
210
+ },
211
+ [pinnedThreadIds]
212
+ );
213
+ const newChat = useCallback((agentId) => {
214
+ setManageOpen(false);
215
+ setChoices((prev) => ({ ...prev, [agentId]: { conversationId: null, nonce: (prev[agentId]?.nonce ?? 0) + 1 } }));
216
+ setActiveRow(agentRowId(agentId));
217
+ }, []);
218
+ const location = useMemo(() => {
219
+ const ref = activeRow ? parseRowId(activeRow) : null;
220
+ if (!ref) return { agentId: null, conversationId: null };
221
+ if (ref.kind === "thread") {
222
+ return { agentId: person.threads.get(ref.conversationId)?.agentId ?? null, conversationId: ref.conversationId };
223
+ }
224
+ return { agentId: ref.agentId, conversationId: currentThreadOf(ref.agentId) ?? null };
225
+ }, [activeRow, person.threads, currentThreadOf]);
226
+ const locationChange = useRef(onLocationChange);
227
+ useEffect(() => {
228
+ locationChange.current = onLocationChange;
229
+ }, [onLocationChange]);
230
+ useEffect(() => {
231
+ locationChange.current?.(location);
232
+ }, [location]);
233
+ useEffect(() => {
234
+ const id = initialLocation?.conversationId;
235
+ if (id && personStatus === "ready" && person.threads.has(id)) setActiveRow(threadRowId(id));
236
+ }, [personStatus]);
237
+ const togglePinThread = useCallback(
238
+ (conversationId, agentId) => {
239
+ const pinned = person.threads.has(conversationId);
240
+ const live = threadsByAgent.get(agentId)?.find((t) => t.conversationId === conversationId);
241
+ void mutate(
242
+ conversationId,
243
+ () => writeThreadPin(conversationId, pinned ? null : { agentId, title: live?.title ?? null }),
244
+ pinned ? "Could not unpin this chat" : "Could not pin this chat"
245
+ );
246
+ if (pinned && activeRow === threadRowId(conversationId)) {
247
+ setChoices((prev) => ({ ...prev, [agentId]: { conversationId, nonce: prev[agentId]?.nonce ?? 0 } }));
248
+ setActiveRow(agentRowId(agentId));
249
+ }
250
+ },
251
+ [person.threads, threadsByAgent, mutate, activeRow]
252
+ );
253
+ const togglePinAgent = useCallback(
254
+ (agent) => {
255
+ const added = agent.mandateKey === null;
256
+ const next = agent.pinned ? added ? { in: true } : null : added ? { in: true, pinned: true } : { pinned: true };
257
+ void mutate(agent.agentId, () => writeAgentSeat(agent.agentId, next), "Could not change the pin");
258
+ },
259
+ [mutate]
260
+ );
261
+ const removeAgent = useCallback(
262
+ (agentId) => {
263
+ const agent = rosterById.get(agentId);
264
+ const starter = agent?.mandateKey != null;
265
+ void mutate(agentId, () => writeAgentSeat(agentId, starter ? { in: false } : null), "Could not remove the agent");
266
+ if (activeRow === agentRowId(agentId)) setActiveRow(null);
267
+ },
268
+ [rosterById, mutate, activeRow]
269
+ );
270
+ const restoreStarter = useCallback(
271
+ (agentId) => void mutate(agentId, () => writeAgentSeat(agentId, null), "Could not restore the agent"),
272
+ [mutate]
273
+ );
274
+ const addAgent = useCallback(
275
+ (agentId) => {
276
+ const removedStarter = roster.removedStarters.some((s) => s.agentId === agentId);
277
+ void mutate(agentId, () => writeAgentSeat(agentId, removedStarter ? null : { in: true }), "Could not add the agent");
278
+ },
279
+ [roster.removedStarters, mutate]
280
+ );
281
+ const seatOf = useCallback(
282
+ (agentId) => rosterById.get(agentId)?.mandateKey ?? null,
283
+ [rosterById]
284
+ );
285
+ const seatRoomFor = useCallback(
286
+ (agentId) => {
287
+ const mandateKey = seatOf(agentId);
288
+ return mandateKey && renderSeatRoom ? renderSeatRoom({ mandateKey, agentId }) : null;
289
+ },
290
+ [seatOf, renderSeatRoom]
291
+ );
292
+ const source = useMemo(() => {
293
+ const rows = orderRows({ roster: roster.agents, threadsByAgent, pinnedThreads: person.threads });
294
+ const contacts = rows.map((row) => {
295
+ if (row.kind === "thread") {
296
+ return {
297
+ id: threadRowId(row.conversationId),
298
+ name: row.title,
299
+ avatar: { kind: "monogram", text: void 0 },
300
+ preview: nameOf(row.agentId),
301
+ time: relativeDate(row.updatedAt),
302
+ pinned: true,
303
+ searchText: nameOf(row.agentId)
304
+ };
305
+ }
306
+ return {
307
+ id: agentRowId(row.agentId),
308
+ name: nameOf(row.agentId),
309
+ avatar: { kind: "monogram" },
310
+ preview: row.latest ? threadTitle(row.latest) : seatOf(row.agentId) ? STARTER_LABEL : void 0,
311
+ time: relativeDate(row.latest?.updatedAt),
312
+ pinned: row.pinned,
313
+ searchText: info.get(row.agentId)?.description ?? void 0
314
+ };
315
+ });
316
+ const visible = space === SPACE_PINNED ? contacts.filter((c) => c.pinned) : contacts;
317
+ const agentOfRow = (contact) => {
318
+ const ref = parseRowId(contact.id);
319
+ if (!ref) return null;
320
+ return ref.kind === "agent" ? ref.agentId : person.threads.get(ref.conversationId)?.agentId ?? null;
321
+ };
322
+ const threadOfRow = (contact) => {
323
+ const ref = parseRowId(contact.id);
324
+ if (!ref) return null;
325
+ return ref.kind === "thread" ? ref.conversationId : currentThreadOf(ref.agentId);
326
+ };
327
+ const agentDoors = (agentId) => ({
328
+ openAgent: () => router.push(hrefs.agent(agentId)),
329
+ editAgent: () => router.push(hrefs.editAgent(agentId))
330
+ });
331
+ const threadDoors = (agentId, conversationId) => ({
332
+ togglePin: () => togglePinThread(conversationId, agentId),
333
+ rename: () => {
334
+ const live = threadsByAgent.get(agentId)?.find((t) => t.conversationId === conversationId);
335
+ setRenaming({ conversationId, title: live?.title ?? person.threads.get(conversationId)?.title ?? "" });
336
+ },
337
+ openInChat: () => router.push(hrefs.conversation(conversationId)),
338
+ copyLink: () => copyText(linkTo({ agentId, conversationId }), "link")
339
+ });
340
+ const agentRowMenu = (agentId) => {
341
+ const agent = rosterById.get(agentId);
342
+ if (!agent) return [];
343
+ return buildAgentRowMenu(
344
+ { pinned: agent.pinned, canNewChat: seatRoomFor(agentId) === null },
345
+ {
346
+ newChat: () => newChat(agentId),
347
+ togglePin: () => togglePinAgent(agent),
348
+ ...agentDoors(agentId),
349
+ remove: () => removeAgent(agentId),
350
+ copyLink: () => copyText(linkTo({ agentId, conversationId: null }), "link")
351
+ }
352
+ );
353
+ };
354
+ const selectSpace = (id) => {
355
+ if (id === SPACE_MANAGE) {
356
+ setManageOpen(true);
357
+ return;
358
+ }
359
+ setManageOpen(false);
360
+ setSpace(id);
361
+ };
362
+ const pinnedCount = contacts.filter((c) => c.pinned).length;
363
+ return {
364
+ title,
365
+ rail: [
366
+ { id: SPACE_ALL, label: "All agents", icon: Bot },
367
+ { id: SPACE_PINNED, label: "Pinned", icon: Pin, count: pinnedCount || void 0 },
368
+ { id: "more", label: "More", spaces: [{ id: SPACE_MANAGE, label: "Manage agents", icon: Settings2 }] }
369
+ ],
370
+ activeSpaceId: manageOpen ? SPACE_MANAGE : space,
371
+ onSelectSpace: selectSpace,
372
+ spaceMenu: (s) => s.id === SPACE_MANAGE ? [{ id: "open", label: "Manage agents", icon: Settings2, onSelect: () => selectSpace(SPACE_MANAGE) }] : [
373
+ { id: "open", label: `Show ${s.label}`, icon: s.icon, onSelect: () => selectSpace(s.id) },
374
+ { id: "refresh", label: "Refresh", icon: RefreshCw, onSelect: () => void reload() }
375
+ ],
376
+ profile,
377
+ contacts: visible,
378
+ listState: {
379
+ loading: personStatus === "loading" || seatsLoading && roster.agents.length === 0,
380
+ error: personStatus === "failed" ? personError : null,
381
+ retry: () => void reload(),
382
+ emptyLabel: space === SPACE_PINNED ? "Nothing pinned" : "No agents yet"
383
+ },
384
+ activeContactId: activeRow,
385
+ onSelectContact: (id) => {
386
+ setManageOpen(false);
387
+ setActiveRow(id);
388
+ const ref = parseRowId(id);
389
+ const agentId = ref?.kind === "agent" ? ref.agentId : ref ? person.threads.get(ref.conversationId)?.agentId : null;
390
+ if (agentId) void loadThreads(agentId);
391
+ },
392
+ contactMenu: (contact) => {
393
+ const ref = parseRowId(contact.id);
394
+ if (!ref) return [];
395
+ if (ref.kind === "agent") return agentRowMenu(ref.agentId);
396
+ const agentId = person.threads.get(ref.conversationId)?.agentId;
397
+ if (!agentId) return [];
398
+ return buildThreadMenu(true, {
399
+ open: () => {
400
+ setManageOpen(false);
401
+ setActiveRow(contact.id);
402
+ },
403
+ ...threadDoors(agentId, ref.conversationId)
404
+ });
405
+ },
406
+ listMenu: () => [
407
+ { id: "manage", label: "Manage agents", icon: Settings2, onSelect: () => selectSpace(SPACE_MANAGE) },
408
+ { id: "refresh", label: "Refresh", icon: RefreshCw, onSelect: () => void reload() }
409
+ ],
410
+ onNew: () => selectSpace(SPACE_MANAGE),
411
+ newLabel: "Add agent",
412
+ header: (contact) => {
413
+ const agentId = agentOfRow(contact);
414
+ const ref = parseRowId(contact.id);
415
+ if (ref?.kind === "thread") return { title: contact.name, subtitle: agentId ? nameOf(agentId) : void 0, avatar: contact.avatar };
416
+ const current = threadOfRow(contact);
417
+ const thread = current ? threadsByAgent.get(agentId ?? "")?.find((t) => t.conversationId === current) : null;
418
+ const subtitle = agentId && seatRoomFor(agentId) !== null ? void 0 : current === null ? "New chat" : thread ? threadTitle(thread) : void 0;
419
+ return { title: contact.name, subtitle, avatar: contact.avatar };
420
+ },
421
+ headerMenu: (contact) => {
422
+ const agentId = agentOfRow(contact);
423
+ if (!agentId) return [];
424
+ const doors = agentDoors(agentId);
425
+ const agentPart = [
426
+ { id: "open-agent", label: "Open agent", icon: Bot, onSelect: doors.openAgent },
427
+ { id: "edit-agent", label: "Edit agent", icon: Pencil, onSelect: doors.editAgent }
428
+ ];
429
+ if (seatRoomFor(agentId) !== null) {
430
+ return [
431
+ ...agentPart,
432
+ { id: "copy-link", label: "Copy link", icon: Link2, onSelect: () => copyText(linkTo({ agentId, conversationId: null }), "link") }
433
+ ];
434
+ }
435
+ const current = threadOfRow(contact);
436
+ const head = [{ id: "new-chat", label: "New chat", icon: Plus, onSelect: () => newChat(agentId) }];
437
+ if (!current) return [...head, { id: "agent-sep", separator: true }, ...agentPart];
438
+ return [
439
+ ...head,
440
+ ...buildThreadMenu(pinnedThreadIds.has(current), threadDoors(agentId, current)),
441
+ { id: "agent-sep", separator: true },
442
+ ...agentPart
443
+ ];
444
+ },
445
+ titleMenu: (contact) => {
446
+ const agentId = agentOfRow(contact);
447
+ if (!agentId || seatRoomFor(agentId) !== null) return [];
448
+ const scope = scopeOf(agentId);
449
+ const current = threadOfRow(contact);
450
+ return buildTitleMenu(
451
+ {
452
+ threads: threadsByAgent.get(agentId) ?? [],
453
+ currentConversationId: current ?? null,
454
+ hasMore: scope?.hasMore ?? false,
455
+ loadingMore: scope?.status === "loading-more",
456
+ pinnedIds: pinnedThreadIds
457
+ },
458
+ {
459
+ openThread: (id) => openAgentThread(agentId, id),
460
+ newChat: () => newChat(agentId),
461
+ loadMore: () => void loadThreads(agentId, false),
462
+ togglePin: (id) => togglePinThread(id, agentId)
463
+ }
464
+ );
465
+ },
466
+ renderThread: (contact) => {
467
+ const agentId = agentOfRow(contact);
468
+ if (!agentId) return null;
469
+ const seatRoom = parseRowId(contact.id)?.kind === "agent" ? seatRoomFor(agentId) : null;
470
+ if (seatRoom !== null) return /* @__PURE__ */ jsx("div", { className: "flex min-h-0 flex-1 flex-col", children: seatRoom });
471
+ const current = threadOfRow(contact);
472
+ if (current === void 0) {
473
+ return /* @__PURE__ */ jsx("div", { className: "flex min-h-0 flex-1 flex-col overflow-hidden bg-textured", children: /* @__PURE__ */ jsx(ChatRoomSkeleton, {}) });
474
+ }
475
+ const nonce = choices[agentId]?.nonce ?? 0;
476
+ return /* @__PURE__ */ jsx("div", { className: "flex min-h-0 flex-1 flex-col", children: /* @__PURE__ */ jsx(
477
+ ChatRoomClient,
478
+ {
479
+ agentId,
480
+ conversationId: current ?? void 0,
481
+ mandateKey: seatOf(agentId) ?? void 0,
482
+ sourceFeature: "chat",
483
+ surfaceBasePath: "/messenger",
484
+ onOpenConversation: (conversationId) => {
485
+ if (parseRowId(contact.id)?.kind === "agent") {
486
+ setChoices((prev) => ({ ...prev, [agentId]: { conversationId, nonce: prev[agentId]?.nonce ?? nonce } }));
487
+ } else {
488
+ openAgentThread(agentId, conversationId);
489
+ }
490
+ void loadThreads(agentId);
491
+ }
492
+ },
493
+ `${agentId}:${nonce}`
494
+ ) });
495
+ },
496
+ renderDetails: (contact) => {
497
+ const agentId = agentOfRow(contact);
498
+ if (!agentId) return null;
499
+ const scope = scopeOf(agentId);
500
+ const agentInfo = info.get(agentId);
501
+ return /* @__PURE__ */ jsx(
502
+ AgentDetailsPanel,
503
+ {
504
+ agentId,
505
+ name: nameOf(agentId),
506
+ description: agentInfo?.description ?? null,
507
+ modelId: agentInfo?.modelId ?? null,
508
+ avatar: { kind: "monogram" },
509
+ seatLabel: seatOf(agentId) ? STARTER_LABEL : null,
510
+ threads: threadsByAgent.get(agentId) ?? [],
511
+ currentConversationId: threadOfRow(contact) ?? null,
512
+ pinnedIds: pinnedThreadIds,
513
+ hasMore: scope?.hasMore ?? false,
514
+ onLoadMore: () => void loadThreads(agentId, false),
515
+ onOpenThread: (id) => openAgentThread(agentId, id),
516
+ onOpenAgent: agentDoors(agentId).openAgent,
517
+ onEditAgent: agentDoors(agentId).editAgent
518
+ }
519
+ );
520
+ },
521
+ renderMain: () => {
522
+ if (!manageOpen) return null;
523
+ const manageRow = (agentId, starter) => ({
524
+ agentId,
525
+ name: nameOf(agentId),
526
+ description: info.get(agentId)?.description ?? null,
527
+ avatar: { kind: "monogram" },
528
+ seatLabel: starter ? STARTER_LABEL : null
529
+ });
530
+ return /* @__PURE__ */ jsx(
531
+ MessengerManageAgents,
532
+ {
533
+ rows: roster.agents.map((a) => manageRow(a.agentId, a.mandateKey !== null)),
534
+ removedStarters: roster.removedStarters.flatMap((s) => s.agentId ? [manageRow(s.agentId, true)] : []),
535
+ pending,
536
+ error: personStatus === "failed" ? personError : null,
537
+ onRemove: removeAgent,
538
+ onRestore: restoreStarter,
539
+ onAdd: addAgent,
540
+ onClose: () => setManageOpen(false)
541
+ }
542
+ );
543
+ }
544
+ };
545
+ }, [
546
+ roster,
547
+ rosterById,
548
+ threadsByAgent,
549
+ person.threads,
550
+ pinnedThreadIds,
551
+ info,
552
+ nameOf,
553
+ space,
554
+ manageOpen,
555
+ activeRow,
556
+ choices,
557
+ scopes,
558
+ currentThreadOf,
559
+ personStatus,
560
+ personError,
561
+ seatsLoading,
562
+ pending,
563
+ profile,
564
+ title,
565
+ router,
566
+ linkTo,
567
+ reload,
568
+ loadThreads,
569
+ newChat,
570
+ openAgentThread,
571
+ togglePinThread,
572
+ togglePinAgent,
573
+ removeAgent,
574
+ restoreStarter,
575
+ addAgent,
576
+ seatOf,
577
+ seatRoomFor
578
+ ]);
579
+ const overlays = /* @__PURE__ */ jsx(
580
+ TextInputDialog,
581
+ {
582
+ open: renaming !== null,
583
+ onOpenChange: (open) => {
584
+ if (!open) setRenaming(null);
585
+ },
586
+ title: "Rename chat",
587
+ defaultValue: renaming?.title ?? "",
588
+ confirmLabel: "Rename",
589
+ onConfirm: async (value) => {
590
+ if (!renaming) return;
591
+ const result = await dispatch(renameConversation({ conversationId: renaming.conversationId, title: value }));
592
+ if (renameConversation.rejected.match(result)) {
593
+ toast.error("Could not rename this chat");
594
+ return;
595
+ }
596
+ setRenaming(null);
597
+ }
598
+ }
599
+ );
600
+ return { source, overlays };
601
+ }
602
+ function AgentsMessenger({ className, ...options }) {
603
+ const { source, overlays } = useAgentsMessengerSource(options);
604
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
605
+ /* @__PURE__ */ jsx(MessengerShell, { source, className }),
606
+ overlays
607
+ ] });
608
+ }
609
+ export {
610
+ AgentsMessenger,
611
+ useAgentsMessengerSource
612
+ };
@@ -0,0 +1,21 @@
1
+ import type { MessengerAvatar as AvatarSpec } from "../contract.js";
2
+ export interface ManageRow {
3
+ readonly agentId: string;
4
+ readonly name: string;
5
+ readonly description: string | null;
6
+ readonly avatar?: AvatarSpec;
7
+ /** "Starter" for a starter seat; null for an agent the person added. */
8
+ readonly seatLabel: string | null;
9
+ }
10
+ export interface MessengerManageAgentsProps {
11
+ readonly rows: readonly ManageRow[];
12
+ readonly removedStarters: readonly ManageRow[];
13
+ /** Agent ids with a write in flight. */
14
+ readonly pending: ReadonlySet<string>;
15
+ readonly error: string | null;
16
+ readonly onRemove: (agentId: string) => void;
17
+ readonly onRestore: (agentId: string) => void;
18
+ readonly onAdd: (agentId: string) => void;
19
+ readonly onClose: () => void;
20
+ }
21
+ export declare function MessengerManageAgents(props: MessengerManageAgentsProps): import("react").JSX.Element;