@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.
- package/CHANGELOG.md +31 -0
- 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/InputActionButtons.d.ts +2 -2
- package/dist/agents/components/inputs/smart-input/InputActionButtons.js +1 -3
- package/dist/agents/components/inputs/smart-input/SmartAgentInputStacked.js +3 -8
- package/dist/agents/components/inputs/smart-input/composer/ComposerSplash.js +2 -1
- package/dist/agents/components/messages-display/assistant/AssistantMessageFooter.js +2 -4
- package/dist/agents/components/shared/transcript-layout.d.ts +6 -7
- package/dist/agents/components/shared/transcript-layout.js +2 -2
- 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,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:
|
|
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
|
-
/**
|
|
61
|
+
/** Compact's row under the card: after + · mic (· live audio). */
|
|
62
62
|
leading?: React.ReactNode;
|
|
63
|
-
/**
|
|
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" ||
|
|
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 &&
|
|
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:
|
|
5
|
-
*
|
|
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`: "
|
|
11
|
-
*
|
|
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`: "
|
|
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: "
|
|
10
|
-
answerActions: "
|
|
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);
|
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("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 {};
|