@ai-matrx/chat 0.7.28 → 0.7.30
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 +23 -0
- package/dist/agents/components/chat/ChatRoomClient.d.ts +17 -1
- package/dist/agents/components/chat/ChatRoomClient.js +17 -8
- package/dist/agents/components/inputs/smart-input/useConversationDisplayRows.d.ts +8 -9
- package/dist/agents/components/inputs/smart-input/useConversationDisplayRows.js +80 -5
- package/dist/agents/redux/execution-system/utils/host-turn.d.ts +9 -0
- package/dist/agents/redux/execution-system/utils/host-turn.js +25 -0
- package/dist/context/sources/scopes.d.ts +1 -1
- package/dist/host/contract.d.ts +6 -0
- package/dist/messenger/MessengerMenu.js +11 -3
- package/dist/messenger/MessengerShell.js +4 -3
- package/dist/messenger/MessengerThreadHeader.js +1 -1
- package/dist/messenger/agents/AgentDetailsPanel.d.ts +20 -0
- package/dist/messenger/agents/AgentDetailsPanel.js +98 -0
- package/dist/messenger/agents/AgentsMessenger.d.ts +63 -0
- package/dist/messenger/agents/AgentsMessenger.js +612 -0
- package/dist/messenger/agents/MessengerManageAgents.d.ts +21 -0
- package/dist/messenger/agents/MessengerManageAgents.js +109 -0
- package/dist/messenger/agents/menus.d.ts +49 -0
- package/dist/messenger/agents/menus.js +89 -0
- package/dist/messenger/agents/roster-store.d.ts +16 -0
- package/dist/messenger/agents/roster-store.js +41 -0
- package/dist/messenger/agents/roster.d.ts +114 -0
- package/dist/messenger/agents/roster.js +137 -0
- package/dist/messenger/contract.d.ts +14 -1
- package/package.json +18 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# @ai-matrx/chat
|
|
2
2
|
|
|
3
|
+
## 0.7.30
|
|
4
|
+
|
|
5
|
+
Automatic changed-only republish (docs/metadata drift since the last tag — see
|
|
6
|
+
`git diff npm/chat/v0.7.29..npm/chat/v0.7.30 -- apps/shared/chat`).
|
|
7
|
+
No source changes intended and no consumer action required.
|
|
8
|
+
|
|
9
|
+
## 0.7.29
|
|
10
|
+
### the agents messenger
|
|
11
|
+
|
|
12
|
+
- **`messenger/agents/AgentsMessenger`: "agents you know" on the messenger shell.** A short roster —
|
|
13
|
+
the starter seats `MESSENGER_STARTER_MANDATE_KEYS` (messenger.answer_anything, chat.default_new_chat,
|
|
14
|
+
personal_staff.front_line, resolved through the mandate resolver) plus agents the person added, minus
|
|
15
|
+
seats they removed — and pinned threads as their own rows. Per-person state is
|
|
16
|
+
`platform.user_entity_state.metadata.messenger`, written only through `platform.ues_merge_metadata`.
|
|
17
|
+
The thread body is THE chat room (`ChatRoomClient`); the header title is the thread dropdown (current,
|
|
18
|
+
past, + New chat, Pin); "Manage agents" adds through THE agent picker. Pure rules in
|
|
19
|
+
`messenger/agents/roster`. Consumer action: mount it (matrx-frontend: `/messenger/agents`).
|
|
20
|
+
- **`ChatRoomClient`: `onOpenConversation` and `surfaceBasePath` props.** A host that keeps the open
|
|
21
|
+
conversation in its own state receives the persisted / fork / retry id instead of a `router.replace`.
|
|
22
|
+
Defaults unchanged. Consumer action: none.
|
|
23
|
+
- **Messenger contract:** menu items take `detail`, `checked`, `keepOpen`; a source may return
|
|
24
|
+
`renderMain` (a screen in the thread area); a contact with no title entries opens details.
|
|
25
|
+
|
|
3
26
|
## 0.7.28
|
|
4
27
|
|
|
5
28
|
Automatic changed-only republish (docs/metadata drift since the last tag — see
|
|
@@ -43,6 +43,22 @@ interface ChatRoomClientProps {
|
|
|
43
43
|
* mid-sentence in.
|
|
44
44
|
*/
|
|
45
45
|
buildConversationHref?: (conversationId: string) => string;
|
|
46
|
+
/**
|
|
47
|
+
* How this room OPENS a conversation it moved to — the fresh chat's id once
|
|
48
|
+
* it persists, and fork / retry / delete targets. Default:
|
|
49
|
+
* `router.replace(buildConversationHref(id))`, the route-owned rooms.
|
|
50
|
+
*
|
|
51
|
+
* A host that keeps the open conversation in its own state rather than in
|
|
52
|
+
* the URL (the messenger's agents source, where a thread is a row and a
|
|
53
|
+
* dropdown, not a path) passes this and owns the switch; the room never
|
|
54
|
+
* navigates for it.
|
|
55
|
+
*/
|
|
56
|
+
onOpenConversation?: (conversationId: string) => void;
|
|
57
|
+
/**
|
|
58
|
+
* The page path pattern this room registers for its surface (action bars
|
|
59
|
+
* read it to know they are on a page). Default `/chat/[conversationId]`.
|
|
60
|
+
*/
|
|
61
|
+
surfaceBasePath?: string;
|
|
46
62
|
/**
|
|
47
63
|
* THE MANDATE DOOR for this room. When the surface knows WHICH JOB it is
|
|
48
64
|
* (`/chat/new` is the `chat.default_new_chat` mandate), pass the key: the
|
|
@@ -116,5 +132,5 @@ interface ChatRoomClientProps {
|
|
|
116
132
|
* (`ready: false`) and we load the existing conversation. The prop is the
|
|
117
133
|
* single source of truth — no parallel state.
|
|
118
134
|
*/
|
|
119
|
-
export declare function ChatRoomClient({ agentId, audience, conversationId: conversationIdProp, landingContent, aboveInput, buildConversationHref, mandateKey, variablesPanelStyle, sandboxBinding, conversationMaterialization, sourceFeature, composer: composerOptions, }: ChatRoomClientProps): import("react").JSX.Element;
|
|
135
|
+
export declare function ChatRoomClient({ agentId, audience, conversationId: conversationIdProp, landingContent, aboveInput, buildConversationHref, onOpenConversation, surfaceBasePath, mandateKey, variablesPanelStyle, sandboxBinding, conversationMaterialization, sourceFeature, composer: composerOptions, }: ChatRoomClientProps): import("react").JSX.Element;
|
|
120
136
|
export {};
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { logFailure } from "../../../ui/expectedRefusal.js";
|
|
4
4
|
import { usePreparedResourceSeed } from "./usePreparedResourceSeed.js";
|
|
5
|
-
import { Suspense, useEffect, useRef, useState } from "react";
|
|
5
|
+
import { Suspense, useCallback, useEffect, useRef, useState } from "react";
|
|
6
6
|
import { shallowEqual } from "react-redux";
|
|
7
7
|
import { useRouter, useSearchParams } from "../../../host/navigation.js";
|
|
8
8
|
import { commitUrlParams } from "@ai-matrx/kit/url-state";
|
|
@@ -64,6 +64,7 @@ import { selectUserId, isNotAuthenticatedError } from "../../../host/identity.js
|
|
|
64
64
|
import { selectOrganizationId } from "../../../host/org.js";
|
|
65
65
|
import { selectAuthReady } from "../../../host/identity.js";
|
|
66
66
|
const defaultConversationHref = (conversationId) => `/chat/${conversationId}`;
|
|
67
|
+
const DEFAULT_SURFACE_BASE_PATH = "/chat/[conversationId]";
|
|
67
68
|
const SOURCE_FEATURE = "chat";
|
|
68
69
|
const CHAT_INITIAL_MESSAGE_LIMIT = 12;
|
|
69
70
|
function ChatRoomClient({
|
|
@@ -73,6 +74,8 @@ function ChatRoomClient({
|
|
|
73
74
|
landingContent,
|
|
74
75
|
aboveInput,
|
|
75
76
|
buildConversationHref = defaultConversationHref,
|
|
77
|
+
onOpenConversation,
|
|
78
|
+
surfaceBasePath = DEFAULT_SURFACE_BASE_PATH,
|
|
76
79
|
mandateKey,
|
|
77
80
|
variablesPanelStyle,
|
|
78
81
|
sandboxBinding = null,
|
|
@@ -100,13 +103,20 @@ function ChatRoomClient({
|
|
|
100
103
|
registerSurface({
|
|
101
104
|
surfaceKey,
|
|
102
105
|
kind: "page",
|
|
103
|
-
basePath:
|
|
106
|
+
basePath: surfaceBasePath
|
|
104
107
|
})
|
|
105
108
|
);
|
|
106
109
|
return () => {
|
|
107
110
|
dispatch(unregisterSurface(surfaceKey));
|
|
108
111
|
};
|
|
109
|
-
}, [dispatch, surfaceKey]);
|
|
112
|
+
}, [dispatch, surfaceKey, surfaceBasePath]);
|
|
113
|
+
const openConversation = useCallback(
|
|
114
|
+
(id) => {
|
|
115
|
+
if (onOpenConversation) onOpenConversation(id);
|
|
116
|
+
else router.replace(buildConversationHref(id));
|
|
117
|
+
},
|
|
118
|
+
[onOpenConversation, router, buildConversationHref]
|
|
119
|
+
);
|
|
110
120
|
const executionPayload = useAppSelector(
|
|
111
121
|
(state) => selectAgentExecutionPayload(state, agentId)
|
|
112
122
|
);
|
|
@@ -194,9 +204,9 @@ function ChatRoomClient({
|
|
|
194
204
|
const pendingNavigation = useAppSelector(selectPendingNavigation(surfaceKey));
|
|
195
205
|
useEffect(() => {
|
|
196
206
|
if (!pendingNavigation) return;
|
|
197
|
-
|
|
207
|
+
openConversation(pendingNavigation.conversationId);
|
|
198
208
|
dispatch(clearPendingNavigation({ surfaceKey }));
|
|
199
|
-
}, [pendingNavigation,
|
|
209
|
+
}, [pendingNavigation, dispatch, surfaceKey, openConversation]);
|
|
200
210
|
const draftInputEntryReady = useAppSelector(
|
|
201
211
|
(state) => liveConversationId ? selectUserInputEntryExists(liveConversationId)(state) : false
|
|
202
212
|
);
|
|
@@ -401,7 +411,7 @@ function ChatRoomClient({
|
|
|
401
411
|
const focusNow = store.getState().conversationFocus?.bySurface[surfaceKey]?.input ?? null;
|
|
402
412
|
if (focusNow !== target) return;
|
|
403
413
|
promotedRef.current = target;
|
|
404
|
-
|
|
414
|
+
openConversation(target);
|
|
405
415
|
})();
|
|
406
416
|
return () => {
|
|
407
417
|
ctrl.abort();
|
|
@@ -410,10 +420,9 @@ function ChatRoomClient({
|
|
|
410
420
|
}, [
|
|
411
421
|
readyToPromote,
|
|
412
422
|
liveConversationId,
|
|
413
|
-
router,
|
|
414
423
|
store,
|
|
415
424
|
surfaceKey,
|
|
416
|
-
|
|
425
|
+
openConversation
|
|
417
426
|
]);
|
|
418
427
|
const conversationId = conversationIdProp ?? liveConversationId ?? null;
|
|
419
428
|
const canRenderLandingDuringInit = !!landingContent && !conversationIdProp && !!conversationId;
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
+
import { type ResolvedContextRow } from "@ai-matrx/agents/context";
|
|
2
|
+
import type { ChatRootState } from "../../../../store/root-state.js";
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* platform.associations edges `AttachedDocumentChips` lists, and the connector
|
|
6
|
-
* resources (repos, Google files, synced records) the attachments handle has
|
|
7
|
-
* read (`selectConversationAttachmentsEntry`, no second fetch). An attachment
|
|
8
|
-
* detached since the last receipt drops out at once; one attached since then
|
|
9
|
-
* shows at once, its size unknown until the next receipt.
|
|
4
|
+
* What the host's context sources (the page the person is on, for a browser host) would add to
|
|
5
|
+
* the next turn, read now and again whenever a source says it changed, the window regains focus,
|
|
6
|
+
* or the conversation changes. A host without sources reads `{}` once and never again.
|
|
10
7
|
*/
|
|
11
|
-
|
|
8
|
+
export declare function useHostContextPreview(conversationId: string): Record<string, unknown>;
|
|
9
|
+
/** The host's would-be context as rows, each under the person's saved rule; keys already shown stay as they are. */
|
|
10
|
+
export declare function hostContextRows(state: ChatRootState, conversationId: string, shown: readonly ResolvedContextRow[], hostContext: Readonly<Record<string, unknown>>): ResolvedContextRow[];
|
|
12
11
|
export declare function useConversationDisplayRows(conversationId: string, mandateKillSwitch: boolean): ResolvedContextRow[];
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_SURFACE_KEY,
|
|
5
|
+
resolveContextRow
|
|
6
|
+
} from "@ai-matrx/agents/context";
|
|
7
|
+
import { humanizeIdentifier } from "@ai-matrx/kit/text-case";
|
|
2
8
|
import { useAppSelector, useAppStore } from "../../../../store/hooks.js";
|
|
3
9
|
import { useConversationMaterialized } from "../../../hooks/useConversationMaterialized.js";
|
|
4
10
|
import { useContainerLinks } from "@ai-matrx/associations/react";
|
|
5
11
|
import { attachmentContextKey } from "@ai-matrx/agents/context";
|
|
6
12
|
import {
|
|
13
|
+
previewHostTurnContext,
|
|
14
|
+
subscribeHostContextChanges
|
|
15
|
+
} from "../../../redux/execution-system/utils/host-turn.js";
|
|
16
|
+
import { resolveClientSurface } from "../../../redux/execution-system/utils/build-tool-injection.js";
|
|
17
|
+
import { selectSavedContextRuleRows } from "../../../redux/execution-system/context-rules/context-rules.thunks.js";
|
|
18
|
+
import {
|
|
19
|
+
selectContextInlineCap,
|
|
7
20
|
DOCUMENT_ATTACHMENT_PREFIXES,
|
|
8
21
|
RESOURCE_ATTACHMENT_PREFIXES,
|
|
9
22
|
durableAttachmentKey,
|
|
@@ -18,6 +31,51 @@ import {
|
|
|
18
31
|
useAttachedDocumentFileNames
|
|
19
32
|
} from "../resources/attached-documents.js";
|
|
20
33
|
import { selectOrganizationId } from "../../../../host/org.js";
|
|
34
|
+
function useHostContextPreview(conversationId) {
|
|
35
|
+
const store = useAppStore();
|
|
36
|
+
const [preview, setPreview] = useState({});
|
|
37
|
+
const latest = useRef(0);
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
let live = true;
|
|
40
|
+
const read = () => {
|
|
41
|
+
const ticket = ++latest.current;
|
|
42
|
+
void previewHostTurnContext(store.getState(), conversationId).then(
|
|
43
|
+
(context) => {
|
|
44
|
+
if (live && ticket === latest.current) setPreview(context);
|
|
45
|
+
},
|
|
46
|
+
() => void 0
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
read();
|
|
50
|
+
const off = subscribeHostContextChanges(read);
|
|
51
|
+
const onFocus = () => read();
|
|
52
|
+
window.addEventListener("focus", onFocus);
|
|
53
|
+
return () => {
|
|
54
|
+
live = false;
|
|
55
|
+
off();
|
|
56
|
+
window.removeEventListener("focus", onFocus);
|
|
57
|
+
};
|
|
58
|
+
}, [store, conversationId]);
|
|
59
|
+
return preview;
|
|
60
|
+
}
|
|
61
|
+
function hostContextRows(state, conversationId, shown, hostContext) {
|
|
62
|
+
const keys = Object.keys(hostContext);
|
|
63
|
+
if (keys.length === 0) return [];
|
|
64
|
+
const have = new Set(shown.map((row) => row.key));
|
|
65
|
+
const saved = selectSavedContextRuleRows(state);
|
|
66
|
+
const cap = selectContextInlineCap(state, conversationId);
|
|
67
|
+
const primarySurface = resolveClientSurface(state, conversationId) ?? null;
|
|
68
|
+
return keys.filter((key) => !have.has(key) && hostContext[key] !== void 0).map((key) => {
|
|
69
|
+
const source = {
|
|
70
|
+
key,
|
|
71
|
+
label: humanizeIdentifier(key) || key,
|
|
72
|
+
surfaceKey: DEFAULT_SURFACE_KEY,
|
|
73
|
+
origin: "page",
|
|
74
|
+
value: hostContext[key]
|
|
75
|
+
};
|
|
76
|
+
return resolveContextRow(source, saved, cap, primarySurface);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
21
79
|
function useConversationDisplayRows(conversationId, mandateKillSwitch) {
|
|
22
80
|
const store = useAppStore();
|
|
23
81
|
const rows = useAppSelector(selectDisplayContextRows(conversationId, mandateKillSwitch));
|
|
@@ -55,11 +113,28 @@ function useConversationDisplayRows(conversationId, mandateKillSwitch) {
|
|
|
55
113
|
key: attachmentContextKey(row.provider, row.resource_ref),
|
|
56
114
|
label: row.display_name
|
|
57
115
|
})) : null;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
116
|
+
const reconciled = reconcileDurableAttachments(
|
|
117
|
+
store.getState(),
|
|
118
|
+
conversationId,
|
|
119
|
+
rows,
|
|
120
|
+
[
|
|
121
|
+
{ prefixes: DOCUMENT_ATTACHMENT_PREFIXES, items: documents },
|
|
122
|
+
{ prefixes: RESOURCE_ATTACHMENT_PREFIXES, items: resources }
|
|
123
|
+
]
|
|
124
|
+
);
|
|
125
|
+
const hostContext = useHostContextPreview(conversationId);
|
|
126
|
+
return useMemo(() => {
|
|
127
|
+
const extra = hostContextRows(
|
|
128
|
+
store.getState(),
|
|
129
|
+
conversationId,
|
|
130
|
+
reconciled,
|
|
131
|
+
hostContext
|
|
132
|
+
);
|
|
133
|
+
return extra.length === 0 ? reconciled : [...reconciled, ...extra];
|
|
134
|
+
}, [store, conversationId, reconciled, hostContext]);
|
|
62
135
|
}
|
|
63
136
|
export {
|
|
64
|
-
|
|
137
|
+
hostContextRows,
|
|
138
|
+
useConversationDisplayRows,
|
|
139
|
+
useHostContextPreview
|
|
65
140
|
};
|
|
@@ -37,6 +37,15 @@ export declare function resetLatchedTurns(): void;
|
|
|
37
37
|
* keeps what the start latched (a resume whose start predates this page load latches then).
|
|
38
38
|
*/
|
|
39
39
|
export declare function collectHostTurnContribution(state: ChatRootState, conversationId: string, phase: ChatHostTurnPhase): Promise<ChatHostTurnContribution | null>;
|
|
40
|
+
/**
|
|
41
|
+
* What the host's context sources would add to the NEXT turn's `context`, read for display (the
|
|
42
|
+
* composer's context chip). Same sources, same bounded wait, same merge as a real send, but
|
|
43
|
+
* nothing is latched. A source that is slow or throws contributes no rows here (the skip is
|
|
44
|
+
* announced through diagnostics like any other).
|
|
45
|
+
*/
|
|
46
|
+
export declare function previewHostTurnContext(state: ChatRootState, conversationId: string): Promise<Record<string, unknown>>;
|
|
47
|
+
/** Subscribe to every context source's "my answer may have changed" signal; returns one unsubscribe. */
|
|
48
|
+
export declare function subscribeHostContextChanges(onChange: () => void): () => void;
|
|
40
49
|
/**
|
|
41
50
|
* Merge the host's context entries under what the request already carries: a key the person
|
|
42
51
|
* or the agent set always wins. Returns the original object when the host adds nothing.
|
|
@@ -93,6 +93,29 @@ async function collectHostTurnContribution(state, conversationId, phase) {
|
|
|
93
93
|
}
|
|
94
94
|
return contribution;
|
|
95
95
|
}
|
|
96
|
+
async function previewHostTurnContext(state, conversationId) {
|
|
97
|
+
if (!isChatHostConfigured()) return {};
|
|
98
|
+
const sources = getChatHost().registry.contextSources ?? [];
|
|
99
|
+
if (sources.length === 0) return {};
|
|
100
|
+
const input = {
|
|
101
|
+
conversationId,
|
|
102
|
+
agentId: agentIdOf(state, conversationId),
|
|
103
|
+
phase: "start"
|
|
104
|
+
};
|
|
105
|
+
const merged = mergeContributions(
|
|
106
|
+
await Promise.all(sources.map((source) => askSource(source, input)))
|
|
107
|
+
);
|
|
108
|
+
return merged?.context ?? {};
|
|
109
|
+
}
|
|
110
|
+
function subscribeHostContextChanges(onChange) {
|
|
111
|
+
if (!isChatHostConfigured()) return () => void 0;
|
|
112
|
+
const offs = (getChatHost().registry.contextSources ?? []).flatMap(
|
|
113
|
+
(source) => source.subscribe ? [source.subscribe(onChange)] : []
|
|
114
|
+
);
|
|
115
|
+
return () => {
|
|
116
|
+
for (const off of offs) off();
|
|
117
|
+
};
|
|
118
|
+
}
|
|
96
119
|
function mergeHostTurnContext(existing, hostContext) {
|
|
97
120
|
if (!hostContext) return existing;
|
|
98
121
|
const keys = Object.keys(hostContext);
|
|
@@ -113,7 +136,9 @@ export {
|
|
|
113
136
|
devicePermissionModeKey,
|
|
114
137
|
getLatchedTurn,
|
|
115
138
|
mergeHostTurnContext,
|
|
139
|
+
previewHostTurnContext,
|
|
116
140
|
readDevicePermissionMode,
|
|
117
141
|
resetLatchedTurns,
|
|
142
|
+
subscribeHostContextChanges,
|
|
118
143
|
writeDevicePermissionMode
|
|
119
144
|
};
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Scopes (user-authored dimensions: organization, project, task, scope types and their
|
|
5
5
|
* values, the active scope selection) are a platform primitive the chat package READS to
|
|
6
|
-
* build what an agent receives. Until
|
|
6
|
+
* build what an agent receives. Until the data/records packages own scope reading (scopes package deleted 2026-10-09), the host hands
|
|
7
7
|
* them over through ONE registration, `registerChatScopes({...})`, keyed by the export names
|
|
8
8
|
* that package will publish — so P17s3 swaps this module's import specifier for
|
|
9
9
|
* `@ai-matrx/scopes` and deletes this file, with no call-site change.
|
package/dist/host/contract.d.ts
CHANGED
|
@@ -873,6 +873,12 @@ export interface ChatContextSource {
|
|
|
873
873
|
/** Stable id, for diagnostics. */
|
|
874
874
|
id: string;
|
|
875
875
|
contribute(input: ChatHostTurnInput): Promise<ChatHostTurnContribution | null>;
|
|
876
|
+
/**
|
|
877
|
+
* Tell the package this source's answer may have changed (the page navigated, the person
|
|
878
|
+
* switched tabs), so the composer's context chip re-reads it. Returns the unsubscribe. Absent:
|
|
879
|
+
* the chip reads the source when the conversation opens and when the window regains focus.
|
|
880
|
+
*/
|
|
881
|
+
subscribe?(onChange: () => void): () => void;
|
|
876
882
|
}
|
|
877
883
|
export interface ChatRoutingPort {
|
|
878
884
|
policy(conversationId: string): ChatRoutingPolicy;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Check } from "lucide-react";
|
|
3
4
|
import {
|
|
4
5
|
ContextMenu,
|
|
5
6
|
ContextMenuContent,
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
DropdownMenuSeparator,
|
|
13
14
|
DropdownMenuTrigger
|
|
14
15
|
} from "@ai-matrx/design-system";
|
|
16
|
+
import { cn } from "@ai-matrx/design-system/cn";
|
|
15
17
|
import { isMenuSeparator } from "./contract.js";
|
|
16
18
|
function Entries({ entries, kind }) {
|
|
17
19
|
const Item = kind === "context" ? ContextMenuItem : DropdownMenuItem;
|
|
@@ -23,12 +25,18 @@ function Entries({ entries, kind }) {
|
|
|
23
25
|
Item,
|
|
24
26
|
{
|
|
25
27
|
disabled: entry.disabled,
|
|
26
|
-
onSelect: () =>
|
|
28
|
+
onSelect: (event) => {
|
|
29
|
+
if (entry.keepOpen) event.preventDefault();
|
|
30
|
+
entry.onSelect();
|
|
31
|
+
},
|
|
27
32
|
className: entry.destructive ? "text-destructive focus:text-destructive" : void 0,
|
|
28
33
|
"data-menu-item": entry.id,
|
|
34
|
+
"data-checked": entry.checked ? "" : void 0,
|
|
29
35
|
children: [
|
|
30
36
|
Icon ? /* @__PURE__ */ jsx(Icon, { className: "mr-2 h-4 w-4 shrink-0" }) : null,
|
|
31
|
-
/* @__PURE__ */ jsx("span", { className: "truncate", children: entry.label })
|
|
37
|
+
/* @__PURE__ */ jsx("span", { className: cn("min-w-0 flex-1 truncate", entry.checked && "font-medium"), children: entry.label }),
|
|
38
|
+
entry.detail ? /* @__PURE__ */ jsx("span", { className: "ml-3 shrink-0 text-[12px] text-muted-foreground", children: entry.detail }) : null,
|
|
39
|
+
entry.checked ? /* @__PURE__ */ jsx(Check, { className: "ml-2 h-4 w-4 shrink-0 text-primary", "aria-label": "Current" }) : null
|
|
32
40
|
]
|
|
33
41
|
},
|
|
34
42
|
entry.id
|
|
@@ -49,7 +57,7 @@ function MessengerDropdownMenu({
|
|
|
49
57
|
}) {
|
|
50
58
|
return /* @__PURE__ */ jsxs(DropdownMenu, { modal: false, children: [
|
|
51
59
|
/* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: trigger }),
|
|
52
|
-
/* @__PURE__ */ jsx(DropdownMenuContent, { align, className: "min-w-48", children: /* @__PURE__ */ jsx(Entries, { entries: entries(), kind: "dropdown" }) })
|
|
60
|
+
/* @__PURE__ */ jsx(DropdownMenuContent, { align, className: "max-h-[min(70vh,32rem)] min-w-48 max-w-[22rem] overflow-y-auto", children: /* @__PURE__ */ jsx(Entries, { entries: entries(), kind: "dropdown" }) })
|
|
53
61
|
] });
|
|
54
62
|
}
|
|
55
63
|
export {
|
|
@@ -28,7 +28,8 @@ function MessengerShell({ source, className }) {
|
|
|
28
28
|
const filter = source.filters?.find((f) => f.id === filterId) ?? null;
|
|
29
29
|
const contacts = useMemo(() => visibleContacts(source.contacts, query, filter), [source.contacts, query, filter]);
|
|
30
30
|
const active = source.contacts.find((c) => c.id === source.activeContactId) ?? null;
|
|
31
|
-
const
|
|
31
|
+
const main = source.renderMain?.() ?? null;
|
|
32
|
+
const showThread = main !== null || pane === "thread" && active !== null;
|
|
32
33
|
const shellSource = useMemo(
|
|
33
34
|
() => ({
|
|
34
35
|
...source,
|
|
@@ -69,7 +70,7 @@ function MessengerShell({ source, className }) {
|
|
|
69
70
|
)
|
|
70
71
|
}
|
|
71
72
|
),
|
|
72
|
-
/* @__PURE__ */ jsx("main", { className: cn("flex min-h-0 min-w-0 flex-1 flex-col", !showThread && "max-md:hidden"), "aria-label": "Conversation", children: active ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
73
|
+
/* @__PURE__ */ jsx("main", { className: cn("flex min-h-0 min-w-0 flex-1 flex-col", !showThread && "max-md:hidden"), "aria-label": "Conversation", children: main !== null ? /* @__PURE__ */ jsx("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col", "data-messenger-main": true, children: main }) : active ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
73
74
|
/* @__PURE__ */ jsx(
|
|
74
75
|
MessengerThreadHeader,
|
|
75
76
|
{
|
|
@@ -85,7 +86,7 @@ function MessengerShell({ source, className }) {
|
|
|
85
86
|
/* @__PURE__ */ jsx(MessageCircle, { className: "h-10 w-10", strokeWidth: 1.25 }),
|
|
86
87
|
/* @__PURE__ */ jsx("span", { className: "text-[14px]", children: "Pick a conversation" })
|
|
87
88
|
] }) }),
|
|
88
|
-
active && renderDetails && detailsOpen ? /* @__PURE__ */ jsxs(
|
|
89
|
+
main === null && active && renderDetails && detailsOpen ? /* @__PURE__ */ jsxs(
|
|
89
90
|
"aside",
|
|
90
91
|
{
|
|
91
92
|
"aria-label": "Details",
|
|
@@ -14,7 +14,7 @@ function MessengerThreadHeader({
|
|
|
14
14
|
}) {
|
|
15
15
|
const info = source.header(contact);
|
|
16
16
|
const headerMenu = source.headerMenu;
|
|
17
|
-
const titleMenu = source.titleMenu;
|
|
17
|
+
const titleMenu = source.titleMenu && source.titleMenu(contact).length > 0 ? source.titleMenu : void 0;
|
|
18
18
|
const titleBlock = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
19
19
|
/* @__PURE__ */ jsxs("span", { className: "flex min-w-0 items-center gap-1", children: [
|
|
20
20
|
/* @__PURE__ */ jsx("span", { className: "truncate text-[15px] font-semibold text-foreground", "data-header-title": true, children: info.title }),
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { MessengerAvatar } from "../contract.js";
|
|
2
|
+
import { type ThreadSummary } from "./roster.js";
|
|
3
|
+
export interface AgentDetailsPanelProps {
|
|
4
|
+
readonly agentId: string;
|
|
5
|
+
readonly name: string;
|
|
6
|
+
readonly description: string | null;
|
|
7
|
+
readonly modelId: string | null;
|
|
8
|
+
readonly avatar?: MessengerAvatar;
|
|
9
|
+
/** What seat this agent fills ("Starter"), shown under the name. */
|
|
10
|
+
readonly seatLabel: string | null;
|
|
11
|
+
readonly threads: readonly ThreadSummary[];
|
|
12
|
+
readonly currentConversationId: string | null;
|
|
13
|
+
readonly pinnedIds: ReadonlySet<string>;
|
|
14
|
+
readonly hasMore: boolean;
|
|
15
|
+
readonly onLoadMore: () => void;
|
|
16
|
+
readonly onOpenThread: (conversationId: string) => void;
|
|
17
|
+
readonly onOpenAgent: () => void;
|
|
18
|
+
readonly onEditAgent: () => void;
|
|
19
|
+
}
|
|
20
|
+
export declare function AgentDetailsPanel(props: AgentDetailsPanelProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Brain, ExternalLink, History, MessageSquareText, Pencil, Pin, Settings2 } from "lucide-react";
|
|
4
|
+
import { MessengerDetails } from "../MessengerDetails.js";
|
|
5
|
+
import { useModelLabel } from "../../agents/identity/model-catalog.js";
|
|
6
|
+
import {
|
|
7
|
+
useHasChatWindowOpener,
|
|
8
|
+
useOpenAgentMemoryWindow,
|
|
9
|
+
useOpenAgentRunHistoryWindow,
|
|
10
|
+
useOpenAgentSettingsWindow
|
|
11
|
+
} from "../../host/window-openers.js";
|
|
12
|
+
import { relativeDate, threadTitle } from "./roster.js";
|
|
13
|
+
function DoorRow({ icon: Icon, label, onClick }) {
|
|
14
|
+
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
|
|
15
|
+
"button",
|
|
16
|
+
{
|
|
17
|
+
type: "button",
|
|
18
|
+
onClick,
|
|
19
|
+
className: "flex w-full min-w-0 items-center gap-3 rounded-lg px-2 py-2 text-left text-[14px] text-foreground hover:bg-accent",
|
|
20
|
+
children: [
|
|
21
|
+
/* @__PURE__ */ jsx(Icon, { className: "h-4 w-4 shrink-0 text-muted-foreground" }),
|
|
22
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: label })
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
) });
|
|
26
|
+
}
|
|
27
|
+
function AgentDetailsPanel(props) {
|
|
28
|
+
const modelLabel = useModelLabel(props.modelId);
|
|
29
|
+
const hasSettings = useHasChatWindowOpener("openAgentSettingsWindow");
|
|
30
|
+
const hasMemory = useHasChatWindowOpener("openAgentMemoryWindow");
|
|
31
|
+
const hasHistory = useHasChatWindowOpener("openAgentRunHistoryWindow");
|
|
32
|
+
const openSettings = useOpenAgentSettingsWindow();
|
|
33
|
+
const openMemory = useOpenAgentMemoryWindow();
|
|
34
|
+
const openHistory = useOpenAgentRunHistoryWindow();
|
|
35
|
+
const details = /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4", children: [
|
|
36
|
+
/* @__PURE__ */ jsxs("dl", { className: "flex flex-col gap-3 px-2 pt-1 text-[13px]", children: [
|
|
37
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-0.5", children: [
|
|
38
|
+
/* @__PURE__ */ jsx("dt", { className: "text-[12px] text-muted-foreground", children: "Model" }),
|
|
39
|
+
/* @__PURE__ */ jsx("dd", { className: "truncate text-foreground", children: modelLabel ?? (props.modelId ? "Loading\u2026" : "Default") })
|
|
40
|
+
] }),
|
|
41
|
+
props.description ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-0.5", children: [
|
|
42
|
+
/* @__PURE__ */ jsx("dt", { className: "text-[12px] text-muted-foreground", children: "About" }),
|
|
43
|
+
/* @__PURE__ */ jsx("dd", { className: "line-clamp-4 text-foreground", children: props.description })
|
|
44
|
+
] }) : null
|
|
45
|
+
] }),
|
|
46
|
+
/* @__PURE__ */ jsxs("ul", { className: "flex flex-col", children: [
|
|
47
|
+
/* @__PURE__ */ jsx(DoorRow, { icon: ExternalLink, label: "Open agent", onClick: props.onOpenAgent }),
|
|
48
|
+
/* @__PURE__ */ jsx(DoorRow, { icon: Pencil, label: "Edit agent", onClick: props.onEditAgent }),
|
|
49
|
+
hasSettings ? /* @__PURE__ */ jsx(DoorRow, { icon: Settings2, label: "Settings", onClick: () => openSettings({ initialAgentId: props.agentId }) }) : null,
|
|
50
|
+
hasMemory ? /* @__PURE__ */ jsx(DoorRow, { icon: Brain, label: "Memory", onClick: () => openMemory() }) : null,
|
|
51
|
+
hasHistory ? /* @__PURE__ */ jsx(DoorRow, { icon: History, label: "Run history", onClick: () => openHistory({ agentId: props.agentId }) }) : null
|
|
52
|
+
] })
|
|
53
|
+
] });
|
|
54
|
+
const threads = /* @__PURE__ */ jsxs("ul", { className: "flex flex-col", children: [
|
|
55
|
+
props.threads.length === 0 ? /* @__PURE__ */ jsx("li", { className: "px-2 py-3 text-[13px] text-muted-foreground", children: "No chats yet" }) : null,
|
|
56
|
+
props.threads.map((thread) => {
|
|
57
|
+
const current = thread.conversationId === props.currentConversationId;
|
|
58
|
+
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
|
|
59
|
+
"button",
|
|
60
|
+
{
|
|
61
|
+
type: "button",
|
|
62
|
+
onClick: () => props.onOpenThread(thread.conversationId),
|
|
63
|
+
"aria-current": current ? "true" : void 0,
|
|
64
|
+
className: "flex w-full min-w-0 items-center gap-3 rounded-lg px-2 py-2 text-left hover:bg-accent " + (current ? "bg-accent/70" : ""),
|
|
65
|
+
children: [
|
|
66
|
+
props.pinnedIds.has(thread.conversationId) ? /* @__PURE__ */ jsx(Pin, { className: "h-4 w-4 shrink-0 rotate-45 text-muted-foreground", "aria-label": "Pinned" }) : /* @__PURE__ */ jsx(MessageSquareText, { className: "h-4 w-4 shrink-0 text-muted-foreground" }),
|
|
67
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-[14px] text-foreground", children: threadTitle(thread) }),
|
|
68
|
+
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-[12px] text-muted-foreground", children: relativeDate(thread.updatedAt) })
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
) }, thread.conversationId);
|
|
72
|
+
}),
|
|
73
|
+
props.hasMore ? /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
|
|
74
|
+
"button",
|
|
75
|
+
{
|
|
76
|
+
type: "button",
|
|
77
|
+
onClick: props.onLoadMore,
|
|
78
|
+
className: "w-full rounded-lg px-2 py-2 text-left text-[13px] text-muted-foreground hover:bg-accent hover:text-foreground",
|
|
79
|
+
children: "Show more"
|
|
80
|
+
}
|
|
81
|
+
) }) : null
|
|
82
|
+
] });
|
|
83
|
+
return /* @__PURE__ */ jsx(
|
|
84
|
+
MessengerDetails,
|
|
85
|
+
{
|
|
86
|
+
name: props.name,
|
|
87
|
+
avatar: props.avatar,
|
|
88
|
+
subtitle: props.seatLabel ?? void 0,
|
|
89
|
+
tabs: [
|
|
90
|
+
{ id: "details", label: "Details", content: details },
|
|
91
|
+
{ id: "threads", label: "Threads", content: threads }
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
export {
|
|
97
|
+
AgentDetailsPanel
|
|
98
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE AGENTS SOURCE — "agents you know": a short, curated roster of agents
|
|
3
|
+
* (the starter seats + agents the person added − seats they removed) and the
|
|
4
|
+
* threads the person pinned, as a MessengerSource on the shared shell.
|
|
5
|
+
*
|
|
6
|
+
* - Roster: `MESSENGER_STARTER_MANDATE_KEYS` resolved through the ONE mandate
|
|
7
|
+
* resolver (`useMandateSet`), merged with the person's
|
|
8
|
+
* `platform.user_entity_state.metadata.messenger` rows (./roster.ts).
|
|
9
|
+
* - Listing: the chat package's own conversation-history reads
|
|
10
|
+
* (`fetchConversationHistory`, one scope per roster agent) — no parallel query layer.
|
|
11
|
+
* - Thread body: THE chat room, `ChatRoomClient` (streaming, tools, composer,
|
|
12
|
+
* renderer). The room never navigates here: it hands the persisted id back
|
|
13
|
+
* through `onOpenConversation`. A seat with ONE fixed thread (the Chief of
|
|
14
|
+
* Staff) mounts the host's own door for it via `renderSeatRoom`.
|
|
15
|
+
* - Header title: the thread dropdown (current, past, + New chat, Pin).
|
|
16
|
+
*
|
|
17
|
+
* `<AgentsMessenger>` is the shell + this source + the rename dialog.
|
|
18
|
+
* Doc: ../FEATURE.md.
|
|
19
|
+
*/
|
|
20
|
+
import { type ReactNode } from "react";
|
|
21
|
+
import type { MessengerProfile, MessengerSource } from "../contract.js";
|
|
22
|
+
import { type MessengerStarterKey } from "./roster.js";
|
|
23
|
+
/** Where the person is: an agent and its open thread (null = a new chat). */
|
|
24
|
+
export interface AgentsMessengerLocation {
|
|
25
|
+
readonly agentId: string | null;
|
|
26
|
+
readonly conversationId: string | null;
|
|
27
|
+
}
|
|
28
|
+
export interface AgentsMessengerOptions {
|
|
29
|
+
/** The list title. Default "Agents". */
|
|
30
|
+
readonly title?: string;
|
|
31
|
+
readonly profile?: MessengerProfile;
|
|
32
|
+
/** Open on this agent / thread (the host's deep link). */
|
|
33
|
+
readonly initialLocation?: AgentsMessengerLocation;
|
|
34
|
+
/** Every move, so the host can keep its URL in step (replaceState, never a navigation). */
|
|
35
|
+
readonly onLocationChange?: (location: AgentsMessengerLocation) => void;
|
|
36
|
+
/** An absolute link to a place in this messenger (Copy link). */
|
|
37
|
+
readonly linkTo?: (location: AgentsMessengerLocation) => string;
|
|
38
|
+
/** Doors onto the rest of the app. Defaults are matrx-frontend's routes. */
|
|
39
|
+
readonly hrefs?: {
|
|
40
|
+
readonly agent?: (agentId: string) => string;
|
|
41
|
+
readonly editAgent?: (agentId: string) => string;
|
|
42
|
+
readonly conversation?: (conversationId: string) => string;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* A seat whose thread is fixed by its own door (the Chief of Staff's ONE
|
|
46
|
+
* staff thread): the host returns that room. Null / absent: the chat room.
|
|
47
|
+
*/
|
|
48
|
+
readonly renderSeatRoom?: (seat: {
|
|
49
|
+
mandateKey: MessengerStarterKey;
|
|
50
|
+
agentId: string;
|
|
51
|
+
}) => ReactNode | null;
|
|
52
|
+
}
|
|
53
|
+
export interface AgentsMessengerModel {
|
|
54
|
+
readonly source: MessengerSource;
|
|
55
|
+
/** Dialogs the source opens (Rename). Render beside the shell. */
|
|
56
|
+
readonly overlays: ReactNode;
|
|
57
|
+
}
|
|
58
|
+
export declare function useAgentsMessengerSource(options?: AgentsMessengerOptions): AgentsMessengerModel;
|
|
59
|
+
export interface AgentsMessengerProps extends AgentsMessengerOptions {
|
|
60
|
+
readonly className?: string;
|
|
61
|
+
}
|
|
62
|
+
/** The agents messenger: the shared shell over the agents source, plus its dialogs. */
|
|
63
|
+
export declare function AgentsMessenger({ className, ...options }: AgentsMessengerProps): import("react").JSX.Element;
|