@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.
Files changed (26) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/agents/components/chat/NewChatGreeting.d.ts +10 -1
  3. package/dist/agents/components/chat/NewChatGreeting.js +4 -2
  4. package/dist/agents/components/chat/PinnedAgentsSection.d.ts +6 -3
  5. package/dist/agents/components/chat/PinnedAgentsSection.js +31 -10
  6. package/dist/agents/components/inputs/smart-input/SmartAgentInput.d.ts +1 -1
  7. package/dist/agents/components/inputs/smart-input/SmartAgentInput.js +9 -0
  8. package/dist/agents/components/inputs/smart-input/composer/ComposerSplash.js +1 -2
  9. package/dist/agents/components/inputs/variable-input-variations/AgentVariableForm.js +5 -5
  10. package/dist/agents/components/messages-display/AgentConversationDisplay.js +23 -1
  11. package/dist/agents/components/shared/AgentConversationColumn.js +1 -11
  12. package/dist/agents/redux/execution-system/messages/messages.selectors.d.ts +3 -1
  13. package/dist/agents/redux/execution-system/messages/messages.selectors.js +5 -1
  14. package/dist/agents/redux/execution-system/messages/messages.slice.d.ts +18 -1
  15. package/dist/agents/redux/execution-system/messages/messages.slice.js +12 -1
  16. package/dist/agents/redux/execution-system/thunks/continue-agent-json.d.ts +1 -1
  17. package/dist/agents/redux/execution-system/thunks/continue-agent-json.js +3 -1
  18. package/dist/agents/redux/execution-system/thunks/conversation-bundle.d.ts +12 -0
  19. package/dist/agents/redux/execution-system/thunks/conversation-bundle.js +22 -0
  20. package/dist/agents/redux/execution-system/thunks/load-conversation.thunk.js +23 -7
  21. package/dist/agents/redux/execution-system/thunks/run-headless-agent-json.d.ts +19 -0
  22. package/dist/agents/redux/execution-system/thunks/run-headless-agent-json.js +99 -2
  23. package/dist/ui/markdown-stream/EnhancedChatMarkdown.js +11 -4
  24. package/dist/ui/markdown-stream/waiting-for-content.d.ts +15 -0
  25. package/dist/ui/markdown-stream/waiting-for-content.js +10 -0
  26. 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
- /** The splash composer's footprint while its conversation is created. */
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: "How can I help you today?" }
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] animate-pulse rounded-[22px] border border-border bg-card"
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
- * The catalogue is asked for after the page is idle (see the effect below),
18
- * so the section is absent at first paint and appears once the list lands —
19
- * or sooner, when the person opens any agent picker.
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 { ensureAgentCatalog } from "../../identity/agent-identity.js";
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
- const controller = new AbortController();
31
- void whenPageIdle(controller.signal).then((idle) => {
32
- if (idle) void ensureAgentCatalog();
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) return null;
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) return null;
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: showVariables,
63
- onClick: () => dispatch(toggleVariablePanel(conversationId)),
62
+ open,
63
+ onClick: () => setOpen((v) => !v),
64
64
  chevron: "end",
65
- title: showVariables ? "Fill in the details below" : `${filledCount}/${definitions.length} details provided`,
65
+ title: open ? "Fill in the details below" : `${filledCount}/${definitions.length} details provided`,
66
66
  className: "shrink-0"
67
67
  }
68
68
  ),
69
- showVariables && /* @__PURE__ */ jsxs("div", { className: "border-t border-border flex-1 min-h-0 overflow-y-auto", children: [
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 { nextTranscriptPosition } from "./messages.slice.js";
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">, clearMessages: import("@reduxjs/toolkit").ActionCreatorWithPayload<string, "messages/clearMessages">;
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
- dispatch(
122
- setMessagesHydrationFailure({
123
- conversationId,
124
- failure: NOT_READABLE_DETAIL
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.22",
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/agents": "latest",
4689
- "@ai-matrx/alchemy": "latest",
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/data": "latest",
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": "ec969cf6169fd1c376feb4eb7e4ec3e5aa07e607"
4821
+ "gitHead": "cab8d9bc66fa516a57684222b15e62677a35d4fa"
4822
4822
  }