@ai-matrx/chat 0.3.2 → 0.3.4

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 (34) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/agent-copy/export.d.ts +1 -1
  3. package/dist/agent-copy/export.js +5 -16
  4. package/dist/agents/components/context-items/bodies/WebpageBody.js +12 -5
  5. package/dist/agents/components/debug/SandboxFileViewer.js +31 -22
  6. package/dist/agents/components/messages-display/TranscriptIntegrityCopyButton.js +14 -3
  7. package/dist/agents/components/messages-display/assistant/AgentAssistantMessage.js +18 -1
  8. package/dist/agents/components/run/AssistantWarning.js +12 -0
  9. package/dist/agents/components/run/IncompleteNote.d.ts +14 -0
  10. package/dist/agents/components/run/IncompleteNote.js +21 -0
  11. package/dist/agents/components/run-controls/PayloadTab.js +5 -1
  12. package/dist/agents/components/run-controls/panels/BackendTargetPanel.js +12 -1
  13. package/dist/agents/hooks/useAgentAutoSave.js +8 -3
  14. package/dist/agents/redux/execution-system/active-requests/active-requests.selectors.d.ts +2 -1
  15. package/dist/agents/redux/execution-system/active-requests/active-requests.selectors.js +2 -1
  16. package/dist/agents/redux/execution-system/active-requests/answer-kept-warning.d.ts +7 -0
  17. package/dist/agents/redux/execution-system/active-requests/answer-kept-warning.js +12 -0
  18. package/dist/agents/redux/execution-system/instance-ui-state/components/InstanceUIStateCore.js +68 -9
  19. package/dist/agents/redux/execution-system/instance-ui-state/components/InstanceUIStateList.js +30 -8
  20. package/dist/agents/redux/execution-system/messages/messages.selectors.d.ts +16 -0
  21. package/dist/agents/redux/execution-system/messages/messages.selectors.js +47 -1
  22. package/dist/agents/redux/execution-system/messages/turn-outcome.d.ts +26 -0
  23. package/dist/agents/redux/execution-system/messages/turn-outcome.js +28 -0
  24. package/dist/agents/redux/execution-system/thunks/process-stream.js +17 -0
  25. package/dist/agents/redux/execution-system/thunks/terminal-request-failure.d.ts +27 -0
  26. package/dist/agents/redux/execution-system/thunks/terminal-request-failure.js +56 -0
  27. package/dist/cx-chat/components/messages/AssistantMessage.js +4 -1
  28. package/dist/cx-chat/components/messages/UserMessage.js +1 -8
  29. package/dist/cx-conversation/AssistantMessage.js +4 -1
  30. package/dist/cx-conversation/UserMessage.js +1 -8
  31. package/dist/tool-call-visualization/components/ToolTabBodies.js +11 -4
  32. package/dist/voice-agent/components/VoiceDebugPanel.js +22 -3
  33. package/dist/window-panels/windows/agents/ObservationalMemoryWindow.js +20 -5
  34. package/package.json +8 -8
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @ai-matrx/chat
2
2
 
3
+ ## 0.3.4 — 2026-10-07
4
+ ### Re-sync before matrx-frontend consumes the published package (P27)
5
+
6
+ - Carries every matrx-frontend `packages/chat` edit after `d8078d15b2` plus the in-flight working-tree
7
+ edits in the shared checkout: an incomplete answer reads the same live and reloaded
8
+ (`messages/turn-outcome`, `active-requests/answer-kept-warning`, `run/IncompleteNote`), ALC-19 copy
9
+ results honored (`copyText` boolean). The kept-out `ToolTabBodies` CopyButton shim stays out.
10
+ - Consumer action: none.
11
+
12
+ ## 0.3.3 — 2026-10-07
13
+ ### Re-sync before matrx-frontend consumes the published package (P27)
14
+
15
+ - Carries every
16
+ matrx-frontend `packages/chat` edit after `7f630d97d9`: a server-ended failed request is a failed turn
17
+ (`thunks/terminal-request-failure`, process-stream, messages selectors), agent autosave recovery.
18
+ Its suite needs the app's content-ir host, so it lives app-side with the other host suites.
19
+ - Consumer action: none.
20
+
3
21
  ## 0.3.2
4
22
 
5
23
  Automatic changed-only republish (docs/metadata drift since the last tag — see
@@ -25,7 +25,7 @@ export interface ExportItem {
25
25
  export declare function exportFilename(base: string, extension: string): string;
26
26
  export declare function jsonExportItem(data: unknown | (() => unknown), label?: string): ExportItem;
27
27
  export declare function textExportItem(content: string | (() => string), label: string, extension?: string): ExportItem;
28
- /** Rows → CSV. Columns: explicit list, or the union of keys across rows. */
28
+ /** Rows → CSV (Alchemy's one writer). Columns: explicit list, or the union of keys across rows. */
29
29
  export declare function rowsToCsv(rows: Array<Record<string, unknown>>, columns?: Array<{
30
30
  key: string;
31
31
  header: string;
@@ -1,3 +1,4 @@
1
+ import { toDelimitedText } from "@ai-matrx/alchemy/operate/read";
1
2
  function exportFilename(base, extension) {
2
3
  const slug = base.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
3
4
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
@@ -34,27 +35,15 @@ function textExportItem(content, label, extension = "txt") {
34
35
  })
35
36
  };
36
37
  }
37
- function csvEscape(value) {
38
- return /[",\n\r]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value;
39
- }
40
38
  function rowsToCsv(rows, columns) {
41
39
  const cols = columns ?? [...new Set(rows.flatMap((row) => Object.keys(row)))].map((key) => ({
42
40
  key,
43
41
  header: key
44
42
  }));
45
- const lines = [cols.map((c) => csvEscape(c.header)).join(",")];
46
- for (const row of rows) {
47
- lines.push(
48
- cols.map((c) => {
49
- const value = row[c.key];
50
- if (value === null || value === void 0) return "";
51
- return csvEscape(
52
- typeof value === "object" ? JSON.stringify(value) : String(value)
53
- );
54
- }).join(",")
55
- );
56
- }
57
- return lines.join("\n");
43
+ return toDelimitedText(
44
+ cols.map((c) => c.header),
45
+ rows.map((row) => cols.map((c) => row[c.key] ?? null))
46
+ );
58
47
  }
59
48
  function csvExportItem(rows, label = "CSV", columns) {
60
49
  return {
@@ -11,10 +11,7 @@ import {
11
11
  } from "@ai-matrx/design-system";
12
12
  import { toast } from "../../../../host/notify.js";
13
13
  import { WebpageSnapshotView } from "../../../../host/ui-slots.js";
14
- import {
15
- webpageTitle,
16
- webpageUrl
17
- } from "../../../resources/webpage-snapshot.js";
14
+ import { webpageTitle, webpageUrl } from "../../../resources/webpage-snapshot.js";
18
15
  import { Button } from "@ai-matrx/design-system/controls";
19
16
  function firstWebpage(item) {
20
17
  return item.refs.webpages?.[0] ?? null;
@@ -61,7 +58,17 @@ function WebpageFooter({ item }) {
61
58
  return /* @__PURE__ */ jsxs(Fragment, { children: [
62
59
  /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate font-mono text-[11px] text-muted-foreground", children: url }),
63
60
  /* @__PURE__ */ jsxs(Tooltip, { children: [
64
- /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "quiet", icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}), glyphTone: copied ? "success" : void 0, onClick: copy, "aria-label": "Copy webpage URL", className: "ml-auto" }) }),
61
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
62
+ Button,
63
+ {
64
+ variant: "quiet",
65
+ icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}),
66
+ glyphTone: copied ? "success" : void 0,
67
+ onClick: copy,
68
+ "aria-label": "Copy webpage URL",
69
+ className: "ml-auto"
70
+ }
71
+ ) }),
65
72
  /* @__PURE__ */ jsx(TooltipContent, { children: "Copy URL" })
66
73
  ] }),
