@ai-matrx/chat 0.7.25 → 0.7.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/action-requests/components/ActionRequestAnswerForm.d.ts +7 -0
  3. package/dist/action-requests/components/ActionRequestAnswerForm.js +3 -1
  4. package/dist/agents/components/inputs/smart-input/InputActionButtons.d.ts +2 -2
  5. package/dist/agents/components/inputs/smart-input/InputActionButtons.js +1 -3
  6. package/dist/agents/components/inputs/smart-input/SmartAgentInputStacked.js +3 -8
  7. package/dist/agents/components/inputs/smart-input/composer/ComposerSplash.js +2 -1
  8. package/dist/agents/components/messages-display/assistant/AssistantMessageFooter.js +2 -4
  9. package/dist/agents/components/shared/transcript-layout.d.ts +6 -7
  10. package/dist/agents/components/shared/transcript-layout.js +2 -2
  11. package/dist/agents/redux/execution-system/thunks/run-headless-agent-json.js +8 -0
  12. package/dist/host/admin-seat.js +2 -0
  13. package/dist/messenger/MessengerAvatar.d.ts +18 -0
  14. package/dist/messenger/MessengerAvatar.js +31 -0
  15. package/dist/messenger/MessengerContactList.d.ts +16 -0
  16. package/dist/messenger/MessengerContactList.js +210 -0
  17. package/dist/messenger/MessengerDetails.d.ts +20 -0
  18. package/dist/messenger/MessengerDetails.js +35 -0
  19. package/dist/messenger/MessengerMenu.d.ts +21 -0
  20. package/dist/messenger/MessengerMenu.js +58 -0
  21. package/dist/messenger/MessengerRail.d.ts +11 -0
  22. package/dist/messenger/MessengerRail.js +184 -0
  23. package/dist/messenger/MessengerShell.d.ts +6 -0
  24. package/dist/messenger/MessengerShell.js +115 -0
  25. package/dist/messenger/MessengerThreadHeader.d.ts +8 -0
  26. package/dist/messenger/MessengerThreadHeader.js +62 -0
  27. package/dist/messenger/contract.d.ts +149 -0
  28. package/dist/messenger/contract.js +28 -0
  29. package/dist/messenger/people/format-time.d.ts +6 -0
  30. package/dist/messenger/people/format-time.js +14 -0
  31. package/dist/messenger/people/usePeopleMessengerSource.d.ts +35 -0
  32. package/dist/messenger/people/usePeopleMessengerSource.js +213 -0
  33. package/package.json +37 -11
package/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # @ai-matrx/chat
2
2
 
3
+ ## 0.7.27
4
+ ### Unreleased
5
+
6
+ ### An answered question is followed to its answer (lane F16)
7
+
8
+ - **`runHeadlessAgentJson` / `adoptHeadlessAgentJson` / `continueAgentJson`: once the ask closes, the
9
+ wait re-reads the conversation every 15 s until the resumed turn's answer lands.** The resumed turn runs
10
+ on the server AFTER the answer is accepted (30-90 s for an Applet build); one follow taken before its
11
+ operation existed followed nothing, and the builder sat on "Waiting for your answer" for 7+ minutes over
12
+ a build the server had written. Guard: `a-run-parked-on-the-person-waits-for-their-answer.test.ts`.
13
+ Consumer action: none.
14
+ - **`useActionRequestAnswer`: a reply that never came back says so — never "Nothing was recorded".**
15
+ New `ACTION_REQUEST_UNCONFIRMED` for a thrown transport (a dropped connection, an edge timeout): it may
16
+ have arrived, and a second answer is refused by the claim, so trying again is safe. Guard:
17
+ `a-lost-reply-never-claims-nothing-was-recorded.test.tsx`. Consumer action: none.
18
+
19
+ ### The messenger shell
20
+
21
+ - **New domain `@ai-matrx/chat/messenger/*`**: ONE desktop-messenger layout for any conversation
22
+ source — `messenger/contract` (`MessengerSource`), `messenger/MessengerShell`,
23
+ `messenger/MessengerDetails`, `messenger/MessengerAvatar`. Rail of spaces with collapsible groups,
24
+ a profile and an expand/collapse toggle remembered per browser; a 320px contact list (search, an
25
+ "All" chip plus the source's chips, rows with avatar, time, preview, unread, pinned, muted and
26
+ draft); a thread header with a generic `titleMenu` dropdown; an optional details panel; and
27
+ right-click plus "⋯" menus on rows, rail items and the header whose items all come from the source.
28
+ Chrome only: the thread body is the source's renderer. The list can never scroll sideways.
29
+ - **`messenger/people/usePeopleMessengerSource`**: `@ai-matrx/messaging` conversations as a source;
30
+ the thread is messaging's `ConversationView` with `appearance="messenger"`.
31
+ - **`@ai-matrx/messaging` is now a dependency** (`workspace:*`), imported only by the people subpath,
32
+ so hosts that never import it load none of it.
33
+
3
34
  ## 0.7.25
