@ai-matrx/chat 0.4.22 → 0.4.26

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 (68) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/agent-copy/RichCopySplit.d.ts +11 -1
  3. package/dist/agent-copy/RichCopySplit.js +9 -3
  4. package/dist/agents/components/chat/ChatConversationRoom.js +6 -4
  5. package/dist/agents/components/chat/ChatNewClient.js +7 -4
  6. package/dist/agents/components/chat/ChatNewHeader.js +3 -2
  7. package/dist/agents/components/chat/default-chat-mandate.d.ts +7 -0
  8. package/dist/agents/components/chat/default-chat-mandate.js +9 -0
  9. package/dist/agents/components/context-items/bodies/UnregisteredBody.d.ts +6 -3
  10. package/dist/agents/components/context-items/bodies/UnregisteredBody.js +3 -9
  11. package/dist/agents/components/inputs/resources/attach-resource.js +1 -0
  12. package/dist/agents/components/inputs/smart-input/SmartAgentInput.js +16 -1
  13. package/dist/agents/components/inputs/smart-input/composer/ComposerMenu.js +3 -2
  14. package/dist/agents/components/inputs/smart-input/composer/ComposerPlusMenu.js +41 -35
  15. package/dist/agents/components/messages-display/assistant/AgentAssistantMessage.js +29 -20
  16. package/dist/agents/components/messages-display/user/AgentUserMessage.js +3 -1
  17. package/dist/agents/components/shared/AgentConversationColumn.d.ts +10 -1
  18. package/dist/agents/components/shared/AgentConversationColumn.js +26 -2
  19. package/dist/agents/redux/execution-system/messages/messages.slice.js +6 -1
  20. package/dist/agents/redux/execution-system/thunks/await-launching-conversation.d.ts +16 -0
  21. package/dist/agents/redux/execution-system/thunks/await-launching-conversation.js +16 -0
  22. package/dist/agents/redux/execution-system/thunks/execute-instance.thunk.js +34 -1
  23. package/dist/agents/redux/execution-system/thunks/process-stream.js +11 -0
  24. package/dist/agents/redux/execution-system/thunks/smart-execute.thunk.js +10 -0
  25. package/dist/agents/redux/execution-system/utils/required-organization.js +2 -2
  26. package/dist/agents/ui-first-tools/redux/build-ambient-context.js +8 -7
  27. package/dist/host/app-data-slots.d.ts +0 -1
  28. package/dist/host/app-data-slots.js +25 -5
  29. package/dist/host/bare-host-chrome.js +4 -0
  30. package/dist/host/contract.d.ts +8 -2
  31. package/dist/host/contract.js +2 -1
  32. package/dist/host/conversation-hooks-react.d.ts +2 -0
  33. package/dist/host/conversation-hooks-react.js +12 -0
  34. package/dist/host/conversation-hooks.d.ts +79 -0
  35. package/dist/host/conversation-hooks.js +91 -0
  36. package/dist/host/defaults/bare-block-dispatch.d.ts +6 -0
  37. package/dist/host/defaults/bare-block-dispatch.js +60 -0
  38. package/dist/host/defaults/files-host.d.ts +26 -0
  39. package/dist/host/defaults/files-host.js +212 -0
  40. package/dist/host/defaults/memory-navigation.d.ts +12 -0
  41. package/dist/host/defaults/memory-navigation.js +102 -0
  42. package/dist/host/defaults/resource-picker-menu.d.ts +14 -0
  43. package/dist/host/defaults/resource-picker-menu.js +117 -0
  44. package/dist/host/defaults/setting-control-input.d.ts +11 -0
  45. package/dist/host/defaults/setting-control-input.js +91 -0
  46. package/dist/host/index.d.ts +2 -0
  47. package/dist/host/index.js +5 -0
  48. package/dist/host/react.d.ts +1 -0
  49. package/dist/host/react.js +2 -0
  50. package/dist/host/ui-slots.d.ts +25 -1
  51. package/dist/host/ui-slots.js +83 -25
  52. package/dist/host/window-openers.d.ts +2 -0
  53. package/dist/host/window-openers.js +4 -0
  54. package/dist/react.cjs +1 -1
  55. package/dist/react.cjs.map +1 -1
  56. package/dist/react.js +1 -1
  57. package/dist/react.js.map +1 -1
  58. package/dist/tool-call-visualization/registry/registry.js +4 -4
  59. package/dist/ui/aspect-ratio-options.d.ts +4 -26
  60. package/dist/ui/aspect-ratio-options.js +7 -75
  61. package/dist/ui/chat-panel/ChatPanel.d.ts +50 -0
  62. package/dist/ui/chat-panel/ChatPanel.js +175 -0
  63. package/dist/ui/chat-panel/drag.d.ts +87 -0
  64. package/dist/ui/chat-panel/drag.js +194 -0
  65. package/dist/window-panels/windows/agents/AgentRunWindow.js +17 -0
  66. package/dist/window-panels/windows/agents/agentRunWindowAddress.d.ts +19 -0
  67. package/dist/window-panels/windows/agents/agentRunWindowAddress.js +11 -1
  68. package/package.json +5 -5