67
74
  /* @__PURE__ */ jsxs(Tooltip, { children: [
@@ -23,28 +23,27 @@ function SandboxFileViewer({
23
23
  const [copied, setCopied] = useState(false);
24
24
  const adapterRef = useRef(null);
25
25
  if (!adapterRef.current || adapterRef.current.instanceId !== sandboxRowId) {
26
- adapterRef.current = createSandboxFilesystemAdapter(sandboxRowId);
26
+ adapterRef.current = createSandboxFilesystemAdapter(
27
+ sandboxRowId
28
+ );
27
29
  }
28
- const load = useCallback(
29
- async (p) => {
30
- const target = p.trim();
31
- if (!target) return;
32
- setLoading(true);
33
- setError(null);
34
- try {
35
- const adapter = adapterRef.current;
36
- if (!adapter) throw new Error("Sandbox adapter not initialized");
37
- const text = await adapter.readFile(target);
38
- setContent(text);
39
- } catch (err) {
40
- setContent(null);
41
- setError(err instanceof Error ? err.message : "Failed to read file");
42
- } finally {
43
- setLoading(false);
44
- }
45
- },
46
- []
47
- );
30
+ const load = useCallback(async (p) => {
31
+ const target = p.trim();
32
+ if (!target) return;
33
+ setLoading(true);
34
+ setError(null);
35
+ try {
36
+ const adapter = adapterRef.current;
37
+ if (!adapter) throw new Error("Sandbox adapter not initialized");
38
+ const text = await adapter.readFile(target);
39
+ setContent(text);
40
+ } catch (err) {
41
+ setContent(null);
42
+ setError(err instanceof Error ? err.message : "Failed to read file");
43
+ } finally {
44
+ setLoading(false);
45
+ }
46
+ }, []);
48
47
  useEffect(() => {
49
48
  if (initialPath) void load(initialPath);
50
49
  }, [initialPath, sandboxRowId, load]);
@@ -91,7 +90,17 @@ function SandboxFileViewer({
91
90
  ),
92
91
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-2 py-1 border-b border-border bg-muted/30", children: [
93
92
  /* @__PURE__ */ jsx("span", { className: "font-mono text-[11px] text-muted-foreground truncate", children: lockPath ? initialPath : content != null ? path : "" }),
94
- content != null && /* @__PURE__ */ jsx(Button, { variant: "quiet", icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}), glyphTone: copied ? "success" : void 0, onClick: handleCopy, className: "shrink-0", children: "Copy" })
93
+ content != null && /* @__PURE__ */ jsx(
94
+ Button,
95
+ {
96
+ variant: "quiet",
97
+ icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}),
98
+ glyphTone: copied ? "success" : void 0,
99
+ onClick: handleCopy,
100
+ className: "shrink-0",
101
+ children: "Copy"
102
+ }
103
+ )
95
104
  ] }),
96
105
  /* @__PURE__ */ jsx("div", { className: "flex-1 min-h-0 overflow-auto", children: loading && content == null ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 p-3 text-xs text-muted-foreground", children: [
97
106
  /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }),
@@ -43,11 +43,11 @@ function TranscriptIntegrityCopyButtonInner({
43
43
  effectiveVisibleGroupLimit
44
44
  });
45
45
  publish({
46
- "Conversation": conversationId,
46
+ Conversation: conversationId,
47
47
  "Loaded rows": report.spine.loadedRows,
48
48
  "Groups rendered": `${report.groups.filter((g) => g.rendered).length}/${report.groups.length}`,
49
49
  "Stream phase": report.request.streamPhase,
50
- "Anomalies": report.anomalies.length === 0 ? "none" : report.anomalies
50
+ Anomalies: report.anomalies.length === 0 ? "none" : report.anomalies
51
51
  });
52
52
  }, [
53
53
  isActive,
@@ -78,7 +78,18 @@ function TranscriptIntegrityCopyButtonInner({
78
78
  );
79
79
  }
80
80
  };
81
- return /* @__PURE__ */ jsx(Button, { variant: "quiet", icon: /* @__PURE__ */ jsx(Stethoscope, {}), onClick: () => void copy(), title: "Copy transcript integrity report (admin) \u2014 paste it to an agent when a message is missing", "aria-label": "Copy transcript integrity report", "data-testid": "transcript-integrity-copy", className: "absolute top-2 right-4 z-10" });
81
+ return /* @__PURE__ */ jsx(
82
+ Button,
83
+ {
84
+ variant: "quiet",
85
+ icon: /* @__PURE__ */ jsx(Stethoscope, {}),
86
+ onClick: () => void copy(),
87
+ title: "Copy transcript integrity report (admin) \u2014 paste it to an agent when a message is missing",
88
+ "aria-label": "Copy transcript integrity report",
89
+ "data-testid": "transcript-integrity-copy",
90
+ className: "absolute top-2 right-4 z-10"
91
+ }
92
+ );
82
93
  }
