@ai-matrx/chat 0.7.26 → 0.7.28

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.
package/CHANGELOG.md CHANGED
@@ -1,11 +1,42 @@
1
1
  # @ai-matrx/chat
2
2
 
3
- ## 0.7.26
3
+ ## 0.7.28
4
4
 
5
5
  Automatic changed-only republish (docs/metadata drift since the last tag — see
6
- `git diff npm/chat/v0.7.25..npm/chat/v0.7.26 -- apps/shared/chat`).
6
+ `git diff npm/chat/v0.7.27..npm/chat/v0.7.28 -- apps/shared/chat`).
7
7
  No source changes intended and no consumer action required.
8
8
 
9
+ ## 0.7.27
10
+ ### Unreleased
11
+
12
+ ### An answered question is followed to its answer (lane F16)
13
+
14
+ - **`runHeadlessAgentJson` / `adoptHeadlessAgentJson` / `continueAgentJson`: once the ask closes, the
15
+ wait re-reads the conversation every 15 s until the resumed turn's answer lands.** The resumed turn runs
16
+ on the server AFTER the answer is accepted (30-90 s for an Applet build); one follow taken before its
17
+ operation existed followed nothing, and the builder sat on "Waiting for your answer" for 7+ minutes over
18
+ a build the server had written. Guard: `a-run-parked-on-the-person-waits-for-their-answer.test.ts`.
19
+ Consumer action: none.
20
+ - **`useActionRequestAnswer`: a reply that never came back says so — never "Nothing was recorded".**
21
+ New `ACTION_REQUEST_UNCONFIRMED` for a thrown transport (a dropped connection, an edge timeout): it may
22
+ have arrived, and a second answer is refused by the claim, so trying again is safe. Guard:
23
+ `a-lost-reply-never-claims-nothing-was-recorded.test.tsx`. Consumer action: none.
24
+
25
+ ### The messenger shell
26
+
27
+ - **New domain `@ai-matrx/chat/messenger/*`**: ONE desktop-messenger layout for any conversation
28
+ source — `messenger/contract` (`MessengerSource`), `messenger/MessengerShell`,
29
+ `messenger/MessengerDetails`, `messenger/MessengerAvatar`. Rail of spaces with collapsible groups,
30
+ a profile and an expand/collapse toggle remembered per browser; a 320px contact list (search, an
31
+ "All" chip plus the source's chips, rows with avatar, time, preview, unread, pinned, muted and
32
+ draft); a thread header with a generic `titleMenu` dropdown; an optional details panel; and
33
+ right-click plus "⋯" menus on rows, rail items and the header whose items all come from the source.
34
+ Chrome only: the thread body is the source's renderer. The list can never scroll sideways.
35
+ - **`messenger/people/usePeopleMessengerSource`**: `@ai-matrx/messaging` conversations as a source;
36
+ the thread is messaging's `ConversationView` with `appearance="messenger"`.
37
+ - **`@ai-matrx/messaging` is now a dependency** (`workspace:*`), imported only by the people subpath,
38
+ so hosts that never import it load none of it.
39
+
9
40
  ## 0.7.25
10
41
 