@@ -1079,10 +1079,10 @@ function notSetUpRendererFor(toolName) {
1079
1079
  if (hit) return hit;
1080
1080
  const NotSetUp = (props) => {
1081
1081
  reportUnregisteredHostSlot(`toolRenderer:${toolName}`, "the generic renderer is drawn");
1082
- return /* @__PURE__ */ jsxs("div", { "data-chat-slot-fallback": `toolRenderer:${toolName}`, children: [
1083
- /* @__PURE__ */ jsx("span", { className: "mb-1 inline-block rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground", children: "Not set up here" }),
1084
- /* @__PURE__ */ jsx(GenericRenderer, { ...props })
1085
- ] });
1082
+ return (
1083
+ // The generic renderer shows the call in full; the app's richer card is reported once, never labelled on screen.
1084
+ /* @__PURE__ */ jsx("div", { "data-chat-slot-fallback": `toolRenderer:${toolName}`, children: /* @__PURE__ */ jsx(GenericRenderer, { ...props }) })
1085
+ );
1086
1086
  };
1087
1087
  NotSetUp.displayName = `NotSetUpRenderer(${toolName})`;
1088
1088
  notSetUpByTool.set(toolName, NotSetUp);
@@ -1,28 +1,6 @@
1
1
  /**
2
- * Aspect-ratio option sets — recognising them and ordering them for a picker.
3
- *
4
- * Model catalogs expose aspect ratio as a plain string enum ("1:1", "16:9",
5
- * "9:19.5", Runway's pixel pairs "1280:720", plus words like "auto" or
6
- * "match_input_image"). Any option list that is mostly ratios renders through
7
- * `AspectRatioSelect` — wherever it came from — so the rule lives on the data,
8
- * not on one control key.
2
+ * Aspect-ratio option sets — the one copy lives in `@ai-matrx/design-system/controls`
3
+ * (moved there with `AspectRatioSelect`, chat host-slot batch 2, 2026-10-08).
4
+ * This subpath keeps `@ai-matrx/chat/ui/aspect-ratio-options` importers working.
9
5
  */
10
- export interface ParsedRatio {
11
- w: number;
12
- h: number;
13
- }
14
- export declare function parseRatio(value: string): ParsedRatio | null;
15
- /** True when at least two options and every option but the words is a ratio. */
16
- export declare function isAspectRatioOptionSet(options: readonly string[]): boolean;
17
- /** "Square", "Landscape", … — a plain word for the shape. */
18
- export declare function ratioShapeName(value: string): string | null;
19
- /** Words that are not ratios ("auto", "match_input_image") read as words. */
20
- export declare function humanizeRatioWord(value: string): string;
21
- export interface RatioGroups {
22
- /** Non-ratio words first (Auto, Match input image). */
23
- words: string[];
24
- common: string[];
25
- more: string[];
26
- }
27
- /** Split options into words, common ratios (fixed order) and the rest (by shape). */
28
- export declare function groupRatioOptions(options: readonly string[]): RatioGroups;
6
+ export { parseRatio, isAspectRatioOptionSet, ratioShapeName, humanizeRatioWord, groupRatioOptions, type ParsedRatio, type RatioGroups, } from "@ai-matrx/design-system/controls";
@@ -1,78 +1,10 @@
1
- import { humanizeIdentifier } from "@ai-matrx/kit/text-case";
2
- const RATIO_RE = /^\s*(\d+(?:\.\d+)?)\s*[:x×/]\s*(\d+(?:\.\d+)?)\s*$/i;
3
- function parseRatio(value) {
4
- const m = RATIO_RE.exec(value);
5
- if (!m) return null;
6
- const w = Number(m[1]);
7
- const h = Number(m[2]);
8
- if (!(w > 0 && h > 0)) return null;
9
- return { w, h };
10
- }
11
- function isAspectRatioOptionSet(options) {
12
- const ratios = options.filter((o) => parseRatio(o) !== null).length;
13
- return ratios >= 2 && ratios >= options.length - 2;
14
- }
15
- const COMMON_ORDER = [
16
- "1:1",
17
- "16:9",
18
- "9:16",
19
- "4:3",
20
- "3:4",
21
- "3:2",
22
- "2:3",
23
- "4:5",
24
- "5:4",
25
- "21:9"
26
- ];
27
- function reduced(r) {
28
- const scale = 1e3;
29
- let a = Math.round(r.w * scale);
30
- let b = Math.round(r.h * scale);
31
- const gcd = (x, y) => y === 0 ? x : gcd(y, x % y);
32
- const g = gcd(a, b);
33
- a /= g;
34
- b /= g;
35
- return `${a}:${b}`;
36
- }
37
- function ratioShapeName(value) {
38
- const r = parseRatio(value);
39
- if (!r) return null;
40
- const k = r.w / r.h;
41
- if (Math.abs(k - 1) < 0.02) return "Square";
42
- if (k >= 2.2) return "Ultrawide";
43
- if (k <= 1 / 2.2) return "Tall";
44
- if (k > 1.6) return "Widescreen";
45
- if (k < 1 / 1.6) return "Story";
46
- return k > 1 ? "Landscape" : "Portrait";
47
- }
48
- function humanizeRatioWord(value) {
49
- return humanizeIdentifier(value) || value;
50
- }
51
- function groupRatioOptions(options) {
52
- const words = [];
53
- const ratioOpts = [];
54
- for (const o of options) (parseRatio(o) ? ratioOpts : words).push(o);
55
- const byReduced = /* @__PURE__ */ new Map();
56
- for (const o of ratioOpts) {
57
- const key = reduced(parseRatio(o));
58
- if (!byReduced.has(key)) byReduced.set(key, o);
59
- }
60
- const common = [];
61
- for (const c of COMMON_ORDER) {
62
- const hit = byReduced.get(reduced(parseRatio(c)));
63
- if (hit) common.push(hit);
64
- }
65
- const commonSet = new Set(common);
66
- const more = ratioOpts.filter((o) => !commonSet.has(o)).sort((a, b) => {
67
- const ra = parseRatio(a);
68
- const rb = parseRatio(b);
69
- return rb.w / rb.h - ra.w / ra.h;
70
- });
71
- if (ratioOpts.length <= 6) {
72
- return { words, common: [...common, ...more], more: [] };
73
- }
74
- return { words, common, more };
75
- }
1
+ import {
2
+ parseRatio,
3
+ isAspectRatioOptionSet,
4
+ ratioShapeName,
5
+ humanizeRatioWord,
6
+ groupRatioOptions
7
+ } from "@ai-matrx/design-system/controls";
76
8
  export {
77
9
  groupRatioOptions,
78
10
  humanizeRatioWord,
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The package's chat panel chrome (HOST-OWNED-SOURCE-PLAN §2.6, P28f): a DOCK
3
+ * (a full-height side column with a resize edge) and a floating WINDOW (a
4
+ * draggable card) around whatever chat the host puts inside — normally an
5
+ * `AgentConversationColumn`. Tailwind + the design-system tokens only; no
6
+ * compat sheet.
7
+ *
8
+ * Slots, all optional and absent when not given (never a dead control):
9
+ * header one compact row: `title` (node), `badge`, `working`, `actions`
10
+ * list a conversation list opened by "Conversations" in the header,
11
+ * with `listHeader` above it (what a click costs, etc.)
12
+ * width / onWidthChange / minWidth / maxWidth the host owns the width
13
+ * label the accessible name (a host's keyboard scope may key on it)
14
+ */
15
+ import { type ReactNode } from "react";
16
+ import { type DraggablePanelPosition } from "./drag.js";
17
+ export declare const CHAT_PANEL_DOCK_MIN_WIDTH = 320;
18
+ export declare const CHAT_PANEL_DOCK_MAX_WIDTH = 720;
19
+ export declare const CHAT_PANEL_WINDOW_WIDTH = 420;
20
+ export interface ChatPanelHeader {
21
+ title?: ReactNode;
22
+ badge?: ReactNode;
23
+ working?: ReactNode;
24
+ actions?: ReactNode;
25
+ }
26
+ interface ChatPanelCommon {
27
+ header?: ChatPanelHeader;
28
+ /** A conversation list; present ⇒ the header carries "Conversations". */
29
+ list?: ReactNode;
30
+ listHeader?: ReactNode;
31
+ label?: string;
32
+ className?: string;
33
+ children: ReactNode;
34
+ }
35
+ export interface ChatDockPanelProps extends ChatPanelCommon {
36
+ side?: "start" | "end";
37
+ width?: number;
38
+ onWidthChange?: (next: number) => void;
39
+ minWidth?: number;
40
+ maxWidth?: number;
41
+ }
42
+ export interface ChatWindowPanelProps extends ChatPanelCommon {
43
+ initialPosition?: DraggablePanelPosition;
44
+ width?: number;
45
+ height?: number | string;
46
+ onPositionCommit?: (position: DraggablePanelPosition) => void;
47
+ }
48
+ export declare function ChatDockPanel({ header, list, listHeader, label, className, children, side, width, onWidthChange, minWidth, maxWidth, }: ChatDockPanelProps): ReactNode;
49
+ export declare function ChatWindowPanel({ header, list, listHeader, label, className, children, initialPosition, width, height, onPositionCommit, }: ChatWindowPanelProps): ReactNode;
50
+ export {};
@@ -0,0 +1,175 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useId, useState } from "react";
4
+ import {
5
+ useDraggablePanel,
6
+ useResizableWidth
7
+ } from "./drag.js";
8
+ const CHAT_PANEL_DOCK_MIN_WIDTH = 320;
9
+ const CHAT_PANEL_DOCK_MAX_WIDTH = 720;
10
+ const CHAT_PANEL_WINDOW_WIDTH = 420;
11
+ function HeaderRow({
12
+ header,
13
+ listToggle,
14
+ dragHandleProps
15
+ }) {
16
+ return /* @__PURE__ */ jsxs(
17
+ "div",
18
+ {
19
+ className: "flex h-10 shrink-0 items-center gap-2 border-b border-border px-2",
20
+ "data-chat-panel-header": "",
21
+ ...dragHandleProps ?? {},
22
+ children: [
23
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-2 text-sm font-medium text-foreground", children: [
24
+ header?.title ? /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: header.title }) : null,
25
+ header?.badge ?? null,
26
+ header?.working ?? null
27
+ ] }),
28
+ /* @__PURE__ */ jsxs(
29
+ "div",
30
+ {
31
+ className: "flex shrink-0 items-center gap-1",
32
+ onPointerDown: (event) => event.stopPropagation(),
33
+ children: [
34
+ listToggle,
35
+ header?.actions ?? null
36
+ ]
37
+ }
38
+ )
39
+ ]
40
+ }
41
+ );
42
+ }
43
+ function useListToggle(list) {
44
+ const [open, setOpen] = useState(false);
45
+ const id = useId();
46
+ const toggle = list === void 0 ? null : /* @__PURE__ */ jsx(
47
+ "button",
48
+ {
49
+ type: "button",
50
+ className: "rounded-md px-2 py-1 text-xs text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
51
+ "aria-expanded": open,
52
+ "aria-controls": id,
53
+ onClick: () => setOpen((v) => !v),
54
+ children: "Conversations"
55
+ }
56
+ );
57
+ return { open, id, toggle };
58
+ }
59
+ function ListArea({
60
+ id,
61
+ listHeader,
62
+ list
63
+ }) {
64
+ return /* @__PURE__ */ jsxs("div", { id, className: "max-h-[45%] shrink-0 overflow-y-auto border-b border-border", "data-chat-panel-list": "", children: [
65
+ listHeader ? /* @__PURE__ */ jsx("div", { className: "px-3 pt-2 text-xs text-muted-foreground", children: listHeader }) : null,
66
+ list
67
+ ] });
68
+ }
69
+ function ChatDockPanel({
70
+ header,
71
+ list,
72
+ listHeader,
73
+ label,
74
+ className,
75
+ children,
76
+ side = "start",
77
+ width = CHAT_PANEL_DOCK_MIN_WIDTH,
78
+ onWidthChange,
79
+ minWidth = CHAT_PANEL_DOCK_MIN_WIDTH,
80
+ maxWidth = CHAT_PANEL_DOCK_MAX_WIDTH
81
+ }) {
82
+ const listToggle = useListToggle(list);
83
+ const resize = useResizableWidth({
84
+ width,
85
+ min: minWidth,
86
+ max: maxWidth,
87
+ edge: side === "start" ? "end" : "start",
88
+ ...onWidthChange === void 0 ? {} : { onWidthChange }
89
+ });
90
+ return /* @__PURE__ */ jsxs(
91
+ "aside",
92
+ {
93
+ ref: resize.panelRef,
94
+ className: `relative flex h-full min-h-0 shrink-0 flex-col bg-background ${side === "start" ? "border-r" : "border-l"} border-border ${className ?? ""}`,
95
+ "data-chat-panel": "dock",
96
+ "data-side": side,
97
+ "data-resizing": resize.resizing,
98
+ style: { width },
99
+ role: "complementary",
100
+ "aria-label": label ?? "Chat",
101
+ children: [
102
+ onWidthChange === void 0 ? null : /* @__PURE__ */ jsx(
103
+ "div",
104
+ {
105
+ className: `absolute inset-y-0 z-10 w-1.5 cursor-col-resize hover:bg-primary/30 focus-visible:bg-primary/40 focus-visible:outline-none ${side === "start" ? "-right-0.5" : "-left-0.5"}`,
106
+ "aria-label": "Resize the chat panel",
107
+ ...resize.handleProps
108
+ }
109
+ ),
110
+ /* @__PURE__ */ jsx(HeaderRow, { header, listToggle: listToggle.toggle }),
111
+ list !== void 0 && listToggle.open ? /* @__PURE__ */ jsx(ListArea, { id: listToggle.id, listHeader, list }) : null,
112
+ /* @__PURE__ */ jsx("div", { className: "flex min-h-0 flex-1 flex-col", children })
113
+ ]
114
+ }
115
+ );
116
+ }
117
+ function ChatWindowPanel({
118
+ header,
119
+ list,
120
+ listHeader,
121
+ label,
122
+ className,
123
+ children,
124
+ initialPosition,
125
+ width = CHAT_PANEL_WINDOW_WIDTH,
126
+ height = "min(640px, calc(100vh - 120px))",
127
+ onPositionCommit
128
+ }) {
129
+ const listToggle = useListToggle(list);
130
+ const [fallback] = useState(() => ({
131
+ x: Math.max(8, (globalThis.innerWidth ?? width + 32) - width - 24),
132
+ y: 96
133
+ }));
134
+ const drag = useDraggablePanel({
135
+ initial: initialPosition ?? fallback,
136
+ ...onPositionCommit === void 0 ? {} : { onCommit: onPositionCommit }
137
+ });
138
+ return /* @__PURE__ */ jsxs(
139
+ "div",
140
+ {
141
+ ref: drag.panelRef,
142
+ className: `fixed z-40 flex flex-col overflow-hidden rounded-xl border border-border bg-background shadow-xl ${className ?? ""}`,
143
+ "data-chat-panel": "window",
144
+ "data-dragging": drag.dragging,
145
+ role: "dialog",
146
+ "aria-label": label ?? "Chat",
147
+ style: {
148
+ left: drag.position.x,
149
+ top: drag.position.y,
150
+ width,
151
+ height,
152
+ transform: "translate(var(--mx-chat-panel-dx, 0px), var(--mx-chat-panel-dy, 0px))"
153
+ },
154
+ children: [
155
+ /* @__PURE__ */ jsx(
156
+ HeaderRow,
157
+ {
158
+ header,
159
+ listToggle: listToggle.toggle,
160
+ dragHandleProps: drag.dragHandleProps
161
+ }
162
+ ),
163
+ list !== void 0 && listToggle.open ? /* @__PURE__ */ jsx(ListArea, { id: listToggle.id, listHeader, list }) : null,
164
+ /* @__PURE__ */ jsx("div", { className: "flex min-h-0 flex-1 flex-col", children })
165
+ ]
166
+ }
167
+ );
168
+ }
169
+ export {
170
+ CHAT_PANEL_DOCK_MAX_WIDTH,
171
+ CHAT_PANEL_DOCK_MIN_WIDTH,
172
+ CHAT_PANEL_WINDOW_WIDTH,
173
+ ChatDockPanel,
174
+ ChatWindowPanel
175
+ };
@@ -0,0 +1,87 @@
1
+ /**
2
+ * `useDraggablePanel()` — dragging that does not re-render the chat.
3
+ *
4
+ * THE RULE, lifted from the Workflow Studio's own `FloatingChatPanel` because
5
+ * it was paid for there: a pointer move paints through CSS CUSTOM PROPERTIES
6
+ * inside ONE requestAnimationFrame, and the position is committed to React
7
+ * state EXACTLY ONCE, on release. Writing React state per pointer event
8
+ * re-renders the transcript sixty times a second while an answer is streaming
9
+ * into it, which is precisely the budget design §7 sets out to protect.
10
+ *
11
+ * Two details that look like noise and are not:
12
+ * - the paint offsets are CLEARED in a layout effect on the frame that adopts
13
+ * the committed position, so the panel never jumps by the offset twice;
14
+ * - a pending frame is cancelled on unmount, because a callback holding a
15
+ * detached element is a leak that only shows up under a long session.
16
+ */
17
+ import { type PointerEvent as ReactPointerEvent, type RefObject } from "react";
18
+ export interface DraggablePanelPosition {
19
+ x: number;
20
+ y: number;
21
+ }
22
+ export interface UseResizableWidthOptions {
23
+ /** The host's stored width. This hook never owns it. */
24
+ width: number;
25
+ min: number;
26
+ max: number;
27
+ /** Which edge the handle sits on — the side the panel grows FROM. */
28
+ edge: "start" | "end";
29
+ /** Told once, on release, and on every keyboard step. */
30
+ onWidthChange?: ((next: number) => void) | undefined;
31
+ }
32
+ export interface ResizableWidthHandle {
33
+ panelRef: RefObject<HTMLElement | null>;
34
+ resizing: boolean;
35
+ handleProps: {
36
+ role: "separator";
37
+ tabIndex: 0;
38
+ "aria-orientation": "vertical";
39
+ "aria-valuenow": number;
40
+ "aria-valuemin": number;
41
+ "aria-valuemax": number;
42
+ onPointerDown(event: ReactPointerEvent): void;
43
+ onPointerMove(event: ReactPointerEvent): void;
44
+ onPointerUp(event: ReactPointerEvent): void;
45
+ onPointerCancel(event: ReactPointerEvent): void;
46
+ onKeyDown(event: {
47
+ key: string;
48
+ preventDefault(): void;
49
+ }): void;
50
+ onDoubleClick(): void;
51
+ };
52
+ }
53
+ /**
54
+ * THE WIDTH IS THE HOST'S, ALWAYS.
55
+ *
56
+ * The package never writes a width to storage: which pref store a surface uses,
57
+ * and whether the width is per-user or per-organization, is the host's to know
58
+ * (Law 6 — opinions become knobs, and the knob is theirs). So this hook paints
59
+ * the drag live and hands the final number back through one callback.
60
+ *
61
+ * The keyboard path is not decoration: a separator that only answers a pointer
62
+ * is unusable with a keyboard or a screen reader, and a panel whose size cannot
63
+ * be changed without a mouse is a dead control for the people who need it most.
64
+ */
65
+ export declare function useResizableWidth(options: UseResizableWidthOptions): ResizableWidthHandle;
66
+ export interface UseDraggablePanelOptions {
67
+ initial: DraggablePanelPosition;
68
+ /** Keep this much of the panel on screen on every edge. */
69
+ keepVisiblePx?: number | undefined;
70
+ /** Told once per drag, on release — a host that persists the position. */
71
+ onCommit?: ((position: DraggablePanelPosition) => void) | undefined;
72
+ }
73
+ export interface DraggablePanelHandle {
74
+ position: DraggablePanelPosition;
75
+ /** Attach to the element that MOVES. */
76
+ panelRef: RefObject<HTMLElement | null>;
77
+ /** Spread onto the element you drag BY (the header). */
78
+ dragHandleProps: {
79
+ onPointerDown(event: ReactPointerEvent): void;
80
+ onPointerMove(event: ReactPointerEvent): void;
81
+ onPointerUp(event: ReactPointerEvent): void;
82
+ onPointerCancel(event: ReactPointerEvent): void;
83
+ };
84
+ /** True while a pointer is down — a shell suppresses transitions with it. */
85
+ dragging: boolean;
86
+ }
87
+ export declare function useDraggablePanel(options: UseDraggablePanelOptions): DraggablePanelHandle;
@@ -0,0 +1,194 @@
1
+ import {
2
+ useCallback,
3
+ useEffect,
4
+ useLayoutEffect,
5
+ useRef,
6
+ useState
7
+ } from "react";
8
+ const KEYBOARD_STEP_PX = 24;
9
+ function useResizableWidth(options) {
10
+ const { width, min, max, edge, onWidthChange } = options;
11
+ const panelRef = useRef(null);
12
+ const [resizing, setResizing] = useState(false);
13
+ const frameRef = useRef(null);
14
+ const dragRef = useRef(
15
+ null
16
+ );
17
+ const initialRef = useRef(width);
18
+ const clamp = useCallback(
19
+ (next) => Math.max(min, Math.min(max, Math.round(next))),
20
+ [min, max]
21
+ );
22
+ const commit = useCallback(
23
+ (next) => {
24
+ panelRef.current?.style.removeProperty("width");
25
+ onWidthChange?.(clamp(next));
26
+ },
27
+ [clamp, onWidthChange]
28
+ );
29
+ const end = useCallback(() => {
30
+ const drag = dragRef.current;
31
+ if (frameRef.current !== null) {
32
+ globalThis.cancelAnimationFrame?.(frameRef.current);
33
+ frameRef.current = null;
34
+ }
35
+ dragRef.current = null;
36
+ setResizing(false);
37
+ if (drag !== null) commit(drag.latest);
38
+ }, [commit]);
39
+ useEffect(
40
+ () => () => {
41
+ if (frameRef.current !== null) globalThis.cancelAnimationFrame?.(frameRef.current);
42
+ },
43
+ []
44
+ );
45
+ return {
46
+ panelRef,
47
+ resizing,
48
+ handleProps: {
49
+ role: "separator",
50
+ tabIndex: 0,
51
+ "aria-orientation": "vertical",
52
+ "aria-valuenow": width,
53
+ "aria-valuemin": min,
54
+ "aria-valuemax": max,
55
+ onPointerDown(event) {
56
+ event.preventDefault();
57
+ dragRef.current = { startX: event.clientX, startWidth: width, latest: width };
58
+ setResizing(true);
59
+ event.target.setPointerCapture?.(event.pointerId);
60
+ },
61
+ onPointerMove(event) {
62
+ const drag = dragRef.current;
63
+ if (drag === null) return;
64
+ const delta = edge === "end" ? event.clientX - drag.startX : drag.startX - event.clientX;
65
+ drag.latest = clamp(drag.startWidth + delta);
66
+ if (frameRef.current !== null) return;
67
+ const schedule = globalThis.requestAnimationFrame;
68
+ const paint = () => {
69
+ frameRef.current = null;
70
+ const active = dragRef.current;
71
+ if (active === null || panelRef.current === null) return;
72
+ panelRef.current.style.width = `${active.latest}px`;
73
+ };
74
+ if (typeof schedule === "function") frameRef.current = schedule(paint);
75
+ else paint();
76
+ },
77
+ onPointerUp: end,
78
+ onPointerCancel: end,
79
+ onKeyDown(event) {
80
+ const grow = edge === "end" ? "ArrowRight" : "ArrowLeft";
81
+ const shrink = edge === "end" ? "ArrowLeft" : "ArrowRight";
82
+ if (event.key === grow) {
83
+ event.preventDefault();
84
+ commit(width + KEYBOARD_STEP_PX);
85
+ return;
86
+ }
87
+ if (event.key === shrink) {
88
+ event.preventDefault();
89
+ commit(width - KEYBOARD_STEP_PX);
90
+ }
91
+ },
92
+ onDoubleClick() {
93
+ commit(initialRef.current);
94
+ }
95
+ }
96
+ };
97
+ }
98
+ const DEFAULT_KEEP_VISIBLE = 64;
99
+ function useDraggablePanel(options) {
100
+ const keepVisible = options.keepVisiblePx ?? DEFAULT_KEEP_VISIBLE;
101
+ const [position, setPosition] = useState(options.initial);
102
+ const [dragging, setDragging] = useState(false);
103
+ const panelRef = useRef(null);
104
+ const frameRef = useRef(null);
105
+ const adoptingRef = useRef(false);
106
+ const dragRef = useRef(null);
107
+ const clamp = useCallback(
108
+ (next) => {
109
+ const width = globalThis.innerWidth ?? 0;
110
+ const height = globalThis.innerHeight ?? 0;
111
+ if (width === 0 || height === 0) return next;
112
+ return {
113
+ x: Math.max(0, Math.min(width - keepVisible, next.x)),
114
+ y: Math.max(0, Math.min(height - keepVisible, next.y))
115
+ };
116
+ },
117
+ [keepVisible]
118
+ );
119
+ const onPointerDown = useCallback(
120
+ (event) => {
121
+ if (event.target.closest("button,a,input,select,textarea")) return;
122
+ event.preventDefault();
123
+ dragRef.current = {
124
+ dx: event.clientX - position.x,
125
+ dy: event.clientY - position.y,
126
+ origin: position,
127
+ latest: position
128
+ };
129
+ setDragging(true);
130
+ event.target.setPointerCapture?.(event.pointerId);
131
+ },
132
+ [position]
133
+ );
134
+ const onPointerMove = useCallback(
135
+ (event) => {
136
+ const drag = dragRef.current;
137
+ if (drag === null) return;
138
+ drag.latest = clamp({ x: event.clientX - drag.dx, y: event.clientY - drag.dy });
139
+ if (frameRef.current !== null) return;
140
+ const schedule = globalThis.requestAnimationFrame;
141
+ const paint = () => {
142
+ frameRef.current = null;
143
+ const active = dragRef.current;
144
+ const element = panelRef.current;
145
+ if (active === null || element === null) return;
146
+ element.style.setProperty("--mx-chat-panel-dx", `${active.latest.x - active.origin.x}px`);
147
+ element.style.setProperty("--mx-chat-panel-dy", `${active.latest.y - active.origin.y}px`);
148
+ };
149
+ if (typeof schedule === "function") frameRef.current = schedule(paint);
150
+ else paint();
151
+ },
152
+ [clamp]
153
+ );
154
+ const end = useCallback(() => {
155
+ const drag = dragRef.current;
156
+ if (frameRef.current !== null) {
157
+ globalThis.cancelAnimationFrame?.(frameRef.current);
158
+ frameRef.current = null;
159
+ }
160
+ dragRef.current = null;
161
+ setDragging(false);
162
+ if (drag === null) return;
163
+ adoptingRef.current = true;
164
+ setPosition(drag.latest);
165
+ options.onCommit?.(drag.latest);
166
+ }, [options]);
167
+ useLayoutEffect(() => {
168
+ if (!adoptingRef.current) return;
169
+ adoptingRef.current = false;
170
+ panelRef.current?.style.removeProperty("--mx-chat-panel-dx");
171
+ panelRef.current?.style.removeProperty("--mx-chat-panel-dy");
172
+ });
173
+ useEffect(
174
+ () => () => {
175
+ if (frameRef.current !== null) globalThis.cancelAnimationFrame?.(frameRef.current);
176
+ },
177
+ []
178
+ );
179
+ return {
180
+ position,
181
+ panelRef,
182
+ dragging,
183
+ dragHandleProps: {
184
+ onPointerDown,
185
+ onPointerMove,
186
+ onPointerUp: end,
187
+ onPointerCancel: end
188
+ }
189
+ };
190
+ }
191
+ export {
192
+ useDraggablePanel,
193
+ useResizableWidth
194
+ };
@@ -49,6 +49,7 @@ import {
49
49
  AGENT_RUN_WINDOW_AGENT_ARG,
50
50
  AGENT_RUN_WINDOW_CONVERSATION_ARG,
51
51
  addressableConversationId,
52
+ adoptLateWindowSeed,
52
53
  AGENT_RUN_WINDOW_URL_MODE
53
54
  } from "./agentRunWindowAddress.js";
