@ai-matrx/chat 0.3.9 → 0.3.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -0
- package/dist/agent-copy/export.d.ts +10 -2
- package/dist/agent-copy/export.js +3 -2
- package/dist/agents/components/context-items/bodies/WebpageBody.js +3 -8
- package/dist/agents/components/debug/SandboxFileViewer.js +1 -3
- package/dist/agents/components/inputs/smart-input/composer/ComposerMenu.js +16 -2
- package/dist/agents/components/live-run/LiveRunDisplay.d.ts +7 -1
- package/dist/agents/components/live-run/LiveRunDisplay.js +10 -3
- package/dist/agents/components/messages-display/TranscriptIntegrityCopyButton.js +5 -11
- package/dist/agents/components/messages-display/user/UserActionBar.js +3 -18
- package/dist/agents/constants/system-agent-registry.d.ts +0 -3
- package/dist/agents/constants/system-agent-registry.js +0 -3
- package/dist/agents/hooks/useFloatingAgentRun.d.ts +19 -0
- package/dist/agents/hooks/useFloatingAgentRun.js +32 -9
- package/dist/agents/prompt-preview/PromptPreviewContent.js +1 -7
- package/dist/agents/redux/execution-system/active-requests/active-requests.selectors.d.ts +14 -0
- package/dist/agents/redux/execution-system/active-requests/active-requests.selectors.js +30 -0
- package/dist/agents/redux/execution-system/client-capabilities/desktop-native.provider.js +10 -1
- package/dist/agents/redux/execution-system/message-crud/refetch-single-message.thunk.js +5 -0
- package/dist/agents/redux/execution-system/thunks/process-stream.js +2 -2
- package/dist/agents/redux/execution-system/utils/stream-block-accumulator.d.ts +14 -0
- package/dist/agents/redux/execution-system/utils/stream-block-accumulator.js +28 -0
- package/dist/context/sources/scopes.d.ts +1 -1
- package/dist/host/db-types.d.ts +1859 -831
- package/dist/host/ui-slots.d.ts +3 -0
- package/dist/host/ui-slots.js +2 -0
- package/dist/host/window-openers.d.ts +2 -0
- package/dist/surfaces/manifests/chat.manifest.js +3 -1
- package/dist/ui/mandates-storage/mandateStorage.d.ts +15 -0
- package/dist/ui/mandates-storage/shortcutStorage.d.ts +3 -0
- package/dist/utils/rich-editor/in-place/caret-handoff.js +2 -1
- package/dist/voice-agent/components/VoiceDebugPanel.js +3 -9
- package/dist/window-panels/windows/agents/LiveRunWindow.d.ts +3 -1
- package/dist/window-panels/windows/agents/LiveRunWindow.js +5 -2
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,60 @@
|
|
|
1
1
|
# @ai-matrx/chat
|
|
2
2
|
|
|
3
|
+
## 0.3.15 — 2026-10-07
|
|
4
|
+
### The Export menu's .csv download is formula-guarded (AP-2)
|
|
5
|
+
|
|
6
|
+
- `csvExportItem` (the Export menu's `.csv` DOWNLOAD) is always formula-guarded: a person opens it in a spreadsheet, so `=HYPERLINK(...)` arrives as text.
|
|
7
|
+
- `rowsToCsv(rows, columns?, { spreadsheetSafe? })` is guarded by default; pass `{ spreadsheetSafe: false }` only where an AI or a parser reads the text.
|
|
8
|
+
|
|
9
|
+
**Consumer action:** none. A caller that feeds `rowsToCsv` text to an AI passes `{ spreadsheetSafe: false }`.
|
|
10
|
+
|
|
11
|
+
## 0.3.14 — 2026-10-07
|
|
12
|
+
### Re-sync before matrx-frontend consumes the published package (P27)
|
|
13
|
+
|
|
14
|
+
- Carries every matrx-frontend `packages/chat` edit after `f0b5af6a6a`: the `CopySplit` host slot (split
|
|
15
|
+
Copy on the person's own message), JSON extraction reading the bytes behind a split wrapper
|
|
16
|
+
(`stream-block-accumulator`, active-request selectors, `process-stream`), agent-copy export, and
|
|
17
|
+
`host/db-types.ts` regenerated from the frontend's current schema (SCOPES-W3 moved the scope tables to
|
|
18
|
+
`deprecated`; `ContextItemRow` reads them there).
|
|
19
|
+
- Consumer action: none.
|
|
20
|
+
|
|
21
|
+
## 0.3.13 — 2026-10-07
|
|
22
|
+
### No signed-in-only reads for a signed-out visitor (Applets L4)
|
|
23
|
+
|
|
24
|
+
- `refetchSingleMessage` makes no `chat.message` read for a signed-out visitor (anon holds no grant; a
|
|
25
|
+
guest run on `/p/<slug>` answered eight 42501s per run) and never retries a refusal — the bounded
|
|
26
|
+
durable read waits on lag, not on an error.
|
|
27
|
+
- The desktop-presence warm waits for the chat host (`onChatHostConfigured`): it loaded before
|
|
28
|
+
`<ChatProvider>` rendered and announced "No chat host is configured" on every `/p` load.
|
|
29
|
+
- Consumer action: none.
|
|
30
|
+
|
|
31
|
+
## 0.3.12 — 2026-10-07
|
|
32
|
+
### One copy, one toast (AP-2)
|
|
33
|
+
|
|
34
|
+
- Sandbox file viewer, transcript integrity report, prompt preview and voice debug copies passed their
|
|
35
|
+
success words to the notifying `useClipboard` hook instead of toasting a second time after the kit's
|
|
36
|
+
own "Text copied to clipboard!".
|
|
37
|
+
- Consumer action: none.
|
|
38
|
+
|
|
39
|
+
## 0.3.11 — 2026-10-07
|
|
40
|
+
### Re-sync before matrx-frontend consumes the published package (P27)
|
|
41
|
+
|
|
42
|
+
- Carries every matrx-frontend `packages/chat` edit after `ebed60e111`: floating runs settle and a failed
|
|
43
|
+
launch says why (`useFloatingAgentRun.settle`), live-run failure display, composer menu, window openers,
|
|
44
|
+
chat manifest, caret hand-off, and `host/db-types.ts` regenerated from the frontend's current schema
|
|
45
|
+
(the app's Supabase client is assignable to the package's again).
|
|
46
|
+
- `system-agent-registry` drops the three `agent_apps.*` builtins whose mandates applets B3 deleted (the
|
|
47
|
+
package did not type-check against `@ai-matrx/agents` 0.50.1).
|
|
48
|
+
- Consumer action: none.
|
|
49
|
+
|
|
50
|
+
## 0.3.10
|
|
51
|
+
|
|
52
|
+
- Clipboard failure handling lives in the `false` branch of the kit copy, which never throws: the dead `catch` blocks around `copyText` in the webpage context body, prompt preview, voice debug panel and transcript-integrity button are gone (the hook's notify already reports the failure).
|
|
53
|
+
|
|
54
|
+
### Consumer action
|
|
55
|
+
|
|
56
|
+
- None required.
|
|
57
|
+
|
|
3
58
|
## 0.3.9
|
|
4
59
|
|
|
5
60
|
Automatic changed-only republish (docs/metadata drift since the last tag — see
|
|
@@ -25,11 +25,19 @@ 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
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Rows → CSV (Alchemy's one writer). Columns: explicit list, or the union of keys across rows.
|
|
30
|
+
* `spreadsheetSafe` defaults ON (kit `spreadsheetSafeByDefault`: a .csv is opened in a
|
|
31
|
+
* spreadsheet, so formula-looking text gets a leading `'`). Pass `{ spreadsheetSafe: false }` ONLY
|
|
32
|
+
* where an AI or a parser reads the text — never for a file a person downloads.
|
|
33
|
+
*/
|
|
29
34
|
export declare function rowsToCsv(rows: Array<Record<string, unknown>>, columns?: Array<{
|
|
30
35
|
key: string;
|
|
31
36
|
header: string;
|
|
32
|
-
}
|
|
37
|
+
}>, options?: {
|
|
38
|
+
spreadsheetSafe?: boolean;
|
|
39
|
+
}): string;
|
|
40
|
+
/** The Export menu's .csv DOWNLOAD — a person opens it in a spreadsheet, so it is always formula-guarded. */
|
|
33
41
|
export declare function csvExportItem(rows: Array<Record<string, unknown>> | (() => Array<Record<string, unknown>>), label?: string, columns?: Array<{
|
|
34
42
|
key: string;
|
|
35
43
|
header: string;
|
|
@@ -35,14 +35,15 @@ function textExportItem(content, label, extension = "txt") {
|
|
|
35
35
|
})
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
|
-
function rowsToCsv(rows, columns) {
|
|
38
|
+
function rowsToCsv(rows, columns, options = {}) {
|
|
39
39
|
const cols = columns ?? [...new Set(rows.flatMap((row) => Object.keys(row)))].map((key) => ({
|
|
40
40
|
key,
|
|
41
41
|
header: key
|
|
42
42
|
}));
|
|
43
43
|
return toDelimitedText(
|
|
44
44
|
cols.map((c) => c.header),
|
|
45
|
-
rows.map((row) => cols.map((c) => row[c.key] ?? null))
|
|
45
|
+
rows.map((row) => cols.map((c) => row[c.key] ?? null)),
|
|
46
|
+
options.spreadsheetSafe === void 0 ? {} : { spreadsheetSafe: options.spreadsheetSafe }
|
|
46
47
|
);
|
|
47
48
|
}
|
|
48
49
|
function csvExportItem(rows, label = "CSV", columns) {
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
TooltipContent,
|
|
10
10
|
TooltipTrigger
|
|
11
11
|
} from "@ai-matrx/design-system";
|
|
12
|
-
import { toast } from "../../../../host/notify.js";
|
|
13
12
|
import { WebpageSnapshotView } from "../../../../host/ui-slots.js";
|
|
14
13
|
import { webpageTitle, webpageUrl } from "../../../resources/webpage-snapshot.js";
|
|
15
14
|
import { Button } from "@ai-matrx/design-system/controls";
|
|
@@ -47,13 +46,9 @@ function WebpageFooter({ item }) {
|
|
|
47
46
|
const [copied, setCopied] = useState(false);
|
|
48
47
|
if (!url) return null;
|
|
49
48
|
const copy = async () => {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
setTimeout(() => setCopied(false), 1500);
|
|
54
|
-
} catch {
|
|
55
|
-
toast.error("Failed to copy");
|
|
56
|
-
}
|
|
49
|
+
if (!await copyText(url)) return;
|
|
50
|
+
setCopied(true);
|
|
51
|
+
setTimeout(() => setCopied(false), 1500);
|
|
57
52
|
};
|
|
58
53
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
59
54
|
/* @__PURE__ */ jsx("span", { className: "min-w-0 truncate font-mono text-[11px] text-muted-foreground", children: url }),
|
|
@@ -5,7 +5,6 @@ import { toast as copyToast } from "../../../host/notify.js";
|
|
|
5
5
|
import { Button } from "@ai-matrx/design-system/controls";
|
|
6
6
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
7
7
|
import { FileText, Loader2, RefreshCw, Copy, Check } from "lucide-react";
|
|
8
|
-
import { toast } from "../../../host/notify.js";
|
|
9
8
|
import { createSandboxFilesystemAdapter } from "../../../host/ui-slots.js";
|
|
10
9
|
import { ErrorAlchemyMenu } from "../../../host/ui-slots.js";
|
|
11
10
|
function SandboxFileViewer({
|
|
@@ -49,9 +48,8 @@ function SandboxFileViewer({
|
|
|
49
48
|
}, [initialPath, sandboxRowId, load]);
|
|
50
49
|
const handleCopy = async () => {
|
|
51
50
|
if (content == null) return;
|
|
52
|
-
if (!await copyText(content)) return;
|
|
51
|
+
if (!await copyText(content, "File contents copied")) return;
|
|
53
52
|
setCopied(true);
|
|
54
|
-
toast.success("File contents copied");
|
|
55
53
|
setTimeout(() => setCopied(false), 1500);
|
|
56
54
|
};
|
|
57
55
|
return /* @__PURE__ */ jsxs("div", { className: "flex h-full flex-col overflow-hidden", children: [
|
|
@@ -225,6 +225,17 @@ function ComposerMenuLevel({ children }) {
|
|
|
225
225
|
};
|
|
226
226
|
return /* @__PURE__ */ jsx(ComposerMenuLevelContext.Provider, { value: { openId, setOpenId, request, cancel }, children });
|
|
227
227
|
}
|
|
228
|
+
function useNarrowViewport() {
|
|
229
|
+
const [narrow, setNarrow] = useState(false);
|
|
230
|
+
useEffect(() => {
|
|
231
|
+
const query = window.matchMedia("(max-width: 639px)");
|
|
232
|
+
const sync = () => setNarrow(query.matches);
|
|
233
|
+
sync();
|
|
234
|
+
query.addEventListener("change", sync);
|
|
235
|
+
return () => query.removeEventListener("change", sync);
|
|
236
|
+
}, []);
|
|
237
|
+
return narrow;
|
|
238
|
+
}
|
|
228
239
|
function ComposerSubmenu({
|
|
229
240
|
row,
|
|
230
241
|
children,
|
|
@@ -249,6 +260,7 @@ function ComposerSubmenu({
|
|
|
249
260
|
const closeAll = useContext(ComposerMenuCloseAllContext);
|
|
250
261
|
const presentation = useContext(ComposerMenuPresentationContext);
|
|
251
262
|
const sheetNav = useContext(ComposerSheetNavContext);
|
|
263
|
+
const narrow = useNarrowViewport();
|
|
252
264
|
if (sheetNav) {
|
|
253
265
|
return /* @__PURE__ */ jsx(
|
|
254
266
|
ComposerMenuRow,
|
|
@@ -286,9 +298,10 @@ function ComposerSubmenu({
|
|
|
286
298
|
/* @__PURE__ */ jsx(
|
|
287
299
|
PopoverContent,
|
|
288
300
|
{
|
|
289
|
-
side: "right",
|
|
301
|
+
side: narrow ? "top" : "right",
|
|
290
302
|
align: "start",
|
|
291
303
|
sideOffset: 6,
|
|
304
|
+
collisionPadding: 8,
|
|
292
305
|
onEscapeKeyDown: () => {
|
|
293
306
|
close();
|
|
294
307
|
closeAll?.();
|
|
@@ -305,7 +318,8 @@ function ComposerSubmenu({
|
|
|
305
318
|
className: cn(
|
|
306
319
|
"flex max-h-[var(--radix-popover-content-available-height)] flex-col overflow-hidden p-1",
|
|
307
320
|
COMPOSER_MENU_NO_ENTRANCE,
|
|
308
|
-
panelClassName ?? "w-72"
|
|
321
|
+
panelClassName ?? "w-72",
|
|
322
|
+
"max-sm:!w-[calc(100vw-1rem)]"
|
|
309
323
|
),
|
|
310
324
|
children: /* @__PURE__ */ jsx(ComposerMenuLevel, { children: drillInPlace ? /* @__PURE__ */ jsx(InPlaceNav, { children: typeof children === "function" ? children(close) : children }) : typeof children === "function" ? children(close) : children })
|
|
311
325
|
}
|
|
@@ -19,6 +19,12 @@ export interface LiveRunDisplayProps {
|
|
|
19
19
|
* the status line alive from the very first click (no dead moment).
|
|
20
20
|
*/
|
|
21
21
|
pending?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* The run failed before any stream existed (the launch was refused — e.g. a
|
|
24
|
+
* Mandate with no Holder). Shown in the error slot instead of "Starting…",
|
|
25
|
+
* so a refused launch never reads as a run still on its way.
|
|
26
|
+
*/
|
|
27
|
+
failure?: string | null;
|
|
22
28
|
/** Renders a dismiss (X) affordance; caller tears the run down. */
|
|
23
29
|
onDismiss?: () => void;
|
|
24
30
|
className?: string;
|
|
@@ -36,4 +42,4 @@ export interface LiveRunDisplayProps {
|
|
|
36
42
|
*/
|
|
37
43
|
variant?: "card" | "bare";
|
|
38
44
|
}
|
|
39
|
-
export declare function LiveRunDisplay({ conversationId, requestId: requestIdProp, label, pending, onDismiss, className, bodyClassName, variant, }: LiveRunDisplayProps): import("react").JSX.Element | null;
|
|
45
|
+
export declare function LiveRunDisplay({ conversationId, requestId: requestIdProp, label, pending, failure, onDismiss, className, bodyClassName, variant, }: LiveRunDisplayProps): import("react").JSX.Element | null;
|
|
@@ -20,12 +20,19 @@ function LiveRunDisplay({
|
|
|
20
20
|
requestId: requestIdProp,
|
|
21
21
|
label,
|
|
22
22
|
pending = false,
|
|
23
|
+
failure = null,
|
|
23
24
|
onDismiss,
|
|
24
25
|
className,
|
|
25
26
|
bodyClassName,
|
|
26
27
|
variant = "card"
|
|
27
28
|
}) {
|
|
28
|
-
const
|
|
29
|
+
const status = useLiveRunStatus(
|
|
30
|
+
conversationId,
|
|
31
|
+
requestIdProp,
|
|
32
|
+
pending && !failure
|
|
33
|
+
);
|
|
34
|
+
const { requestId, isActive, statusText, chunkCount } = status;
|
|
35
|
+
const errorMessage = status.errorMessage ?? (failure || null);
|
|
29
36
|
const conversationMessages = useAppSelector(
|
|
30
37
|
conversationId ? selectConversationMessages(conversationId) : selectNoConversationMessages
|
|
31
38
|
);
|
|
@@ -40,7 +47,7 @@ function LiveRunDisplay({
|
|
|
40
47
|
if (el) el.scrollTop = el.scrollHeight;
|
|
41
48
|
}, [chunkCount, isActive]);
|
|
42
49
|
const hasRun = Boolean(conversationId || requestId);
|
|
43
|
-
if (!hasRun && !pending) return null;
|
|
50
|
+
if (!hasRun && !pending && !failure) return null;
|
|
44
51
|
if (variant === "bare") {
|
|
45
52
|
return /* @__PURE__ */ jsxs("div", { className: cn("h-full min-h-0", className), children: [
|
|
46
53
|
errorMessage ? /* @__PURE__ */ jsxs("p", { className: "pb-2 text-xs text-destructive", children: [
|
|
@@ -94,7 +101,7 @@ function LiveRunDisplay({
|
|
|
94
101
|
),
|
|
95
102
|
children: [
|
|
96
103
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-b border-border px-2.5 py-1.5 text-xs text-muted-foreground", children: [
|
|
97
|
-
isActive || pending && !requestId ? /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 shrink-0 animate-spin text-primary" }) : errorMessage ? /* @__PURE__ */ jsx(TriangleAlert, { className: "h-3.5 w-3.5 shrink-0 text-destructive" }) : null,
|
|
104
|
+
isActive || pending && !requestId && !errorMessage ? /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 shrink-0 animate-spin text-primary" }) : errorMessage ? /* @__PURE__ */ jsx(TriangleAlert, { className: "h-3.5 w-3.5 shrink-0 text-destructive" }) : null,
|
|
98
105
|
/* @__PURE__ */ jsxs("span", { className: "min-w-0 truncate font-medium", children: [
|
|
99
106
|
label ?? "AI run",
|
|
100
107
|
serverPhase ? /* @__PURE__ */ jsxs("span", { className: "font-normal text-muted-foreground/80", children: [
|
|
@@ -8,7 +8,6 @@ import { Stethoscope } from "lucide-react";
|
|
|
8
8
|
import { useAppSelector, useAppStore } from "../../../store/hooks.js";
|
|
9
9
|
import { selectMessageCount } from "../../redux/execution-system/messages/messages.selectors.js";
|
|
10
10
|
import { selectStreamPhase } from "../../redux/execution-system/selectors/aggregate.selectors.js";
|
|
11
|
-
import { toast } from "../../../host/notify.js";
|
|
12
11
|
import { useDebugContext } from "../../../host/prefs-react.js";
|
|
13
12
|
import {
|
|
14
13
|
buildTranscriptIntegrityReport,
|
|
@@ -67,16 +66,11 @@ function TranscriptIntegrityCopyButtonInner({
|
|
|
67
66
|
pathname,
|
|
68
67
|
effectiveVisibleGroupLimit
|
|
69
68
|
});
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
} catch (err) {
|
|
76
|
-
toast.error(
|
|
77
|
-
`Could not copy the transcript report: ${err instanceof Error ? err.message : String(err)}`
|
|
78
|
-
);
|
|
79
|
-
}
|
|
69
|
+
if (!await copyText(
|
|
70
|
+
formatTranscriptIntegrityReport(report),
|
|
71
|
+
report.anomalies.length === 0 ? "Transcript report copied \u2014 no anomalies detected" : `Transcript report copied \u2014 ${report.anomalies.length} anomal${report.anomalies.length === 1 ? "y" : "ies"} flagged`
|
|
72
|
+
))
|
|
73
|
+
return;
|
|
80
74
|
};
|
|
81
75
|
return /* @__PURE__ */ jsx(
|
|
82
76
|
Button,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { RegistryActionMenu, buildChatMessageActions, useDocumentDialogsHost } from "../../../../host/rich-document-slots.js";
|
|
4
4
|
import { useState, Suspense, useCallback } from "react";
|
|
5
|
-
import {
|
|
5
|
+
import { Edit, Send, MoreHorizontal, Pin, PinOff, Loader2 } from "lucide-react";
|
|
6
6
|
import {
|
|
7
7
|
togglePinnedMessage,
|
|
8
8
|
usePendingPinMessageIds,
|
|
@@ -12,8 +12,7 @@ import {
|
|
|
12
12
|
TapTargetButtonForGroup,
|
|
13
13
|
TapTargetButtonGroup
|
|
14
14
|
} from "@ai-matrx/tap-target";
|
|
15
|
-
import {
|
|
16
|
-
import { SpeakerButton } from "../../../../host/ui-slots.js";
|
|
15
|
+
import { CopySplit, SpeakerButton } from "../../../../host/ui-slots.js";
|
|
17
16
|
import { useAppDispatch, useAppSelector, useAppStore } from "../../../../store/hooks.js";
|
|
18
17
|
import { useOpenFullScreenMarkdownEditorBridge } from "../../../../host/window-openers.js";
|
|
19
18
|
import { selectMessagePosition } from "../../../redux/execution-system/messages/messages.selectors.js";
|
|
@@ -69,7 +68,6 @@ function UserActionBar({
|
|
|
69
68
|
const dispatch = useAppDispatch();
|
|
70
69
|
const store = useAppStore();
|
|
71
70
|
const openEditor = useOpenFullScreenMarkdownEditorBridge();
|
|
72
|
-
const [isCopied, setIsCopied] = useState(false);
|
|
73
71
|
const isPinned = usePinnedMessageIds().has(messageId);
|
|
74
72
|
const pinPending = usePendingPinMessageIds().has(messageId);
|
|
75
73
|
const [showOptionsMenu, setShowOptionsMenu] = useState(false);
|
|
@@ -82,12 +80,6 @@ function UserActionBar({
|
|
|
82
80
|
const showOptions = useAppSelector(
|
|
83
81
|
selectShowUserMessageOptions(conversationId)
|
|
84
82
|
);
|
|
85
|
-
const handleCopy = async () => {
|
|
86
|
-
if (await copyRichContent(content, "default", { toast: false })) {
|
|
87
|
-
setIsCopied(true);
|
|
88
|
-
setTimeout(() => setIsCopied(false), 2e3);
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
83
|
const handleEditAction = useCallback(
|
|
92
84
|
(actionId, newContent) => routeUserEditAction(dispatch, {
|
|
93
85
|
actionId,
|
|
@@ -183,14 +175,7 @@ function UserActionBar({
|
|
|
183
175
|
const canFork = (messagePosition ?? 0) > 0;
|
|
184
176
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
185
177
|
/* @__PURE__ */ jsxs(TapTargetButtonGroup, { surface: "solid", children: [
|
|
186
|
-
/* @__PURE__ */ jsx(
|
|
187
|
-
TapTargetButtonForGroup,
|
|
188
|
-
{
|
|
189
|
-
onClick: handleCopy,
|
|
190
|
-
ariaLabel: "Copy message",
|
|
191
|
-
icon: isCopied ? /* @__PURE__ */ jsx(Check, { className: "w-4 h-4 text-blue-500 dark:text-blue-400" }) : /* @__PURE__ */ jsx(Copy, { className: "w-4 h-4 text-muted-foreground" })
|
|
192
|
-
}
|
|
193
|
-
),
|
|
178
|
+
/* @__PURE__ */ jsx(CopySplit, { text: content, label: "Copy message" }),
|
|
194
179
|
/* @__PURE__ */ jsx(SpeakerButton, { text: content, variant: "group" }),
|
|
195
180
|
/* @__PURE__ */ jsx(
|
|
196
181
|
TapTargetButtonForGroup,
|
|
@@ -15,9 +15,6 @@ import { type MandateKey } from "@ai-matrx/agents/mandates";
|
|
|
15
15
|
* `services/mandates/client_mandates.py`, then add its key here.
|
|
16
16
|
*/
|
|
17
17
|
export declare const SYSTEM_AGENT_MANDATES: {
|
|
18
|
-
readonly "prompt-app-auto-create": "agent_apps.auto_create";
|
|
19
|
-
readonly "prompt-app-auto-create-lightning": "agent_apps.auto_create_lightning";
|
|
20
|
-
readonly "prompt-app-metadata-generator": "agent_apps.metadata";
|
|
21
18
|
readonly "prompt-app-ui-editor": "code_editor.prompt_app_ui_edit";
|
|
22
19
|
readonly "generic-code-editor": "code_editor.code_edit";
|
|
23
20
|
readonly "code-editor-dynamic-context": "code_editor.dynamic_context_edit";
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { MANDATE_KEYS } from "@ai-matrx/agents/mandates";
|
|
2
2
|
const SYSTEM_AGENT_MANDATES = {
|
|
3
|
-
"prompt-app-auto-create": MANDATE_KEYS.agent_apps__auto_create,
|
|
4
|
-
"prompt-app-auto-create-lightning": MANDATE_KEYS.agent_apps__auto_create_lightning,
|
|
5
|
-
"prompt-app-metadata-generator": MANDATE_KEYS.agent_apps__metadata,
|
|
6
3
|
"prompt-app-ui-editor": MANDATE_KEYS.code_editor__prompt_app_ui_edit,
|
|
7
4
|
"generic-code-editor": MANDATE_KEYS.code_editor__code_edit,
|
|
8
5
|
"code-editor-dynamic-context": MANDATE_KEYS.code_editor__dynamic_context_edit,
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { type LiveAgentRunOptions } from "./useLiveAgentRun.js";
|
|
2
|
+
/**
|
|
3
|
+
* The words a failed launch shows in its window: the server's own reason when
|
|
4
|
+
* the run carried one (a Mandate with no Holder names the rungs it checked),
|
|
5
|
+
* else the error's message.
|
|
6
|
+
*/
|
|
7
|
+
export declare function floatingFailureReason(error: unknown): string;
|
|
2
8
|
/**
|
|
3
9
|
* A run this hook only WATCHES — launched and owned elsewhere, its state read
|
|
4
10
|
* straight out of React. Pass it as `track` and the window follows it; there is
|
|
@@ -48,6 +54,19 @@ export interface FloatingRun {
|
|
|
48
54
|
bindRequest: (requestId: string) => void;
|
|
49
55
|
/** Close this run's window (and release its instance when owned). */
|
|
50
56
|
close: () => void;
|
|
57
|
+
/**
|
|
58
|
+
* The launch failed: the window stops saying "Starting…" and shows `reason`.
|
|
59
|
+
* Call it from the launch's failure path — a window left pending after a
|
|
60
|
+
* refused launch reads as a run that is still coming (the Spaces "Build
|
|
61
|
+
* with AI" incident, 2026-10-07).
|
|
62
|
+
*/
|
|
63
|
+
fail: (reason: string) => void;
|
|
64
|
+
/**
|
|
65
|
+
* Call once the launch has returned, however it ended: a window still
|
|
66
|
+
* waiting for its first stream handle shows `reason` instead of "Starting…"
|
|
67
|
+
* forever. A bound window is left alone — its own stream tells the story.
|
|
68
|
+
*/
|
|
69
|
+
settle: (reason: string) => void;
|
|
51
70
|
}
|
|
52
71
|
export interface UseFloatingRunWindow {
|
|
53
72
|
/** Open the window BEFORE launching, then bind the run to it. */
|
|
@@ -7,6 +7,13 @@ import {
|
|
|
7
7
|
useLiveAgentRun
|
|
8
8
|
} from "./useLiveAgentRun.js";
|
|
9
9
|
import { useLiveRunHandle } from "./useLiveRunHandle.js";
|
|
10
|
+
import { HeadlessAgentRunError } from "./useHeadlessAgentJson.js";
|
|
11
|
+
import { isOrganizationSelectionCancelled } from "../../host/org.js";
|
|
12
|
+
function floatingFailureReason(error) {
|
|
13
|
+
if (error instanceof HeadlessAgentRunError && error.detail) return error.detail;
|
|
14
|
+
if (error instanceof Error && error.message) return error.message;
|
|
15
|
+
return "The run could not start.";
|
|
16
|
+
}
|
|
10
17
|
function useFloatingRunWindow(options = {}) {
|
|
11
18
|
const { instanceId, width, height, owns = true, track } = options;
|
|
12
19
|
const openWindow = useOpenLiveRunWindow();
|
|
@@ -76,13 +83,22 @@ function useFloatingRunWindow(options = {}) {
|
|
|
76
83
|
height
|
|
77
84
|
});
|
|
78
85
|
handleRef.current = handle;
|
|
86
|
+
let bound = false;
|
|
79
87
|
return {
|
|
80
88
|
bind: (conversationId) => {
|
|
89
|
+
bound = true;
|
|
81
90
|
if (owns) run.claim(conversationId);
|
|
82
91
|
handle.update({ conversationId, pending: false });
|
|
83
92
|
},
|
|
84
|
-
bindRequest: (requestId) =>
|
|
85
|
-
|
|
93
|
+
bindRequest: (requestId) => {
|
|
94
|
+
bound = true;
|
|
95
|
+
handle.update({ requestId, pending: false });
|
|
96
|
+
},
|
|
97
|
+
close,
|
|
98
|
+
fail: (reason) => handle.update({ pending: false, failure: reason }),
|
|
99
|
+
settle: (reason) => {
|
|
100
|
+
if (!bound) handle.update({ pending: false, failure: reason });
|
|
101
|
+
}
|
|
86
102
|
};
|
|
87
103
|
};
|
|
88
104
|
return { start, close };
|
|
@@ -93,13 +109,19 @@ function useFloatingAgentRun(options = {}) {
|
|
|
93
109
|
async function run(opts) {
|
|
94
110
|
const { label, ...runOptions } = opts;
|
|
95
111
|
const bound = floating.start(label ?? options.label ?? "AI is working");
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
112
|
+
try {
|
|
113
|
+
return await live.run({
|
|
114
|
+
...runOptions,
|
|
115
|
+
onConversationCreated: (conversationId) => {
|
|
116
|
+
bound.bind(conversationId);
|
|
117
|
+
opts.onConversationCreated?.(conversationId);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (isOrganizationSelectionCancelled(error)) bound.close();
|
|
122
|
+
else bound.fail(floatingFailureReason(error));
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
103
125
|
}
|
|
104
126
|
return {
|
|
105
127
|
run,
|
|
@@ -114,6 +136,7 @@ function useFloatingAgentRun(options = {}) {
|
|
|
114
136
|
};
|
|
115
137
|
}
|
|
116
138
|
export {
|
|
139
|
+
floatingFailureReason,
|
|
117
140
|
useFloatingAgentRun,
|
|
118
141
|
useFloatingRunWindow
|
|
119
142
|
};
|
|
@@ -5,7 +5,6 @@ import { toast as copyToast } from "../../host/notify.js";
|
|
|
5
5
|
import { useEffect, useState } from "react";
|
|
6
6
|
import { Loader2, Copy, RefreshCw, AlertTriangle } from "lucide-react";
|
|
7
7
|
import { Button } from "@ai-matrx/design-system/controls";
|
|
8
|
-
import { toast } from "../../host/notify.js";
|
|
9
8
|
import { useAppStore } from "../../store/hooks.js";
|
|
10
9
|
import { requestPromptPreview } from "./service.js";
|
|
11
10
|
import { ErrorAlchemyMenu } from "../../host/ui-slots.js";
|
|
@@ -43,12 +42,7 @@ function PromptPreviewContent({
|
|
|
43
42
|
};
|
|
44
43
|
}, [conversationId, store, nonce]);
|
|
45
44
|
const copy = async (label, text) => {
|
|
46
|
-
|
|
47
|
-
if (!await copyText(text)) return;
|
|
48
|
-
toast.success(`${label} copied`);
|
|
49
|
-
} catch {
|
|
50
|
-
toast.error("Couldn't copy");
|
|
51
|
-
}
|
|
45
|
+
if (!await copyText(text, `${label} copied`)) return;
|
|
52
46
|
};
|
|
53
47
|
return /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-3 bg-textured p-4", children: [
|
|
54
48
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
|
|
@@ -122,6 +122,20 @@ export declare const NON_ANSWER_BLOCK_TYPES: Set<string>;
|
|
|
122
122
|
* chunk text. `editedText` wins, mirroring the selectors.
|
|
123
123
|
*/
|
|
124
124
|
export declare function deriveAnswerText(request: Pick<ActiveRequest, "renderBlockOrder" | "renderBlocks" | "editedText">): string;
|
|
125
|
+
/**
|
|
126
|
+
* The answer text FOR DATA CAPTURE (JSON extraction). Identical to
|
|
127
|
+
* {@link deriveAnswerText} except where a JSON wrapper holding kinds was split
|
|
128
|
+
* for display (A7: kinds + a residual that has the kinds REMOVED): those
|
|
129
|
+
* blocks are replaced by the bytes they were cut from. Without this, a
|
|
130
|
+
* structured answer whose arrays hold `{"__kind": …}` items was captured with
|
|
131
|
+
* those arrays EMPTY — an Applet's `sources` saved as `[]` (2026-10-07).
|
|
132
|
+
* A split whose blocks are no longer all present (a rewind) falls back to the
|
|
133
|
+
* blocks themselves.
|
|
134
|
+
*/
|
|
135
|
+
export declare function deriveAnswerDataText(request: Pick<ActiveRequest, "renderBlockOrder" | "renderBlocks" | "editedText">, wrapperSplits: ReadonlyArray<{
|
|
136
|
+
blockIds: string[];
|
|
137
|
+
source: string;
|
|
138
|
+
}>): string;
|
|
125
139
|
/**
|
|
126
140
|
* Lossless document projection of an answer for string destinations that
|
|
127
141
|
* intentionally accept Content IR. Plain-answer consumers keep using
|
|
@@ -47,6 +47,35 @@ function deriveAnswerText(request) {
|
|
|
47
47
|
const answer = order.map((id) => blocks[id]).filter((b) => b && !NON_ANSWER_BLOCK_TYPES.has(b.type)).map((b) => b?.content ?? "").filter(Boolean).join("\n");
|
|
48
48
|
return stripThinkingStreaming(answer).visible;
|
|
49
49
|
}
|
|
50
|
+
function deriveAnswerDataText(request, wrapperSplits) {
|
|
51
|
+
if (wrapperSplits.length === 0) return deriveAnswerText(request);
|
|
52
|
+
const { renderBlockOrder: order, renderBlocks: blocks, editedText } = request;
|
|
53
|
+
if (editedText !== null && editedText !== void 0) return editedText;
|
|
54
|
+
if (!order || !blocks || order.length === 0) return "";
|
|
55
|
+
const present = new Set(order);
|
|
56
|
+
const splitOf = /* @__PURE__ */ new Map();
|
|
57
|
+
for (const split of wrapperSplits) {
|
|
58
|
+
if (split.blockIds.length === 0) continue;
|
|
59
|
+
if (!split.blockIds.every((id) => present.has(id))) continue;
|
|
60
|
+
for (const id of split.blockIds) splitOf.set(id, split);
|
|
61
|
+
}
|
|
62
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
63
|
+
const parts = [];
|
|
64
|
+
for (const id of order) {
|
|
65
|
+
const split = splitOf.get(id);
|
|
66
|
+
if (split) {
|
|
67
|
+
if (!emitted.has(split)) {
|
|
68
|
+
emitted.add(split);
|
|
69
|
+
parts.push(split.source);
|
|
70
|
+
}
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const block = blocks[id];
|
|
74
|
+
if (!block || NON_ANSWER_BLOCK_TYPES.has(block.type)) continue;
|
|
75
|
+
if (block.content) parts.push(block.content);
|
|
76
|
+
}
|
|
77
|
+
return stripThinkingStreaming(parts.join("\n")).visible;
|
|
78
|
+
}
|
|
50
79
|
function hasKindMarker(value) {
|
|
51
80
|
return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.__kind === "string";
|
|
52
81
|
}
|
|
@@ -1169,6 +1198,7 @@ export {
|
|
|
1169
1198
|
NON_ANSWER_BLOCK_TYPES,
|
|
1170
1199
|
blockCarriesDataNotText,
|
|
1171
1200
|
blockHasSomethingToRender,
|
|
1201
|
+
deriveAnswerDataText,
|
|
1172
1202
|
deriveAnswerDocumentText,
|
|
1173
1203
|
deriveAnswerText,
|
|
1174
1204
|
deriveDecisionResultText,
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import { getLiveDesktopInstance } from "./desktop-presence.js";
|
|
2
2
|
import { registerClientCapability } from "./registry.js";
|
|
3
|
+
import { isChatHostConfigured, onChatHostConfigured } from "../../../../host/configure.js";
|
|
3
4
|
function toSysPlatform(platform) {
|
|
4
5
|
return platform === "windows" ? "win32" : platform;
|
|
5
6
|
}
|
|
6
7
|
const PRESENCE_BUDGET_MS = 150;
|
|
7
8
|
if (typeof window !== "undefined") {
|
|
8
|
-
setTimeout(() => void getLiveDesktopInstance(), 0);
|
|
9
|
+
const warm = () => setTimeout(() => void getLiveDesktopInstance(), 0);
|
|
10
|
+
if (isChatHostConfigured()) {
|
|
11
|
+
warm();
|
|
12
|
+
} else {
|
|
13
|
+
const stop = onChatHostConfigured(() => {
|
|
14
|
+
stop();
|
|
15
|
+
warm();
|
|
16
|
+
});
|
|
17
|
+
}
|
|
9
18
|
}
|
|
10
19
|
registerClientCapability({
|
|
11
20
|
name: "desktop-native",
|
|
@@ -3,6 +3,7 @@ import { supabase } from "../../../../host/db.js";
|
|
|
3
3
|
import { updateMessageRecord } from "../messages/messages.slice.js";
|
|
4
4
|
import { messageRowToRecord } from "../thunks/conversation-bundle.js";
|
|
5
5
|
import { durableRecordId } from "@ai-matrx/kit/ids";
|
|
6
|
+
import { isSignedOutVisitor } from "../../../../host/identity.js";
|
|
6
7
|
function hasDisplayableMessageContent(value) {
|
|
7
8
|
if (!Array.isArray(value)) return false;
|
|
8
9
|
return value.some((part) => {
|
|
@@ -27,6 +28,9 @@ const refetchSingleMessage = createAsyncThunk(
|
|
|
27
28
|
message: "This answer has no saved copy yet \u2014 nothing to reload."
|
|
28
29
|
});
|
|
29
30
|
}
|
|
31
|
+
if (await isSignedOutVisitor()) {
|
|
32
|
+
return { conversationId, messageId, found: false, refreshed: false };
|
|
33
|
+
}
|
|
30
34
|
const read = () => {
|
|
31
35
|
const byId = supabase.schema("chat").from("message").select("*").eq("id", messageId);
|
|
32
36
|
const scoped = persistedConversationId ? byId.eq("conversation_id", persistedConversationId) : byId;
|
|
@@ -35,6 +39,7 @@ const refetchSingleMessage = createAsyncThunk(
|
|
|
35
39
|
let result = await read();
|
|
36
40
|
let delay = 250;
|
|
37
41
|
for (let attempt = 0; waitForReadable && attempt < 7; attempt++) {
|
|
42
|
+
if (result.error) break;
|
|
38
43
|
const hasBody = hasDisplayableMessageContent(
|
|
39
44
|
result.data?.user_content ?? result.data?.content
|
|
40
45
|
);
|
|
@@ -111,7 +111,7 @@ import { docKindForContextKey } from "../../../utils/workingDocumentContext.js";
|
|
|
111
111
|
import { isCanvasItemContextKey } from "../../../utils/canvasItemContext.js";
|
|
112
112
|
import { StreamingJsonTracker } from "@ai-matrx/kit/json-extract";
|
|
113
113
|
import { StreamBlockAccumulator } from "../utils/stream-block-accumulator.js";
|
|
114
|
-
import {
|
|
114
|
+
import { deriveAnswerDataText } from "../active-requests/active-requests.selectors.js";
|
|
115
115
|
import { captureError } from "../../../../host/diagnostics.js";
|
|
116
116
|
import { readReceiptThread, withReceiptHandle } from "../instance-resources/remark-handles.js";
|
|
117
117
|
import { stampPersistedRemarkHandles } from "../message-crud/stamp-remark-handles.thunk.js";
|
|
@@ -351,7 +351,7 @@ async function processStream({
|
|
|
351
351
|
const runJsonExtraction = (isFinal) => {
|
|
352
352
|
if (!jsonTracker) return;
|
|
353
353
|
const req = getState().activeRequests.byRequestId[requestId];
|
|
354
|
-
const answerText = req ?
|
|
354
|
+
const answerText = req ? deriveAnswerDataText(req, blockAccumulator.getWrapperSplitSources()) : "";
|
|
355
355
|
if (!reasoningLeakReported && hasReasoningLeak(answerText)) {
|
|
356
356
|
reasoningLeakReported = true;
|
|
357
357
|
captureError({
|
|
@@ -38,6 +38,15 @@ export declare class StreamBlockAccumulator {
|
|
|
38
38
|
* to be a different typed block (see the bare-JSON duplication note there).
|
|
39
39
|
*/
|
|
40
40
|
private currentBlockEmitted;
|
|
41
|
+
/**
|
|
42
|
+
* THE DATA KEEPS ITS KINDS. A JSON wrapper that holds kinds is drawn as its
|
|
43
|
+
* kinds plus a residual (A7) — the residual is the wrapper's data WITH ITS
|
|
44
|
+
* KINDS REMOVED, so it is a display projection, never the answer. Every
|
|
45
|
+
* such split remembers the bytes it was cut from, keyed by its first block
|
|
46
|
+
* index, so data capture (`deriveAnswerDataText`) reads what the model wrote.
|
|
47
|
+
* Live only; a rewind forgets every split at or past its boundary.
|
|
48
|
+
*/
|
|
49
|
+
private wrapperSplits;
|
|
41
50
|
private subState;
|
|
42
51
|
/**
|
|
43
52
|
* URL + label for the current `audio` block. The accumulator's single-line
|
|
@@ -148,6 +157,11 @@ export declare class StreamBlockAccumulator {
|
|
|
148
157
|
* the local streaming state so the replacement provider attempt cannot
|
|
149
158
|
* concatenate onto or resurrect the failed attempt.
|
|
150
159
|
*/
|
|
160
|
+
/** The source bytes behind every live A7 wrapper split (see `wrapperSplits`). */
|
|
161
|
+
getWrapperSplitSources(): Array<{
|
|
162
|
+
blockIds: string[];
|
|
163
|
+
source: string;
|
|
164
|
+
}>;
|
|
151
165
|
rewindToBlockCount(blockCount: number): void;
|
|
152
166
|
/**
|
|
153
167
|
* Close the current text block at an interleaved-content boundary — a tool
|