@ai-matrx/chat 0.2.0 → 0.2.2

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 (36) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/agent-copy/AlchemySurfaceBridge.d.ts +1 -1
  3. package/dist/agent-copy/AlchemySurfaceBridge.js +1 -1
  4. package/dist/agent-copy/buildAgentPayload.d.ts +1 -1
  5. package/dist/agent-copy/buildAgentPayload.js +1 -1
  6. package/dist/agent-copy/page-capture/pageCapture.d.ts +1 -1
  7. package/dist/agents/components/inputs/smart-input/AgentTextarea.d.ts +1 -2
  8. package/dist/agents/components/inputs/smart-input/AgentTextarea.js +0 -12
  9. package/dist/agents/components/inputs/smart-input/InputActionButtons.js +3 -3
  10. package/dist/agents/components/inputs/smart-input/SmartAgentInputStacked.js +13 -7
  11. package/dist/agents/components/inputs/smart-input/SmartInputFileDropTarget.d.ts +8 -1
  12. package/dist/agents/components/inputs/smart-input/SmartInputFileDropTarget.js +3 -1
  13. package/dist/agents/components/inputs/smart-input/composer/ComposerMetaRow.js +1 -1
  14. package/dist/agents/components/inputs/smart-input/composer/ComposerModeSwitch.js +10 -15
  15. package/dist/agents/components/inputs/smart-input/composer/useComposerFold.d.ts +3 -2
  16. package/dist/agents/components/inputs/smart-input/composer/useComposerFold.js +30 -5
  17. package/dist/agents/components/messages-display/MessageTimestamp.d.ts +8 -1
  18. package/dist/agents/components/messages-display/MessageTimestamp.js +3 -4
  19. package/dist/agents/components/messages-display/assistant/AgentAssistantMessage.js +5 -3
  20. package/dist/agents/components/messages-display/assistant/AssistantMessageFooter.d.ts +5 -0
  21. package/dist/agents/components/messages-display/assistant/AssistantMessageFooter.js +36 -13
  22. package/dist/agents/components/messages-display/assistant/AssistantTurnGroup.js +3 -1
  23. package/dist/surfaces/manifests/_baseline.manifest.d.ts +59 -0
  24. package/dist/surfaces/manifests/_baseline.manifest.js +56 -1
  25. package/dist/surfaces/runtime/SurfaceRuntimeContext.d.ts +1 -1
  26. package/dist/surfaces/runtime/custom-field-targets.d.ts +6 -6
  27. package/dist/surfaces/runtime/custom-field-targets.js +7 -21
  28. package/dist/surfaces/runtime/surface-feedback.d.ts +7 -9
  29. package/dist/surfaces/runtime/surface-feedback.js +8 -21
  30. package/dist/surfaces/runtime/surface-writeback.d.ts +5 -2
  31. package/dist/surfaces/runtime/surface-writeback.js +171 -135
  32. package/dist/surfaces/runtime/window-forms.d.ts +2 -2
  33. package/dist/surfaces/runtime/window-forms.js +3 -9
  34. package/dist/utils/connectors/marks.d.ts +2 -2
  35. package/dist/utils/connectors/marks.js +2 -9
  36. package/package.json +14 -8
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # @ai-matrx/chat
2
2
 