83
94
  export {
84
95
  TranscriptIntegrityCopyButton
@@ -34,8 +34,15 @@ import {
34
34
  isFailedRecord,
35
35
  extractRecordError,
36
36
  selectIsLatestAssistantMessage,
37
- persistedBodyBlocks
37
+ persistedBodyBlocks,
38
+ incompleteAnswerReason
38
39
  } from "../../../redux/execution-system/messages/messages.selectors.js";
40
+ import {
41
+ incompleteDetail,
42
+ incompleteLabel,
43
+ isAnswerKeptWarning
44
+ } from "../../../redux/execution-system/messages/turn-outcome.js";
45
+ import { IncompleteNote } from "../../run/IncompleteNote.js";
39
46
  import {
40
47
  isAttachmentMessagePart,
41
48
  isInlineAssistantMedia
@@ -443,6 +450,16 @@ function AgentAssistantMessage({
443
450
  !isClosedStreamSegment && displaySources.length > 0 && /* @__PURE__ */ jsx(MessageSourcesRow, { sources: displaySources, className: "mt-2" }),
444
451
  !isStreamActive && /* @__PURE__ */ jsx(PrefillNote, { metadata: record?.metadata }),
445
452
  !isStreamActive && /* @__PURE__ */ jsx(StoppedNote, { metadata: record?.metadata, requestStatus }),
453
+ !isStreamActive && !visibleWarnings?.some(isAnswerKeptWarning) && (() => {
454
+ const reason = incompleteAnswerReason(record);
455
+ return reason ? /* @__PURE__ */ jsx(
456
+ IncompleteNote,
457
+ {
458
+ label: incompleteLabel(reason),
459
+ detail: incompleteDetail(reason)
460
+ }
461
+ ) : null;
462
+ })(),
446
463
  isStreamActive && !failed && (phase === "text_streaming" || phase === "interstitial") && /* @__PURE__ */ jsx("div", { className: "relative h-0 overflow-visible", children: /* @__PURE__ */ jsx(BreathingOrb, { className: "absolute left-0 top-[3px]", size: 40 }) })
447
464
  ] }),
448
465
  !isClosedStreamSegment && visibleWarnings?.map((warning, index) => /* @__PURE__ */ jsx(
@@ -3,8 +3,20 @@ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useState } from "react";
4
4
  import { AlertTriangle } from "lucide-react";
5
5
  import { Button } from "@ai-matrx/design-system/controls";
6
+ import { IncompleteNote } from "./IncompleteNote.js";
7
+ import { isAnswerKeptWarning } from "../../redux/execution-system/active-requests/answer-kept-warning.js";
8
+ import { incompleteLabel } from "../../redux/execution-system/messages/turn-outcome.js";
6
9
  function AssistantWarning({ warning }) {
7
10
  const [showDetails, setShowDetails] = useState(false);
11
+ if (isAnswerKeptWarning(warning)) {
12
+ return /* @__PURE__ */ jsx(
13
+ IncompleteNote,
14
+ {
15
+ label: incompleteLabel(warning.code),
16
+ detail: warning.user_message?.trim() || void 0
17
+ }
18
+ );
19
+ }
8
20
  const message = warning.user_message?.trim() || "The response completed with a warning.";
9
21
  const detail = warning.system_message?.trim();
10
22
  const hasDetails = Boolean(
@@ -0,0 +1,14 @@
1
+ interface IncompleteNoteProps {
2
+ /** The short label ("Ending may be cut off"). */
3
+ label: string;
4
+ /** The reason-specific sentence, shown under Details. */
5
+ detail?: string;
6
+ }
7
+ /**
8
+ * The quiet mark on an answer that was delivered and kept but whose end may be
9
+ * missing (recitation stop, length limit). Live (from the stream warning) and
10
+ * after a reload (from the saved finish reason) both render THIS component, so
11
+ * the two never look different. A state marker — not an error, no Retry.
12
+ */
13
+ export declare function IncompleteNote({ label, detail }: IncompleteNoteProps): import("react").JSX.Element;
14
+ export {};
@@ -0,0 +1,21 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useState } from "react";
4
+ import { AlertTriangle } from "lucide-react";
5
+ import { Button } from "@ai-matrx/design-system/controls";
6
+ function IncompleteNote({ label, detail }) {
7
+ const [open, setOpen] = useState(false);
8
+ return /* @__PURE__ */ jsxs("div", { className: "mt-1 text-xs", "data-testid": "incomplete-note", children: [
9
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-2.5 gap-y-1", children: [
10
+ /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5 text-amber-700 dark:text-amber-400", children: [
11
+ /* @__PURE__ */ jsx(AlertTriangle, { className: "h-3.5 w-3.5 shrink-0" }),
12
+ label
13
+ ] }),
14
+ detail && /* @__PURE__ */ jsx(Button, { variant: "link", onClick: () => setOpen((v) => !v), children: open ? "Hide details" : "Details" })
15
+ ] }),
16
+ detail && open && /* @__PURE__ */ jsx("div", { className: "mt-1 rounded bg-amber-500/10 px-2 py-1.5 text-[11px] leading-relaxed text-muted-foreground", children: detail })
17
+ ] });
18
+ }
19
+ export {
20
+ IncompleteNote
21
+ };
@@ -115,7 +115,11 @@ function roledImagesOf(messages) {
115
115
  continue;
116
116
  }
117
117
  const source = typeof b.url === "string" ? b.url : String(b.file_id ?? "");
118
- rows.push({ role: b.role, variable: variableNameOfImageUrl(source), source });
118
+ rows.push({
119
+ role: b.role,
120
+ variable: variableNameOfImageUrl(source),
121
+ source
122
+ });
119
123
  }
120
124
  }
121
125
  return rows;
@@ -44,7 +44,18 @@ function CopyableValue({
44
44
  children: value
45
45
  }
46
46
  ),
47
- /* @__PURE__ */ jsx(Button, { variant: "quiet", icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}), glyphTone: copied ? "success" : void 0, onClick: handleCopy, title: "Copy", "aria-label": "Copy", className: "shrink-0" })
47
+ /* @__PURE__ */ jsx(
48
+ Button,
49
+ {
50
+ variant: "quiet",
51
+ icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}),
52
+ glyphTone: copied ? "success" : void 0,
53
+ onClick: handleCopy,
54
+ title: "Copy",
55
+ "aria-label": "Copy",
56
+ className: "shrink-0"
57
+ }
58
+ )
48
59
  ] });
49
60
  }
