@ai-matrx/chat 0.7.22 → 0.7.24
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 +47 -0
- package/dist/agents/components/chat/NewChatGreeting.d.ts +10 -1
- package/dist/agents/components/chat/NewChatGreeting.js +4 -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/continue-agent-json.d.ts +1 -1
- package/dist/agents/redux/execution-system/thunks/continue-agent-json.js +3 -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/dist/agents/redux/execution-system/thunks/run-headless-agent-json.d.ts +19 -0
- package/dist/agents/redux/execution-system/thunks/run-headless-agent-json.js +99 -2
- package/dist/ui/markdown-stream/EnhancedChatMarkdown.js +11 -4
- package/dist/ui/markdown-stream/waiting-for-content.d.ts +15 -0
- package/dist/ui/markdown-stream/waiting-for-content.js +10 -0
- package/package.json +10 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# @ai-matrx/chat
|
|
2
2
|
|
|
3
|
+
## 0.7.24
|
|
4
|
+
|
|
5
|
+
Automatic changed-only republish (docs/metadata drift since the last tag — see
|
|
6
|
+
`git diff npm/chat/v0.7.23..npm/chat/v0.7.24 -- apps/shared/chat`).
|
|
7
|
+
No source changes intended and no consumer action required.
|
|
8
|
+
|
|
9
|
+
## 0.7.23
|
|
10
|
+
### Unreleased
|
|
11
|
+
|
|
12
|
+
### The Form inputs layout folds, it never disappears
|
|
13
|
+
|
|
14
|
+
- **`AgentVariableForm`: the "Fill in the details below" header folds the fields and opens them again.**
|
|
15
|
+
It used to write the host's `showVariablePanel` flag, so one click removed the whole form, header
|
|
16
|
+
included — a host with no variables button of its own (the form-style composer, a Board agent-form
|
|
17
|
+
tile) could never get the fields back. The fold is the form's own state; `showVariablePanel` stays the
|
|
18
|
+
host's hide and the composer button's. Guard: `form-fold-never-removes-the-form.test.tsx`.
|
|
19
|
+
Consumer action: none.
|
|
20
|
+
|
|
21
|
+
### A run parked on the person waits for them (lane F15)
|
|
22
|
+
|
|
23
|
+
- **`runHeadlessAgentJson` / `adoptHeadlessAgentJson` / `continueAgentJson`: waiting on the person is a
|
|
24
|
+
waiting state, never a failure.** A turn that ends parked on a question (`ask_person`,
|
|
25
|
+
`suspended_awaiting_client`) no longer settles from the parked call (the Applet builder said "The
|
|
26
|
+
builder answered without an app" over a question it never showed). The wait fires the new
|
|
27
|
+
`onAwaitingPerson`, pauses `timeoutMs`, re-reads the conversation (`rereadAndFollow`) when the ask
|
|
28
|
+
closes anywhere, and resolves from the resumed turn's answer; `personWaitMs` (default 6h) bounds it.
|
|
29
|
+
A parked run that stops waiting harvests nothing (`awaitingPerson: true`). Consumer action: a surface
|
|
30
|
+
that runs an agent which may ask passes `onAwaitingPerson` to say it is waiting.
|
|
31
|
+
- **A dropped connection is not a failed run.** When the stream dies on a connection loss
|
|
32
|
+
(`transport_lost` / `heartbeat_timeout`) the server finishes the turn and `reconnectServerOperation`
|
|
33
|
+
re-reads it; the headless wait now waits for that (5 min ceiling) and resolves from the answer it
|
|
34
|
+
brings, instead of failing "before returning a result" while the build finished on the server. A
|
|
35
|
+
reconnect that brings nothing ends with the stream's own reason. Consumer action: none.
|
|
36
|
+
- **A settled turn that ended on a tool call shows the call, not "Processing…"** (`isWaitingForFirstContent`):
|
|
37
|
+
the parked ask's card now draws in place of a shimmer that never ended. Consumer action: none.
|
|
38
|
+
|
|
3
39
|
## 0.7.22
|
|
4
40
|
|
|
5
41
|
Automatic changed-only republish (docs/metadata drift since the last tag — see
|
|
@@ -9,6 +45,17 @@ No source changes intended and no consumer action required.
|
|
|
9
45
|
## 0.7.21
|
|
10
46
|
### Unreleased
|
|
11
47
|
|
|
48
|
+
### /chat/new: the last two startup costs
|
|
49
|
+
|
|
50
|
+
- **Pinned agents read only the pins, only on screen.** `PinnedAgentsSection` waits for
|
|
51
|
+
`useInView` (never from a closed mobile sheet or collapsed rail), then calls the catalog's
|
|
52
|
+
`ensureFavoriteRows()` — `ues_list` + `agx_get_list_full?id=in.(…)` — instead of loading the
|
|
53
|
+
whole agent catalogue after page idle. It shows a loading skeleton and a retry on failure.
|
|
54
|
+
- **The splash paints its final footprint.** The server-painted composer shell carries the
|
|
55
|
+
composer's placeholder (no pulse), the landing no longer fades in from opacity 0, and the
|
|
56
|
+
quick-action row keeps its skeleton until the organization is set instead of vanishing. On a
|
|
57
|
+
phone the textarea placeholder was the page's Largest Contentful Paint at 5.7–15 s.
|
|
58
|
+
|
|
12
59
|
### /chat/new: the live residuals of the startup-read work
|
|
13
60
|
|
|
14
61
|
- **A minted id is a draft before its record exists.** `generateConversationId` remembers the ids this tab
|
|
@@ -20,6 +20,15 @@ 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 its placeholder in the real textarea's type and inset.
|
|
26
|
+
*
|
|
27
|
+
* THE PLACEHOLDER PAINTS WITH THE PAGE (2026-10-09). It is the largest text on
|
|
28
|
+
* a phone's /chat/new (larger than the greeting), so a shell without it made
|
|
29
|
+
* the live textarea's placeholder the page's Largest Contentful Paint seconds
|
|
30
|
+
* later. Same words, same size: the live box replaces it without a new paint
|
|
31
|
+
* winning. No pulse — an opacity animation would hide the text from that count.
|
|
32
|
+
*/
|
|
24
33
|
export declare function ChatSplashComposerShell(): import("react").JSX.Element;
|
|
25
34
|
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,8 @@ 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)]",
|
|
65
|
+
children: /* @__PURE__ */ jsx("div", { className: "px-1.5 py-1 text-[15px] leading-6 text-muted-foreground/60", children: SPLASH_PLACEHOLDER })
|
|
64
66
|
}
|
|
65
67
|
);
|
|
66
68
|
}
|
|
@@ -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,
|
|
@@ -22,7 +22,7 @@ import type { RequestInitiation } from "../../../types/instance.types.js";
|
|
|
22
22
|
import type { Resource } from "../../../resources/types.js";
|
|
23
23
|
import { type HostTurnInput } from "./execute-instance.thunk.js";
|
|
24
24
|
import { type AdoptedAgentJsonOptions, type HeadlessAgentJsonResult } from "./run-headless-agent-json.js";
|
|
25
|
-
export interface ContinueAgentJsonOptions extends Pick<AdoptedAgentJsonOptions, "surfaceKey" | "timeoutMs" | "pollIntervalMs" | "settleMs" | "expect" | "onResult" | "signal" | "failureMessages"> {
|
|
25
|
+
export interface ContinueAgentJsonOptions extends Pick<AdoptedAgentJsonOptions, "surfaceKey" | "timeoutMs" | "pollIntervalMs" | "settleMs" | "expect" | "onResult" | "signal" | "failureMessages" | "onAwaitingPerson" | "personWaitMs"> {
|
|
26
26
|
conversationId: string;
|
|
27
27
|
/** What the person typed for this turn. Exclusive with `hostTurn`. */
|
|
28
28
|
userInput?: string;
|
|
@@ -54,7 +54,9 @@ async function continueAgentJson(dispatch, getState, opts) {
|
|
|
54
54
|
...opts.expect ? { expect: opts.expect } : {},
|
|
55
55
|
...opts.onResult ? { onResult: opts.onResult } : {},
|
|
56
56
|
...opts.signal ? { signal: opts.signal } : {},
|
|
57
|
-
...opts.failureMessages ? { failureMessages: opts.failureMessages } : {}
|
|
57
|
+
...opts.failureMessages ? { failureMessages: opts.failureMessages } : {},
|
|
58
|
+
...opts.onAwaitingPerson ? { onAwaitingPerson: opts.onAwaitingPerson } : {},
|
|
59
|
+
...opts.personWaitMs !== void 0 ? { personWaitMs: opts.personWaitMs } : {}
|
|
58
60
|
});
|
|
59
61
|
} catch (err) {
|
|
60
62
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -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
|
}
|
|
@@ -168,6 +168,19 @@ export interface HeadlessAgentJsonOptions {
|
|
|
168
168
|
* discards. Pair it with `onResult` so an unmounting surface still persists.
|
|
169
169
|
*/
|
|
170
170
|
signal?: AbortSignal;
|
|
171
|
+
/**
|
|
172
|
+
* THE RUN IS WAITING ON THE PERSON, NOT FAILED. The turn ended parked on a question
|
|
173
|
+
* (`ask_person`, or a server park on a person): its answer is the turn that resumes once
|
|
174
|
+
* they answer. The wait pauses `timeoutMs`, fires this once so the surface can say so and
|
|
175
|
+
* show the question, re-reads the conversation when the ask closes (answered here, in
|
|
176
|
+
* another tab or by text), and resolves from the resumed turn. Bounded by `personWaitMs`.
|
|
177
|
+
*/
|
|
178
|
+
onAwaitingPerson?: (info: {
|
|
179
|
+
requestId: string;
|
|
180
|
+
conversationId: string;
|
|
181
|
+
}) => void;
|
|
182
|
+
/** Ceiling on waiting for the person once a run parks on them. Default 6h; `signal` stops it sooner. */
|
|
183
|
+
personWaitMs?: number;
|
|
171
184
|
/** Fires with the conversation id BEFORE the stream runs (live UI handle). */
|
|
172
185
|
onConversationCreated?: (conversationId: string) => void;
|
|
173
186
|
/**
|
|
@@ -271,6 +284,8 @@ export interface HeadlessAgentJsonResult {
|
|
|
271
284
|
* from a failure that carries its own reason.
|
|
272
285
|
*/
|
|
273
286
|
noResult?: boolean;
|
|
287
|
+
/** `true` when the run ended parked on the person and stopped waiting before they answered. */
|
|
288
|
+
awaitingPerson?: boolean;
|
|
274
289
|
}
|
|
275
290
|
/**
|
|
276
291
|
* 🚨 A WARNED CHOICE THAT ANSWERS WITHOUT A REQUIRED KEY FAILS PLAINLY.
|
|
@@ -333,6 +348,10 @@ export interface AdoptedAgentJsonOptions {
|
|
|
333
348
|
onResult?: HeadlessAgentJsonOptions["onResult"];
|
|
334
349
|
/** Stop waiting and harvest — same contract as `HeadlessAgentJsonOptions.signal`. */
|
|
335
350
|
signal?: AbortSignal;
|
|
351
|
+
/** Same contract as `HeadlessAgentJsonOptions.onAwaitingPerson`. */
|
|
352
|
+
onAwaitingPerson?: HeadlessAgentJsonOptions["onAwaitingPerson"];
|
|
353
|
+
/** Same contract as `HeadlessAgentJsonOptions.personWaitMs`. */
|
|
354
|
+
personWaitMs?: number;
|
|
336
355
|
/** Per-feature user-facing failure copy. */
|
|
337
356
|
failureMessages?: HeadlessAgentJsonOptions["failureMessages"];
|
|
338
357
|
/** See `deferNoJsonCapture` on `HeadlessAgentJsonOptions`. */
|
|
@@ -8,10 +8,17 @@ import {
|
|
|
8
8
|
selectExtractedJson,
|
|
9
9
|
selectFirstExtractedObject,
|
|
10
10
|
selectJsonExtractionComplete,
|
|
11
|
+
selectRequestAwaitingPerson,
|
|
11
12
|
selectRequestError,
|
|
12
13
|
selectRequestStatus,
|
|
13
14
|
selectRenderBlocksByType
|
|
14
15
|
} from "../active-requests/active-requests.selectors.js";
|
|
16
|
+
import {
|
|
17
|
+
selectLatestAnswerText as selectCommittedAnswerText,
|
|
18
|
+
selectLatestAssistantMessageId
|
|
19
|
+
} from "../messages/messages.selectors.js";
|
|
20
|
+
import { countOpenAsksForConversation } from "../../../runtime-reconnect/parked-on-person.js";
|
|
21
|
+
import { rereadAndFollow } from "../../../runtime-reconnect/reread-and-follow.js";
|
|
15
22
|
import {
|
|
16
23
|
selectLatestAnswerText,
|
|
17
24
|
selectLatestRequestId
|
|
@@ -44,6 +51,10 @@ function safeShape(err) {
|
|
|
44
51
|
const DEFAULT_TIMEOUT_MS = 12e4;
|
|
45
52
|
const DEFAULT_POLL_MS = 250;
|
|
46
53
|
const DEFAULT_SETTLE_MS = 6e3;
|
|
54
|
+
const DEFAULT_PERSON_WAIT_MS = 6 * 60 * 6e4;
|
|
55
|
+
const RECONNECT_WAIT_MS = 5 * 6e4;
|
|
56
|
+
const isConnectionLoss = (errorType) => errorType === "transport_lost" || errorType === "heartbeat_timeout";
|
|
57
|
+
const PERSON_ASK_POLL_MS = 1e4;
|
|
47
58
|
const DEFAULT_MESSAGES = {
|
|
48
59
|
streamError: "The agent failed before returning a result.",
|
|
49
60
|
noJson: "The agent finished but produced no structured JSON.",
|
|
@@ -243,6 +254,8 @@ async function adoptHeadlessAgentJson(dispatch, getState, opts) {
|
|
|
243
254
|
msgs,
|
|
244
255
|
expect: opts.expect ?? "json",
|
|
245
256
|
...opts.signal ? { signal: opts.signal } : {},
|
|
257
|
+
...opts.onAwaitingPerson ? { onAwaitingPerson: opts.onAwaitingPerson } : {},
|
|
258
|
+
personWaitMs: opts.personWaitMs ?? DEFAULT_PERSON_WAIT_MS,
|
|
246
259
|
deferNoJsonCapture: opts.deferNoJsonCapture === true
|
|
247
260
|
});
|
|
248
261
|
} catch (err) {
|
|
@@ -388,6 +401,8 @@ async function launchAndWait(dispatch, getState, opts) {
|
|
|
388
401
|
msgs,
|
|
389
402
|
expect: opts.expect ?? "json",
|
|
390
403
|
...opts.signal ? { signal: opts.signal } : {},
|
|
404
|
+
...opts.onAwaitingPerson ? { onAwaitingPerson: opts.onAwaitingPerson } : {},
|
|
405
|
+
personWaitMs: opts.personWaitMs ?? DEFAULT_PERSON_WAIT_MS,
|
|
391
406
|
deferNoJsonCapture: opts.deferNoJsonCapture === true
|
|
392
407
|
});
|
|
393
408
|
} catch (err) {
|
|
@@ -549,13 +564,45 @@ async function waitForExtraction(dispatch, getState, args) {
|
|
|
549
564
|
...base()
|
|
550
565
|
};
|
|
551
566
|
};
|
|
567
|
+
const answerSince = (state) => {
|
|
568
|
+
if (!state.messages) return `live\0${selectLatestAnswerText(conversationId)(state)}`;
|
|
569
|
+
const committed = selectCommittedAnswerText(conversationId)(state);
|
|
570
|
+
if (committed.trim()) return `${selectLatestAssistantMessageId(conversationId)(state) ?? ""}\0${committed}`;
|
|
571
|
+
return `live\0${selectLatestAnswerText(conversationId)(state)}`;
|
|
572
|
+
};
|
|
573
|
+
const unanswered = (why) => ({
|
|
574
|
+
success: false,
|
|
575
|
+
data: null,
|
|
576
|
+
error: why === "timeout" ? "Your agent's question was not answered in time." : "Stopped waiting for your answer.",
|
|
577
|
+
awaitingPerson: true,
|
|
578
|
+
...base()
|
|
579
|
+
});
|
|
580
|
+
const startMarker = answerSince(getState());
|
|
581
|
+
let lostAt = null;
|
|
582
|
+
const reconnectedAnswer = () => {
|
|
583
|
+
const marker = answerSince(getState());
|
|
584
|
+
if (marker === startMarker) return null;
|
|
585
|
+
return fuzzyFallback(marker.slice(marker.indexOf("\0") + 1));
|
|
586
|
+
};
|
|
587
|
+
let person = null;
|
|
552
588
|
try {
|
|
553
|
-
while (Date.now() - start < args.timeoutMs) {
|
|
589
|
+
while (person ? Date.now() - person.since < args.personWaitMs : Date.now() - start < args.timeoutMs || lostAt !== null && Date.now() - lostAt < RECONNECT_WAIT_MS) {
|
|
554
590
|
if (args.signal?.aborted) {
|
|
555
|
-
return settle("aborted", noResultMsg);
|
|
591
|
+
return person ? unanswered("aborted") : settle("aborted", noResultMsg);
|
|
556
592
|
}
|
|
557
593
|
const state = getState();
|
|
558
594
|
const status = selectRequestStatus(requestId)(state);
|
|
595
|
+
if (lostAt !== null) {
|
|
596
|
+
const recovered = reconnectedAnswer();
|
|
597
|
+
if (recovered != null) return { success: true, data: recovered, ...base() };
|
|
598
|
+
}
|
|
599
|
+
if (status === "error" && isConnectionLoss(selectRequestError(requestId)(state)?.error_type)) {
|
|
600
|
+
lostAt ??= Date.now();
|
|
601
|
+
if (Date.now() - lostAt < RECONNECT_WAIT_MS) {
|
|
602
|
+
await sleep(args.pollMs);
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
559
606
|
if (status === "error") {
|
|
560
607
|
const reqError = selectRequestError(requestId)(state);
|
|
561
608
|
const { data } = resolveRunData(getState, requestId, conversationId);
|
|
@@ -572,6 +619,44 @@ async function waitForExtraction(dispatch, getState, args) {
|
|
|
572
619
|
...base()
|
|
573
620
|
};
|
|
574
621
|
}
|
|
622
|
+
if (!person && status !== void 0 && TERMINAL_STATUSES.has(status) && selectRequestAwaitingPerson(requestId)(state)) {
|
|
623
|
+
person = {
|
|
624
|
+
since: Date.now(),
|
|
625
|
+
baseline: answerSince(state),
|
|
626
|
+
askedAt: Date.now(),
|
|
627
|
+
following: false,
|
|
628
|
+
changedAt: null
|
|
629
|
+
};
|
|
630
|
+
try {
|
|
631
|
+
args.onAwaitingPerson?.({ requestId, conversationId });
|
|
632
|
+
} catch (err) {
|
|
633
|
+
console.error("[runHeadlessAgentJson] onAwaitingPerson threw", err);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (person) {
|
|
637
|
+
const now = Date.now();
|
|
638
|
+
if (!person.following && now - person.askedAt >= PERSON_ASK_POLL_MS) {
|
|
639
|
+
person.askedAt = now;
|
|
640
|
+
try {
|
|
641
|
+
if (await countOpenAsksForConversation(conversationId) === 0) {
|
|
642
|
+
person.following = true;
|
|
643
|
+
await rereadAndFollow(dispatch, conversationId, "runHeadlessAgentJson: the ask closed");
|
|
644
|
+
}
|
|
645
|
+
} catch {
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
const now2 = getState();
|
|
649
|
+
const marker = answerSince(now2);
|
|
650
|
+
const answer = marker.slice(marker.indexOf("\0") + 1);
|
|
651
|
+
if (answer.trim() && marker !== person.baseline) {
|
|
652
|
+
const data = fuzzyFallback(answer);
|
|
653
|
+
if (data != null) return { success: true, data, ...base() };
|
|
654
|
+
person.changedAt ??= now;
|
|
655
|
+
if (now - person.changedAt > args.settleMs) return settle("stream-ended", noResultMsg);
|
|
656
|
+
}
|
|
657
|
+
await sleep(args.pollMs);
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
575
660
|
if (selectJsonExtractionComplete(requestId)(state)) {
|
|
576
661
|
return settle("extraction-complete", noResultMsg);
|
|
577
662
|
}
|
|
@@ -586,6 +671,18 @@ async function waitForExtraction(dispatch, getState, args) {
|
|
|
586
671
|
}
|
|
587
672
|
await sleep(args.pollMs);
|
|
588
673
|
}
|
|
674
|
+
if (person) return unanswered("timeout");
|
|
675
|
+
if (lostAt !== null && selectRequestStatus(requestId)(getState()) === "error") {
|
|
676
|
+
const reqError = selectRequestError(requestId)(getState());
|
|
677
|
+
const { data } = resolveRunData(getState, requestId, conversationId);
|
|
678
|
+
return {
|
|
679
|
+
success: false,
|
|
680
|
+
data,
|
|
681
|
+
error: reqError?.user_message ?? reqError?.message ?? msgs.streamError,
|
|
682
|
+
...reqError?.error_type ? { errorDetail: reqError.error_type } : {},
|
|
683
|
+
...base()
|
|
684
|
+
};
|
|
685
|
+
}
|
|
589
686
|
return settle("timeout", msgs.timeout);
|
|
590
687
|
} finally {
|
|
591
688
|
dispatch(releaseRequestForViewer({ requestId, viewerId }));
|
|
@@ -5,6 +5,7 @@ import { useState, useEffect, useLayoutEffect, useMemo, useCallback, useRef } fr
|
|
|
5
5
|
import { cn } from "@ai-matrx/design-system";
|
|
6
6
|
import { WIDE_FIGURE_HOST_CLASS, WIDE_FIGURE_PROSE_CLIP_CLASS } from "../../agents/components/shared/assistant-message-layout.js";
|
|
7
7
|
import { splitContentIntoBlocksV2 } from "@ai-matrx/rich-content/display/markdown-classification/processors/utils/content-splitter-v2";
|
|
8
|
+
import { isWaitingForFirstContent } from "./waiting-for-content.js";
|
|
8
9
|
import { renderSettledFromRecord, settledOneShotBlocks } from "@ai-matrx/rich-content/display/chat-markdown/settle-stream-blocks";
|
|
9
10
|
import { expandTextBlocksInList } from "@ai-matrx/rich-content/display/markdown-classification/processors/utils/expand-text-blocks";
|
|
10
11
|
import { reuseUnchangedBlocks } from "@ai-matrx/rich-content/display/chat-markdown/stable-blocks";
|
|
@@ -316,13 +317,19 @@ const EnhancedChatMarkdownInternal = ({
|
|
|
316
317
|
const currentContent = editedContent ?? resolvedContent;
|
|
317
318
|
const hasRequestOrTaskId = requestId || taskId;
|
|
318
319
|
const hasReceivedNonTextContent = serverProcessedBlocks && serverProcessedBlocks.length > 0 || hasReduxRenderBlocks;
|
|
319
|
-
const isWaitingForContent = hasRequestOrTaskId && !resolvedContent.trim() && !hasReceivedNonTextContent;
|
|
320
|
-
const hasUnifiedSpecial = !settledFromRecord && (streamSlotStart !== void 0 || streamSlotEnd !== void 0 || agentCallId !== void 0 || unifiedSlots.some(
|
|
321
|
-
(s) => s.kind === "tool" || s.kind === "status" || s.kind === "error" || s.kind === "thinking" || s.kind === "render_block" && blockCarriesDataNotText(renderBlocksMap[s.blockId])
|
|
322
|
-
));
|
|
323
320
|
const hasDbInterleavedSpecial = messageInterleavedContent.some(
|
|
324
321
|
(s) => s.type === "db_tool" || s.type === "thinking"
|
|
325
322
|
);
|
|
323
|
+
const isWaitingForContent = isWaitingForFirstContent({
|
|
324
|
+
hasRequestOrTaskId: !!hasRequestOrTaskId,
|
|
325
|
+
resolvedContent,
|
|
326
|
+
hasReceivedNonTextContent: !!hasReceivedNonTextContent,
|
|
327
|
+
settledFromRecord,
|
|
328
|
+
recordHasNonText: hasDbInterleavedSpecial
|
|
329
|
+
});
|
|
330
|
+
const hasUnifiedSpecial = !settledFromRecord && (streamSlotStart !== void 0 || streamSlotEnd !== void 0 || agentCallId !== void 0 || unifiedSlots.some(
|
|
331
|
+
(s) => s.kind === "tool" || s.kind === "status" || s.kind === "error" || s.kind === "thinking" || s.kind === "render_block" && blockCarriesDataNotText(renderBlocksMap[s.blockId])
|
|
332
|
+
));
|
|
326
333
|
const toolLifecycleMapSelector = useMemo(
|
|
327
334
|
() => requestId ? selectToolLifecycleMap(requestId) : () => void 0,
|
|
328
335
|
[requestId]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is this assistant turn still waiting for its first content (the "Processing…" shimmer)?
|
|
3
|
+
*
|
|
4
|
+
* A turn that ENDED on a tool call with no text — an `ask_person` parked on the person — is
|
|
5
|
+
* settled: its record holds the call, and the call is what the person must see. Before this rule
|
|
6
|
+
* the empty text kept the shimmer up forever over the question it was hiding (the Applet
|
|
7
|
+
* builder, live 2026-10-09: "Processing…" while the server waited on a choose_one ask).
|
|
8
|
+
*/
|
|
9
|
+
export declare function isWaitingForFirstContent(args: {
|
|
10
|
+
hasRequestOrTaskId: boolean;
|
|
11
|
+
resolvedContent: string;
|
|
12
|
+
hasReceivedNonTextContent: boolean;
|
|
13
|
+
settledFromRecord: boolean;
|
|
14
|
+
recordHasNonText: boolean;
|
|
15
|
+
}): boolean;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
function isWaitingForFirstContent(args) {
|
|
2
|
+
if (!args.hasRequestOrTaskId) return false;
|
|
3
|
+
if (args.resolvedContent.trim()) return false;
|
|
4
|
+
if (args.hasReceivedNonTextContent) return false;
|
|
5
|
+
if (args.settledFromRecord && args.recordHasNonText) return false;
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
export {
|
|
9
|
+
isWaitingForFirstContent
|
|
10
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-matrx/chat",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.24",
|
|
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/agents": "latest",
|
|
4685
|
+
"@ai-matrx/alchemy": "latest",
|
|
4686
|
+
"@ai-matrx/associations": "latest",
|
|
4684
4687
|
"@ai-matrx/browser-audio": "latest",
|
|
4685
4688
|
"@ai-matrx/canvas": "latest",
|
|
4686
|
-
"@ai-matrx/associations": "latest",
|
|
4687
4689
|
"@ai-matrx/content-ir": "latest",
|
|
4688
|
-
"@ai-matrx/
|
|
4689
|
-
"@ai-matrx/
|
|
4690
|
+
"@ai-matrx/content-ir-react": "latest",
|
|
4691
|
+
"@ai-matrx/data": "latest",
|
|
4690
4692
|
"@ai-matrx/design-system": "latest",
|
|
4693
|
+
"@ai-matrx/icons": "latest",
|
|
4694
|
+
"@ai-matrx/diff": "latest",
|
|
4691
4695
|
"@ai-matrx/kit": "latest",
|
|
4692
4696
|
"@ai-matrx/media": "latest",
|
|
4693
4697
|
"@ai-matrx/print": "latest",
|
|
4694
|
-
"@ai-matrx/diff": "latest",
|
|
4695
4698
|
"@ai-matrx/realtime": "latest",
|
|
4696
4699
|
"@ai-matrx/records": "latest",
|
|
4697
4700
|
"@ai-matrx/records-ui": "latest",
|
|
4698
4701
|
"@ai-matrx/rich-content": "latest",
|
|
4699
|
-
"@ai-matrx/
|
|
4700
|
-
"@ai-matrx/rich-editor": "latest",
|
|
4701
|
-
"@ai-matrx/content-ir-react": "latest",
|
|
4702
|
-
"@ai-matrx/icons": "latest"
|
|
4702
|
+
"@ai-matrx/rich-editor": "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": "cab8d9bc66fa516a57684222b15e62677a35d4fa"
|
|
4822
4822
|
}
|