54
55
  import { ErrorAlchemyMenu } from "../../../host/ui-slots.js";
@@ -534,6 +535,22 @@ function AgentRunWindowInner({
534
535
  const liveResourceIdentity = seedApplies ? initialResourceIdentity : null;
535
536
  const liveAutoRun = seedApplies ? initialAutoRun : false;
536
537
  const [selectedConversationId, setSelectedConversationId] = useState(initialSelectedConversationId);
538
+ const [seenSeed, setSeenSeed] = useState({
539
+ agent: initialAgentId,
540
+ conversation: initialSelectedConversationId
541
+ });
542
+ const lateSeed = adoptLateWindowSeed({
543
+ seen: seenSeed,
544
+ incoming: { agent: initialAgentId, conversation: initialSelectedConversationId },
545
+ current: { agent: agentId, conversation: selectedConversationId }
546
+ });
547
+ if (lateSeed) {
548
+ setSeenSeed(lateSeed.seen);
549
+ if (lateSeed.agent !== agentId) setAgentId(lateSeed.agent);
550
+ if (lateSeed.conversation !== selectedConversationId) {
551
+ setSelectedConversationId(lateSeed.conversation);
552
+ }
553
+ }
537
554
  const siblingWindowCount = useChatManagedWindowCount("agent-run-window:");
538
555
  const initialPosition = AGENT_RUN_WINDOW_POSITIONS[siblingWindowCount % AGENT_RUN_WINDOW_POSITIONS.length];
539
556
  const liveAgentName = useAgentName(agentId) ?? null;