50
61
  function Row({
@@ -90,9 +90,14 @@ function useAgentAutoSave(agentId) {
90
90
  try {
91
91
  const snapshot = { _dirty: true };
92
92
  if (record._dirtyFields) {
93
- for (const field of Object.keys(
94
- record._dirtyFields
95
- )) {
93
+ const fields = new Set(
94
+ Object.keys(record._dirtyFields)
95
+ );
96
+ if (fields.has("modelId") || fields.has("settings")) {
97
+ fields.add("modelId");
98
+ fields.add("settings");
99
+ }
100
+ for (const field of fields) {
96
101
  snapshot[field] = readField(record, field);
97
102
  }
98
103
  }
@@ -3356,7 +3356,8 @@ export declare const selectHighWarnings: (requestId: string) => ((state: {
3356
3356
  argsMemoize: typeof import("reselect").weakMapMemoize;
3357
3357
  };
3358
3358
  /**
3359
- * High-severity warnings plus any warning whose `code` is explicitly
3359
+ * High-severity warnings, any warning that says the answer was kept (an
3360
+ * incomplete answer, never silent) and any warning whose `code` is explicitly
3360
3361
  * promoted (see `PROMOTED_WARNING_CODES`). This is the set `AssistantWarning`
3361
3362
  * actually renders inline — `selectHighWarnings` stays available for callers
3362
3363
  * that want the stricter, level-only cut.
@@ -3,6 +3,7 @@ import {
3
3
  liveAgentCallTrace
4
4
  } from "../utils/agent-call-trace.js";
5
5
  import { createSelector } from "@reduxjs/toolkit";
6
+ import { isAnswerKeptWarning } from "./answer-kept-warning.js";
6
7
  import { blockMediaFileId } from "../utils/block-media-identity.js";
7
8
  import { DECISION_ANSWERS_BLOCK_TYPE } from "../../../../utils/content-ir/decision-answers-ids.js";
8
9
  import { decisionAnswersText } from "@ai-matrx/agents/presentation/decision-answers";
@@ -872,7 +873,7 @@ const selectVisibleWarnings = (requestId) => createSelector(
872
873
  (warnings) => {
873
874
  if (!warnings) return void 0;
874
875
  return warnings.filter(
875
- (w) => w.level === "high" || PROMOTED_WARNING_CODES.has(w.code)
876
+ (w) => w.level === "high" || PROMOTED_WARNING_CODES.has(w.code) || isAnswerKeptWarning(w)
876
877
  );
877
878
  }
878
879
  );
@@ -0,0 +1,7 @@
1
+ /**
2
+ * A stream warning that says "the answer was delivered and kept, but the
3
+ * provider stopped early so the end may be missing". Leaf module (no store
4
+ * imports) so selectors on either side can read it.
5
+ */
6
+ import type { WarningPayload } from "@ai-matrx/agents/generated/stream-events";
7
+ export declare function isAnswerKeptWarning(warning: WarningPayload): boolean;
@@ -0,0 +1,12 @@
1
+ const ANSWER_KEPT_WARNING_CODES = /* @__PURE__ */ new Set([
2
+ "provider_recitation_stop",
3
+ "truncated_response"
4
+ ]);
5
+ function isAnswerKeptWarning(warning) {
6
+ if (ANSWER_KEPT_WARNING_CODES.has(warning.code)) return true;
7
+ const md = warning.metadata;
8
+ return !!md && typeof md === "object" && md.answer_kept === true;
9
+ }
10
+ export {
11
+ isAnswerKeptWarning
12
+ };
@@ -49,7 +49,18 @@ function UuidField({ value, label }) {
49
49
  ),
50
50
  /* @__PURE__ */ jsxs("span", { className: "flex-1 min-w-0 flex items-center gap-1.5", children: [
51
51
  /* @__PURE__ */ jsx("span", { className: "text-[11px] font-mono text-foreground break-all", children: value }),
52
- /* @__PURE__ */ jsx(Button, { variant: "quiet", icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}), glyphTone: copied ? "success" : void 0, onClick: copy, title: "Copy UUID", "aria-label": "Copy UUID", className: "shrink-0" })
52
+ /* @__PURE__ */ jsx(
53
+ Button,
54
+ {
55
+ variant: "quiet",
56
+ icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}),
57
+ glyphTone: copied ? "success" : void 0,
58
+ onClick: copy,
59
+ title: "Copy UUID",
60
+ "aria-label": "Copy UUID",
61
+ className: "shrink-0"
62
+ }
63
+ )
53
64
  ] })
54
65
  ] });
55
66
  }
@@ -131,10 +142,21 @@ function SectionHeader({
131
142
  open ? /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 text-muted-foreground shrink-0" }) : /* @__PURE__ */ jsx(ChevronRight, { className: "h-3 w-3 text-muted-foreground shrink-0" }),
132
143
  /* @__PURE__ */ jsx("span", { className: "text-xs font-semibold uppercase tracking-wider text-muted-foreground flex-1", children: title }),
133
144
  count !== void 0 && /* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground/60 shrink-0 mr-1", children: count }),
134
- copyValue && /* @__PURE__ */ jsx(Button, { variant: "quiet", icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}), glyphTone: copied ? "success" : void 0, onClick: (e) => {
135
- e.stopPropagation();
136
- copy();
137
- }, title: "Copy section as JSON", "aria-label": "Copy section as JSON", className: "opacity-0 group-hover:opacity-100 shrink-0" })
145
+ copyValue && /* @__PURE__ */ jsx(
146
+ Button,
147
+ {
148
+ variant: "quiet",
149
+ icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}),
150
+ glyphTone: copied ? "success" : void 0,
151
+ onClick: (e) => {
152
+ e.stopPropagation();
153
+ copy();
154
+ },
155
+ title: "Copy section as JSON",
156
+ "aria-label": "Copy section as JSON",
157
+ className: "opacity-0 group-hover:opacity-100 shrink-0"
158
+ }
159
+ )
138
160
  ]
139
161
  }
140
162
  )
@@ -195,8 +217,28 @@ function RawJsonSection({ data }) {
195
217
  const { copied, copy } = useCopyText(json);
196
218
  return /* @__PURE__ */ jsxs("div", { className: "border border-border rounded-sm overflow-hidden", children: [
197
219
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 px-2 py-1.5", children: [
198
- /* @__PURE__ */ jsx(Tile, { variant: "quiet", icon: open ? /* @__PURE__ */ jsx(ChevronDown, {}) : /* @__PURE__ */ jsx(ChevronRight, {}), title: "Raw JSON", onClick: () => setOpen((v) => !v), className: "flex-1" }),
199
- /* @__PURE__ */ jsx(Button, { variant: "quiet", icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}), glyphTone: copied ? "success" : void 0, onClick: copy, title: "Copy full JSON", "aria-label": "Copy full JSON", className: "shrink-0" }),
220
+ /* @__PURE__ */ jsx(
221
+ Tile,
222
+ {
223
+ variant: "quiet",
224
+ icon: open ? /* @__PURE__ */ jsx(ChevronDown, {}) : /* @__PURE__ */ jsx(ChevronRight, {}),
225
+ title: "Raw JSON",
226
+ onClick: () => setOpen((v) => !v),
227
+ className: "flex-1"
228
+ }
229
+ ),
230
+ /* @__PURE__ */ jsx(
231
+ Button,
232
+ {
233
+ variant: "quiet",
234
+ icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}),
235
+ glyphTone: copied ? "success" : void 0,
236
+ onClick: copy,
237
+ title: "Copy full JSON",
238
+ "aria-label": "Copy full JSON",
239
+ className: "shrink-0"
240
+ }
241
+ ),
200
242
  open ? /* @__PURE__ */ jsx(EyeOff, { className: "h-3.5 w-3.5 text-muted-foreground shrink-0" }) : /* @__PURE__ */ jsx(Eye, { className: "h-3.5 w-3.5 text-muted-foreground shrink-0" })
201
243
  ] }),
202
244
  open && /* @__PURE__ */ jsx("pre", { className: "text-xs font-mono text-foreground bg-muted/30 px-3 py-2 overflow-x-auto border-t border-border/40", children: json })
@@ -233,7 +275,14 @@ function InstanceUIStateCore({
233
275
  agentName && instanceTitle !== agentName && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground truncate", children: agentName })
234
276
  ] }),
235
277
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 shrink-0", children: [
236
- /* @__PURE__ */ jsx(Badge, { className: "shrink-0", tone: state.displayMode === "direct" ? "info" : "neutral", children: state.displayMode }),
278
+ /* @__PURE__ */ jsx(
279
+ Badge,
280
+ {
281
+ className: "shrink-0",
282
+ tone: state.displayMode === "direct" ? "info" : "neutral",
283
+ children: state.displayMode
284
+ }
285
+ ),
237
286
  /* @__PURE__ */ jsx(CopyEntireButton, { json: instanceJson })
238
287
  ] })
239
288
  ] }),
