@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 +33 -2
- package/dist/action-requests/components/ActionRequestAnswerForm.d.ts +7 -0
- package/dist/action-requests/components/ActionRequestAnswerForm.js +3 -1
- package/dist/agents/components/inputs/smart-input/composer/ComposerSplash.js +2 -1
- package/dist/agents/redux/execution-system/thunks/run-headless-agent-json.js +8 -0
- package/dist/host/admin-seat.js +2 -0
- package/dist/messenger/MessengerAvatar.d.ts +18 -0
- package/dist/messenger/MessengerAvatar.js +31 -0
- package/dist/messenger/MessengerContactList.d.ts +16 -0
- package/dist/messenger/MessengerContactList.js +210 -0
- package/dist/messenger/MessengerDetails.d.ts +20 -0
- package/dist/messenger/MessengerDetails.js +35 -0
- package/dist/messenger/MessengerMenu.d.ts +21 -0
- package/dist/messenger/MessengerMenu.js +58 -0
- package/dist/messenger/MessengerRail.d.ts +11 -0
- package/dist/messenger/MessengerRail.js +184 -0
- package/dist/messenger/MessengerShell.d.ts +6 -0
- package/dist/messenger/MessengerShell.js +115 -0
- package/dist/messenger/MessengerThreadHeader.d.ts +8 -0
- package/dist/messenger/MessengerThreadHeader.js +62 -0
- package/dist/messenger/contract.d.ts +149 -0
- package/dist/messenger/contract.js +28 -0
- package/dist/messenger/people/format-time.d.ts +6 -0
- package/dist/messenger/people/format-time.js +14 -0
- package/dist/messenger/people/usePeopleMessengerSource.d.ts +35 -0
- package/dist/messenger/people/usePeopleMessengerSource.js +213 -0
- package/package.json +37 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,42 @@
|
|
|
1
1
|
# @ai-matrx/chat
|
|
2
2
|
|
|
3
|
-
## 0.7.
|
|
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.
|
|
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:
|
|
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);
|
package/dist/host/admin-seat.js
CHANGED
|
@@ -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;
|