4
35
 
5
36
  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
@@ -58,9 +58,9 @@ interface InputActionButtonsProps {
58
58
  * audio) live in the + menu; Output and Effort in the agent pill's menu.
59
59
  */
60
60
  folded?: boolean;
61
- /** Scope · values: after + · mic (· live audio) in Compact's row, after + in the card's own bar. */
61
+ /** Compact's row under the card: after + · mic (· live audio). */
62
62
  leading?: React.ReactNode;
63
- /** Agent · output · effort: the right end of Compact's row, or just left of the mic in the card's own bar. */
63
+ /** Compact's row under the card: the right end (agent · output · effort). */
64
64
  trailing?: React.ReactNode;
65
65
  /**
66
66
  * Which part this instance draws. Compact mounts two: `"send"` (↵, inside
@@ -202,11 +202,9 @@ function InputActionButtons({
202
202
  /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-1.5", children: [
203
203
  plusMenu,
204
204
  database ? /* @__PURE__ */ jsx(DesktopPresenceIndicator, { conversationId }) : null,
205
- variablesToggle,
206
- composer.leading
205
+ variablesToggle
207
206
  ] }),
208
207
  /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-1.5", children: [
209
- composer.trailing,
210
208
  extraRightControls,
211
209
  micGroup,
212
210
  liveAudioButton,
@@ -20,7 +20,7 @@ import { Button } from "@ai-matrx/design-system";
20
20
  import { cn } from "@ai-matrx/design-system";
21
21
  import { composerShows } from "./composer/composer-mode-visibility.js";
22
22
  import { ComposerChipsRow } from "./composer/ComposerChipsRow.js";
23
- import { ComposerPills, ComposerScopeCluster, ComposerValueGroupChip } from "./composer/ComposerMetaRow.js";
23
+ import { ComposerMetaRow, ComposerPills, ComposerScopeCluster, ComposerValueGroupChip } from "./composer/ComposerMetaRow.js";
24
24
  import { useComposerFold } from "./composer/useComposerFold.js";
25
25
  import { useTouchOnlyDevice } from "../../../../ui/composer/useTouchOnlyDevice.js";
26
26
  function SmartAgentInputStacked({
@@ -323,12 +323,7 @@ function SmartAgentInputStacked({
323
323
  onVoiceBusyChange: setVoiceBusy,
324
324
  extraRightControls,
325
325
  onRequestInputExpand: () => setExpandRequestKey((key) => key + 1),
326
- composer: {
327
- ...composerParts,
328
- // The pills live in the card's own bar: no strip under the card (rule: existing bars).
329
- leading: composer.meta === "none" ? void 0 : /* @__PURE__ */ jsx(ComposerScopeCluster, { conversationId, composer, folded }),
330
- trailing: composer.meta === "none" ? void 0 : /* @__PURE__ */ jsx(ComposerPills, { conversationId, composer, menuSide, folded })
331
- }
326
+ composer: composerParts
332
327
  }
333
328
  )
334
329
  ] })
@@ -349,7 +344,7 @@ function SmartAgentInputStacked({
349
344
  trailing: composer.meta === "none" ? void 0 : /* @__PURE__ */ jsx(ComposerPills, { conversationId, composer, menuSide, folded })
350
345
  }
351
346
  }