3
+ ## 0.2.2
4
+ ### Last re-sync before matrx-frontend consumes the published package (P27)
5
+
6
+ - Carries the frontend's latest `packages/chat` changes: the composer's run-controls row folds when
7
+ it overflows (`useComposerFold`, `ComposerMetaRow`), message timestamp and assistant footer/turn
8
+ group updates, input action buttons and stacked input.
9
+ - The module tree's suites run here under Jest (`pnpm test:modules`); the host-tie ratchet moved
10
+ in from matrx-frontend (`pnpm check:host-boundary`, budget 0).
11
+ - Consumer action: none.
12
+
13
+ ## 0.2.1
14
+ ### Re-sync with matrx-frontend's packages/chat (P27)
15
+
16
+ - Carries every frontend-side change made to `packages/chat` after the 0.2.0 copy: a pasted image
17
+ attaches once (react-dropzone owns paste; the hand-rolled paste handler is gone; the host's
18
+ no-paste setting reaches the drop target), the Chat/Work/Advanced switch is the design system's
19
+ `SegmentedControl`, the send tooltip uses `isMacLike` and the Connect chip the one GitHub glyph,
20
+ surfaces import `@ai-matrx/alchemy/operate` (ALC-16/17: platform write targets, Save to
21
+ document/workbook through the one door, `receipt.result` carries what the page's handler made).
22
+ - Fixes the copy's stale host suites (they import `testing/fake-db`, as the frontend moved them).
23
+ - Consumer action: none.
24
+
3
25
  ## 0.2.0
4
26
  ### The full chat system moves in (P27, PACKAGE-INDEPENDENCE)
5
27
 
@@ -1,5 +1,5 @@
1
1
  import { type ReactNode } from "react";
2
- import { type SurfaceHandle } from "@ai-matrx/kit/content-transfer";
2
+ import { type SurfaceHandle } from "@ai-matrx/alchemy/operate";
3
3
  import type { SurfaceScopePayload } from "../surfaces/types.js";
4
4
  /**
5
5
  * Creates a handle for this exact React mount. Hook-only runtime registrations
@@ -3,7 +3,7 @@ import { jsx } from "react/jsx-runtime";
3
3
  import { useEffect, useId, useRef } from "react";
4
4
  import {
5
5
  createSurfaceTransferHandle
6
- } from "@ai-matrx/kit/content-transfer";
6
+ } from "@ai-matrx/alchemy/operate";
7
7
  import { ContentTransferSurfaceProvider } from "@ai-matrx/design-system/content-transfer";
8
8
  import { getSurfaceManifest } from "../host/ui-slots.js";
9
9
  function useAlchemySurfaceHandle(surfaceName, getScope) {
@@ -1,2 +1,2 @@
1
1
  /** Compatibility import path; agent envelope serialization is package-owned. */
2
- export { buildAgentPayload, fenceJsonBlock, type AgentPayloadInput, } from "@ai-matrx/kit/content-transfer";
2
+ export { buildAgentPayload, fenceJsonBlock, type AgentPayloadInput, } from "@ai-matrx/alchemy/operate";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  buildAgentPayload,
3
3
  fenceJsonBlock
4
- } from "@ai-matrx/kit/content-transfer";
4
+ } from "@ai-matrx/alchemy/operate";
5
5
  export {
6
6
  buildAgentPayload,
7
7
  fenceJsonBlock
@@ -14,7 +14,7 @@
14
14
  * sections, and the recent requests with their timings. Pure: no React, no
15
15
  * network — the suite drives it directly.
16
16
  */
17
- import type { AgentPayloadInput } from "@ai-matrx/kit/content-transfer";
17
+ import type { AgentPayloadInput } from "@ai-matrx/alchemy/operate";
18
18
  import type { AlchemyGroomerConfig } from "@ai-matrx/design-system/content-transfer";
19
19
  export type PageCaptureKind = "table-page" | "record" | "dialog" | "admin-page";
20
20
  /** A chosen thing, by name and id. Either may be null (not chosen / not loaded). */
@@ -21,7 +21,6 @@ interface AgentTextareaProps {
21
21
  singleRow?: boolean;
22
22
  uploadRoot?: string;
23
23
  uploadPath?: string;
24
- enablePasteImages?: boolean;
25
24
  surfaceKey?: string;
26
25
  /**
27
26
  * The key an unsent draft is also kept under, so it survives a reload of a
@@ -62,5 +61,5 @@ interface AgentTextareaProps {
62
61
  /** With `composerType`: each text line is one 32px composer row (splash · page). */
63
62
  composerRows?: boolean;
64
63
  }
65
- export declare function AgentTextarea({ conversationId, compact, singleRow, uploadRoot, uploadPath, enablePasteImages, surfaceKey, draftAlias, disableSend, initiallyExpanded, autoFocus, showExpandToggle, placeholder, minHeightPx, flush, composerType, composerRows, maxHeightPx, textMenu, }: AgentTextareaProps): React.JSX.Element;
64
+ export declare function AgentTextarea({ conversationId, compact, singleRow, uploadRoot, uploadPath, surfaceKey, draftAlias, disableSend, initiallyExpanded, autoFocus, showExpandToggle, placeholder, minHeightPx, flush, composerType, composerRows, maxHeightPx, textMenu, }: AgentTextareaProps): React.JSX.Element;
66
65
  export {};
@@ -23,14 +23,12 @@ import {
23
23
  selectComposerHasSomethingToSend,
24
24
  selectIsExecuting
25
25
  } from "../../../redux/execution-system/selectors/aggregate.selectors.js";
26
- import { useClipboardPaste } from "../../../../host/ui-slots.js";
27
26
  import { focusUnlessTypingElsewhere } from "../../../../utils/dom/focus-guard.js";
28
27
  import {
29
28
  composerKeyIntent,
30
29
  intentTakesTheKey
31
30
  } from "../../../../ui/composer/composerSubmit.js";
32
31
  import { readVerticalChrome, snapToLineGrid } from "./textarea-line-grid.js";
33
- import { usePasteImageResource } from "../resources/usePasteImageResource.js";
34
32
  import { useInstanceInputUndoRedo } from "../../../hooks/useInstanceInputUndoRedo.js";
35
33
  import { ComposerDraftNotice } from "./ComposerDraftNotice.js";
36
34
  import { ComposerToolsNotice } from "./ComposerToolsNotice.js";
@@ -48,7 +46,6 @@ function AgentTextarea({
48
46
  singleRow = false,
49
47
  uploadRoot = "userContent",
50
48
  uploadPath = "agent-attachments",
51
- enablePasteImages = true,
52
49
  surfaceKey,
53
50
  draftAlias,
54
51
  disableSend = false,
@@ -128,15 +125,6 @@ function AgentTextarea({
128
125
  if (disableSend) return;
129
126
  dispatch(interruptAndSend({ conversationId, surfaceKey }));
130
127
  }, [disableSend, conversationId, surfaceKey, dispatch]);
131
- const handlePasteImage = usePasteImageResource(conversationId, {
132
- uploadRoot,
133
- uploadPath
134
- });
135
- useClipboardPaste({
136
- textareaRef,
137
- onPasteImage: handlePasteImage,
138
- disabled: !enablePasteImages
139
- });
140
128
  const handleTextChange = useCallback(
141
129
  (value) => {
142
130
  dispatch(
@@ -36,6 +36,7 @@ import {
36
36
  smartExecute,
37
37
  cancelExecution
38
38
  } from "../../../redux/execution-system/thunks/smart-execute.thunk.js";
39
+ import { isMacLike } from "../../../hooks/useAgentUndoRedo.js";
39
40
  import { MicDeviceMenu } from "../../../../host/ui-slots.js";
40
41
  import { Button, SplitButton } from "@ai-matrx/design-system/controls";
41
42
  import { cn } from "@ai-matrx/design-system";
@@ -171,7 +172,7 @@ function InputActionButtons({
171
172
  ] });
172
173
  }
173
174
  if (composer.part === "controls") {
174
- return /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center justify-between gap-1.5 shrink-0", children: [
175
+ return /* @__PURE__ */ jsxs("div", { "data-composer-row": "", className: "flex min-w-0 items-center justify-between gap-1.5 shrink-0", children: [
175
176
  /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 shrink-0 items-center gap-1.5", children: [
176
177
  plusMenu,
177
178
  /* @__PURE__ */ jsx(DesktopPresenceIndicator, { conversationId }),
@@ -212,7 +213,6 @@ function ComposerSendSlot({
212
213
  );
213
214
  return /* @__PURE__ */ jsx(Fragment, { children: children(hasSomethingToSend) });
214
215
  }
215
- const IS_MAC = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
216
216
  function Keys({ keys }) {
217
217
  return /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center gap-0.5", children: keys.map((key) => /* @__PURE__ */ jsx(
218
218
  "kbd",
@@ -230,7 +230,7 @@ function ComposerSendButton({
230
230
  disabled,
231
231
  onSend
232
232
  }) {
233
- const mod = IS_MAC ? "\u2318" : "Ctrl";
233
+ const mod = isMacLike() ? "\u2318" : "Ctrl";
234
234
  const rows = isExecuting ? [
235
235
  { label: "Queue for when it finishes", keys: submitOnEnter ? ["\u21B5"] : [mod, "\u21B5"] },
236
236
  ...submitOnEnter ? [{ label: "Steer in now", keys: [mod, "\u21B5"] }] : [],
@@ -22,6 +22,7 @@ import { composerShows } from "./composer/composer-mode-visibility.js";
22
22
  import { ComposerChipsRow } from "./composer/ComposerChipsRow.js";
23
23
  import { ComposerMetaRow, ComposerPills, ComposerScopeCluster, ComposerValueGroupChip } from "./composer/ComposerMetaRow.js";
24
24
  import { useComposerFold } from "./composer/useComposerFold.js";
25
+ import { useTouchOnlyDevice } from "../../../../ui/composer/useTouchOnlyDevice.js";
25
26
  function SmartAgentInputStacked({
26
27
  conversationId,
27
28
  uploadRoot = "userContent",
@@ -44,6 +45,7 @@ function SmartAgentInputStacked({
44
45
  const [voiceBusy, setVoiceBusy] = useState(false);
45
46
  const [expandRequestKey, setExpandRequestKey] = useState(0);
46
47
  const { ref: composerRootRef, folded } = useComposerFold();
48
+ const touchOnly = useTouchOnlyDevice();
47
49
  const showFreeformInput = useAppSelector(
48
50
  selectShowFreeformInput(conversationId ?? "")
49
51
  );
@@ -65,7 +67,8 @@ function SmartAgentInputStacked({
65
67
  "div",
66
68
  {
67
69
  className: cn(
68
- "matrx-touch-targets mx-auto flex w-full min-w-0 shrink-0 flex-col",
70
+ touchOnly && "matrx-touch-targets",
71
+ "mx-auto flex w-full min-w-0 shrink-0 flex-col",
69
72
  formCompact ? void 0 : "max-w-[768px]"
70
73
  ),
71
74
  "data-composer-size": composer.size,
@@ -77,6 +80,7 @@ function SmartAgentInputStacked({
77
80
  conversationId,
78
81
  uploadRoot,
79
82
  uploadPath,
83
+ pasteFiles: enablePasteImages,
80
84
  className: cn(
81
85
  "relative flex w-full min-h-0 flex-col gap-1.5 border border-border bg-card transition-colors focus-within:border-foreground/25",
82
86
  "[&_[data-variable-row]]:px-1.5 [&_[data-variable-heading]]:px-1.5 [&>input[type=file]]:!hidden",
@@ -152,7 +156,6 @@ function SmartAgentInputStacked({
152
156
  compact: isCompact,
153
157
  uploadRoot,
154
158
  uploadPath,
155
- enablePasteImages,
156
159
  surfaceKey,
157
160
  disableSend: sendBlocked,
158
161
  initiallyExpanded: expandRequestKey > 0,
@@ -182,7 +185,7 @@ function SmartAgentInputStacked({
182
185
  "div",
183
186
  {
184
187
  ref: composerRootRef,
185
- className: "matrx-touch-targets mx-auto flex w-full min-w-0 max-w-[420px] shrink-0 flex-col",
188
+ className: cn(touchOnly && "matrx-touch-targets", "mx-auto flex w-full min-w-0 max-w-[420px] shrink-0 flex-col"),
186
189
  "data-composer-size": composer.size,
187
190
  "data-composer-mode": composer.mode,
188
191
  "data-assist-dock-avoid": "",
@@ -192,6 +195,7 @@ function SmartAgentInputStacked({
192
195
  conversationId,
193
196
  uploadRoot,
194
197
  uploadPath,
198
+ pasteFiles: enablePasteImages,
195
199
  className: cn(
196
200
  "relative flex w-full min-h-0 flex-col gap-1 rounded-[18px] border p-1.5",
197
201
  "border-glass-edge bg-glass shadow-glass backdrop-blur-glass backdrop-saturate-glass transition-[border-color,background-color,box-shadow]",
@@ -230,7 +234,6 @@ function SmartAgentInputStacked({
230
234
  compact: true,
231
235
  uploadRoot,
232
236
  uploadPath,
233
- enablePasteImages,
234
237
  surfaceKey,
235
238
  disableSend: sendBlocked,
236
239
  autoFocus: false,
@@ -263,9 +266,11 @@ function SmartAgentInputStacked({
263
266
  {
264
267
  ref: composerRootRef,
265
268
  className: cn(
266
- // THE 44px FLOOR for every control on a touch layout (globals.css);
267
- // desktop density is untouched.
268
- "matrx-touch-targets mx-auto flex w-full min-w-0 shrink-0 flex-col gap-1",
269
+ // THE 44px FLOOR on a TOUCH device only. The shared class also fires
270
+ // on any viewport under 1024px, which stretched every button and pill
271
+ // of a narrowed desktop window to 44px (2026-10-06).
272
+ touchOnly && "matrx-touch-targets",
273
+ "mx-auto flex w-full min-w-0 shrink-0 flex-col gap-1",
269
274
  isCompact ? void 0 : "max-w-[768px]"
270
275
  ),
271
276
  "data-composer-size": composer.size,
@@ -280,6 +285,7 @@ function SmartAgentInputStacked({
280
285
  conversationId,
281
286
  uploadRoot,
282
287
  uploadPath,
288
+ pasteFiles: enablePasteImages,
283
289
  className: cardClassName,
284
290
  children: [
285
291
  /* @__PURE__ */ jsx(
@@ -2,7 +2,14 @@ interface SmartInputFileDropTargetProps extends Omit<React.ComponentPropsWithout
2
2
  conversationId: string;
3
3
  uploadRoot?: string;
4
4
  uploadPath?: string;
5
+ /**
6
+ * Pasted files (a screenshot, Cmd/Ctrl+V of a copied file) attach through
7
+ * the same upload as a drop. react-dropzone OWNS paste-to-upload (v20+,
8
+ * on by default); nothing else in the input may also catch the paste, or
9
+ * every pasted image attaches twice (2026-10-06). False = paste stays text.
10
+ */
11
+ pasteFiles?: boolean;
5
12
  children: React.ReactNode;
6
13
  }
7
- export declare function SmartInputFileDropTarget({ conversationId, uploadRoot, uploadPath, className, children, ...rootProps }: SmartInputFileDropTargetProps): import("react").JSX.Element;
14
+ export declare function SmartInputFileDropTarget({ conversationId, uploadRoot, uploadPath, pasteFiles, className, children, ...rootProps }: SmartInputFileDropTargetProps): import("react").JSX.Element;
8
15
  export {};
@@ -8,6 +8,7 @@ function SmartInputFileDropTarget({
8
8
  conversationId,
9
9
  uploadRoot,
10
10
  uploadPath,
11
+ pasteFiles = true,
11
12
  className,
12
13
  children,
13
14
  ...rootProps
@@ -19,7 +20,8 @@ function SmartInputFileDropTarget({
19
20
  const { getRootProps, getInputProps, isDragActive } = useDropzone({
20
21
  onDrop: (files) => void uploadResources(files),
21
22
  noClick: true,
22
- noKeyboard: true
23
+ noKeyboard: true,
24
+ noPaste: !pasteFiles
23
25
  });
24
26
  return /* @__PURE__ */ jsxs(
25
27
  "div",
@@ -89,7 +89,7 @@ function ComposerMetaRow({
89
89
  return (
90
90
  // One line, always: a narrow column scrolls the row sideways, never wraps it.
91
91
  // Every gap is the tap-target 6px; the clusters sit at the two ends.
92
- /* @__PURE__ */ jsxs("div", { className: cn(COMPOSER_ROW_CLASS, "justify-between gap-1.5 px-1"), children: [
92
+ /* @__PURE__ */ jsxs("div", { "data-composer-row": "", className: cn(COMPOSER_ROW_CLASS, "justify-between gap-1.5 px-1"), children: [
93
93
  /* @__PURE__ */ jsx(ComposerScopeCluster, { conversationId, composer, folded }),
94
94
  /* @__PURE__ */ jsx(ComposerPills, { conversationId, composer, menuSide, folded })
95
95
  ] })
@@ -16,7 +16,7 @@ import {
16
16
  COMPOSER_MODE_LABELS
17
17
  } from "./composer-types.js";
18
18
  import { useComposerMode } from "./useComposerMode.js";
19
- import { Button } from "@ai-matrx/design-system/controls";
19
+ import { SegmentedControl } from "@ai-matrx/design-system/controls";
20
20
  function ComposerModeSwitch({
21
21
  initialMode,
22
22
  size = "bar",
@@ -30,20 +30,15 @@ function ComposerModeSwitch({
30
30
  const fit = useCenterControlFit(cellRef, [fullRef, triggerRef], mode);
31
31
  const measured = useSyncExternalStore(noopSubscribe, () => true, () => false);
32
32
  const renderSegments = (interactive) => /* @__PURE__ */ jsx(
33
- "div",
33
+ SegmentedControl,
34
34
  {
35
- role: interactive ? "tablist" : void 0,
36
- "aria-label": interactive ? "Composer mode" : void 0,
37
- className: cn(
38
- "shrink-0 items-center gap-0.5 rounded-lg bg-muted p-0.5",
39
- panel || !interactive ? "inline-flex rounded-lg" : measured ? "inline-flex rounded-[10px]" : "hidden rounded-[10px] sm:inline-flex",
40
- !interactive && "w-max max-w-none",
41
- className
42
- ),
43
- children: COMPOSER_MODES.map((value) => {
44
- const on = value === mode;
45
- return /* @__PURE__ */ jsx(Button, { variant: "quiet", pressed: !!on, role: interactive ? "tab" : void 0, "aria-selected": interactive ? on : void 0, "aria-pressed": interactive ? void 0 : on, tabIndex: interactive ? void 0 : -1, onClick: interactive ? () => setMode(value) : void 0, children: COMPOSER_MODE_LABELS[value] }, value);
46
- })
35
+ "aria-label": "Composer mode",
36
+ value: mode,
37
+ onValueChange: (value) => {
38
+ if (interactive) setMode(value);
39
+ },
40
+ data: COMPOSER_MODES.map((value) => ({ value, label: COMPOSER_MODE_LABELS[value] })),
41
+ className: cn("shrink-0", !interactive && "w-max max-w-none", className)
47
42
  }
48
43
  );
49
44
  if (panel) return renderSegments(true);
@@ -61,7 +56,7 @@ function ComposerModeSwitch({
61
56
  className: "relative flex w-full min-w-0 justify-center",
62
57
  "data-route-nav-inflow": fit.inflow ? "" : void 0,
63
58
  children: [
64
- /* @__PURE__ */ jsxs("div", { "aria-hidden": true, className: "pointer-events-none invisible absolute left-0 top-0", children: [
59
+ /* @__PURE__ */ jsxs("div", { "aria-hidden": true, inert: true, className: "pointer-events-none invisible absolute left-0 top-0", children: [
65
60
  /* @__PURE__ */ jsx("div", { ref: fullRef, className: "w-max max-w-none", children: renderSegments(false) }),
66
61
  /* @__PURE__ */ jsx("span", { ref: triggerRef, "data-route-nav-min": true, className: cn(triggerClass, "w-max max-w-none"), children: triggerFace })
67
62
  ] }),
@@ -5,8 +5,9 @@
5
5
  */
6
6
  export declare const COMPOSER_FOLD_WIDTH_PX = 480;
7
7
  /**
8
- * Watches the composer's own width and re-renders only when it crosses the
9
- * fold threshold — never per pixel while a panel is dragged.
8
+ * Watches the composer's own width — and whether a row under the card
9
+ * overflows — and re-renders only when the fold flips, never per pixel while
10
+ * a panel is dragged.
10
11
  */
11
12
  export declare function useComposerFold(): {
12
13
  ref: import("react").Dispatch<import("react").SetStateAction<HTMLDivElement | null>>;
@@ -6,15 +6,40 @@ function useComposerFold() {
6
6
  const [folded, setFolded] = useState(false);
7
7
  useEffect(() => {
8
8
  if (!el || typeof ResizeObserver === "undefined") return void 0;
9
- const observer = new ResizeObserver(([entry]) => {
10
- const width = entry?.contentRect.width ?? 0;
11
- setFolded(width > 0 && width < COMPOSER_FOLD_WIDTH_PX);
12
- });
9
+ let foldedAt = 0;
10
+ const evaluate = () => {
11
+ const width = el.getBoundingClientRect().width;
12
+ if (width === 0) return;
13
+ const rows = Array.from(el.querySelectorAll("[data-composer-row]"));
14
+ const overflowing = rows.some((row) => row.scrollWidth > row.clientWidth + 1);
15
+ setFolded((was) => {
16
+ if (width < COMPOSER_FOLD_WIDTH_PX) {
17
+ foldedAt = Math.max(foldedAt, width);
18
+ return true;
19
+ }
20
+ if (!was && overflowing) {
21
+ foldedAt = width;
22
+ return true;
23
+ }
24
+ if (was && width > foldedAt + UNFOLD_MARGIN_PX && !overflowing) {
25
+ foldedAt = 0;
26
+ return false;
27
+ }
28
+ return was;
29
+ });
30
+ };
31
+ const observer = new ResizeObserver(evaluate);
13
32
  observer.observe(el);
14
- return () => observer.disconnect();
33
+ const mutations = new MutationObserver(() => requestAnimationFrame(evaluate));
34
+ mutations.observe(el, { subtree: true, childList: true, characterData: true });
35
+ return () => {
36
+ observer.disconnect();
37
+ mutations.disconnect();
38
+ };
15
39
  }, [el]);
16
40
  return { ref, folded };
17
41
  }
42
+ const UNFOLD_MARGIN_PX = 80;
18
43
  export {
19
44
  COMPOSER_FOLD_WIDTH_PX,
20
45
  useComposerFold
@@ -9,6 +9,13 @@ interface TimestampDisplay {
9
9
  compact: string;
10
10
  }
11
11
  export declare function formatTimestampDisplay(timestamp: TimestampInput): TimestampDisplay | null;
12
- /** Hover-only compact time; the exact date, seconds and zone remain available on focus. */
12
+ /**
13
+ * The time at the END of the action row. It always occupies its space — it is
14
+ * revealed by opacity on hover/focus, never inserted — so showing it shifts
15
+ * nothing. It never wraps: a narrow row gets the time alone ("2:23 PM"), a
16
+ * wider one the date too ("Oct 6, 2:23 PM"). The exact date, seconds and zone
17
+ * stay in the tooltip and accessible name. A touch screen has no hover to
18
+ * reveal it, so there it takes no space at all and the actions get the row.
19
+ */
13
20
  export declare function MessageTimestamp({ timestamp }: MessageTimestampProps): import("react").JSX.Element | null;
14
21
  export {};
@@ -50,11 +50,10 @@ function MessageTimestamp({ timestamp }) {
50
50
  tabIndex: 0,
51
51
  "aria-label": `${display.absolute} (${display.relative})`,
52
52
  title: `${display.absolute} (${display.relative})`,
53
- style: { whiteSpace: "nowrap" },
54
- className: "sr-only text-[10px] font-normal text-muted-foreground/65 focus:not-sr-only [@media(hover:hover)]:group-hover/assistant-msg:not-sr-only [@media(hover:hover)]:group-focus-within/assistant-msg:not-sr-only",
53
+ className: "shrink-0 whitespace-nowrap [@media(hover:none)]:hidden text-[10px] font-normal text-muted-foreground/65 opacity-0 transition-opacity focus:opacity-100 [@media(hover:hover)]:group-hover/assistant-msg:opacity-100 [@media(hover:hover)]:group-focus-within/assistant-msg:opacity-100",
55
54
  children: [
56
- /* @__PURE__ */ jsx("span", { className: "@min-[400px]/message-footer:hidden", children: display.compact }),
57
- /* @__PURE__ */ jsx("span", { className: "hidden @min-[400px]/message-footer:inline", children: display.short })
55
+ /* @__PURE__ */ jsx("span", { className: "@min-[420px]/message-footer:hidden", children: display.compact }),
56
+ /* @__PURE__ */ jsx("span", { className: "hidden @min-[420px]/message-footer:inline", children: display.short })
58
57
  ]
59
58
  }
60
59
  );
@@ -52,7 +52,8 @@ import { AssistantWarning } from "../../run/AssistantWarning.js";
52
52
  import { BreathingOrb } from "./BreathingOrb.js";
53
53
  import {
54
54
  AssistantMessageContextMenu,
55
- AssistantMessageFooter
55
+ AssistantMessageFooter,
56
+ AssistantMessageFooterPlaceholder
56
57
  } from "./AssistantMessageFooter.js";
57
58
  import { AssistantNoAnswer } from "./AssistantNoAnswer.js";
58
59
  import { selectInstanceStatus } from "../../../redux/execution-system/conversations/conversations.selectors.js";
@@ -442,7 +443,7 @@ function AgentAssistantMessage({
442
443
  !isClosedStreamSegment && displaySources.length > 0 && /* @__PURE__ */ jsx(MessageSourcesRow, { sources: displaySources, className: "mt-2" }),
443
444
  !isStreamActive && /* @__PURE__ */ jsx(PrefillNote, { metadata: record?.metadata }),
444
445
  !isStreamActive && /* @__PURE__ */ jsx(StoppedNote, { metadata: record?.metadata, requestStatus }),
445
- isStreamActive && !failed && (phase === "text_streaming" || phase === "interstitial") && /* @__PURE__ */ jsx(BreathingOrb, { className: "mt-1.5", size: 48 })
446
+ 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 }) })
446
447
  ] }),
447
448
  !isClosedStreamSegment && visibleWarnings?.map((warning, index) => /* @__PURE__ */ jsx(
448
449
  AssistantWarning,
@@ -475,7 +476,8 @@ function AgentAssistantMessage({
475
476
  isCapturing,
476
477
  surfaceKey
477
478
  }
478
- )
479
+ ),
480
+ !hideActionBar && (isStreamActive || !messageId) && !failed && !editingInPlace && /* @__PURE__ */ jsx(AssistantMessageFooterPlaceholder, {})
479
481
  ]
480
482
  }
481
483
  );
@@ -51,6 +51,11 @@ export declare function useAssistantMessageActions({ messageId, conversationId,
51
51
  showOptions: boolean;
52
52
  isLatestAssistant: boolean;
53
53
  };
54
+ /**
55
+ * Holds the action row's space while a reply streams (the real footer needs
56
+ * the saved message id), so nothing below jumps when the actions arrive.
57
+ */
58
+ export declare function AssistantMessageFooterPlaceholder(): React.JSX.Element;
54
59
  export declare function AssistantMessageFooter(props: AssistantMessageFooterProps): React.JSX.Element;
55
60
  export default AssistantMessageFooter;
56
61
  /** Agent-menu placements over a rendered answer (never "Content Blocks"). */
@@ -146,6 +146,10 @@ function useAssistantMessageActions({
146
146
  isLatestAssistant
147
147
  };
148
148
  }
149
+ let lastFooterRowHeight = 46;
150
+ function AssistantMessageFooterPlaceholder() {
151
+ return /* @__PURE__ */ jsx("div", { "aria-hidden": true, style: { height: lastFooterRowHeight } });
152
+ }
149
153
  function AssistantMessageFooter(props) {
150
154
  const { messageId, conversationId, surfaceKey } = props;
151
155
  const {
@@ -162,6 +166,17 @@ function AssistantMessageFooter(props) {
162
166
  isLatestAssistant
163
167
  } = useAssistantMessageActions(props);
164
168
  const isHoverOnly = density === "compact" && !isLatestAssistant;
169
+ const rowRef = useRef(null);
170
+ useEffect(() => {
171
+ const row = rowRef.current;
172
+ if (!row) return;
173
+ const observer = new ResizeObserver(() => {
174
+ const height = row.getBoundingClientRect().height;
175
+ if (height > 0) lastFooterRowHeight = height;
176
+ });
177
+ observer.observe(row);
178
+ return () => observer.disconnect();
179
+ }, []);
165
180
  return /* @__PURE__ */ jsxs(Fragment, { children: [
166
181
  /* @__PURE__ */ jsxs(
167
182
  "div",
@@ -171,19 +186,26 @@ function AssistantMessageFooter(props) {
171
186
  isHoverOnly && "opacity-0 group-hover/assistant-msg:opacity-100 focus-within:opacity-100"
172
187
  ),
173
188
  children: [
174
- /* @__PURE__ */ jsxs("div", { className: "@container/message-footer w-full min-w-0", children: [
175
- /* @__PURE__ */ jsx(
176
- RichDocumentActions,
177
- {
178
- content: config.content,
179
- source: config.source,
180
- actions: config.actions,
181
- hideOverflow: !showOptions,
182
- className: "px-0"
183
- }
184
- ),
185
- /* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx(MessageTimestamp, { timestamp: record?.createdAt }) })
186
- ] }),
189
+ /* @__PURE__ */ jsxs(
190
+ "div",
191
+ {
192
+ ref: rowRef,
193
+ className: "@container/message-footer flex w-full min-w-0 items-center gap-2",
194
+ children: [
195
+ /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx(
196
+ RichDocumentActions,
197
+ {
198
+ content: config.content,
199
+ source: config.source,
200
+ actions: config.actions,
201
+ hideOverflow: !showOptions,
202
+ className: "px-0"
203
+ }
204
+ ) }),
205
+ /* @__PURE__ */ jsx(MessageTimestamp, { timestamp: record?.createdAt })
206
+ ]
207
+ }
208
+ ),
187
209
  /* @__PURE__ */ jsx(
188
210
  NegativeVerdictFollowUp,
189
211
  {
@@ -243,6 +265,7 @@ export {
243
265
  ASSISTANT_ANSWER_PLACEMENT_MODE,
244
266
  AssistantMessageContextMenu,
245
267
  AssistantMessageFooter,
268
+ AssistantMessageFooterPlaceholder,
246
269
  AssistantMessageFooter_default as default,
247
270
  useAssistantMessageActions
248
271
  };
@@ -5,7 +5,8 @@ import { useDomCapturePrint } from "../../../../conversation/hooks/useDomCapture
5
5
  import { AgentAssistantMessage } from "./AgentAssistantMessage.js";
6
6
  import {
7
7
  AssistantMessageContextMenu,
8
- AssistantMessageFooter
8
+ AssistantMessageFooter,
9
+ AssistantMessageFooterPlaceholder
9
10
  } from "./AssistantMessageFooter.js";
10
11
  import {
11
12
  membersForRender,
@@ -111,6 +112,7 @@ function AssistantTurnGroup({
111
112
  },
112
113
  `files-${m.key}`
113
114
  )),
115
+ lastMember && !showBar && /* @__PURE__ */ jsx(AssistantMessageFooterPlaceholder, {}),
114
116
  showBar && anchorMessageId && // The group's bar sits BESIDE its members, so it carries the turn's
115
117
  // one menu itself — its ⋯ opens the same menu the answer's
116
118
  // right-click does (RC-B6), never a second drawn menu.
@@ -150,6 +150,65 @@ export declare const PLATFORM_CONTEXT_VALUES: {
150
150
  readonly sortOrder: 9997;
151
151
  };
152
152
  };
153
+ /**
154
+ * THE BASELINE SURFACE — where the platform's own write targets are declared
155
+ * (Matrx Alchemy ALC-17). Every surface inherits it: the one write door reads
156
+ * it as the root ancestor of every surface (`surfaceWriteDeclarations` in
157
+ * `runtime/surface-writeback.ts`), so a write to a platform target from ANY
158
+ * surface resolves here, and the platform's headless handlers are registered
159
+ * on this name. It is not a page and is never mounted.
160
+ */
161
+ export declare const BASELINE_SURFACE_NAME = "matrx-platform/baseline";
162
+ /** `surface_feedback` value bounds — the description and the check read the same numbers. */
163
+ export declare const SURFACE_FEEDBACK_KINDS: readonly ["missing_capability", "wrong_or_unclear_description", "bug", "missing_data", "suggestion"];
164
+ export declare const SURFACE_FEEDBACK_MESSAGE_MIN = 10;
165
+ export declare const SURFACE_FEEDBACK_MESSAGE_MAX = 4000;
166
+ /** At most this many custom fields per `custom_fields_add` write. */
167
+ export declare const CUSTOM_FIELDS_MAX_PER_WRITE = 10;
168
+ /**
169
+ * THE PLATFORM WRITE TARGETS, declared once on the baseline surface. Each has
170
+ * the apply policy it always had: window fields and custom fields are `ask`
171
+ * (an agent's write waits for the person's card; a person's own write is the
172
+ * approval); feedback is `auto` (it changes nothing on the page). The runtime
173
+ * files word the live offer from these (`customFieldsTarget()` adds the
174
+ * mounted sections), and register the handlers that apply them.
175
+ */
176
+ export declare const PLATFORM_WRITE_TARGETS: {
177
+ readonly window_form_fields: {
178
+ readonly name: "window_form_fields";
179
+ readonly label: "Fields in an open window";
180
+ readonly description: 'Change fields in an open window that has no registered surface (listed in your context as window::<title>). Value: { "window": "<title exactly as listed>", "changes": [{ "field": "<field key>", "value": <new value> }] }. A text or number field takes a string or number, a checkbox or switch takes true/false, a list takes the text of one of its choices (a wrong choice is refused with the real ones). Every change is checked against the field\'s rules before anything lands; the person approves first, and the window\'s own Save still decides.';
181
+ readonly valueType: "object";
182
+ readonly mode: "draft";
183
+ readonly applyPolicy: "ask";
184
+ };
185
+ readonly surface_feedback: {
186
+ readonly name: "surface_feedback";
187
+ readonly label: "Feedback about this page";
188
+ readonly description: string;
189
+ readonly valueType: "object";
190
+ readonly mode: "entity";
191
+ readonly applyPolicy: "auto";
192
+ };
193
+ readonly custom_fields_add: {
194
+ readonly name: "custom_fields_add";
195
+ readonly label: "Add custom fields";
196
+ readonly description: string;
197
+ readonly valueType: "object";
198
+ readonly mode: "entity";
199
+ readonly applyPolicy: "ask";
200
+ };
201
+ readonly custom_fields_set: {
202
+ readonly name: "custom_fields_set";
203
+ readonly label: "Fill in custom fields";
204
+ readonly description: string;
205
+ readonly valueType: "object";
206
+ readonly mode: "entity";
207
+ readonly applyPolicy: "ask";
208
+ };
209
+ };
210
+ export type PlatformWriteTargetName = keyof typeof PLATFORM_WRITE_TARGETS;
211
+ export declare function isPlatformWriteTarget(name: string): name is PlatformWriteTargetName;
153
212
  /** Names only the platform writes — a manifest declaring one is refused. */
154
213
  export declare const PLATFORM_RESERVED_NAMES: {
155
214
  values: ReadonlyArray<string>;