@@ -478,7 +527,17 @@ function InstanceUIStateCore({
478
527
  }
479
528
  function CopyEntireButton({ json }) {
480
529
  const { copied, copy } = useCopyText(json);
481
- return /* @__PURE__ */ jsx(Button, { variant: "quiet", icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}), glyphTone: copied ? "success" : void 0, onClick: copy, title: "Copy full state as JSON", "aria-label": "Copy full state as JSON" });
530
+ return /* @__PURE__ */ jsx(
531
+ Button,
532
+ {
533
+ variant: "quiet",
534
+ icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}),
535
+ glyphTone: copied ? "success" : void 0,
536
+ onClick: copy,
537
+ title: "Copy full state as JSON",
538
+ "aria-label": "Copy full state as JSON"
539
+ }
540
+ );
482
541
  }
483
542
  export {
484
543
  InstanceUIStateCore
@@ -77,10 +77,21 @@ function InstanceRow({
77
77
  }
78
78
  ),
79
79
  title && /* @__PURE__ */ jsx("span", { className: "text-[11px] font-mono text-muted-foreground/50 shrink-0 hidden group-hover:inline", children: shortId }),
80
- /* @__PURE__ */ jsx(Button, { variant: "quiet", icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}), glyphTone: copied ? "success" : void 0, onClick: (e) => {
81
- e.stopPropagation();
82
- copy();
83
- }, title: "Copy conversation ID", "aria-label": "Copy conversation ID", className: "opacity-0 group-hover:opacity-100 shrink-0" })
80
+ /* @__PURE__ */ jsx(
81
+ Button,
82
+ {
83
+ variant: "quiet",
84
+ icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}),
85
+ glyphTone: copied ? "success" : void 0,
86
+ onClick: (e) => {
87
+ e.stopPropagation();
88
+ copy();
89
+ },
90
+ title: "Copy conversation ID",
91
+ "aria-label": "Copy conversation ID",
92
+ className: "opacity-0 group-hover:opacity-100 shrink-0"
93
+ }
94
+ )
84
95
  ]
85
96
  }
86
97
  );
@@ -129,10 +140,21 @@ function AgentGroup({
129
140
  }
130
141
  ),
131
142
  /* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground/50 shrink-0 ml-1", children: group.conversationIds.length }),
132
- group.agentId && /* @__PURE__ */ jsx(Button, { variant: "quiet", icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}), glyphTone: copied ? "success" : void 0, onClick: (e) => {
133
- e.stopPropagation();
134
- copy();
135
- }, title: "Copy agent ID", "aria-label": "Copy agent ID", className: "opacity-0 group-hover:opacity-100 shrink-0 ml-0.5" })
143
+ group.agentId && /* @__PURE__ */ jsx(
144
+ Button,
145
+ {
146
+ variant: "quiet",
147
+ icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}),
148
+ glyphTone: copied ? "success" : void 0,
149
+ onClick: (e) => {
150
+ e.stopPropagation();
151
+ copy();
152
+ },
153
+ title: "Copy agent ID",
154
+ "aria-label": "Copy agent ID",
155
+ className: "opacity-0 group-hover:opacity-100 shrink-0 ml-0.5"
156
+ }
157
+ )
136
158
  ]
137
159
  }
138
160
  ),
