@ai-matrx/chat 0.7.3 → 0.7.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.
- package/CHANGELOG.md +10 -0
- package/dist/agents/addressing/agentAddress.d.ts +8 -0
- package/dist/agents/addressing/agentAddress.js +1 -0
- package/dist/agents/addressing/useAgentHref.js +8 -1
- package/dist/agents/components/conversation-history/ConversationHistorySidebar.d.ts +7 -0
- package/dist/agents/components/conversation-history/ConversationHistorySidebar.js +13 -7
- package/dist/agents/components/shared/assistant-message-layout.d.ts +21 -0
- package/dist/agents/components/shared/assistant-message-layout.js +6 -0
- package/dist/agents/hooks/useCreatorOwnershipSync.js +3 -1
- package/dist/agents/redux/conversation-history/thunks.js +2 -1
- package/dist/agents/redux/execution-system/instance-resources/rendered-artifact-draft.d.ts +23 -0
- package/dist/agents/redux/execution-system/instance-resources/rendered-artifact-draft.js +100 -0
- package/dist/agents/redux/execution-system/instance-user-input/composer-draft.middleware.js +9 -0
- package/dist/agents/redux/execution-system/instance-user-input/useComposerDraftRestore.js +3 -1
- package/dist/agents/redux/execution-system/thunks/execute-instance.thunk.js +1 -1
- package/dist/agents/redux/execution-system/thunks/execute-manual-instance.thunk.js +1 -1
- package/dist/agents/redux/execution-system/thunks/frozen-submission.d.ts +5 -0
- package/dist/agents/redux/execution-system/thunks/frozen-submission.js +2 -1
- package/dist/agents/redux/execution-system/utils/content-prefilter.d.ts +50 -0
- package/dist/agents/redux/execution-system/utils/content-prefilter.js +243 -0
- package/dist/agents/redux/execution-system/utils/stream-block-accumulator.d.ts +335 -65
- package/dist/agents/redux/execution-system/utils/stream-block-accumulator.js +1909 -224
- package/dist/conversation/hooks/useDomCapturePrint.js +13 -2
- package/dist/conversation/utils/frame-print.js +20 -2
- package/dist/conversation/utils/isolate-for-capture.d.ts +19 -0
- package/dist/conversation/utils/isolate-for-capture.js +55 -0
- package/dist/host/admin-seat.d.ts +19 -0
- package/dist/host/admin-seat.js +8 -0
- package/dist/ui/markdown-stream/EnhancedChatMarkdown.js +5 -1
- package/package.json +9 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @ai-matrx/chat
|
|
2
2
|
|
|
3
|
+
## 0.7.4
|
|
4
|
+
### Unreleased
|
|
5
|
+
|
|
6
|
+
- THE ADMIN SEAT beyond reply: new `adminSeatOpen(state)` (`host/admin-seat.ts`). On an admin page a platform admin is the
|
|
7
|
+
creator of every agent (`useCreatorOwnershipSync`), every agent link opens in the System Agents tree
|
|
8
|
+
(`AgentAddressViewer.adminSeat`), and an agent-scoped history read shows every person's runs. User pages unchanged.
|
|
9
|
+
- `ConversationHistorySidebar` (dense): new `titleFirst` prop for narrow hosts (a board tile's sidebar). Rows drop the agent
|
|
10
|
+
chip and origin badge and show the star only on a favorite (adding one stays in the row menu), so the title wins the row.
|
|
11
|
+
Default unchanged.
|
|
12
|
+
|
|
3
13
|
## 0.7.3
|
|
4
14
|
|
|
5
15
|
Automatic changed-only republish (docs/metadata drift since the last tag — see
|
|
@@ -112,6 +112,14 @@ export declare function isSystemAgentType(agentType: string | null): boolean;
|
|
|
112
112
|
*/
|
|
113
113
|
export interface AgentAddressViewer {
|
|
114
114
|
isAdmin: boolean;
|
|
115
|
+
/**
|
|
116
|
+
* THE ADMIN SEAT (`adminSeatOpen`): an admin standing on an admin page. There
|
|
117
|
+
* EVERY agent — user agents included — opens in the admin System Agents
|
|
118
|
+
* tree, so editing someone's agent from the admin system stays inside it
|
|
119
|
+
* (the admin lane, and with it the admin's write access, is open only
|
|
120
|
+
* there). Off the admin pages the seat is closed and the kind rule applies.
|
|
121
|
+
*/
|
|
122
|
+
adminSeat?: boolean;
|
|
115
123
|
}
|
|
116
124
|
/** The route prefix an agent of this kind lives under, for this viewer. */
|
|
117
125
|
export declare function agentBasePathFor(agentType: string | null, viewer?: AgentAddressViewer): string;
|
|
@@ -8,6 +8,7 @@ function isSystemAgentType(agentType) {
|
|
|
8
8
|
return agentType === "builtin";
|
|
9
9
|
}
|
|
10
10
|
function agentBasePathFor(agentType, viewer) {
|
|
11
|
+
if (viewer?.adminSeat && viewer.isAdmin) return SYSTEM_AGENT_BASE_PATH;
|
|
11
12
|
return isSystemAgentType(agentType) && (viewer?.isAdmin ?? true) ? SYSTEM_AGENT_BASE_PATH : AGENT_BASE_PATH;
|
|
12
13
|
}
|
|
13
14
|
const ADMIN_ONLY_AGENT_SUBROUTES = /* @__PURE__ */ new Set(["samples"]);
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
resolveAgentAddress
|
|
14
14
|
} from "./agentAddressCache.js";
|
|
15
15
|
import { selectIsAdmin } from "../../host/identity.js";
|
|
16
|
+
import { adminSeatOpen } from "../../host/admin-seat.js";
|
|
16
17
|
const noSubscribe = () => () => {
|
|
17
18
|
};
|
|
18
19
|
function useAgentAddressViewer() {
|
|
@@ -25,7 +26,13 @@ function useAgentAddressViewer() {
|
|
|
25
26
|
read,
|
|
26
27
|
read
|
|
27
28
|
);
|
|
28
|
-
|
|
29
|
+
const readSeat = () => store ? adminSeatOpen(store.getState()) : false;
|
|
30
|
+
const adminSeat = useSyncExternalStore(
|
|
31
|
+
store ? store.subscribe : noSubscribe,
|
|
32
|
+
readSeat,
|
|
33
|
+
() => false
|
|
34
|
+
);
|
|
35
|
+
return isAdmin === void 0 ? void 0 : { isAdmin, adminSeat };
|
|
29
36
|
}
|
|
30
37
|
function useAgentHref({
|
|
31
38
|
id,
|
|
@@ -143,6 +143,13 @@ export interface ConversationHistorySidebarProps {
|
|
|
143
143
|
* list refreshes each time it mounts.
|
|
144
144
|
*/
|
|
145
145
|
keepLoaded?: boolean;
|
|
146
|
+
/**
|
|
147
|
+
* Rows in a narrow host (a board tile's sidebar): the title wins the row.
|
|
148
|
+
* The agent chip is dropped (every row would repeat the one agent) and the
|
|
149
|
+
* star shows only on a row that is already a favorite — adding one lives in
|
|
150
|
+
* the row menu. The time stays, compact. Default false.
|
|
151
|
+
*/
|
|
152
|
+
titleFirst?: boolean;
|
|
146
153
|
className?: string;
|
|
147
154
|
/** Extra classes for the built-in search input (dense). */
|
|
148
155
|
searchInputClassName?: string;
|
|
@@ -380,6 +380,7 @@ const DenseView = ({
|
|
|
380
380
|
headerActions,
|
|
381
381
|
showSearch = true,
|
|
382
382
|
showGroupingToggle = true,
|
|
383
|
+
titleFirst = false,
|
|
383
384
|
className,
|
|
384
385
|
searchInputClassName,
|
|
385
386
|
sectionHeaderClassName
|
|
@@ -510,7 +511,8 @@ const DenseView = ({
|
|
|
510
511
|
onToggleFavorite: onToggleFavoriteResolved,
|
|
511
512
|
resolveHref,
|
|
512
513
|
surfaceKey,
|
|
513
|
-
getSourceMenuCtx
|
|
514
|
+
getSourceMenuCtx,
|
|
515
|
+
titleFirst
|
|
514
516
|
},
|
|
515
517
|
`search-${conv.conversationId}`
|
|
516
518
|
))
|
|
@@ -536,7 +538,8 @@ const DenseView = ({
|
|
|
536
538
|
onToggleFavorite: onToggleFavoriteResolved,
|
|
537
539
|
resolveHref,
|
|
538
540
|
surfaceKey,
|
|
539
|
-
getSourceMenuCtx
|
|
541
|
+
getSourceMenuCtx,
|
|
542
|
+
titleFirst
|
|
540
543
|
},
|
|
541
544
|
`fav-${conv.conversationId}`
|
|
542
545
|
))
|
|
@@ -562,7 +565,8 @@ const DenseView = ({
|
|
|
562
565
|
onToggleFavorite: onToggleFavoriteResolved,
|
|
563
566
|
resolveHref,
|
|
564
567
|
surfaceKey,
|
|
565
|
-
getSourceMenuCtx
|
|
568
|
+
getSourceMenuCtx,
|
|
569
|
+
titleFirst
|
|
566
570
|
},
|
|
567
571
|
conv.conversationId
|
|
568
572
|
))
|
|
@@ -598,6 +602,7 @@ const DenseView = ({
|
|
|
598
602
|
resolveHref,
|
|
599
603
|
surfaceKey,
|
|
600
604
|
getSourceMenuCtx,
|
|
605
|
+
titleFirst,
|
|
601
606
|
showAgentHint: false
|
|
602
607
|
},
|
|
603
608
|
conv.conversationId
|
|
@@ -886,19 +891,20 @@ const Row = ({
|
|
|
886
891
|
resolveHref,
|
|
887
892
|
surfaceKey,
|
|
888
893
|
getSourceMenuCtx,
|
|
889
|
-
showAgentHint = true
|
|
894
|
+
showAgentHint = true,
|
|
895
|
+
titleFirst = false
|
|
890
896
|
}) => {
|
|
891
897
|
const title = conv.title?.trim() || untitled(conv);
|
|
892
898
|
const agentName = useAppSelector(
|
|
893
899
|
(s) => conv.agentId ? selectAgentById(s, conv.agentId)?.name ?? null : null
|
|
894
900
|
);
|
|
895
|
-
const star = onToggleFavorite ? /* @__PURE__ */ jsx(Button, { variant: "quiet", icon: isFavorite ? /* @__PURE__ */ jsx(Star, { fill: "currentColor" }) : /* @__PURE__ */ jsx(StarOff, {}), onClick: (e) => {
|
|
901
|
+
const star = onToggleFavorite && (!titleFirst || isFavorite) ? /* @__PURE__ */ jsx(Button, { variant: "quiet", icon: isFavorite ? /* @__PURE__ */ jsx(Star, { fill: "currentColor" }) : /* @__PURE__ */ jsx(StarOff, {}), onClick: (e) => {
|
|
896
902
|
e.stopPropagation();
|
|
897
903
|
e.preventDefault();
|
|
898
904
|
onToggleFavorite(conv);
|
|
899
905
|
}, "aria-label": isFavorite ? "Remove from favorites" : "Add to favorites", title: isFavorite ? "Remove from favorites" : "Add to favorites" }) : null;
|
|
900
906
|
const meta = formatRelative(conv.updatedAt);
|
|
901
|
-
const originBadge = conv.originClass && conv.originClass !== "human" ? /* @__PURE__ */ jsx(
|
|
907
|
+
const originBadge = !titleFirst && conv.originClass && conv.originClass !== "human" ? /* @__PURE__ */ jsx(
|
|
902
908
|
"span",
|
|
903
909
|
{
|
|
904
910
|
className: "inline-flex h-4 shrink-0 items-center rounded-full border border-border px-1.5 text-[9px] font-medium text-muted-foreground",
|
|
@@ -906,7 +912,7 @@ const Row = ({
|
|
|
906
912
|
children: originClassLabel(conv.originClass)
|
|
907
913
|
}
|
|
908
914
|
) : null;
|
|
909
|
-
const trailing = showAgentHint && conv.agentId ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
915
|
+
const trailing = showAgentHint && !titleFirst && conv.agentId ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
910
916
|
originBadge,
|
|
911
917
|
/* @__PURE__ */ jsx(
|
|
912
918
|
"span",
|
|
@@ -27,3 +27,24 @@ export declare const ASSISTANT_MESSAGE_COLUMN_INSET_CLASS = "px-2";
|
|
|
27
27
|
export declare const WIDE_FIGURE_MAX_WIDTH_PX = 1100;
|
|
28
28
|
export declare const WIDE_FIGURE_SCOPE_ATTRIBUTE = "data-wide-figures";
|
|
29
29
|
export declare const WIDE_FIGURE_CLASS = "[[data-wide-figures]_&]:w-[max(100%,min(1100px,100cqi_-_2rem))] [[data-wide-figures]_&]:ml-[calc((100%_-_max(100%,min(1100px,100cqi_-_2rem)))/2)]";
|
|
30
|
+
/**
|
|
31
|
+
* The marker a wide figure carries (beside `WIDE_FIGURE_CLASS`) so the message
|
|
32
|
+
* wrappers around it can make room for it — see `WIDE_FIGURE_HOST_CLASS`.
|
|
33
|
+
*/
|
|
34
|
+
export declare const WIDE_FIGURE_ATTRIBUTE = "data-wide-figure";
|
|
35
|
+
/**
|
|
36
|
+
* A message's own wrappers clip sideways (`overflow-x: clip`) so wide prose
|
|
37
|
+
* never blows out the column — which also cut a wide figure back down to the
|
|
38
|
+
* column, hiding its left edge and its header actions (2026-10-08). A wrapper
|
|
39
|
+
* carrying this class, in an opted-in transcript, when it holds a wide figure,
|
|
40
|
+
* moves its clip edge out to the figure's band: negative margins + equal
|
|
41
|
+
* padding, so its CONTENT box stays exactly the column while its clip box is
|
|
42
|
+
* the band. Inert everywhere else (no scope, no figure → no change).
|
|
43
|
+
*/
|
|
44
|
+
export declare const WIDE_FIGURE_HOST_CLASS = "[[data-wide-figures]_&:has([data-wide-figure])]:[--wf-bleed:max(0px,calc((min(1100px,100cqi_-_2rem)_-_100%)/2))] [[data-wide-figures]_&:has([data-wide-figure])]:w-auto [[data-wide-figures]_&:has([data-wide-figure])]:mx-[calc(-1*var(--wf-bleed))] [[data-wide-figures]_&:has([data-wide-figure])]:px-[var(--wf-bleed)]";
|
|
45
|
+
/**
|
|
46
|
+
* On the wrapper that holds the blocks: while it has been widened for a figure,
|
|
47
|
+
* every OTHER block keeps the column's clip edge, so prose still never spills
|
|
48
|
+
* into the band.
|
|
49
|
+
*/
|
|
50
|
+
export declare const WIDE_FIGURE_PROSE_CLIP_CLASS = "[[data-wide-figures]_&:has([data-wide-figure])>*:not(:has([data-wide-figure]))]:overflow-x-clip";
|
|
@@ -3,10 +3,16 @@ const ASSISTANT_MESSAGE_COLUMN_INSET_CLASS = "px-2";
|
|
|
3
3
|
const WIDE_FIGURE_MAX_WIDTH_PX = 1100;
|
|
4
4
|
const WIDE_FIGURE_SCOPE_ATTRIBUTE = "data-wide-figures";
|
|
5
5
|
const WIDE_FIGURE_CLASS = "[[data-wide-figures]_&]:w-[max(100%,min(1100px,100cqi_-_2rem))] [[data-wide-figures]_&]:ml-[calc((100%_-_max(100%,min(1100px,100cqi_-_2rem)))/2)]";
|
|
6
|
+
const WIDE_FIGURE_ATTRIBUTE = "data-wide-figure";
|
|
7
|
+
const WIDE_FIGURE_HOST_CLASS = "[[data-wide-figures]_&:has([data-wide-figure])]:[--wf-bleed:max(0px,calc((min(1100px,100cqi_-_2rem)_-_100%)/2))] [[data-wide-figures]_&:has([data-wide-figure])]:w-auto [[data-wide-figures]_&:has([data-wide-figure])]:mx-[calc(-1*var(--wf-bleed))] [[data-wide-figures]_&:has([data-wide-figure])]:px-[var(--wf-bleed)]";
|
|
8
|
+
const WIDE_FIGURE_PROSE_CLIP_CLASS = "[[data-wide-figures]_&:has([data-wide-figure])>*:not(:has([data-wide-figure]))]:overflow-x-clip";
|
|
6
9
|
export {
|
|
7
10
|
ASSISTANT_MESSAGE_COLUMN_CLASS,
|
|
8
11
|
ASSISTANT_MESSAGE_COLUMN_INSET_CLASS,
|
|
12
|
+
WIDE_FIGURE_ATTRIBUTE,
|
|
9
13
|
WIDE_FIGURE_CLASS,
|
|
14
|
+
WIDE_FIGURE_HOST_CLASS,
|
|
10
15
|
WIDE_FIGURE_MAX_WIDTH_PX,
|
|
16
|
+
WIDE_FIGURE_PROSE_CLIP_CLASS,
|
|
11
17
|
WIDE_FIGURE_SCOPE_ATTRIBUTE
|
|
12
18
|
};
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { useEffect } from "react";
|
|
3
3
|
import { useAppDispatch, useAppSelector } from "../../store/hooks.js";
|
|
4
4
|
import { setIsCreator } from "../../host/prefs.js";
|
|
5
|
+
import { adminSeatOpen } from "../../host/admin-seat.js";
|
|
5
6
|
import { selectAgentIsOwner } from "../redux/agent-definition/selectors.js";
|
|
6
7
|
import { fetchAgentAccessLevel } from "../redux/agent-definition/thunks.js";
|
|
7
8
|
function useCreatorOwnershipSync(agentId) {
|
|
@@ -14,7 +15,8 @@ function useCreatorOwnershipSync(agentId) {
|
|
|
14
15
|
void dispatch(fetchAgentAccessLevel(agentId));
|
|
15
16
|
}
|
|
16
17
|
}, [dispatch, agentId, isOwnerFlag]);
|
|
17
|
-
const
|
|
18
|
+
const adminSeat = useAppSelector((state) => adminSeatOpen(state));
|
|
19
|
+
const isOwner = !!agentId && (isOwnerFlag === true || adminSeat);
|
|
18
20
|
useEffect(() => {
|
|
19
21
|
dispatch(setIsCreator(isOwner));
|
|
20
22
|
return () => {
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from "./types.js";
|
|
25
25
|
import { readListRpc } from "../../../host/ui-slots.js";
|
|
26
26
|
import { getUserId, hasBrowserSession } from "../../../host/identity.js";
|
|
27
|
+
import { adminSeatOpen } from "../../../host/admin-seat.js";
|
|
27
28
|
const fetchConversationHistory = createAsyncThunk(
|
|
28
29
|
"conversationHistory/fetchPage",
|
|
29
30
|
async (args, { dispatch, getState, rejectWithValue }) => {
|
|
@@ -66,7 +67,7 @@ const fetchConversationHistory = createAsyncThunk(
|
|
|
66
67
|
return { scopeId: args.scopeId, items: [], hasMore: false, nextOffset: 0, replace };
|
|
67
68
|
}
|
|
68
69
|
let query = supabase.schema("chat").from("conversation").select(CONVERSATION_ROW_COLUMNS).is("deleted_at", null).eq("is_ephemeral", false).order("updated_at", { ascending: false }).range(offset, offset + pageSize - 1);
|
|
69
|
-
if (viewerId) {
|
|
70
|
+
if (viewerId && !(agentIds.length > 0 && adminSeatOpen(getState()))) {
|
|
70
71
|
query = query.eq("created_by", viewerId);
|
|
71
72
|
}
|
|
72
73
|
if (agentIds.length > 0) {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE COMPOSER'S ATTACHED CODE / TEXT CHIPS SURVIVE A RELOAD.
|
|
3
|
+
*
|
|
4
|
+
* A picked file outlives a reload because attaching it writes a durable edge on
|
|
5
|
+
* the conversation; an "Attach code" / "Attach text" chip (a `rendered_artifact`
|
|
6
|
+
* reference to a canvas item or html page) lived only in Redux, so a reload
|
|
7
|
+
* took it away while the files beside it came back (2026-10-08). This keeps
|
|
8
|
+
* those chips with the composer's text draft — same storage, same scope (this
|
|
9
|
+
* browser, this signed-in person, `composer-draft-store.ts`), written by the
|
|
10
|
+
* composer draft middleware whenever the conversation's resources change and
|
|
11
|
+
* offered back by `useComposerDraftRestore`. A chip that was sent is gone from
|
|
12
|
+
* the record the moment its turn is submitted.
|
|
13
|
+
*/
|
|
14
|
+
import type { Dispatch } from "@reduxjs/toolkit";
|
|
15
|
+
import type { ChatRootState } from "../../../../store/root-state.js";
|
|
16
|
+
/** The conversation a resource action is about, or null for any other action. */
|
|
17
|
+
export declare function renderedArtifactDraftConversation(action: unknown): string | null;
|
|
18
|
+
/** Write (or clear) the conversation's unsent code/text chips from the current state. */
|
|
19
|
+
export declare function persistRenderedArtifactChips(state: ChatRootState, conversationId: string, owner: string | null): void;
|
|
20
|
+
/** Put back this person's unsent code/text chips for a conversation; answers how many came back. */
|
|
21
|
+
export declare function restoreRenderedArtifactChips(dispatch: Dispatch, getState: () => ChatRootState, conversationId: string, owner: string | null): number;
|
|
22
|
+
/** Test seam ONLY. */
|
|
23
|
+
export declare function __resetRenderedArtifactDraftForTest(): void;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { RENDERED_ARTIFACT_BLOCK_TYPE } from "../../../utils/renderedArtifactContext.js";
|
|
2
|
+
import {
|
|
3
|
+
addResource,
|
|
4
|
+
clearAllResources,
|
|
5
|
+
clearSubmittedResources,
|
|
6
|
+
markResourcesSubmitted,
|
|
7
|
+
removeInstanceResources,
|
|
8
|
+
removeResource,
|
|
9
|
+
setResourcePreview,
|
|
10
|
+
updateResourceOptions
|
|
11
|
+
} from "./instance-resources.slice.js";
|
|
12
|
+
const KEY_PREFIX = "matrx:composer-artifact-chips:";
|
|
13
|
+
const TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
14
|
+
const RESOURCE_ACTIONS = /* @__PURE__ */ new Set([
|
|
15
|
+
addResource.type,
|
|
16
|
+
removeResource.type,
|
|
17
|
+
setResourcePreview.type,
|
|
18
|
+
updateResourceOptions.type,
|
|
19
|
+
markResourcesSubmitted.type,
|
|
20
|
+
clearSubmittedResources.type,
|
|
21
|
+
clearAllResources.type,
|
|
22
|
+
removeInstanceResources.type
|
|
23
|
+
]);
|
|
24
|
+
function storage() {
|
|
25
|
+
try {
|
|
26
|
+
return typeof window === "undefined" ? null : window.localStorage;
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function renderedArtifactDraftConversation(action) {
|
|
32
|
+
if (typeof action !== "object" || action === null) return null;
|
|
33
|
+
const { type, payload } = action;
|
|
34
|
+
if (typeof type !== "string" || !RESOURCE_ACTIONS.has(type)) return null;
|
|
35
|
+
if (typeof payload === "string") return payload;
|
|
36
|
+
if (payload && typeof payload === "object" && typeof payload.conversationId === "string") {
|
|
37
|
+
return payload.conversationId;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const restoreAttempted = /* @__PURE__ */ new Set();
|
|
42
|
+
function persistRenderedArtifactChips(state, conversationId, owner) {
|
|
43
|
+
const store = storage();
|
|
44
|
+
if (!store || !restoreAttempted.has(conversationId)) return;
|
|
45
|
+
const submitted = new Set(state.instanceResources.submittedIds?.[conversationId] ?? []);
|
|
46
|
+
const chips = Object.values(state.instanceResources.byConversationId[conversationId] ?? {}).filter((r) => r.blockType === RENDERED_ARTIFACT_BLOCK_TYPE && !submitted.has(r.resourceId) && r.status !== "error").map((r) => ({
|
|
47
|
+
resourceId: r.resourceId,
|
|
48
|
+
source: r.source,
|
|
49
|
+
options: { ...r.options ?? {} },
|
|
50
|
+
preview: typeof r.preview === "string" ? r.preview : null
|
|
51
|
+
}));
|
|
52
|
+
try {
|
|
53
|
+
if (chips.length === 0) store.removeItem(KEY_PREFIX + conversationId);
|
|
54
|
+
else store.setItem(KEY_PREFIX + conversationId, JSON.stringify({ owner, at: Date.now(), chips }));
|
|
55
|
+
} catch {
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function restoreRenderedArtifactChips(dispatch, getState, conversationId, owner) {
|
|
59
|
+
restoreAttempted.add(conversationId);
|
|
60
|
+
const store = storage();
|
|
61
|
+
if (!store) return 0;
|
|
62
|
+
let record = null;
|
|
63
|
+
try {
|
|
64
|
+
const raw = store.getItem(KEY_PREFIX + conversationId);
|
|
65
|
+
record = raw ? JSON.parse(raw) : null;
|
|
66
|
+
} catch {
|
|
67
|
+
record = null;
|
|
68
|
+
}
|
|
69
|
+
if (!record || !Array.isArray(record.chips)) return 0;
|
|
70
|
+
if (Date.now() - record.at > TTL_MS || (record.owner ?? null) !== (owner ?? null)) {
|
|
71
|
+
store.removeItem(KEY_PREFIX + conversationId);
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
const present = getState().instanceResources.byConversationId[conversationId] ?? {};
|
|
75
|
+
let restored = 0;
|
|
76
|
+
for (const chip of record.chips) {
|
|
77
|
+
if (present[chip.resourceId]) continue;
|
|
78
|
+
dispatch(
|
|
79
|
+
addResource({
|
|
80
|
+
conversationId,
|
|
81
|
+
blockType: RENDERED_ARTIFACT_BLOCK_TYPE,
|
|
82
|
+
resourceId: chip.resourceId,
|
|
83
|
+
source: chip.source,
|
|
84
|
+
options: chip.options
|
|
85
|
+
})
|
|
86
|
+
);
|
|
87
|
+
if (chip.preview) dispatch(setResourcePreview({ conversationId, resourceId: chip.resourceId, preview: chip.preview }));
|
|
88
|
+
restored += 1;
|
|
89
|
+
}
|
|
90
|
+
return restored;
|
|
91
|
+
}
|
|
92
|
+
function __resetRenderedArtifactDraftForTest() {
|
|
93
|
+
restoreAttempted.clear();
|
|
94
|
+
}
|
|
95
|
+
export {
|
|
96
|
+
__resetRenderedArtifactDraftForTest,
|
|
97
|
+
persistRenderedArtifactChips,
|
|
98
|
+
renderedArtifactDraftConversation,
|
|
99
|
+
restoreRenderedArtifactChips
|
|
100
|
+
};
|
|
@@ -14,6 +14,10 @@ import {
|
|
|
14
14
|
} from "./composer-draft-store.js";
|
|
15
15
|
import { selectRestoreUnsentDrafts } from "../../../../host/prefs.js";
|
|
16
16
|
import { selectUserId } from "../../../../host/identity.js";
|
|
17
|
+
import {
|
|
18
|
+
persistRenderedArtifactChips,
|
|
19
|
+
renderedArtifactDraftConversation
|
|
20
|
+
} from "../instance-resources/rendered-artifact-draft.js";
|
|
17
21
|
const WRITE_DEBOUNCE_MS = 400;
|
|
18
22
|
const timers = /* @__PURE__ */ new Map();
|
|
19
23
|
const pending = /* @__PURE__ */ new Map();
|
|
@@ -91,6 +95,11 @@ const composerDraftMiddleware = (api) => (next) => (action) => {
|
|
|
91
95
|
return next(action);
|
|
92
96
|
}
|
|
93
97
|
const result = next(action);
|
|
98
|
+
const chipConversation = renderedArtifactDraftConversation(action);
|
|
99
|
+
if (chipConversation) {
|
|
100
|
+
persistRenderedArtifactChips(api.getState(), chipConversation, selectUserId(api.getState()));
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
94
103
|
if (type === setUserInputText.type) {
|
|
95
104
|
const { conversationId, text } = action.payload;
|
|
96
105
|
scheduleWrite(conversationId, text, selectUserId(api.getState()));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { restoreRenderedArtifactChips } from "../instance-resources/rendered-artifact-draft.js";
|
|
2
3
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
4
|
import { useAppDispatch, useAppSelector, useAppStore } from "../../../../store/hooks.js";
|
|
4
5
|
import {
|
|
@@ -50,12 +51,13 @@ function useComposerDraftRestore(conversationId, alias) {
|
|
|
50
51
|
setStorageAvailable(isComposerDraftStorageAvailable());
|
|
51
52
|
if (!enabled) return;
|
|
52
53
|
if (chatHostHasDatabase()) restoreComposerRemarks(conversationId);
|
|
54
|
+
restoreRenderedArtifactChips(dispatch, store.getState, conversationId, ownerId ?? null);
|
|
53
55
|
const token = peekComposerDraft(conversationId, liveAlias, ownerId);
|
|
54
56
|
if (!token) return;
|
|
55
57
|
if (dispatch(applyComposerDraft(token)) === "restored") {
|
|
56
58
|
setRestoredValue(token.value);
|
|
57
59
|
}
|
|
58
|
-
}, [conversationId, liveAlias, dispatch, enabled, entryReady, ownerId]);
|
|
60
|
+
}, [conversationId, liveAlias, dispatch, store, enabled, entryReady, ownerId]);
|
|
59
61
|
useEffect(() => {
|
|
60
62
|
if (restoredValue !== null && text !== restoredValue) setRestoredValue(null);
|
|
61
63
|
}, [text, restoredValue]);
|
|
@@ -617,7 +617,7 @@ const executeInstance = createAsyncThunk(
|
|
|
617
617
|
}
|
|
618
618
|
})
|
|
619
619
|
);
|
|
620
|
-
const submitAt = performance.now();
|
|
620
|
+
const submitAt = submission?.capturedAt ?? performance.now();
|
|
621
621
|
const RUN_IN_FLIGHT_RETRY_MS = 750;
|
|
622
622
|
const RUN_IN_FLIGHT_DEADLINE = Date.now() + 12e4;
|
|
623
623
|
for (; ; ) {
|
|
@@ -560,7 +560,7 @@ const executeManualInstance = createAsyncThunk(
|
|
|
560
560
|
requestId,
|
|
561
561
|
sandboxAttached: payload.sandbox != null || payload.client?.state?.["sandbox-fs"] != null
|
|
562
562
|
});
|
|
563
|
-
const submitAt = performance.now();
|
|
563
|
+
const submitAt = submission.capturedAt ?? performance.now();
|
|
564
564
|
const abortController = new AbortController();
|
|
565
565
|
registerAbortController(conversationId, abortController);
|
|
566
566
|
try {
|
|
@@ -31,6 +31,11 @@ export interface FrozenSubmission {
|
|
|
31
31
|
editorResourceXml: string;
|
|
32
32
|
/** The variable values the person had when they pressed send. */
|
|
33
33
|
userValues: Record<string, unknown>;
|
|
34
|
+
/**
|
|
35
|
+
* `performance.now()` at the keypress — t=0 for every client timing (prep, TTFT). Absent
|
|
36
|
+
* on a submission built after the fact (a resend); the send then stamps its own.
|
|
37
|
+
*/
|
|
38
|
+
capturedAt?: number;
|
|
34
39
|
}
|
|
35
40
|
export declare function captureSubmission(state: ChatRootState, conversationId: string): FrozenSubmission;
|
|
36
41
|
/** True when the submission carries nothing a person sent. */
|
|
@@ -15,7 +15,8 @@ function captureSubmission(state, conversationId) {
|
|
|
15
15
|
editorResourceXml: selectEditorResourceXml(conversationId)(state) ?? "",
|
|
16
16
|
userValues: {
|
|
17
17
|
...state.instanceVariableValues?.byConversationId[conversationId]?.userValues ?? {}
|
|
18
|
-
}
|
|
18
|
+
},
|
|
19
|
+
capturedAt: performance.now()
|
|
19
20
|
};
|
|
20
21
|
}
|
|
21
22
|
function isEmptySubmission(submission) {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Pre-Filter — Fast-reject layers for splitContentIntoBlocksV2
|
|
3
|
+
*
|
|
4
|
+
* Designed to run BEFORE any regex, JSON parsing, or XML extraction.
|
|
5
|
+
* Returns a set of candidate categories for a given line so the main
|
|
6
|
+
* parser can skip entire detection branches.
|
|
7
|
+
*
|
|
8
|
+
* Layer 0: Single-character scan (nanoseconds)
|
|
9
|
+
* Layer 1: Cheap secondary validation (tens of nanoseconds)
|
|
10
|
+
*
|
|
11
|
+
* If no candidates are found → the line is guaranteed to be plain text.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Bit flags — a line can be a candidate for multiple categories simultaneously.
|
|
15
|
+
* Using bits so the main parser can do `if (candidates & Candidate.CODE)`.
|
|
16
|
+
*/
|
|
17
|
+
export declare const Candidate: {
|
|
18
|
+
readonly NONE: 0;
|
|
19
|
+
readonly TEXT: 0;
|
|
20
|
+
readonly CODE: number;
|
|
21
|
+
readonly XML_SIMPLE: number;
|
|
22
|
+
readonly XML_ATTR: number;
|
|
23
|
+
readonly TABLE: number;
|
|
24
|
+
readonly IMAGE: number;
|
|
25
|
+
readonly VIDEO: number;
|
|
26
|
+
readonly DIVIDER: number;
|
|
27
|
+
readonly TREE: number;
|
|
28
|
+
readonly BARE_JSON: number;
|
|
29
|
+
readonly AUDIO: number;
|
|
30
|
+
readonly MATRX_FILE: number;
|
|
31
|
+
};
|
|
32
|
+
export type CandidateFlags = number;
|
|
33
|
+
/**
|
|
34
|
+
* Scans a line and returns a bitmask of candidate categories.
|
|
35
|
+
*
|
|
36
|
+
* This scans the FULL line (not just the start) to support inline blocks
|
|
37
|
+
* like "Hello <decision prompt="...">". The scan is still cheap because
|
|
38
|
+
* we only look at individual characters — no regex, no string splitting.
|
|
39
|
+
*
|
|
40
|
+
* @param line The raw line (before trimming)
|
|
41
|
+
* @param trimmed The normalized+trimmed line (caller already has this)
|
|
42
|
+
* @returns Bitmask of Candidate flags
|
|
43
|
+
*/
|
|
44
|
+
export declare function classifyLine(line: string, trimmed: string): CandidateFlags;
|
|
45
|
+
export declare function isPlainText(flags: CandidateFlags): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Quick check for whether flags include a specific candidate.
|
|
48
|
+
* Usage: `if (hasCandidate(flags, Candidate.CODE)) { ... }`
|
|
49
|
+
*/
|
|
50
|
+
export declare function hasCandidate(flags: CandidateFlags, candidate: number): boolean;
|