@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.
Files changed (30) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/agents/addressing/agentAddress.d.ts +8 -0
  3. package/dist/agents/addressing/agentAddress.js +1 -0
  4. package/dist/agents/addressing/useAgentHref.js +8 -1
  5. package/dist/agents/components/conversation-history/ConversationHistorySidebar.d.ts +7 -0
  6. package/dist/agents/components/conversation-history/ConversationHistorySidebar.js +13 -7
  7. package/dist/agents/components/shared/assistant-message-layout.d.ts +21 -0
  8. package/dist/agents/components/shared/assistant-message-layout.js +6 -0
  9. package/dist/agents/hooks/useCreatorOwnershipSync.js +3 -1
  10. package/dist/agents/redux/conversation-history/thunks.js +2 -1
  11. package/dist/agents/redux/execution-system/instance-resources/rendered-artifact-draft.d.ts +23 -0
  12. package/dist/agents/redux/execution-system/instance-resources/rendered-artifact-draft.js +100 -0
  13. package/dist/agents/redux/execution-system/instance-user-input/composer-draft.middleware.js +9 -0
  14. package/dist/agents/redux/execution-system/instance-user-input/useComposerDraftRestore.js +3 -1
  15. package/dist/agents/redux/execution-system/thunks/execute-instance.thunk.js +1 -1
  16. package/dist/agents/redux/execution-system/thunks/execute-manual-instance.thunk.js +1 -1
  17. package/dist/agents/redux/execution-system/thunks/frozen-submission.d.ts +5 -0
  18. package/dist/agents/redux/execution-system/thunks/frozen-submission.js +2 -1
  19. package/dist/agents/redux/execution-system/utils/content-prefilter.d.ts +50 -0
  20. package/dist/agents/redux/execution-system/utils/content-prefilter.js +243 -0
  21. package/dist/agents/redux/execution-system/utils/stream-block-accumulator.d.ts +335 -65
  22. package/dist/agents/redux/execution-system/utils/stream-block-accumulator.js +1909 -224
  23. package/dist/conversation/hooks/useDomCapturePrint.js +13 -2
  24. package/dist/conversation/utils/frame-print.js +20 -2
  25. package/dist/conversation/utils/isolate-for-capture.d.ts +19 -0
  26. package/dist/conversation/utils/isolate-for-capture.js +55 -0
  27. package/dist/host/admin-seat.d.ts +19 -0
  28. package/dist/host/admin-seat.js +8 -0
  29. package/dist/ui/markdown-stream/EnhancedChatMarkdown.js +5 -1
  30. 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
- return isAdmin === void 0 ? void 0 : { isAdmin };
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 isOwner = !!agentId && isOwnerFlag === true;
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;