@@ -169,6 +169,22 @@ export declare function extractInspectableText(record: MessageRecord | undefined
169
169
  * CONVERSATION_FAILURE_AND_RETRY_FE_GUIDE.md.
170
170
  */
171
171
  export declare function isFailedRecord(record: MessageRecord | undefined): boolean;
172
+ /**
173
+ * The provider stop reason a reloaded turn was saved with, when it is NOT a
174
+ * normal finish — or null. A turn the provider stopped early can be saved as
175
+ * an ordinary `active` message with its partial text and only
176
+ * `metadata.finish_reason` telling the truth (a recitation stop, 2026-10-06);
177
+ * that row reloaded with no error at all.
178
+ */
179
+ export declare function abnormalFinishReason(record: MessageRecord | undefined): string | null;
180
+ /**
181
+ * The stop reason of a reloaded turn that is INCOMPLETE rather than failed:
182
+ * an answer-stands stop on a turn that actually holds answer text and is not
183
+ * saved failed. Null otherwise (normal finish, no content, safety stops…).
184
+ */
185
+ export declare function incompleteAnswerReason(record: MessageRecord | undefined): string | null;
186
+ /** The sentence a reloaded early-stopped turn shows. */
187
+ export declare function earlyStopMessage(reason: string): string;
172
188
  /**
173
189
  * The persisted error text for a failed turn. Prefers the structured top-level
174
190
  * `error.message`; falls back to the legacy `metadata.error` string, then to
@@ -282,7 +282,48 @@ function isFailedRecord(record) {
282
282
  if (record.status === "failed") return true;
283
283
  if (record.error) return true;
284
284
  const md = record.metadata;
285
- return !!md && typeof md === "object" && !Array.isArray(md) && md.failed === true;
285
+ if (!!md && typeof md === "object" && !Array.isArray(md) && md.failed === true) {
286
+ return true;
287
+ }
288
+ return abnormalFinishReason(record) !== null && incompleteAnswerReason(record) === null;
289
+ }
290
+ const NORMAL_FINISH_REASONS = /* @__PURE__ */ new Set([
291
+ "stop",
292
+ "end_turn",
293
+ "stop_sequence",
294
+ "tool_use",
295
+ "tool_calls",
296
+ "function_call",
297
+ "pause_turn",
298
+ "completed",
299
+ "success"
300
+ ]);
301
+ function abnormalFinishReason(record) {
302
+ if (!record || record.role !== "assistant") return null;
303
+ const md = record.metadata;
304
+ if (!md || typeof md !== "object" || Array.isArray(md)) return null;
305
+ const reason = md.finish_reason;
306
+ if (typeof reason !== "string" || reason.length === 0) return null;
307
+ const normalized = reason.toLowerCase().replace(/^finishreason\./, "");
308
+ return NORMAL_FINISH_REASONS.has(normalized) ? null : normalized;
309
+ }
310
+ const ANSWER_STANDS_REASONS = /* @__PURE__ */ new Set([
311
+ "recitation",
312
+ "max_tokens",
313
+ "length",
314
+ "max_output_tokens"
315
+ ]);
316
+ function incompleteAnswerReason(record) {
317
+ const reason = abnormalFinishReason(record);
318
+ if (!reason || !ANSWER_STANDS_REASONS.has(reason)) return null;
319
+ if (!record || record.status === "failed" || record.error) return null;
320
+ const md = record.metadata;
321
+ if (md && !Array.isArray(md) && md.failed === true)
322
+ return null;
323
+ return extractFlatText(record).trim().length > 0 ? reason : null;
324
+ }
325
+ function earlyStopMessage(reason) {
326
+ return reason === "max_tokens" || reason === "length" ? "The response hit its length limit before it finished." : "The response was stopped early by the model provider, so it may be incomplete.";
286
327
  }
287
328
  function extractRecordError(record) {
288
329
  if (!record) return void 0;
@@ -294,6 +335,8 @@ function extractRecordError(record) {
294
335
  const err = md.error;
295
336
  if (typeof err === "string" && err.length > 0) return err;
296
337
  }
338
+ const stopped = abnormalFinishReason(record);
339
+ if (stopped) return earlyStopMessage(stopped);
297
340
  const flat = extractFlatText(record);
298
341
  return flat.length > 0 ? flat : void 0;
299
342
  }
@@ -545,12 +588,15 @@ const selectMessagesInterleavedRuns = (conversationId, messageIds) => {
545
588
  };
546
589
  export {
547
590
  EMPTY_CONVERSATION_MESSAGES,
591
+ abnormalFinishReason,
592
+ earlyStopMessage,
548
593
  extractAnswerDocumentText,
549
594
  extractContentBlocks,
550
595
  extractFlatText,
551
596
  extractInspectableText,
552
597
  extractRecordError,
553
598
  fenceBoundarySeparator,
599
+ incompleteAnswerReason,
554
600
  isFailedRecord,
555
601
  persistedBodyBlocks,
556
602
  selectApiEndpointMode,
@@ -0,0 +1,26 @@
1
+ /**
2
+ * ONE verdict for how a turn ended, read the same way live and after reload.
3
+ *
4
+ * - "failed" no usable answer, a safety/other stop, or the server saved it failed
5
+ * - "incomplete" the answer was delivered and stands, but the provider stopped
6
+ * early (recitation stop, length limit): content + a short notice
7
+ * - "complete" a normal finish
8
+ *
9
+ * Live reads the request status + stream warnings; reload reads the saved row
10
+ * (`isFailedRecord` / `incompleteAnswerReason`). A guard test drives both over
11
+ * the same cases so they cannot drift (incomplete-answer-live-equals-reload).
12
+ */
13
+ import type { WarningPayload } from "@ai-matrx/agents/generated/stream-events";
14
+ import type { MessageRecord } from "./messages.slice.js";
15
+ import { isAnswerKeptWarning } from "../active-requests/answer-kept-warning.js";
16
+ export { isAnswerKeptWarning };
17
+ export type TurnOutcome = "complete" | "incomplete" | "failed";
18
+ export declare function liveTurnOutcome(request: {
19
+ status: string;
20
+ warnings?: readonly WarningPayload[];
21
+ } | undefined): TurnOutcome;
22
+ export declare function reloadTurnOutcome(record: MessageRecord | undefined): TurnOutcome;
23
+ /** The short label on an incomplete answer. A label, not prose. */
24
+ export declare function incompleteLabel(reason: string | null): string;
25
+ /** The reason-specific sentence kept under Details, for a reloaded turn. */
26
+ export declare function incompleteDetail(reason: string): string;
@@ -0,0 +1,28 @@
1
+ import {
2
+ earlyStopMessage,
3
+ incompleteAnswerReason,
4
+ isFailedRecord
5
+ } from "./messages.selectors.js";
6
+ import { isAnswerKeptWarning } from "../active-requests/answer-kept-warning.js";
7
+ function liveTurnOutcome(request) {
8
+ if (!request) return "complete";
9
+ if (request.status === "error") return "failed";
10
+ return (request.warnings ?? []).some(isAnswerKeptWarning) ? "incomplete" : "complete";
11
+ }
12
+ function reloadTurnOutcome(record) {
13
+ if (isFailedRecord(record)) return "failed";
14
+ return incompleteAnswerReason(record) ? "incomplete" : "complete";
15
+ }
16
+ function incompleteLabel(reason) {
17
+ return reason === "max_tokens" || reason === "length" || reason === "max_output_tokens" || reason === "truncated_response" ? "Cut off at its length limit" : "Ending may be cut off";
18
+ }
19
+ function incompleteDetail(reason) {
20
+ return earlyStopMessage(reason);
21
+ }
22
+ export {
23
+ incompleteDetail,
24
+ incompleteLabel,
25
+ isAnswerKeptWarning,
26
+ liveTurnOutcome,
27
+ reloadTurnOutcome
28
+ };
@@ -69,6 +69,10 @@ import {
69
69
  appendCitation
70
70
  } from "../active-requests/active-requests.slice.js";
71
71
  import { parseNormalizedCitation } from "../messages/message-citations.js";
72
+ import {
73
+ terminalFailureFromCompletion,
74
+ terminalFailureFromRecordUpdate
75
+ } from "./terminal-request-failure.js";
72
76
  import { confirmServerSync } from "../conversations/conversations.slice.js";
73
77
  import { receivedFsChange } from "../../../../host/ui-slots.js";
74
78
  import {
@@ -430,6 +434,17 @@ async function processStream({
430
434
  terminalTransportController.abort("post-terminal-grace");
431
435
  }, POST_TERMINAL_GRACE_MS);
432
436
  };
437
+ const failRequestFromTerminalStatus = (error) => {
438
+ if (!error) return;
439
+ if (getState().activeRequests.byRequestId[requestId]?.status === "error") {
440
+ return;
441
+ }
442
+ console.warn(
443
+ `[stream:${requestId.slice(0, 8)}] user request ended FAILED without an error event (${error.error_type}) \u2014 surfacing it as a failed turn.`
444
+ );
445
+ dispatch(setRequestStatus({ requestId, status: "error", error }));
446
+ dispatch(setInstanceStatus({ conversationId, status: "error" }));
447
+ };
433
448
  let lastTransportSeq = getState().activeRequests.byRequestId[requestId]?.lastTransportSeq ?? 0;
434
449
  let transportStreamId = getState().activeRequests.byRequestId[requestId]?.transportStreamId ?? null;
435
450
  let retainedEntry;
@@ -635,6 +650,7 @@ async function processStream({
635
650
  }
636
651
  if (d.operation === "user_request") {
637
652
  dispatch(setCompletion({ requestId, data: d }));
653
+ failRequestFromTerminalStatus(terminalFailureFromCompletion(d));
638
654
  completionStats = result;
639
655
  const totals = completionStats.total_usage?.total;
640
656
  if (totals) {
@@ -1664,6 +1680,7 @@ async function processStream({
1664
1680
  refreshUserMessageWithoutBody(d.record_id);
1665
1681
  }
1666
1682
  } else if (d.table === "user_request") {
1683
+ failRequestFromTerminalStatus(terminalFailureFromRecordUpdate(d));
1667
1684
  dispatch(
1668
1685
  patchUserRequest({
1669
1686
  id: d.record_id,
@@ -0,0 +1,27 @@
1
+ /**
2
+ * A request that ENDED FAILED without an `error` event.
3
+ *
4
+ * The server has three ways to say a user request failed, and only one of
5
+ * them is the `error` event: a terminal failure it persisted on purpose (a
6
+ * provider safety / recitation stop, a model refusal, any finish-reason
7
+ * failure) is announced as a `user_request` completion with
8
+ * `status: "failed"` and/or a `record_update` moving the `user_request` row
9
+ * to `failed`, then a normal `end`. Before this module the stream treated
10
+ * both as bookkeeping, the `end` handler marked the request `complete`, and
11
+ * the turn showed NO error — while its half-written content (a flashcard set
12
+ * cut off by a recitation stop, 2026-10-06) sat there looking finished.
13
+ *
14
+ * Pure: reads the event payload, returns the `ErrorPayload` the request
15
+ * should carry, or null when the event is not a terminal failure. It never
16
+ * matches on the message TEXT (the server is free to reword it) — only on
17
+ * the operation/table, the status, and the structured fields.
18
+ */
19
+ import type { CompletionPayload, ErrorPayload, RecordUpdatePayload } from "@ai-matrx/agents/generated/stream-events";
20
+ /** error_type when the server sent no structured one. */
21
+ export declare const TERMINAL_FAILURE_ERROR_TYPE = "request_failed";
22
+ /** Shown when the server said "failed" and nothing more. */
23
+ export declare const TERMINAL_FAILURE_FALLBACK_MESSAGE = "The response stopped before it finished.";
24
+ /** A `user_request` completion whose status is not success. */
25
+ export declare function terminalFailureFromCompletion(d: CompletionPayload): ErrorPayload | null;
26
+ /** A `record_update` moving the `user_request` row to `failed`. */
27
+ export declare function terminalFailureFromRecordUpdate(d: RecordUpdatePayload): ErrorPayload | null;
@@ -0,0 +1,56 @@
1
+ const TERMINAL_FAILURE_ERROR_TYPE = "request_failed";
2
+ const TERMINAL_FAILURE_FALLBACK_MESSAGE = "The response stopped before it finished.";
3
+ function asRecord(value) {
4
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
5
+ }
6
+ function readStructuredError(raw) {
7
+ if (typeof raw === "string") {
8
+ const trimmed = raw.trim();
9
+ if (trimmed.startsWith("{")) {
10
+ try {
11
+ return readStructuredError(JSON.parse(trimmed));
12
+ } catch {
13
+ }
14
+ }
15
+ return { errorType: null, message: trimmed || null };
16
+ }
17
+ const obj = asRecord(raw);
18
+ if (!obj) return { errorType: null, message: null };
19
+ const errorType = typeof obj.error_type === "string" ? obj.error_type : typeof obj.type === "string" ? obj.type : null;
20
+ const message = typeof obj.user_message === "string" && obj.user_message ? obj.user_message : typeof obj.message === "string" && obj.message ? obj.message : null;
21
+ return { errorType, message };
22
+ }
23
+ function buildPayload(source, fields, finishReason) {
24
+ const structured = readStructuredError(fields?.error);
25
+ const errorType = structured.errorType ?? (typeof fields?.error_type === "string" ? fields.error_type : null) ?? TERMINAL_FAILURE_ERROR_TYPE;
26
+ const message = structured.message ?? TERMINAL_FAILURE_FALLBACK_MESSAGE;
27
+ return {
28
+ error_type: errorType,
29
+ message,
30
+ user_message: message,
31
+ details: {
32
+ source,
33
+ ...finishReason ? { finish_reason: finishReason } : {},
34
+ ...typeof fields?.reason === "string" ? { reason: fields.reason } : {}
35
+ }
36
+ };
37
+ }
38
+ function terminalFailureFromCompletion(d) {
39
+ if (d.operation !== "user_request" || d.status !== "failed") return null;
40
+ const result = asRecord(d.result);
41
+ const metadata = asRecord(result?.metadata);
42
+ const finishReason = typeof result?.finish_reason === "string" ? result.finish_reason : null;
43
+ return buildPayload("completion", metadata ?? result, finishReason);
44
+ }
45
+ function terminalFailureFromRecordUpdate(d) {
46
+ if (d.table !== "user_request" || d.status !== "failed") return null;
47
+ const metadata = asRecord(d.metadata);
48
+ const finishReason = typeof metadata?.finish_reason === "string" ? metadata.finish_reason : null;
49
+ return buildPayload("record_update", metadata, finishReason);
50
+ }
51
+ export {
52
+ TERMINAL_FAILURE_ERROR_TYPE,
53
+ TERMINAL_FAILURE_FALLBACK_MESSAGE,
54
+ terminalFailureFromCompletion,
55
+ terminalFailureFromRecordUpdate
56
+ };
@@ -13,7 +13,10 @@ import {
13
13
  Save
14
14
  } from "lucide-react";
15
15
  import { Button } from "@ai-matrx/design-system/controls";
16
- import { MarkdownStream, AudioOutputBlockSkeleton } from "../../../host/markdown-slots.js";
16
+ import {
17
+ MarkdownStream,
18
+ AudioOutputBlockSkeleton
19
+ } from "../../../host/markdown-slots.js";
17
20
  import { useDomCapturePrint } from "../../../conversation/hooks/useDomCapturePrint.js";
18
21
  import { useAppSelector, useAppDispatch } from "../../../store/hooks.js";
19
22
  import { selectMessageHasUnsavedChanges } from "../../_legacy-stubs.js";
@@ -193,14 +193,7 @@ function UserMessage({
193
193
  }
194
194
  ),
195
195
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 justify-end", children: [
196
- /* @__PURE__ */ jsx(
197
- Button,
198
- {
199
- variant: "quiet",
200
- onClick: handleEditCancel,
201
- children: "Cancel"
202
- }
203
- ),
196
+ /* @__PURE__ */ jsx(Button, { variant: "quiet", onClick: handleEditCancel, children: "Cancel" }),
204
197
  /* @__PURE__ */ jsx(
205
198
  Button,
206
199
  {
@@ -13,7 +13,10 @@ import {
13
13
  Save
14
14
  } from "lucide-react";
15
15
  import { Button } from "@ai-matrx/design-system/controls";
16
- import { MarkdownStream, AudioOutputBlockSkeleton } from "../host/markdown-slots.js";
16
+ import {
17
+ MarkdownStream,
18
+ AudioOutputBlockSkeleton
19
+ } from "../host/markdown-slots.js";
17
20
  import { useDomCapturePrint } from "../conversation/hooks/useDomCapturePrint.js";
18
21
  import { useAppSelector, useAppDispatch } from "../store/hooks.js";
19
22
  import { selectMessageHasUnsavedChanges } from "./_legacy-stubs.js";
@@ -193,14 +193,7 @@ function UserMessage({
193
193
  }
194
194
  ),
195
195
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 justify-end", children: [
196
- /* @__PURE__ */ jsx(
197
- Button,
198
- {
199
- variant: "quiet",
200
- onClick: handleEditCancel,
201
- children: "Cancel"
202
- }
203
- ),
196
+ /* @__PURE__ */ jsx(Button, { variant: "quiet", onClick: handleEditCancel, children: "Cancel" }),
204
197
  /* @__PURE__ */ jsx(
205
198
  Button,
206
199
  {
@@ -106,10 +106,17 @@ const ErrorView = ({
106
106
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
107
107
  routed ? /* @__PURE__ */ jsx(Route, { className: "h-3.5 w-3.5 text-muted-foreground" }) : /* @__PURE__ */ jsx(CircleAlert, { className: "h-3.5 w-3.5 text-destructive" }),
108
108
  /* @__PURE__ */ jsx("span", { children: routed ? "Routed" : "Error" }),
109
- entry.errorType && /* @__PURE__ */ jsxs(Badge, { variant: "outline", className: "px-1.5 py-0 font-mono text-[10px]", children: [
110
- entry.errorType,
111
- /* @__PURE__ */ jsx(ErrorAlchemyMenu, { error: entry.errorType })
112
- ] })
109
+ entry.errorType && /* @__PURE__ */ jsxs(
110
+ Badge,
111
+ {
112
+ variant: "outline",
113
+ className: "px-1.5 py-0 font-mono text-[10px]",
114
+ children: [
115
+ entry.errorType,
116
+ /* @__PURE__ */ jsx(ErrorAlchemyMenu, { error: entry.errorType })
117
+ ]
118
+ }
119
+ )
113
120
  ] }),
114
121
  /* @__PURE__ */ jsx(
115
122
  CopyButtons,
@@ -85,7 +85,17 @@ function VoiceDebugPanel({
85
85
  };
86
86
  return /* @__PURE__ */ jsxs("div", { className: "pointer-events-auto w-full overflow-hidden rounded-lg border border-border bg-zinc-950/95 text-zinc-100 shadow-lg backdrop-blur", children: [
87
87
  /* @__PURE__ */ jsxs("div", { className: "flex w-full items-center justify-between gap-2 px-3 py-1.5", children: [
88
- /* @__PURE__ */ jsx(Tile, { variant: "quiet", icon: /* @__PURE__ */ jsx(Bug, {}), glyphTone: "success", title: "Live voice debug", onClick: () => setOpen((o) => !o), className: "flex-1" }),
88
+ /* @__PURE__ */ jsx(
89
+ Tile,
90
+ {
91
+ variant: "quiet",
92
+ icon: /* @__PURE__ */ jsx(Bug, {}),
93
+ glyphTone: "success",
94
+ title: "Live voice debug",
95
+ onClick: () => setOpen((o) => !o),
96
+ className: "flex-1"
97
+ }
98
+ ),
89
99
  /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
90
100
  /* @__PURE__ */ jsx(
91
101
  Badge,
@@ -106,7 +116,15 @@ function VoiceDebugPanel({
106
116
  glyphTone: copied ? "success" : void 0
107
117
  }
108
118
  ),
109
- /* @__PURE__ */ jsx(Button, { variant: "quiet", icon: open ? /* @__PURE__ */ jsx(ChevronDown, {}) : /* @__PURE__ */ jsx(ChevronUp, {}), "aria-label": open ? "Collapse" : "Expand", onClick: () => setOpen((o) => !o) })
119
+ /* @__PURE__ */ jsx(
120
+ Button,
121
+ {
122
+ variant: "quiet",
123
+ icon: open ? /* @__PURE__ */ jsx(ChevronDown, {}) : /* @__PURE__ */ jsx(ChevronUp, {}),
124
+ "aria-label": open ? "Collapse" : "Expand",
125
+ onClick: () => setOpen((o) => !o)
126
+ }
127
+ )
110
128
  ] })
111
129
  ] }),
112
130
  open && /* @__PURE__ */ jsxs("div", { className: "space-y-2 border-t border-zinc-800 px-3 py-2", children: [
@@ -164,7 +182,8 @@ function VoiceDebugPanel({
164
182
  " ",
165
183
  /* @__PURE__ */ jsxs("span", { className: "text-zinc-200", children: [
166
184
  flags.lastEventType ?? "\u2014",
167
- " \xB7 ",
185
+ " \xB7",
186
+ " ",
168
187
  formatRelativeTime(flags.lastEventAt)
169
188
  ] })
170
189
  ] }),
@@ -102,10 +102,21 @@ function SidebarRow({
102
102
  children: row.label
103
103
  }
104
104
  ),
105
- /* @__PURE__ */ jsx(Button, { variant: "quiet", icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}), glyphTone: copied ? "success" : void 0, onClick: (e) => {
106
- e.stopPropagation();
107
- copy();
108
- }, title: "Copy conversation id", "aria-label": "Copy conversation id", className: "opacity-0 group-hover:opacity-100 shrink-0" }),
105
+ /* @__PURE__ */ jsx(
106
+ Button,
107
+ {
108
+ variant: "quiet",
109
+ icon: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}),
110
+ glyphTone: copied ? "success" : void 0,
111
+ onClick: (e) => {
112
+ e.stopPropagation();
113
+ copy();
114
+ },
115
+ title: "Copy conversation id",
116
+ "aria-label": "Copy conversation id",
117
+ className: "opacity-0 group-hover:opacity-100 shrink-0"
118
+ }
119
+ ),
109
120
  /* @__PURE__ */ jsx(
110
121
  EntityDoorControls,
111
122
  {
@@ -354,7 +365,11 @@ function ObservationalMemoryWindowInner({
354
365
  sourceFeature: "agent-builder",
355
366
  surfaceName: OBSERVATIONAL_MEMORY_SURFACE_NAME,
356
367
  contentSource: { type: "raw" },
357
- entity: selectedConversationId ? { type: "cx_observational_memory", id: selectedConversationId, title: "Observational memory" } : void 0,
368
+ entity: selectedConversationId ? {
369
+ type: "cx_observational_memory",
370
+ id: selectedConversationId,
371
+ title: "Observational memory"
372
+ } : void 0,
358
373
  children: selectedConversationId ? /* @__PURE__ */ jsx(
359
374
  ObservationalMemoryCore,
360
375
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-matrx/chat",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
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 per-module subpaths @ai-matrx/chat/<domain>/<module> over a host contract (/host, /host/react) whose only required value is a db; plus the kernel compatibility entries (., /external-store, /matrx, /react, /tools, styles.css, tokens.css) until P28.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -201,24 +201,24 @@
201
201
  "server-only": "^0.0.1",
202
202
  "uuid": "^14.0.2",
203
203
  "zod": "^4.6.5",
204
- "@ai-matrx/associations": "latest",
205
- "@ai-matrx/browser-audio": "latest",
204
+ "@ai-matrx/alchemy": "latest",
206
205
  "@ai-matrx/agents": "latest",
206
+ "@ai-matrx/browser-audio": "latest",
207
207
  "@ai-matrx/canvas": "latest",
208
208
  "@ai-matrx/content-ir": "latest",
209
209
  "@ai-matrx/content-ir-react": "latest",
210
210
  "@ai-matrx/data": "latest",
211
211
  "@ai-matrx/design-system": "latest",
212
+ "@ai-matrx/associations": "latest",
212
213
  "@ai-matrx/diff": "latest",
213
- "@ai-matrx/icons": "latest",
214
+ "@ai-matrx/kit": "latest",
214
215
  "@ai-matrx/media": "latest",
216
+ "@ai-matrx/icons": "latest",
215
217
  "@ai-matrx/realtime": "latest",
216
218
  "@ai-matrx/records": "latest",
219
+ "@ai-matrx/print": "latest",
217
220
  "@ai-matrx/records-ui": "latest",
218
- "@ai-matrx/kit": "latest",
219
- "@ai-matrx/tap-target": "latest",
220
- "@ai-matrx/alchemy": "latest",
221
- "@ai-matrx/print": "latest"
221
+ "@ai-matrx/tap-target": "latest"
222
222
  },
223
223
  "devDependencies": {
224
224
  "@arethetypeswrong/cli": "^0.18.5",