352
- ) : null
347
+ ) : composer.meta === "none" ? null : /* @__PURE__ */ jsx(ComposerMetaRow, { conversationId, composer, menuSide, folded })
353
348
  ]
354
349
  }
355
350
  );
@@ -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) {
@@ -165,7 +165,7 @@ function AssistantMessageFooter(props) {
165
165
  isLatestAssistant
166
166
  } = useAssistantMessageActions(props);
167
167
  const answerActions = useTranscriptLayout().answerActions;
168
- const isHoverOnly = answerActions === "hover" || answerActions === "latest" && !isLatestAssistant || density === "compact" && !isLatestAssistant;
168
+ const isHoverOnly = answerActions === "hover" || density === "compact" && !isLatestAssistant;
169
169
  const rowRef = useRef(null);
170
170
  useEffect(() => {
171
171
  const row = rowRef.current;
@@ -183,9 +183,7 @@ function AssistantMessageFooter(props) {
183
183
  {
184
184
  className: cn(
185
185
  "transition-opacity",
186
- isHoverOnly && // Hidden by opacity, never display: the row keeps its height, so revealing it shifts
187
- // nothing. A touch screen has no hover, so it is always visible there.
188
- "opacity-0 group-hover/assistant-msg:opacity-100 focus-within:opacity-100 [@media(hover:none)]:opacity-100"
186
+ isHoverOnly && "opacity-0 group-hover/assistant-msg:opacity-100 focus-within:opacity-100"
189
187
  ),
190
188
  children: [
191
189
  /* @__PURE__ */ jsxs(
@@ -1,20 +1,19 @@
1
1
  /**
2
2
  * How a host lays the ONE conversation column out (HOST-OWNED-SOURCE-PLAN P28e look bar).
3
3
  *
4
- * Knobs, never forks: a host that says nothing gets the default look (flat compact user turn,
5
- * actions on hover). Matrx 2 (desktop) fills its pane, starts at the left edge and draws the
4
+ * Knobs, never forks: every value has today's web default, and a host that says nothing gets
5
+ * exactly the web chat. Matrx 2 (desktop) fills its pane, starts at the left edge and draws the
6
6
  * person's turn as a compact block — the look its coding screen had before the column.
7
7
  *
8
8
  * - `width`: "reading" (the canonical `max-w-3xl` reading column) | "fill" (the host's pane).
9
9
  * - `align`: "center" | "start" — where a reading-width column sits in a wider pane.
10
- * - `userMessage`: "compact" (DEFAULT: a flat tight block; only a genuinely long message
11
- * collapses) | "card" (the bordered card that collapses a long message behind a chevron) |
10
+ * - `userMessage`: "card" (DEFAULT: the bordered card that collapses a long message behind a chevron) |
11
+ * "compact" (a flat tight block; only a genuinely long message collapses) |
12
12
  * "bubble" (Messages / Xcode's coding assistant: the person's turn on the trailing side in a
13
13
  * rounded bubble as wide as its words, answers full width; collapses like "compact").
14
- * - `answerActions`: "hover" (DEFAULT: the action row is revealed on hover / keyboard focus, and
14
+ * - `answerActions`: "shown" (DEFAULT: the latest answer's action row always visible) | "hover" (the action row is revealed on hover / keyboard focus, and
15
15
  * always on touch — its height is reserved, so revealing it moves nothing; a finished answer ends
16
- * on its last word) | "latest" (the latest answer's row is always visible, older ones on hover) |
17
- * "shown" (every answer's row always visible).
16
+ * on its last word) | "latest" (the latest answer's row is always visible, older ones on hover).
18
17
  */
19
18
  import { type ReactNode } from "react";
20
19
  export interface TranscriptLayout {
@@ -6,8 +6,8 @@ import {
6
6
  const DEFAULT_TRANSCRIPT_LAYOUT = {
7
7
  width: "reading",
8
8
  align: "center",
9
- userMessage: "compact",
10
- answerActions: "hover",
9
+ userMessage: "card",
10
+ answerActions: "shown",
11
11
  timestamps: "hover"
12
12
  };
13
13
  const TranscriptLayoutContext = createContext(DEFAULT_TRANSCRIPT_LAYOUT);
@@ -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("label", { 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 {};