11
42
  Automatic changed-only republish (docs/metadata drift since the last tag — see
@@ -28,6 +28,13 @@ export type ActionRequestDone = {
28
28
  next: string | null;
29
29
  };
30
30
  export declare const ACTION_REQUEST_UNREACHED = "That did not reach us. Nothing was recorded, so it is safe to try again.";
31
+ /**
32
+ * The call never came back (a dropped connection, an edge timeout). It may or may not have
33
+ * reached the server, so the page never claims "nothing recorded" — the Applet builder said
34
+ * that over an answer the server had already recorded (live, 2026-10-09). A second answer is
35
+ * refused by the claim, so trying again is always safe.
36
+ */
37
+ export declare const ACTION_REQUEST_UNCONFIRMED = "We lost the connection before hearing back. Trying again is safe; an answer is never recorded twice.";
31
38
  /**
32
39
  * The submit state machine, shared by every surface that answers an ask.
33
40
  * A second tap while in flight does nothing — `busy` is a render behind the
@@ -15,6 +15,7 @@ import {
15
15
  parseApprovalUsd
16
16
  } from "./spendAmount.js";
17
17
  const ACTION_REQUEST_UNREACHED = "That did not reach us. Nothing was recorded, so it is safe to try again.";
18
+ const ACTION_REQUEST_UNCONFIRMED = "We lost the connection before hearing back. Trying again is safe; an answer is never recorded twice.";
18
19
  function useActionRequestAnswer(transport, onDone) {
19
20
  const [busy, setBusy] = useState(false);
20
21
  const [refusal, setRefusal] = useState(null);
@@ -59,7 +60,7 @@ function useActionRequestAnswer(transport, onDone) {
59
60
  }
60
61
  setDone({ message: body.message ?? ACTION_REQUEST_UNREACHED, next: null });
61
62
  } catch {
62
- setRefusal({ code: "offline", message: ACTION_REQUEST_UNREACHED, remedy: null });
63
+ setRefusal({ code: "offline", message: ACTION_REQUEST_UNCONFIRMED, remedy: null });
63
64
  } finally {
64
65
  inFlight.current = false;
65
66
  setBusy(false);
@@ -548,6 +549,7 @@ function safeFormat(format, iso) {
548
549
  return Number.isNaN(at.getTime()) ? iso : format.format(at);
549
550
  }
550
551
  export {
552
+ ACTION_REQUEST_UNCONFIRMED,
551
553
  ACTION_REQUEST_UNREACHED,
552
554
  ActionRequestAnswerForm,
553
555
  useActionRequestAnswer
@@ -36,6 +36,7 @@ function ComposerGreeting({ className }) {
36
36
  () => timeOfDayGreeting((/* @__PURE__ */ new Date()).getHours()),
37
37
  () => null
38
38
  );
