@ai-matrx/chat 0.7.23 → 0.7.25
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 -0
- package/dist/agents/components/chat/NewChatGreeting.d.ts +15 -1
- package/dist/agents/components/chat/NewChatGreeting.js +14 -2
- package/dist/agents/components/chat/PinnedAgentsSection.d.ts +6 -3
- package/dist/agents/components/chat/PinnedAgentsSection.js +31 -10
- package/dist/agents/components/inputs/smart-input/SmartAgentInput.d.ts +1 -1
- package/dist/agents/components/inputs/smart-input/SmartAgentInput.js +9 -0
- package/dist/agents/components/inputs/smart-input/composer/ComposerSplash.js +1 -2
- package/dist/agents/components/inputs/variable-input-variations/AgentVariableForm.js +5 -5
- package/dist/agents/components/messages-display/AgentConversationDisplay.js +23 -1
- package/dist/agents/components/shared/AgentConversationColumn.js +1 -11
- package/dist/agents/redux/execution-system/messages/messages.selectors.d.ts +3 -1
- package/dist/agents/redux/execution-system/messages/messages.selectors.js +5 -1
- package/dist/agents/redux/execution-system/messages/messages.slice.d.ts +18 -1
- package/dist/agents/redux/execution-system/messages/messages.slice.js +12 -1
- package/dist/agents/redux/execution-system/thunks/conversation-bundle.d.ts +12 -0
- package/dist/agents/redux/execution-system/thunks/conversation-bundle.js +22 -0
- package/dist/agents/redux/execution-system/thunks/load-conversation.thunk.js +23 -7
- package/package.json +10 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
# @ai-matrx/chat
|
|
2
2
|
|
|
3
|
+
## 0.7.25
|
|
4
|
+
|
|
5
|
+
Automatic changed-only republish (docs/metadata drift since the last tag — see
|
|
6
|
+
`git diff npm/chat/v0.7.24..npm/chat/v0.7.25 -- apps/shared/chat`).
|
|
7
|
+
No source changes intended and no consumer action required.
|
|
8
|
+
|
|
9
|
+
## 0.7.24
|
|
10
|
+
|
|
11
|
+
Automatic changed-only republish (docs/metadata drift since the last tag — see
|
|
12
|
+
`git diff npm/chat/v0.7.23..npm/chat/v0.7.24 -- apps/shared/chat`).
|
|
13
|
+
No source changes intended and no consumer action required.
|
|
14
|
+
|
|
3
15
|
## 0.7.23
|
|
4
16
|
### Unreleased
|
|
5
17
|
|
|
18
|
+
### The Form inputs layout folds, it never disappears
|
|
19
|
+
|
|
20
|
+
- **`AgentVariableForm`: the "Fill in the details below" header folds the fields and opens them again.**
|
|
21
|
+
It used to write the host's `showVariablePanel` flag, so one click removed the whole form, header
|
|
22
|
+
included — a host with no variables button of its own (the form-style composer, a Board agent-form
|
|
23
|
+
tile) could never get the fields back. The fold is the form's own state; `showVariablePanel` stays the
|
|
24
|
+
host's hide and the composer button's. Guard: `form-fold-never-removes-the-form.test.tsx`.
|
|
25
|
+
Consumer action: none.
|
|
26
|
+
|
|
6
27
|
### A run parked on the person waits for them (lane F15)
|
|
7
28
|
|
|
8
29
|
- **`runHeadlessAgentJson` / `adoptHeadlessAgentJson` / `continueAgentJson`: waiting on the person is a
|
|
@@ -30,6 +51,18 @@ No source changes intended and no consumer action required.
|
|
|
30
51
|
## 0.7.21
|
|
31
52
|
### Unreleased
|
|
32
53
|
|
|
54
|
+
### /chat/new: the last two startup costs
|
|
55
|
+
|
|
56
|
+
- **Pinned agents read only the pins, only on screen.** `PinnedAgentsSection` waits for
|
|
57
|
+
`useInView` (never from a closed mobile sheet or collapsed rail), then calls the catalog's
|
|
58
|
+
`ensureFavoriteRows()` — `ues_list` + `agx_get_list_full?id=in.(…)` — instead of loading the
|
|
59
|
+
whole agent catalogue after page idle. It shows a loading skeleton and a retry on failure.
|
|
60
|
+
- **The splash paints its final footprint.** The server-painted composer shell is a read-only
|
|
61
|
+
textarea carrying the composer's classes and placeholder (no pulse; a div measured narrower than
|
|
62
|
+
the live box on a phone's 17px zoom floor), the landing no longer fades in from opacity 0, and the
|
|
63
|
+
quick-action row keeps its skeleton until the organization is set instead of vanishing. On a
|
|
64
|
+
phone the textarea placeholder was the page's Largest Contentful Paint at 5.7–15 s.
|
|
65
|
+
|
|
33
66
|
### /chat/new: the live residuals of the startup-read work
|
|
34
67
|
|
|
35
68
|
- **A minted id is a draft before its record exists.** `generateConversationId` remembers the ids this tab
|
|
@@ -20,6 +20,20 @@ interface NewChatGreetingProps {
|
|
|
20
20
|
* agent's fresh-chat route, never a launch with a resolved id.
|
|
21
21
|
*/
|
|
22
22
|
export declare function NewChatGreeting({ sourceConversationId, surfaceKey, composer, }: NewChatGreetingProps): import("react").JSX.Element;
|
|
23
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* The splash composer as the server paints it, before the page is live: the
|
|
25
|
+
* real card's box and a read-only textarea carrying the real one's classes and
|
|
26
|
+
* placeholder.
|
|
27
|
+
*
|
|
28
|
+
* THE PLACEHOLDER PAINTS WITH THE PAGE (2026-10-09). It is the largest text on
|
|
29
|
+
* a phone's /chat/new (larger than the greeting), so a shell without it made
|
|
30
|
+
* the live textarea's placeholder the page's Largest Contentful Paint seconds
|
|
31
|
+
* later. It is a TEXTAREA, not a div: every rule that sizes the live box (the
|
|
32
|
+
* coarse-pointer zoom floor raises it to 17px; a div measured 191px against
|
|
33
|
+
* the live 217px) sizes this one identically, so the live box replaces it
|
|
34
|
+
* without a new paint winning. No pulse — an opacity animation would hide the
|
|
35
|
+
* text from that count. Read-only and out of the tab order: nothing typed here
|
|
36
|
+
* could survive hydration.
|
|
37
|
+
*/
|
|
24
38
|
export declare function ChatSplashComposerShell(): import("react").JSX.Element;
|
|
25
39
|
export {};
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
ComposerQuickActions
|
|
12
12
|
} from "../inputs/smart-input/composer/ComposerSplash.js";
|
|
13
13
|
import { IntelligenceIndicator } from "../../../surfaces/runtime/intelligence.js";
|
|
14
|
+
const SPLASH_PLACEHOLDER = "How can I help you today?";
|
|
14
15
|
function NewChatGreeting({
|
|
15
16
|
sourceConversationId,
|
|
16
17
|
surfaceKey,
|
|
@@ -35,7 +36,7 @@ function NewChatGreeting({
|
|
|
35
36
|
{
|
|
36
37
|
conversationId: sourceConversationId,
|
|
37
38
|
surfaceKey,
|
|
38
|
-
composer: { ...composer, size: "splash", placeholder:
|
|
39
|
+
composer: { ...composer, size: "splash", placeholder: SPLASH_PLACEHOLDER }
|
|
39
40
|
}
|
|
40
41
|
) : /* @__PURE__ */ jsx(ChatSplashComposerShell, {}),
|
|
41
42
|
/* @__PURE__ */ jsx(
|
|
@@ -60,7 +61,18 @@ function ChatSplashComposerShell() {
|
|
|
60
61
|
"div",
|
|
61
62
|
{
|
|
62
63
|
"aria-hidden": true,
|
|
63
|
-
className: "h-[132px] w-full max-w-[760px]
|
|
64
|
+
className: "h-[132px] w-full max-w-[760px] rounded-[22px] border border-border bg-card p-2 shadow-[0_2px_10px_rgba(0,0,0,0.05)] [&_textarea]:block",
|
|
65
|
+
children: /* @__PURE__ */ jsx(
|
|
66
|
+
"textarea",
|
|
67
|
+
{
|
|
68
|
+
readOnly: true,
|
|
69
|
+
tabIndex: -1,
|
|
70
|
+
rows: 1,
|
|
71
|
+
placeholder: SPLASH_PLACEHOLDER,
|
|
72
|
+
className: "w-full bg-transparent border-none outline-none text-[15px] leading-6 px-1.5 py-1 text-foreground placeholder:text-muted-foreground/60 resize-none overflow-hidden",
|
|
73
|
+
style: { minHeight: 64 }
|
|
74
|
+
}
|
|
75
|
+
)
|
|
64
76
|
}
|
|
65
77
|
);
|
|
66
78
|
}
|
|
@@ -14,9 +14,12 @@ interface PinnedAgentsSectionProps {
|
|
|
14
14
|
* with the rest of the app, and shares the agent registry with the dropdown
|
|
15
15
|
* (no parallel fetches).
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* PINS READ ONLY THE PINS, AND ONLY ON SCREEN (2026-10-09). The section asks
|
|
18
|
+
* the catalog for the caller's starred rows (`ensureFavoriteRows`: `ues_list`,
|
|
19
|
+
* then `agx_get_list_full` filtered to those ids) once the section is actually
|
|
20
|
+
* visible (`useInView`) — never the whole catalogue, and never from a closed
|
|
21
|
+
* mobile sheet or collapsed rail. The full list loads only when a picker opens
|
|
22
|
+
* or the person searches agents; when it has, its rows answer here too.
|
|
20
23
|
*/
|
|
21
24
|
export declare function PinnedAgentsSection({ activeAgentId, }: PinnedAgentsSectionProps): import("react").JSX.Element | null;
|
|
22
25
|
export {};
|
|
@@ -5,18 +5,20 @@ import { useRouter, Link } from "../../../host/navigation.js";
|
|
|
5
5
|
import { cn } from "@ai-matrx/design-system";
|
|
6
6
|
import {
|
|
7
7
|
FavoriteAgentButton,
|
|
8
|
+
useAgentCatalog,
|
|
8
9
|
useAgentCatalogRows,
|
|
10
|
+
useAgentCatalogState,
|
|
9
11
|
useAgentConsumer
|
|
10
12
|
} from "@ai-matrx/agents/catalog/react";
|
|
11
13
|
import { makeSelectFilteredAgents } from "@ai-matrx/agents/catalog";
|
|
12
14
|
import { useAppStore } from "../../../store/hooks.js";
|
|
13
15
|
import { stageChatAgentSwitch } from "./begin-fresh-chat.js";
|
|
14
|
-
import { Button, DisclosureHeader } from "@ai-matrx/design-system/controls";
|
|
15
|
-
import {
|
|
16
|
-
import { whenPageIdle } from "@ai-matrx/kit/idle-scheduler";
|
|
16
|
+
import { Button, DisclosureHeader, RegionSkeleton } from "@ai-matrx/design-system/controls";
|
|
17
|
+
import { useInView } from "@ai-matrx/kit/hooks";
|
|
17
18
|
const CONSUMER_ID = "chat-sidebar-pinned";
|
|
18
19
|
const PINNED_COLLAPSED_LIMIT = 5;
|
|
19
20
|
const selectPinnedAgents = makeSelectFilteredAgents();
|
|
21
|
+
const selectFavoriteRowsStatus = (state) => state.favoriteRowsStatus;
|
|
20
22
|
function pinnedAgentHref(agentId) {
|
|
21
23
|
return `/chat/a/${encodeURIComponent(agentId)}`;
|
|
22
24
|
}
|
|
@@ -26,13 +28,13 @@ function PinnedAgentsSection({
|
|
|
26
28
|
const router = useRouter();
|
|
27
29
|
const store = useAppStore();
|
|
28
30
|
const consumer = useAgentConsumer(CONSUMER_ID);
|
|
31
|
+
const catalog = useAgentCatalog();
|
|
32
|
+
const favoriteRowsStatus = useAgentCatalogState(selectFavoriteRowsStatus);
|
|
33
|
+
const [visibleRef, visible] = useInView({ rootMargin: "64px" });
|
|
29
34
|
useEffect(() => {
|
|
30
|
-
|
|
31
|
-
void
|
|
32
|
-
|
|
33
|
-
});
|
|
34
|
-
return () => controller.abort();
|
|
35
|
-
}, []);
|
|
35
|
+
if (!visible) return;
|
|
36
|
+
void catalog.ensureFavoriteRows().catch(() => void 0);
|
|
37
|
+
}, [catalog, visible]);
|
|
36
38
|
useEffect(() => {
|
|
37
39
|
if (consumer.favFilter !== "yes") consumer.setFavFilter("yes");
|
|
38
40
|
}, [consumer]);
|
|
@@ -42,7 +44,26 @@ function PinnedAgentsSection({
|
|
|
42
44
|
const [showAll, setShowAll] = useState(false);
|
|
43
45
|
const hasMorePins = pinned.length > PINNED_COLLAPSED_LIMIT;
|
|
44
46
|
const visiblePins = showAll || !hasMorePins ? pinned : pinned.slice(0, PINNED_COLLAPSED_LIMIT);
|
|
45
|
-
if (pinned.length === 0)
|
|
47
|
+
if (pinned.length === 0) {
|
|
48
|
+
if (!visible) {
|
|
49
|
+
return /* @__PURE__ */ jsx("div", { ref: visibleRef, "aria-hidden": "true" });
|
|
50
|
+
}
|
|
51
|
+
if (favoriteRowsStatus === "idle" || favoriteRowsStatus === "loading") {
|
|
52
|
+
return /* @__PURE__ */ jsx("div", { className: "shrink-0 border-b border-border px-2 py-1.5", children: /* @__PURE__ */ jsx(RegionSkeleton, { shape: "rows", count: 2, "aria-label": "Loading pinned agents" }) });
|
|
53
|
+
}
|
|
54
|
+
if (favoriteRowsStatus === "failed") {
|
|
55
|
+
return /* @__PURE__ */ jsx("div", { className: "shrink-0 border-b border-border px-2 py-1.5", children: /* @__PURE__ */ jsx(
|
|
56
|
+
Button,
|
|
57
|
+
{
|
|
58
|
+
variant: "quiet",
|
|
59
|
+
className: "w-full",
|
|
60
|
+
onClick: () => void catalog.ensureFavoriteRows().catch(() => void 0),
|
|
61
|
+
children: "Pinned agents did not load. Retry"
|
|
62
|
+
}
|
|
63
|
+
) });
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
46
67
|
return /* @__PURE__ */ jsxs("div", { className: "shrink-0 border-b border-border", children: [
|
|
47
68
|
/* @__PURE__ */ jsx(
|
|
48
69
|
DisclosureHeader,
|
|
@@ -44,5 +44,5 @@ interface SmartAgentInputProps {
|
|
|
44
44
|
*/
|
|
45
45
|
composer: ComposerPresentation;
|
|
46
46
|
}
|
|
47
|
-
export declare function SmartAgentInput({ conversationId, uploadRoot, uploadPath, enablePasteImages, compact, showSendButton, showVariableIcon, surfaceKey, draftAlias, disableSend, variablesPanelStyle, contextRailPresentation, contextRailAttachedItems, extraRightControls, surfaceValueAnchors, composer, }: SmartAgentInputProps): React.JSX.Element;
|
|
47
|
+
export declare function SmartAgentInput({ conversationId, uploadRoot, uploadPath, enablePasteImages, compact, showSendButton, showVariableIcon, surfaceKey, draftAlias, disableSend, variablesPanelStyle, contextRailPresentation, contextRailAttachedItems, extraRightControls, surfaceValueAnchors, composer, }: SmartAgentInputProps): React.JSX.Element | null;
|
|
48
48
|
export {};
|
|
@@ -5,6 +5,7 @@ import { useAppDispatch, useAppSelector } from "../../../../store/hooks.js";
|
|
|
5
5
|
import { setVariablesPanelStyle } from "../../../redux/execution-system/instance-ui-state/instance-ui-state.slice.js";
|
|
6
6
|
import { SmartAgentInputStacked } from "./SmartAgentInputStacked.js";
|
|
7
7
|
import { InboxQueueStrip } from "./InboxQueueStrip.js";
|
|
8
|
+
import { selectMessagesUnreadableReason } from "../../../redux/execution-system/messages/messages.selectors.js";
|
|
8
9
|
import { selectViewerCanReply } from "../../../redux/execution-system/conversations/conversations.selectors.js";
|
|
9
10
|
import { ViewOnlyComposerBar } from "./ViewOnlyComposerBar.js";
|
|
10
11
|
import { selectHasUserInput } from "../../../redux/execution-system/instance-user-input/instance-user-input.selectors.js";
|
|
@@ -45,11 +46,19 @@ function SmartAgentInput({
|
|
|
45
46
|
);
|
|
46
47
|
const heldSentence = useConversationHeld(conversationId);
|
|
47
48
|
const sendDisabled = disableSend || ambientHasNothingToSend || heldSentence !== null;
|
|
49
|
+
const unreadableReason = useAppSelector((state) => {
|
|
50
|
+
if (!conversationId) return null;
|
|
51
|
+
const reason = selectMessagesUnreadableReason(conversationId)(state);
|
|
52
|
+
return reason === "private" || reason === "removed" ? reason : null;
|
|
53
|
+
});
|
|
48
54
|
const viewerCanReply = useAppSelector(
|
|
49
55
|
(state) => conversationId ? selectViewerCanReply(conversationId)(state) : true
|
|
50
56
|
);
|
|
51
57
|
const variableIconShown = showVariableIcon && variablesPanelStyle !== "hidden";
|
|
52
58
|
const queueStrip = conversationId && !isLauncher ? /* @__PURE__ */ jsx(InboxQueueStrip, { conversationId }) : null;
|
|
59
|
+
if (unreadableReason) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
53
62
|
if (conversationId && !viewerCanReply) {
|
|
54
63
|
return /* @__PURE__ */ jsx(ViewOnlyComposerBar, {});
|
|
55
64
|
}
|
|
@@ -110,8 +110,7 @@ function ComposerQuickActions({
|
|
|
110
110
|
observer.disconnect();
|
|
111
111
|
};
|
|
112
112
|
}, [actions.length]);
|
|
113
|
-
if (!organizationId)
|
|
114
|
-
if (knob === void 0) {
|
|
113
|
+
if (!organizationId || knob === void 0) {
|
|
115
114
|
return /* @__PURE__ */ jsx(ComposerQuickActionsSkeleton, { className });
|
|
116
115
|
}
|
|
117
116
|
if (actions.length === 0) return null;
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
} from "../../../redux/execution-system/instance-variable-values/instance-variable-values.selectors.js";
|
|
13
13
|
import { setUserVariableValue } from "../../../redux/execution-system/instance-variable-values/instance-variable-values.slice.js";
|
|
14
14
|
import { selectShowVariablePanel } from "../../../redux/execution-system/instance-ui-state/instance-ui-state.selectors.js";
|
|
15
|
-
import { toggleVariablePanel } from "../../../redux/execution-system/instance-ui-state/instance-ui-state.slice.js";
|
|
16
15
|
import { Label } from "@ai-matrx/design-system";
|
|
17
16
|
import { Input, DisclosureHeader } from "@ai-matrx/design-system/controls";
|
|
18
17
|
import { Switch } from "../../../../ui/switch.js";
|
|
@@ -46,6 +45,7 @@ function AgentVariableForm({ conversationId }) {
|
|
|
46
45
|
);
|
|
47
46
|
const userValues = useAppSelector(selectUserVariableValues(conversationId));
|
|
48
47
|
const showVariables = useAppSelector(selectShowVariablePanel(conversationId));
|
|
48
|
+
const [open, setOpen] = useState(true);
|
|
49
49
|
if (!definitions || definitions.length === 0) return null;
|
|
50
50
|
if (!showVariables) return null;
|
|
51
51
|
const filledCount = definitions.filter((def) => {
|
|
@@ -59,14 +59,14 @@ function AgentVariableForm({ conversationId }) {
|
|
|
59
59
|
/* @__PURE__ */ jsx(
|
|
60
60
|
DisclosureHeader,
|
|
61
61
|
{
|
|
62
|
-
open
|
|
63
|
-
onClick: () =>
|
|
62
|
+
open,
|
|
63
|
+
onClick: () => setOpen((v) => !v),
|
|
64
64
|
chevron: "end",
|
|
65
|
-
title:
|
|
65
|
+
title: open ? "Fill in the details below" : `${filledCount}/${definitions.length} details provided`,
|
|
66
66
|
className: "shrink-0"
|
|
67
67
|
}
|
|
68
68
|
),
|
|
69
|
-
|
|
69
|
+
open && /* @__PURE__ */ jsxs("div", { className: "border-t border-border flex-1 min-h-0 overflow-y-auto", children: [
|
|
70
70
|
/* @__PURE__ */ jsx(BoundVariableChips, { conversationId }),
|
|
71
71
|
/* @__PURE__ */ jsx("div", { className: "px-2.5 py-2", children: visibleDefs.map((def, i) => /* @__PURE__ */ jsxs("div", { children: [
|
|
72
72
|
i > 0 && /* @__PURE__ */ jsx("div", { className: "border-t border-border my-2" }),
|
|
@@ -8,6 +8,7 @@ import { StaleDataNotice } from "../../../host/ui-slots.js";
|
|
|
8
8
|
import {
|
|
9
9
|
selectConversationMessages,
|
|
10
10
|
selectMessagesHydrationFailure,
|
|
11
|
+
selectMessagesUnreadableReason,
|
|
11
12
|
selectVisibleMessageGroupLimit
|
|
12
13
|
} from "../../redux/execution-system/messages/messages.selectors.js";
|
|
13
14
|
import {
|
|
@@ -32,7 +33,8 @@ import { AgentAssistantMessage } from "./assistant/AgentAssistantMessage.js";
|
|
|
32
33
|
import { AgentEmptyMessageDisplay } from "./assistant/AgentEmptyMessageDisplay.js";
|
|
33
34
|
import { ErrorBoundaryWithCapture } from "../../../host/ui-slots.js";
|
|
34
35
|
import { ExampleTurnsGroup } from "../../message-flags/ExampleTurnsGroup.js";
|
|
35
|
-
import { Pin } from "lucide-react";
|
|
36
|
+
import { Lock, Pin, Trash2 } from "lucide-react";
|
|
37
|
+
import { EmptyState } from "@ai-matrx/design-system/controls";
|
|
36
38
|
import {
|
|
37
39
|
hydratePinnedMessages,
|
|
38
40
|
usePendingPinMessageIds,
|
|
@@ -63,6 +65,10 @@ function AgentConversationDisplay({
|
|
|
63
65
|
const hydrationFailure = useAppSelector(
|
|
64
66
|
selectMessagesHydrationFailure(conversationId)
|
|
65
67
|
);
|
|
68
|
+
const unreadableReason = useAppSelector((state) => {
|
|
69
|
+
const reason = selectMessagesUnreadableReason(conversationId)(state);
|
|
70
|
+
return reason === "private" || reason === "removed" ? reason : null;
|
|
71
|
+
});
|
|
66
72
|
const phase = useAppSelector(selectStreamPhase(conversationId));
|
|
67
73
|
const latestRequestId = useAppSelector(selectLatestRequestId(conversationId));
|
|
68
74
|
const visibleGroupLimit = useAppSelector(
|
|
@@ -206,6 +212,22 @@ function AgentConversationDisplay({
|
|
|
206
212
|
]);
|
|
207
213
|
const resolveMenuContext = (target) => resolveMarkdownContext(target, conversationId);
|
|
208
214
|
if (displayGroups.length === 0) {
|
|
215
|
+
if (unreadableReason) {
|
|
216
|
+
return /* @__PURE__ */ jsx("div", { className: "p-4", children: unreadableReason === "private" ? /* @__PURE__ */ jsx(
|
|
217
|
+
EmptyState,
|
|
218
|
+
{
|
|
219
|
+
icon: /* @__PURE__ */ jsx(Lock, { className: "h-5 w-5" }),
|
|
220
|
+
title: "Private chat",
|
|
221
|
+
line: "Only its owner can open it"
|
|
222
|
+
}
|
|
223
|
+
) : /* @__PURE__ */ jsx(
|
|
224
|
+
EmptyState,
|
|
225
|
+
{
|
|
226
|
+
icon: /* @__PURE__ */ jsx(Trash2, { className: "h-5 w-5" }),
|
|
227
|
+
title: "Chat deleted"
|
|
228
|
+
}
|
|
229
|
+
) });
|
|
230
|
+
}
|
|
209
231
|
if (hydrationFailure) {
|
|
210
232
|
return /* @__PURE__ */ jsx("div", { className: "p-4", children: /* @__PURE__ */ jsx(
|
|
211
233
|
StaleDataNotice,
|
|
@@ -232,17 +232,7 @@ function AgentConversationColumn({
|
|
|
232
232
|
),
|
|
233
233
|
children: [
|
|
234
234
|
/* @__PURE__ */ jsxs("div", { className: "relative flex-1 min-h-0", children: [
|
|
235
|
-
showLanding ? /* @__PURE__ */ jsx(
|
|
236
|
-
motion.div,
|
|
237
|
-
{
|
|
238
|
-
className: "absolute inset-0 overflow-y-auto",
|
|
239
|
-
initial: { opacity: 0 },
|
|
240
|
-
animate: { opacity: 1 },
|
|
241
|
-
transition: { duration: 0.2 },
|
|
242
|
-
children: landingContent
|
|
243
|
-
},
|
|
244
|
-
"landing"
|
|
245
|
-
) : /* @__PURE__ */ jsxs(
|
|
235
|
+
showLanding ? /* @__PURE__ */ jsx("div", { className: "absolute inset-0 overflow-y-auto", children: landingContent }, "landing") : /* @__PURE__ */ jsxs(
|
|
246
236
|
motion.div,
|
|
247
237
|
{
|
|
248
238
|
ref: scrollRef,
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* python-generated discriminated union).
|
|
9
9
|
*/
|
|
10
10
|
import type { ChatRootState } from "../../../../store/root-state.js";
|
|
11
|
-
import { type MessageRecord } from "./messages.slice.js";
|
|
11
|
+
import { type ConversationUnreadableReason, type MessageRecord } from "./messages.slice.js";
|
|
12
12
|
import type { ContentSegment } from "../active-requests/active-requests.selectors.js";
|
|
13
13
|
import { type MessagePart, type RenderBlockPayload } from "@ai-matrx/agents/generated/stream-events";
|
|
14
14
|
import type { ApiEndpointMode } from "../../../types/instance.types.js";
|
|
@@ -69,6 +69,8 @@ export declare const selectHasMessages: (conversationId: string) => (state: Chat
|
|
|
69
69
|
* including a genuinely empty conversation.
|
|
70
70
|
*/
|
|
71
71
|
export declare const selectMessagesHydrationFailure: (conversationId: string) => (state: ChatRootState) => string | null;
|
|
72
|
+
/** `private` / `removed` when the conversation is not this person's to read. */
|
|
73
|
+
export declare const selectMessagesUnreadableReason: (conversationId: string) => (state: ChatRootState) => ConversationUnreadableReason | null;
|
|
72
74
|
export declare const selectMessageContent: (conversationId: string, messageId: string) => (state: ChatRootState) => MessageRecord["content"] | undefined;
|
|
73
75
|
export declare const selectMessageStatus: (conversationId: string, messageId: string) => (state: ChatRootState) => MessageRecord["status"] | undefined;
|
|
74
76
|
export declare const selectMessageClientStatus: (conversationId: string, messageId: string) => (state: ChatRootState) => MessageRecord["_clientStatus"] | undefined;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createSelector } from "@reduxjs/toolkit";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
nextTranscriptPosition
|
|
4
|
+
} from "./messages.slice.js";
|
|
3
5
|
import { normalizeContentBlocks } from "../utils/normalize-content-blocks.js";
|
|
4
6
|
import { isAttachmentMessagePart } from "../../../components/context-items/normalize.js";
|
|
5
7
|
import {
|
|
@@ -58,6 +60,7 @@ const selectMessageCount = (conversationId) => (state) => state.messages.byConve
|
|
|
58
60
|
const selectNextMessagePosition = (conversationId) => (state) => nextTranscriptPosition(state.messages.byConversationId[conversationId]);
|
|
59
61
|
const selectHasMessages = (conversationId) => (state) => (state.messages.byConversationId[conversationId]?.orderedIds?.length ?? 0) > 0;
|
|
60
62
|
const selectMessagesHydrationFailure = (conversationId) => (state) => state.messages.byConversationId[conversationId]?.hydrationFailure ?? null;
|
|
63
|
+
const selectMessagesUnreadableReason = (conversationId) => (state) => state.messages.byConversationId[conversationId]?.unreadableReason ?? null;
|
|
61
64
|
const selectMessageContent = (conversationId, messageId) => (state) => state.messages.byConversationId[conversationId]?.byId?.[messageId]?.content;
|
|
62
65
|
const selectMessageStatus = (conversationId, messageId) => (state) => state.messages.byConversationId[conversationId]?.byId?.[messageId]?.status;
|
|
63
66
|
const selectMessageClientStatus = (conversationId, messageId) => (state) => state.messages.byConversationId[conversationId]?.byId?.[messageId]?._clientStatus;
|
|
@@ -626,6 +629,7 @@ export {
|
|
|
626
629
|
selectMessageStreamRequestId,
|
|
627
630
|
selectMessagesHydrationFailure,
|
|
628
631
|
selectMessagesInterleavedRuns,
|
|
632
|
+
selectMessagesUnreadableReason,
|
|
629
633
|
selectNextMessagePosition,
|
|
630
634
|
selectOldestLoadedPosition,
|
|
631
635
|
selectOrderedMessageIds,
|
|
@@ -252,7 +252,21 @@ export interface MessagesEntry {
|
|
|
252
252
|
* conversation leaves it `null`. Cleared by any successful `hydrateMessages`.
|
|
253
253
|
*/
|
|
254
254
|
hydrationFailure: string | null;
|
|
255
|
+
/**
|
|
256
|
+
* Why a reopen read came back empty, when that is not a failure:
|
|
257
|
+
* - `missing`: the server never saved it (a chat whose first turn was never
|
|
258
|
+
* sent). It opens as an empty new chat; callers that wait for a row to
|
|
259
|
+
* appear read this instead of a failure.
|
|
260
|
+
* - `private`: it exists and belongs to someone else.
|
|
261
|
+
* - `removed`: it was deleted.
|
|
262
|
+
* A retry can never change `private`/`removed`, so the transcript says what
|
|
263
|
+
* is true instead of offering "Try again" and the composer is absent. Set by
|
|
264
|
+
* `loadConversation`; cleared by any successful `hydrateMessages`.
|
|
265
|
+
*/
|
|
266
|
+
unreadableReason: ConversationUnreadableReason | null;
|
|
255
267
|
}
|
|
268
|
+
/** Why a held conversation id cannot be read by this person. */
|
|
269
|
+
export type ConversationUnreadableReason = "missing" | "private" | "removed";
|
|
256
270
|
export interface MessagesState {
|
|
257
271
|
byConversationId: Record<string, MessagesEntry>;
|
|
258
272
|
}
|
|
@@ -359,6 +373,9 @@ export declare const initInstanceMessages: import("@reduxjs/toolkit").ActionCrea
|
|
|
359
373
|
}, "messages/setConversationLabel">, setMessagesHydrationFailure: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
|
|
360
374
|
conversationId: string;
|
|
361
375
|
failure: string | null;
|
|
362
|
-
}, "messages/setMessagesHydrationFailure">,
|
|
376
|
+
}, "messages/setMessagesHydrationFailure">, setMessagesUnreadable: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
|
|
377
|
+
conversationId: string;
|
|
378
|
+
reason: ConversationUnreadableReason | null;
|
|
379
|
+
}, "messages/setMessagesUnreadable">, clearMessages: import("@reduxjs/toolkit").ActionCreatorWithPayload<string, "messages/clearMessages">;
|
|
363
380
|
declare const _default: import("redux").Reducer<MessagesState>;
|
|
364
381
|
export default _default;
|
|
@@ -81,7 +81,8 @@ function getOrCreate(state, conversationId, apiEndpointMode = "agent") {
|
|
|
81
81
|
hasMoreOlder: false,
|
|
82
82
|
isLoadingOlder: false,
|
|
83
83
|
visibleGroupLimit: null,
|
|
84
|
-
hydrationFailure: null
|
|
84
|
+
hydrationFailure: null,
|
|
85
|
+
unreadableReason: null
|
|
85
86
|
};
|
|
86
87
|
state.byConversationId[conversationId] = entry;
|
|
87
88
|
}
|
|
@@ -379,6 +380,7 @@ const messagesSlice = createSlice({
|
|
|
379
380
|
}
|
|
380
381
|
entry.isLoadingOlder = false;
|
|
381
382
|
entry.hydrationFailure = null;
|
|
383
|
+
entry.unreadableReason = null;
|
|
382
384
|
},
|
|
383
385
|
/**
|
|
384
386
|
* Prepend an older page of messages to the transcript. **Strictly
|
|
@@ -487,6 +489,13 @@ const messagesSlice = createSlice({
|
|
|
487
489
|
const entry = getOrCreate(state, conversationId);
|
|
488
490
|
entry.hydrationFailure = failure;
|
|
489
491
|
},
|
|
492
|
+
/** Record that this conversation is not this person's to read. */
|
|
493
|
+
setMessagesUnreadable(state, action) {
|
|
494
|
+
const { conversationId, reason } = action.payload;
|
|
495
|
+
const entry = getOrCreate(state, conversationId);
|
|
496
|
+
entry.unreadableReason = reason;
|
|
497
|
+
if (reason) entry.hydrationFailure = null;
|
|
498
|
+
},
|
|
490
499
|
/** Clear the transcript (e.g. auto-clear on a new run). */
|
|
491
500
|
clearMessages(state, action) {
|
|
492
501
|
const entry = state.byConversationId[action.payload];
|
|
@@ -544,6 +553,7 @@ const {
|
|
|
544
553
|
removeMessage,
|
|
545
554
|
setConversationLabel,
|
|
546
555
|
setMessagesHydrationFailure,
|
|
556
|
+
setMessagesUnreadable,
|
|
547
557
|
clearMessages
|
|
548
558
|
} = messagesSlice.actions;
|
|
549
559
|
var messages_slice_default = messagesSlice.reducer;
|
|
@@ -563,6 +573,7 @@ export {
|
|
|
563
573
|
sentWithRequest,
|
|
564
574
|
setConversationLabel,
|
|
565
575
|
setMessagesHydrationFailure,
|
|
576
|
+
setMessagesUnreadable,
|
|
566
577
|
setOlderLoading,
|
|
567
578
|
setVisibleGroupLimit,
|
|
568
579
|
shouldCreateOptimisticUserMessage,
|
|
@@ -81,6 +81,18 @@ export interface FetchBundleOptions {
|
|
|
81
81
|
}
|
|
82
82
|
declare function describeSupabaseError(err: unknown): Record<string, unknown>;
|
|
83
83
|
export { describeSupabaseError };
|
|
84
|
+
/**
|
|
85
|
+
* Why a conversation id this person holds read back empty, asked only after
|
|
86
|
+
* the bundle RPC answered NULL:
|
|
87
|
+
* - `missing` — the server never saved it (an id minted in a browser whose
|
|
88
|
+
* first turn was never sent). Nothing to load and nothing wrong.
|
|
89
|
+
* - `removed` — it was deleted.
|
|
90
|
+
* - `private` — it exists and belongs to someone else (AI chat is Private).
|
|
91
|
+
* `null` when the question itself could not be answered — the caller then
|
|
92
|
+
* keeps the honest "couldn't load" state.
|
|
93
|
+
*/
|
|
94
|
+
export type ConversationEmptyReadReason = "missing" | "removed" | "private";
|
|
95
|
+
export declare function fetchConversationUnreadableReason(conversationId: string): Promise<ConversationEmptyReadReason | null>;
|
|
84
96
|
/**
|
|
85
97
|
* Fetches a page of conversation history via `get_cx_conversation_bundle`.
|
|
86
98
|
* Falls back to direct table queries when the RPC is unavailable (dev DBs
|
|
@@ -19,6 +19,27 @@ function describeSupabaseError(err) {
|
|
|
19
19
|
}
|
|
20
20
|
return { raw: err };
|
|
21
21
|
}
|
|
22
|
+
async function fetchConversationUnreadableReason(conversationId) {
|
|
23
|
+
try {
|
|
24
|
+
const { data, error } = await supabase.schema("chat").rpc("conversation_unreadable_reason", {
|
|
25
|
+
p_conversation_id: conversationId
|
|
26
|
+
});
|
|
27
|
+
if (error) {
|
|
28
|
+
console.error(
|
|
29
|
+
"[conversation-bundle] conversation_unreadable_reason failed:",
|
|
30
|
+
describeSupabaseError(error)
|
|
31
|
+
);
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return data === "missing" || data === "removed" || data === "private" ? data : null;
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error(
|
|
37
|
+
"[conversation-bundle] conversation_unreadable_reason threw:",
|
|
38
|
+
describeSupabaseError(err)
|
|
39
|
+
);
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
22
43
|
async function fetchConversationBundle(conversationId, options = {}) {
|
|
23
44
|
const {
|
|
24
45
|
messageLimit = 50,
|
|
@@ -295,6 +316,7 @@ export {
|
|
|
295
316
|
describeSupabaseError,
|
|
296
317
|
extractBundleToolCalls,
|
|
297
318
|
fetchConversationBundle,
|
|
319
|
+
fetchConversationUnreadableReason,
|
|
298
320
|
messageRowToRecord,
|
|
299
321
|
requestRowToRecord,
|
|
300
322
|
toolCallRowToRecord,
|
|
@@ -13,7 +13,8 @@ import {
|
|
|
13
13
|
} from "../conversations/conversations.slice.js";
|
|
14
14
|
import {
|
|
15
15
|
hydrateMessages,
|
|
16
|
-
setMessagesHydrationFailure
|
|
16
|
+
setMessagesHydrationFailure,
|
|
17
|
+
setMessagesUnreadable
|
|
17
18
|
} from "../messages/messages.slice.js";
|
|
18
19
|
import { reconcileMessagesArtifacts } from "../../../../host/ui-slots.js";
|
|
19
20
|
import {
|
|
@@ -62,6 +63,7 @@ import {
|
|
|
62
63
|
import { loadCodeEditHistoryThunk } from "../../../../host/ui-slots.js";
|
|
63
64
|
import {
|
|
64
65
|
CONVERSATION_NOT_MATERIALIZED,
|
|
66
|
+
fetchConversationUnreadableReason,
|
|
65
67
|
fetchConversationBundle,
|
|
66
68
|
describeSupabaseError,
|
|
67
69
|
extractBundleToolCalls,
|
|
@@ -118,12 +120,26 @@ const loadConversation = createAsyncThunk(
|
|
|
118
120
|
if (err?.code === CONVERSATION_NOT_MATERIALIZED) {
|
|
119
121
|
void historyPromise.catch?.(() => void 0);
|
|
120
122
|
if (expectMaterialized) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
123
|
+
const reason = await fetchConversationUnreadableReason(conversationId);
|
|
124
|
+
if ((getState().messages.byConversationId[conversationId]?.orderedIds?.length ?? 0) > 0) {
|
|
125
|
+
return { conversationId };
|
|
126
|
+
}
|
|
127
|
+
if (reason) {
|
|
128
|
+
if (reason === "missing") {
|
|
129
|
+
console.info(
|
|
130
|
+
"[loadConversation] %s was never saved \u2014 opening it as a new chat",
|
|
131
|
+
conversationId
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
dispatch(setMessagesUnreadable({ conversationId, reason }));
|
|
135
|
+
} else {
|
|
136
|
+
dispatch(
|
|
137
|
+
setMessagesHydrationFailure({
|
|
138
|
+
conversationId,
|
|
139
|
+
failure: NOT_READABLE_DETAIL
|
|
140
|
+
})
|
|
141
|
+
);
|
|
142
|
+
}
|
|
127
143
|
}
|
|
128
144
|
return { conversationId };
|
|
129
145
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-matrx/chat",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.25",
|
|
4
4
|
"description": "THE one AI Matrx person-to-agent chat system: the full chat (running agents, conversations, tool calls, streaming, context, surfaces, voice) as explicit per-module subpaths @ai-matrx/chat/<domain>/<module> (no wildcards: scripts/public-modules.json) over a host contract (/host, /host/react) whose only required value is a db. One chat, one renderer: the old kernel entries (., /external-store, /matrx, /react, /tools, styles.css, tokens.css) are retired.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -4681,25 +4681,25 @@
|
|
|
4681
4681
|
"server-only": "^0.0.1",
|
|
4682
4682
|
"uuid": "^14.0.2",
|
|
4683
4683
|
"zod": "^4.6.5",
|
|
4684
|
+
"@ai-matrx/alchemy": "latest",
|
|
4684
4685
|
"@ai-matrx/associations": "latest",
|
|
4686
|
+
"@ai-matrx/agents": "latest",
|
|
4685
4687
|
"@ai-matrx/browser-audio": "latest",
|
|
4686
|
-
"@ai-matrx/alchemy": "latest",
|
|
4687
|
-
"@ai-matrx/canvas": "latest",
|
|
4688
4688
|
"@ai-matrx/content-ir": "latest",
|
|
4689
|
-
"@ai-matrx/
|
|
4690
|
-
"@ai-matrx/data": "latest",
|
|
4689
|
+
"@ai-matrx/canvas": "latest",
|
|
4691
4690
|
"@ai-matrx/design-system": "latest",
|
|
4692
4691
|
"@ai-matrx/diff": "latest",
|
|
4693
|
-
"@ai-matrx/
|
|
4692
|
+
"@ai-matrx/data": "latest",
|
|
4694
4693
|
"@ai-matrx/kit": "latest",
|
|
4695
4694
|
"@ai-matrx/media": "latest",
|
|
4696
|
-
"@ai-matrx/print": "latest",
|
|
4697
4695
|
"@ai-matrx/realtime": "latest",
|
|
4696
|
+
"@ai-matrx/print": "latest",
|
|
4698
4697
|
"@ai-matrx/records": "latest",
|
|
4699
4698
|
"@ai-matrx/records-ui": "latest",
|
|
4700
|
-
"@ai-matrx/rich-content": "latest",
|
|
4701
4699
|
"@ai-matrx/rich-editor": "latest",
|
|
4702
|
-
"@ai-matrx/
|
|
4700
|
+
"@ai-matrx/rich-content": "latest",
|
|
4701
|
+
"@ai-matrx/icons": "latest",
|
|
4702
|
+
"@ai-matrx/content-ir-react": "latest"
|
|
4703
4703
|
},
|
|
4704
4704
|
"devDependencies": {
|
|
4705
4705
|
"@arethetypeswrong/cli": "^0.18.5",
|
|
@@ -4818,5 +4818,5 @@
|
|
|
4818
4818
|
"check:side-effects": "node ../scripts/check-side-effects-listed.mjs . && node ../scripts/check-side-effects-listed.mjs --self-test",
|
|
4819
4819
|
"check:public-surface": "node ./scripts/sync-public-surface.mjs --check && node ./scripts/sync-public-surface.mjs --self-test"
|
|
4820
4820
|
},
|
|
4821
|
-
"gitHead": "
|
|
4821
|
+
"gitHead": "e79576768f5d315f1b8413c353e3f7dc3ee1438c"
|
|
4822
4822
|
}
|