39
+ const onServerHtml = useSyncExternalStore(noSubscription, () => false, () => true);
39
40
  return /* @__PURE__ */ jsxs("div", { className: cn("flex items-center justify-center gap-3.5", className), children: [
40
41
  /* @__PURE__ */ jsx(
41
42
  "span",
@@ -48,7 +49,7 @@ function ComposerGreeting({ className }) {
48
49
  /* @__PURE__ */ jsx("span", { "data-matrx-greeting": "", suppressHydrationWarning: true, children: greeting ?? "Hello" }),
49
50
  firstName ? `, ${firstName}` : ""
50
51
  ] }),
51
- /* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: GREETING_SCRIPT } })
52
+ onServerHtml ? /* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: GREETING_SCRIPT } }) : null
52
53
  ] });
53
54
  }
54
55
  function parseQuickActions(value) {
@@ -19,6 +19,7 @@ import {
19
19
  } from "../messages/messages.selectors.js";
20
20
  import { countOpenAsksForConversation } from "../../../runtime-reconnect/parked-on-person.js";
21
21
  import { rereadAndFollow } from "../../../runtime-reconnect/reread-and-follow.js";
22
+ import { loadConversation } from "./load-conversation.thunk.js";
22
23
  import {
23
24
  selectLatestAnswerText,
24
25
  selectLatestRequestId
@@ -55,6 +56,7 @@ const DEFAULT_PERSON_WAIT_MS = 6 * 60 * 6e4;
55
56
  const RECONNECT_WAIT_MS = 5 * 6e4;
56
57
  const isConnectionLoss = (errorType) => errorType === "transport_lost" || errorType === "heartbeat_timeout";
57
58
  const PERSON_ASK_POLL_MS = 1e4;
59
+ const PERSON_REREAD_MS = 15e3;
58
60
  const DEFAULT_MESSAGES = {
59
61
  streamError: "The agent failed before returning a result.",
60
62
  noJson: "The agent finished but produced no structured JSON.",
@@ -644,6 +646,12 @@ async function waitForExtraction(dispatch, getState, args) {
644
646
  }
645
647
  } catch {
646
648
  }
649
+ } else if (person.following && now - person.askedAt >= PERSON_REREAD_MS) {
650
+ person.askedAt = now;
651
+ try {
652
+ await dispatch(loadConversation({ conversationId })).unwrap();
653
+ } catch {
654
+ }
647
655
  }
648
656
  const now2 = getState();
649
657
  const marker = answerSince(now2);
@@ -1,6 +1,8 @@
1
1
  import { adminLaneOrganizationId } from "./server/admin-lane.js";
2
2
  import { selectIsAdmin } from "./identity.js";
3
+ import { isChatHostConfigured } from "./configure.js";
3
4
  function adminSeatOpen(state) {
5
+ if (!isChatHostConfigured()) return false;
4
6
  return adminLaneOrganizationId() !== null && selectIsAdmin(state);
5
7
  }
6
8
  export {
@@ -0,0 +1,18 @@
1
+ import { type MessengerAvatar as AvatarSpec } from "./contract.js";
2
+ /** Box sizes measured from WhatsApp desktop: row 48, header 40, rail profile 28, details 80. */
3
+ declare const SIZES: {
4
+ readonly rail: "h-7 w-7 text-[11px]";
5
+ readonly header: "h-10 w-10 text-sm";
6
+ readonly row: "h-12 w-12 text-base";
7
+ readonly details: "h-20 w-20 text-2xl";
8
+ };
9
+ export type MessengerAvatarSize = keyof typeof SIZES;
10
+ export interface MessengerAvatarProps {
11
+ readonly name: string;
12
+ readonly avatar?: AvatarSpec;
13
+ readonly size: MessengerAvatarSize;
14
+ readonly className?: string;
15
+ }
16
+ /** One round avatar: image, emoji, icon or monogram. Never grows or shrinks in a flex row. */
17
+ export declare function MessengerAvatar({ name, avatar, size, className }: MessengerAvatarProps): import("react").JSX.Element;
18
+ export {};
@@ -0,0 +1,31 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { cn } from "@ai-matrx/design-system/cn";
4
+ import { initialsOf } from "./contract.js";
5
+ const SIZES = {
6
+ rail: "h-7 w-7 text-[11px]",
7
+ header: "h-10 w-10 text-sm",
8
+ row: "h-12 w-12 text-base",
9
+ details: "h-20 w-20 text-2xl"
10
+ };
11
+ function MessengerAvatar({ name, avatar, size, className }) {
12
+ const box = cn(
13
+ "relative flex shrink-0 select-none items-center justify-center overflow-hidden rounded-full bg-muted font-medium text-muted-foreground",
14
+ SIZES[size],
15
+ className
16
+ );
17
+ if (avatar?.kind === "image") {
18
+ return /* @__PURE__ */ jsx("span", { className: box, "data-avatar": "image", children: /* @__PURE__ */ jsx("img", { src: avatar.src, alt: "", className: "h-full w-full object-cover", draggable: false }) });
19
+ }
20
+ if (avatar?.kind === "emoji") {
21
+ return /* @__PURE__ */ jsx("span", { className: box, "data-avatar": "emoji", "aria-hidden": true, children: /* @__PURE__ */ jsx("span", { className: "leading-none", children: avatar.emoji }) });
22
+ }
23
+ if (avatar?.kind === "icon") {
24
+ const Icon = avatar.icon;
25
+ return /* @__PURE__ */ jsx("span", { className: cn(box, "bg-primary/10 text-primary"), "data-avatar": "icon", "aria-hidden": true, children: /* @__PURE__ */ jsx(Icon, { className: "h-1/2 w-1/2" }) });
26
+ }
27
+ return /* @__PURE__ */ jsx("span", { className: box, "data-avatar": "monogram", "aria-hidden": true, children: avatar?.kind === "monogram" && avatar.text ? avatar.text : initialsOf(name) });
28
+ }
29
+ export {
30
+ MessengerAvatar
31
+ };
@@ -0,0 +1,16 @@
1
+ import type { MessengerContact, MessengerFilter, MessengerSource } from "./contract.js";
2
+ export declare function MessengerContactRow({ contact, active, source, }: {
3
+ contact: MessengerContact;
4
+ active: boolean;
5
+ source: MessengerSource;
6
+ }): import("react").JSX.Element;
7
+ export interface MessengerContactListProps {
8
+ readonly source: MessengerSource;
9
+ readonly contacts: readonly MessengerContact[];
10
+ readonly query: string;
11
+ readonly onQueryChange: (query: string) => void;
12
+ readonly activeFilter: MessengerFilter | null;
13
+ readonly onFilterChange: (filterId: string | null) => void;
14
+ readonly className?: string;
15
+ }
16
+ export declare function MessengerContactList({ source, contacts, query, onQueryChange, activeFilter, onFilterChange, className, }: MessengerContactListProps): import("react").JSX.Element;
@@ -0,0 +1,210 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { BellOff, MoreHorizontal, Pin, Plus, Search, X } from "lucide-react";
4
+ import { cn } from "@ai-matrx/design-system/cn";
5
+ import { MessengerAvatar } from "./MessengerAvatar.js";
6
+ import { MessengerContextMenu, MessengerDropdownMenu } from "./MessengerMenu.js";
7
+ function formatUnread(count) {
8
+ return count > 99 ? "99+" : String(count);
9
+ }
10
+ function MessengerContactRow({
11
+ contact,
12
+ active,
13
+ source
14
+ }) {
15
+ const unread = contact.unread ?? 0;
16
+ const menu = source.contactMenu;
17
+ return /* @__PURE__ */ jsx(MessengerContextMenu, { entries: menu ? () => menu(contact) : void 0, children: /* @__PURE__ */ jsxs(
18
+ "div",
19
+ {
20
+ className: cn(
21
+ "group relative w-full min-w-0 rounded-lg transition-colors",
22
+ active ? "bg-accent" : "hover:bg-accent/50"
23
+ ),
24
+ "data-contact-id": contact.id,
25
+ "data-active": active ? "true" : void 0,
26
+ children: [
27
+ /* @__PURE__ */ jsxs(
28
+ "button",
29
+ {
30
+ type: "button",
31
+ onClick: () => source.onSelectContact(contact.id),
32
+ "aria-current": active ? "true" : void 0,
33
+ className: "flex h-[68px] w-full min-w-0 items-center gap-3 px-2 text-left outline-none focus-visible:ring-2 focus-visible:ring-ring/50",
34
+ children: [
35
+ /* @__PURE__ */ jsx(MessengerAvatar, { name: contact.name, avatar: contact.avatar, size: "row" }),
36
+ /* @__PURE__ */ jsxs("span", { className: "flex h-full min-w-0 flex-1 flex-col justify-center gap-0.5 overflow-hidden", children: [
37
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 items-baseline gap-2", children: [
38
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-[15px] font-medium text-foreground", "data-row-name": true, children: contact.name }),
39
+ contact.time ? /* @__PURE__ */ jsx(
40
+ "span",
41
+ {
42
+ className: cn(
43
+ "shrink-0 whitespace-nowrap text-[12px] tabular-nums",
44
+ unread > 0 ? "font-medium text-primary" : "text-muted-foreground"
45
+ ),
46
+ children: contact.time
47
+ }
48
+ ) : null
49
+ ] }),
50
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 items-center gap-1.5", children: [
51
+ /* @__PURE__ */ jsx(
52
+ "span",
53
+ {
54
+ className: cn(
55
+ "min-w-0 flex-1 truncate text-[14px]",
56
+ unread > 0 ? "text-foreground" : "text-muted-foreground"
57
+ ),
58
+ "data-row-preview": true,
59
+ children: contact.draft !== void 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
60
+ /* @__PURE__ */ jsx("span", { className: "text-destructive", children: "Draft: " }),
61
+ contact.draft
62
+ ] }) : contact.preview ?? ""
63
+ }
64
+ ),
65
+ /* @__PURE__ */ jsxs(
66
+ "span",
67
+ {
68
+ className: cn(
69
+ "flex shrink-0 items-center gap-1.5 text-muted-foreground",
70
+ menu ? "group-hover:invisible group-focus-within:invisible" : void 0
71
+ ),
72
+ children: [
73
+ contact.muted ? /* @__PURE__ */ jsx(BellOff, { className: "h-3.5 w-3.5", "aria-label": "Muted" }) : null,
74
+ contact.pinned ? /* @__PURE__ */ jsx(Pin, { className: "h-3.5 w-3.5 rotate-45", "aria-label": "Pinned" }) : null,
75
+ unread > 0 ? /* @__PURE__ */ jsx(
76
+ "span",
77
+ {
78
+ className: "min-w-5 rounded-full bg-primary px-1.5 text-center text-[11px] font-semibold leading-5 text-primary-foreground",
79
+ "aria-label": `${unread} unread`,
80
+ children: formatUnread(unread)
81
+ }
82
+ ) : null
83
+ ]
84
+ }
85
+ )
86
+ ] })
87
+ ] })
88
+ ]
89
+ }
90
+ ),
91
+ menu ? /* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute bottom-2.5 right-2 opacity-0 group-hover:pointer-events-auto group-hover:opacity-100 group-focus-within:pointer-events-auto group-focus-within:opacity-100", children: /* @__PURE__ */ jsx(
92
+ MessengerDropdownMenu,
93
+ {
94
+ entries: () => menu(contact),
95
+ trigger: /* @__PURE__ */ jsx(
96
+ "button",
97
+ {
98
+ type: "button",
99
+ "aria-label": `More for ${contact.name}`,
100
+ className: "flex h-6 w-6 items-center justify-center rounded-full text-muted-foreground hover:bg-background hover:text-foreground",
101
+ children: /* @__PURE__ */ jsx(MoreHorizontal, { className: "h-4 w-4" })
102
+ }
103
+ )
104
+ }
105
+ ) }) : null
106
+ ]
107
+ }
108
+ ) });
109
+ }
110
+ function MessengerContactList({
111
+ source,
112
+ contacts,
113
+ query,
114
+ onQueryChange,
115
+ activeFilter,
116
+ onFilterChange,
117
+ className
118
+ }) {
119
+ const state = source.listState ?? {};
120
+ const listMenu = source.listMenu;
121
+ return /* @__PURE__ */ jsxs(
122
+ "section",
123
+ {
124
+ "aria-label": source.title,
125
+ className: cn("flex h-full min-h-0 w-full min-w-0 flex-col overflow-hidden bg-card", className),
126
+ children: [
127
+ /* @__PURE__ */ jsxs("header", { className: "grid h-14 shrink-0 grid-cols-[36px_1fr_36px] items-center gap-2 px-3", children: [
128
+ listMenu ? /* @__PURE__ */ jsx(
129
+ MessengerDropdownMenu,
130
+ {
131
+ align: "start",
132
+ entries: listMenu,
133
+ trigger: /* @__PURE__ */ jsx(
134
+ "button",
135
+ {
136
+ type: "button",
137
+ "aria-label": `${source.title} menu`,
138
+ className: "flex h-9 w-9 items-center justify-center rounded-full border border-border text-foreground hover:bg-accent",
139
+ children: /* @__PURE__ */ jsx(MoreHorizontal, { className: "h-4 w-4" })
140
+ }
141
+ )
142
+ }
143
+ ) : /* @__PURE__ */ jsx("span", {}),
144
+ /* @__PURE__ */ jsx("h2", { className: "truncate text-center text-[15px] font-semibold text-foreground", children: source.title }),
145
+ source.onNew ? /* @__PURE__ */ jsx(
146
+ "button",
147
+ {
148
+ type: "button",
149
+ onClick: source.onNew,
150
+ "aria-label": source.newLabel ?? "New",
151
+ title: source.newLabel ?? "New",
152
+ className: "flex h-9 w-9 items-center justify-center rounded-full bg-primary text-primary-foreground hover:bg-primary/90",
153
+ children: /* @__PURE__ */ jsx(Plus, { className: "h-5 w-5", strokeWidth: 2.25 })
154
+ }
155
+ ) : /* @__PURE__ */ jsx("span", {})
156
+ ] }),
157
+ /* @__PURE__ */ jsx("div", { className: "shrink-0 px-3 pb-2", children: /* @__PURE__ */ jsxs("div", { className: "flex h-9 w-full min-w-0 items-center gap-2 rounded-full border border-border bg-background px-3 text-muted-foreground focus-within:border-ring", children: [
158
+ /* @__PURE__ */ jsx(Search, { className: "h-4 w-4 shrink-0" }),
159
+ /* @__PURE__ */ jsx(
160
+ "input",
161
+ {
162
+ type: "search",
163
+ value: query,
164
+ onChange: (event) => onQueryChange(event.target.value),
165
+ placeholder: "Search",
166
+ "aria-label": `Search ${source.title}`,
167
+ className: "min-w-0 flex-1 bg-transparent text-[16px] text-foreground outline-none placeholder:text-muted-foreground md:text-[14px] [&::-webkit-search-cancel-button]:hidden"
168
+ }
169
+ ),
170
+ query !== "" ? /* @__PURE__ */ jsx("button", { type: "button", "aria-label": "Clear search", onClick: () => onQueryChange(""), className: "shrink-0 hover:text-foreground", children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" }) }) : null
171
+ ] }) }),
172
+ source.filters && source.filters.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex shrink-0 gap-1.5 overflow-x-auto px-3 pb-2 [scrollbar-width:none]", role: "tablist", "aria-label": "Filters", children: [{ id: "", label: "All" }, ...source.filters].map((filter) => {
173
+ const on = filter.id === "" ? activeFilter === null : activeFilter?.id === filter.id;
174
+ return /* @__PURE__ */ jsx(
175
+ "button",
176
+ {
177
+ type: "button",
178
+ role: "tab",
179
+ "aria-selected": on,
180
+ onClick: () => onFilterChange(filter.id === "" || on ? null : filter.id),
181
+ className: cn(
182
+ "h-7 shrink-0 rounded-full px-3 text-[13px] transition-colors",
183
+ on ? "bg-primary/15 font-medium text-primary" : "bg-muted text-muted-foreground hover:text-foreground"
184
+ ),
185
+ children: filter.label
186
+ },
187
+ filter.id
188
+ );
189
+ }) }) : null,
190
+ /* @__PURE__ */ jsx("div", { className: "min-h-0 w-full min-w-0 flex-1 overflow-y-auto overflow-x-hidden px-2 pb-2", "data-messenger-list": true, children: state.error ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-2 px-4 py-8 text-center text-[13px] text-muted-foreground", role: "alert", children: [
191
+ /* @__PURE__ */ jsx("span", { children: state.error }),
192
+ state.retry ? /* @__PURE__ */ jsx("button", { type: "button", onClick: state.retry, className: "font-medium text-primary hover:underline", children: "Try again" }) : null
193
+ ] }) : state.loading && contacts.length === 0 ? /* @__PURE__ */ jsx("div", { className: "flex flex-col", "aria-busy": "true", "aria-label": "Loading", children: Array.from({ length: 6 }, (_, i) => /* @__PURE__ */ jsxs("div", { className: "flex h-[68px] items-center gap-3 px-2", children: [
194
+ /* @__PURE__ */ jsx("span", { className: "h-12 w-12 shrink-0 animate-pulse rounded-full bg-muted" }),
195
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 flex-col gap-2", children: [
196
+ /* @__PURE__ */ jsx("span", { className: "h-3 w-1/2 animate-pulse rounded bg-muted" }),
197
+ /* @__PURE__ */ jsx("span", { className: "h-3 w-3/4 animate-pulse rounded bg-muted" })
198
+ ] })
199
+ ] }, i)) }) : contacts.length === 0 ? /* @__PURE__ */ jsx("p", { className: "px-4 py-8 text-center text-[13px] text-muted-foreground", children: query !== "" || activeFilter !== null ? "No matches" : state.emptyLabel ?? "Nothing here yet" }) : /* @__PURE__ */ jsxs("ul", { className: "flex w-full min-w-0 flex-col", children: [
200
+ contacts.map((contact) => /* @__PURE__ */ jsx("li", { className: "w-full min-w-0", children: /* @__PURE__ */ jsx(MessengerContactRow, { contact, active: source.activeContactId === contact.id, source }) }, contact.id)),
201
+ state.hasMore && state.loadMore ? /* @__PURE__ */ jsx("li", { className: "flex justify-center py-2", children: /* @__PURE__ */ jsx("button", { type: "button", onClick: state.loadMore, className: "text-[13px] font-medium text-primary hover:underline", children: "Load more" }) }) : null
202
+ ] }) })
203
+ ]
204
+ }
205
+ );
206
+ }
207
+ export {
208
+ MessengerContactList,
209
+ MessengerContactRow
210
+ };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The details panel's chrome — big avatar, name, a subtitle, and tabs — the
3
+ * look of Matrx Desktop's agent panel (Details / Library / Computer). A source
4
+ * returns this from `renderDetails` with its own tabs; the shell draws the
5
+ * panel frame and the close button around it.
6
+ */
7
+ import { type ReactNode } from "react";
8
+ import type { MessengerAvatar as AvatarSpec } from "./contract.js";
9
+ export interface MessengerDetailsTab {
10
+ readonly id: string;
11
+ readonly label: string;
12
+ readonly content: ReactNode;
13
+ }
14
+ export interface MessengerDetailsProps {
15
+ readonly name: string;
16
+ readonly avatar?: AvatarSpec;
17
+ readonly subtitle?: ReactNode;
18
+ readonly tabs: readonly MessengerDetailsTab[];
19
+ }
20
+ export declare function MessengerDetails({ name, avatar, subtitle, tabs }: MessengerDetailsProps): import("react").JSX.Element;
@@ -0,0 +1,35 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useState } from "react";
4
+ import { cn } from "@ai-matrx/design-system/cn";
5
+ import { MessengerAvatar } from "./MessengerAvatar.js";
6
+ function MessengerDetails({ name, avatar, subtitle, tabs }) {
7
+ const [tabId, setTabId] = useState(tabs[0]?.id ?? "");
8
+ const tab = tabs.find((t) => t.id === tabId) ?? tabs[0];
9
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [
10
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 flex-col items-center gap-1 px-4 pb-3 pt-6 text-center", children: [
11
+ /* @__PURE__ */ jsx(MessengerAvatar, { name, avatar, size: "details" }),
12
+ /* @__PURE__ */ jsx("h3", { className: "mt-3 max-w-full truncate text-[17px] font-semibold text-foreground", children: name }),
13
+ subtitle ? /* @__PURE__ */ jsx("div", { className: "max-w-full truncate text-[12px] text-muted-foreground", children: subtitle }) : null
14
+ ] }),
15
+ tabs.length > 1 ? /* @__PURE__ */ jsx("div", { className: "flex shrink-0 justify-center gap-1 px-4 pb-3", role: "tablist", children: tabs.map((t) => /* @__PURE__ */ jsx(
16
+ "button",
17
+ {
18
+ type: "button",
19
+ role: "tab",
20
+ "aria-selected": t.id === tab?.id,
21
+ onClick: () => setTabId(t.id),
22
+ className: cn(
23
+ "h-7 rounded-full px-2.5 text-[13px] transition-colors",
24
+ t.id === tab?.id ? "bg-muted font-medium text-foreground" : "text-muted-foreground hover:text-foreground"
25
+ ),
26
+ children: t.label
27
+ },
28
+ t.id
29
+ )) }) : null,
30
+ /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-3 pb-4", role: "tabpanel", children: tab?.content })
31
+ ] });
32
+ }
33
+ export {
34
+ MessengerDetails
35
+ };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The source's menu entries drawn with the design system's menus — the SAME
3
+ * entries as a right-click menu and as a "⋯" dropdown. Never a hand-rolled menu.
4
+ * Entries are read when the menu OPENS (Radix mounts content only then), so a
5
+ * source computes them about the clicked item at that moment.
6
+ */
7
+ import type { ReactNode } from "react";
8
+ import { type MessengerMenuEntry } from "./contract.js";
9
+ type EntriesFn = () => readonly MessengerMenuEntry[];
10
+ /** Right-click on `children` opens the entries. No entries function: children render alone. */
11
+ export declare function MessengerContextMenu({ entries, children }: {
12
+ entries?: EntriesFn;
13
+ children: ReactNode;
14
+ }): import("react").JSX.Element;
15
+ /** A trigger (the "⋯" button, a header title) that opens the same entries as a dropdown. */
16
+ export declare function MessengerDropdownMenu({ entries, trigger, align, }: {
17
+ entries: EntriesFn;
18
+ trigger: ReactNode;
19
+ align?: "start" | "end";
20
+ }): import("react").JSX.Element;
21
+ export {};
@@ -0,0 +1,58 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import {
4
+ ContextMenu,
5
+ ContextMenuContent,
6
+ ContextMenuItem,
7
+ ContextMenuSeparator,
8
+ ContextMenuTrigger,
9
+ DropdownMenu,
10
+ DropdownMenuContent,
11
+ DropdownMenuItem,
12
+ DropdownMenuSeparator,
13
+ DropdownMenuTrigger
14
+ } from "@ai-matrx/design-system";
15
+ import { isMenuSeparator } from "./contract.js";
16
+ function Entries({ entries, kind }) {
17
+ const Item = kind === "context" ? ContextMenuItem : DropdownMenuItem;
18
+ const Separator = kind === "context" ? ContextMenuSeparator : DropdownMenuSeparator;
19
+ return /* @__PURE__ */ jsx(Fragment, { children: entries.map((entry) => {
20
+ if (isMenuSeparator(entry)) return /* @__PURE__ */ jsx(Separator, {}, entry.id);
21
+ const Icon = entry.icon;
22
+ return /* @__PURE__ */ jsxs(
23
+ Item,
24
+ {
25
+ disabled: entry.disabled,
26
+ onSelect: () => entry.onSelect(),
27
+ className: entry.destructive ? "text-destructive focus:text-destructive" : void 0,
28
+ "data-menu-item": entry.id,
29
+ children: [
30
+ Icon ? /* @__PURE__ */ jsx(Icon, { className: "mr-2 h-4 w-4 shrink-0" }) : null,
31
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: entry.label })
32
+ ]
33
+ },
34
+ entry.id
35
+ );
36
+ }) });
37
+ }
38
+ function MessengerContextMenu({ entries, children }) {
39
+ if (entries === void 0) return /* @__PURE__ */ jsx(Fragment, { children });
40
+ return /* @__PURE__ */ jsxs(ContextMenu, { children: [
41
+ /* @__PURE__ */ jsx(ContextMenuTrigger, { asChild: true, children }),
42
+ /* @__PURE__ */ jsx(ContextMenuContent, { className: "min-w-48", children: /* @__PURE__ */ jsx(Entries, { entries: entries(), kind: "context" }) })
43
+ ] });
44
+ }
45
+ function MessengerDropdownMenu({
46
+ entries,
47
+ trigger,
48
+ align = "end"
49
+ }) {
50
+ return /* @__PURE__ */ jsxs(DropdownMenu, { modal: false, children: [
51
+ /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: trigger }),
52
+ /* @__PURE__ */ jsx(DropdownMenuContent, { align, className: "min-w-48", children: /* @__PURE__ */ jsx(Entries, { entries: entries(), kind: "dropdown" }) })
53
+ ] });
54
+ }
55
+ export {
56
+ MessengerContextMenu,
57
+ MessengerDropdownMenu
58
+ };
@@ -0,0 +1,11 @@
1
+ import { type MessengerSource } from "./contract.js";
2
+ /** The one localStorage key for the rail's state, shared by every messenger source. */
3
+ export declare const RAIL_COLLAPSED_KEY = "ai-matrx.messenger.rail-collapsed";
4
+ /** The rail's collapsed state, remembered in this browser (and kept in step across tabs). */
5
+ export declare function useRailCollapsed(): readonly [boolean, (next: boolean) => void];
6
+ export declare function MessengerRail({ source, className, forceCollapsed, }: {
7
+ source: MessengerSource;
8
+ className?: string;
9
+ /** Icon-only regardless of the remembered choice (a phone); the toggle hides. */
10
+ forceCollapsed?: boolean;
11
+ }): import("react").JSX.Element;