@ai-matrx/chat 0.7.18 → 0.7.20

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 (47) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/agents/components/chat/ChatHistorySidebar.js +3 -0
  3. package/dist/agents/components/chat/ChatSidebarMenu.js +3 -0
  4. package/dist/agents/components/chat/PinnedAgentsSection.d.ts +3 -5
  5. package/dist/agents/components/chat/PinnedAgentsSection.js +7 -4
  6. package/dist/agents/components/conversation-history/ConversationHistorySidebar.d.ts +14 -3
  7. package/dist/agents/components/conversation-history/ConversationHistorySidebar.js +57 -37
  8. package/dist/agents/components/conversation-history/ConversationSourceFilterTree.js +3 -3
  9. package/dist/agents/components/inputs/smart-input/ConversationContextChip.js +18 -7
  10. package/dist/agents/components/inputs/smart-input/ConversationContextRail.js +4 -2
  11. package/dist/agents/components/inputs/smart-input/DesktopPresenceIndicator.js +6 -0
  12. package/dist/agents/components/inputs/smart-input/composer/ComposerAgentPill.js +21 -7
  13. package/dist/agents/components/inputs/smart-input/composer/ComposerChipsRow.js +3 -3
  14. package/dist/agents/components/inputs/smart-input/composer/ComposerOutput.js +22 -15
  15. package/dist/agents/components/inputs/smart-input/composer/ComposerSplash.js +19 -3
  16. package/dist/agents/components/inputs/smart-input/composer/useComposerAgent.d.ts +24 -3
  17. package/dist/agents/components/inputs/smart-input/composer/useComposerAgent.js +32 -30
  18. package/dist/agents/components/inputs/smart-input/use-compute-target-actions.d.ts +20 -0
  19. package/dist/agents/components/inputs/smart-input/use-compute-target-actions.js +11 -0
  20. package/dist/agents/hooks/useConversationMaterialized.js +42 -9
  21. package/dist/agents/hooks/useCreatorOwnershipSync.js +7 -3
  22. package/dist/agents/hooks/useWorkingDocument.d.ts +12 -5
  23. package/dist/agents/hooks/useWorkingDocument.js +18 -6
  24. package/dist/agents/identity/agent-identity.d.ts +7 -1
  25. package/dist/agents/identity/agent-identity.js +11 -6
  26. package/dist/agents/identity/model-catalog.d.ts +7 -0
  27. package/dist/agents/identity/model-catalog.js +16 -1
  28. package/dist/agents/redux/conversation-history/slice.d.ts +2 -0
  29. package/dist/agents/redux/conversation-history/slice.js +2 -0
  30. package/dist/agents/redux/conversation-history/thunks.d.ts +2 -0
  31. package/dist/agents/redux/conversation-history/thunks.js +6 -3
  32. package/dist/agents/redux/conversation-history/types.d.ts +6 -0
  33. package/dist/agents/redux/conversation-history/types.js +1 -0
  34. package/dist/agents/redux/conversation-list/conversation-rows.d.ts +7 -0
  35. package/dist/agents/redux/conversation-list/conversation-rows.js +3 -1
  36. package/dist/agents/redux/execution-system/client-capabilities/desktop-native.provider.js +4 -1
  37. package/dist/agents/redux/execution-system/context-rules/mandate-kill-switch.d.ts +4 -0
  38. package/dist/agents/redux/execution-system/context-rules/mandate-kill-switch.js +26 -2
  39. package/dist/agents/redux/execution-system/conversations/conversations.selectors.d.ts +10 -0
  40. package/dist/agents/redux/execution-system/conversations/conversations.selectors.js +3 -0
  41. package/dist/agents/redux/execution-system/instance-user-input/useComposerDraftRestore.js +6 -1
  42. package/dist/agents/redux/execution-system/instance-working-document/scratchpad.thunks.js +9 -0
  43. package/dist/agents/redux/execution-system/thunks/launch-agent-execution.thunk.js +1 -8
  44. package/dist/agents/ui-first-tools/ui/lists/TaskPanelChip.js +4 -2
  45. package/dist/mandates/service.js +47 -32
  46. package/dist/next/navigation.js +5 -1
  47. package/package.json +6 -6
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @ai-matrx/chat
2
2
 
3
+ ## 0.7.20
4
+
5
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
6
+ `git diff npm/chat/v0.7.19..npm/chat/v0.7.20 -- apps/shared/chat`).
7
+ No source changes intended and no consumer action required.
8
+
3
9
  ## 0.7.18
4
10
 
5
11
  Automatic changed-only republish (docs/metadata drift since the last tag — see
@@ -8,6 +14,17 @@ No source changes intended and no consumer action required.
8
14
 
9
15
  ## 0.7.17
10
16
  ### Unreleased
17
+ ### the chat sidebar loads a short list of names, only when seen; a draft reads nothing
18
+
19
+ - `ConversationHistorySidebar` reads only once on screen (`useInView`); new prop `nameRowsOnly` (slim
20
+ `CONVERSATION_NAME_ROW_COLUMNS` read; `ChatHistorySidebar` uses it with 20 rows). Source facets load on
21
+ engagement (filter open, search, "Hide"). `useNextNavigation().Link` defaults `prefetch={false}`.
22
+ - New `selectIsDraftConversation`; draft conversations skip agent-lists, document/scratchpad restores,
23
+ remark restore and the materialized probe (now shared per conversation). Active scratchpad reads when the
24
+ conversation's scratch gate is on. Desktop-presence warm waits for page idle. New `useComputeBinding`
25
+ (bound box without the targets list) — at-rest chips no longer request `/api/compute-targets`.
26
+ - **Consumer action:** none. A host Link that should prefetch passes `prefetch` explicitly.
27
+
11
28
  ### a turn carries what the person attached (lane A1, Applet builder)
12
29
 
13
30
  - **`runHeadlessAgentJson({ resources })`** and **`continueAgentJson({ resources })`**: the person's "+" picks (`Resource[]`) go
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsx } from "react/jsx-runtime";
3
3
  import { ConversationHistorySidebar } from "../conversation-history/ConversationHistorySidebar.js";
4
+ const CHAT_HISTORY_PAGE_SIZE = 20;
4
5
  function ChatHistorySidebar({
5
6
  scopeId,
6
7
  activeConversationId,
@@ -30,6 +31,8 @@ function ChatHistorySidebar({
30
31
  autoFocusSearch,
31
32
  hideSearchAffordance,
32
33
  serverSearch: true,
34
+ pageSize: CHAT_HISTORY_PAGE_SIZE,
35
+ nameRowsOnly: true,
33
36
  className
34
37
  }
35
38
  );
@@ -105,6 +105,7 @@ function ChatSidebarMenu({ expanded }) {
105
105
  Link,
106
106
  {
107
107
  href: CHAT_HOME_HREF,
108
+ prefetch: false,
108
109
  title: "Chat",
109
110
  "aria-label": "Chat",
110
111
  "aria-current": isChatHome ? "page" : void 0,
@@ -249,6 +250,7 @@ function ChatSidebarMenu({ expanded }) {
249
250
  Link,
250
251
  {
251
252
  href: MESSAGE_TEMPLATES_HREF,
253
+ prefetch: false,
252
254
  title: "Message templates",
253
255
  "aria-label": "Message templates",
254
256
  "aria-current": isMessageTemplatesRoute ? "page" : void 0,
@@ -272,6 +274,7 @@ function ChatSidebarMenu({ expanded }) {
272
274
  Link,
273
275
  {
274
276
  href: VOICE_AGENT_HREF,
277
+ prefetch: false,
275
278
  title: "Voice agent",
276
279
  "aria-label": "Voice agent",
277
280
  "aria-current": isVoiceRoute ? "page" : void 0,
@@ -14,11 +14,9 @@ interface PinnedAgentsSectionProps {
14
14
  * with the rest of the app, and shares the agent registry with the dropdown
15
15
  * (no parallel fetches).
16
16
  *
17
- * We intentionally do NOT trigger an agent-list fetch from this component:
18
- * if the registry hasn't been populated yet (user hasn't opened the picker),
19
- * we render nothing rather than pulling every agent down just to filter to
20
- * favorites. The list populates as soon as the user opens the dropdown — same
21
- * lazy behavior as everywhere else.
17
+ * The catalogue is asked for after the page is idle (see the effect below),
18
+ * so the section is absent at first paint and appears once the list lands —
19
+ * or sooner, when the person opens any agent picker.
22
20
  */
23
21
  export declare function PinnedAgentsSection({ activeAgentId, }: PinnedAgentsSectionProps): import("react").JSX.Element | null;
24
22
  export {};
@@ -3,7 +3,6 @@ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useState } from "react";
4
4
  import { useRouter, Link } from "../../../host/navigation.js";
5
5
  import { cn } from "@ai-matrx/design-system";
6
- import { useAppDispatch } from "../../../store/hooks.js";
7
6
  import {
8
7
  FavoriteAgentButton,
9
8
  useAgentCatalogRows,
@@ -14,6 +13,7 @@ import { useAppStore } from "../../../store/hooks.js";
14
13
  import { stageChatAgentSwitch } from "./begin-fresh-chat.js";
15
14
  import { Button, DisclosureHeader } from "@ai-matrx/design-system/controls";
16
15
  import { ensureAgentCatalog } from "../../identity/agent-identity.js";
16
+ import { whenPageIdle } from "@ai-matrx/kit/idle-scheduler";
17
17
  const CONSUMER_ID = "chat-sidebar-pinned";
18
18
  const PINNED_COLLAPSED_LIMIT = 5;
19
19
  const selectPinnedAgents = makeSelectFilteredAgents();
@@ -23,13 +23,16 @@ function pinnedAgentHref(agentId) {
23
23
  function PinnedAgentsSection({
24
24
  activeAgentId
25
25
  }) {
26
- const dispatch = useAppDispatch();
27
26
  const router = useRouter();
28
27
  const store = useAppStore();
29
28
  const consumer = useAgentConsumer(CONSUMER_ID);
30
29
  useEffect(() => {
31
- void ensureAgentCatalog();
32
- }, [dispatch]);
30
+ const controller = new AbortController();
31
+ void whenPageIdle(controller.signal).then((idle) => {
32
+ if (idle) void ensureAgentCatalog();
33
+ });
34
+ return () => controller.abort();
35
+ }, []);
33
36
  useEffect(() => {
34
37
  if (consumer.favFilter !== "yes") consumer.setFavFilter("yes");
35
38
  }, [consumer]);
@@ -16,9 +16,14 @@
16
16
  * apps) or `"consumer"` (the comfortable /chat look). Both share ONE data
17
17
  * controller, so they never drift.
18
18
  *
19
- * Self-contained data-loading: the component triggers the initial fetch on
20
- * mount (and whenever `agentIds` or the resolved source filter changes) via
21
- * `fetchConversationHistory`. Consumers don't need to prewire a thunk.
19
+ * Self-contained data-loading: the component triggers the initial fetch once
20
+ * the list is ON SCREEN (and again whenever `agentIds` or the resolved source
21
+ * filter changes) via `fetchConversationHistory`. A list mounted in a collapsed
22
+ * rail, a closed drawer, a hidden desktop sidebar on a phone or an inactive tab
23
+ * reads nothing until it can be seen (`useInView`); until then, and while the
24
+ * first page loads, it shows its loading row — never a blank. The source facets
25
+ * (filter tree, lane counts, "Hide <source>") load when the person engages
26
+ * them: opening the filter, searching, or choosing "Hide" in a row menu.
22
27
  */
23
28
  import React from "react";
24
29
  import { type HistoryGrouping } from "../../redux/conversation-history/types.js";
@@ -61,6 +66,12 @@ export interface ConversationHistorySidebarProps {
61
66
  defaultGrouping?: HistoryGrouping;
62
67
  /** Page size passed to the fetch thunk. Default: 30. */
63
68
  pageSize?: number;
69
+ /**
70
+ * Read only what a row shows (name, time, provenance for its menu) — no
71
+ * description, message count or model. For a list whose rows show nothing
72
+ * else (the /chat sidebar). Default false.
73
+ */
74
+ nameRowsOnly?: boolean;
64
75
  /**
65
76
  * Legacy DENY-list on `source_feature` (server-side `.neq`). Prefer
66
77
  * `surfaceId` (ALLOW-list) for new surfaces. Both AND together if set.
@@ -8,6 +8,7 @@ import {
8
8
  useState
9
9
  } from "react";
10
10
  import { formatRelativeTime } from "@ai-matrx/kit/format";
11
+ import { useInView } from "@ai-matrx/kit/hooks";
11
12
  import {
12
13
  Cpu,
13
14
  CalendarDays,
@@ -78,6 +79,7 @@ function useConversationHistoryController(props) {
78
79
  surfaceId,
79
80
  defaultGrouping = "date",
80
81
  pageSize = 30,
82
+ nameRowsOnly = false,
81
83
  excludeSourceFeatures,
82
84
  isFavorite,
83
85
  onToggleFavorite,
@@ -89,6 +91,7 @@ function useConversationHistoryController(props) {
89
91
  } = props;
90
92
  const dispatch = useAppDispatch();
91
93
  const readFor = useRef(null);
94
+ const [rootRef, inView] = useInView();
92
95
  const selectScope = useMemo(
93
96
  () => makeSelectConversationHistoryScope(scopeId),
94
97
  [scopeId]
@@ -154,16 +157,18 @@ function useConversationHistoryController(props) {
154
157
  })
155
158
  );
156
159
  }
160
+ if (!inView) return;
157
161
  const fetchArgs = {
158
162
  scopeId,
159
163
  agentIds: agentIds.slice(),
160
164
  pageSize,
165
+ nameRowsOnly,
161
166
  replace: true
162
167
  };
163
168
  if (excludeSourceFeatures !== void 0) {
164
169
  fetchArgs.excludeSourceFeatures = excludeSourceFeatures;
165
170
  }
166
- const readKey = [scopeId, agentIdsKey, pageSize, excludeKey, resolvedKey, lanesKey, onlyKey].join("|");
171
+ const readKey = [scopeId, agentIdsKey, pageSize, nameRowsOnly, excludeKey, resolvedKey, lanesKey, onlyKey].join("|");
167
172
  if (keepLoaded) {
168
173
  if (readFor.current === readKey) return;
169
174
  const fresh = dispatch((_d, getState) => {
@@ -187,7 +192,9 @@ function useConversationHistoryController(props) {
187
192
  lanesKey,
188
193
  surfaceId,
189
194
  keepLoaded,
190
- onlyKey
195
+ onlyKey,
196
+ inView,
197
+ nameRowsOnly
191
198
  ]);
192
199
  const onSearchChange = useCallback(
193
200
  (value) => {
@@ -207,7 +214,7 @@ function useConversationHistoryController(props) {
207
214
  }, [dispatch, scopeId, hasMore, status]);
208
215
  const onRefresh = () => {
209
216
  void dispatch(fetchConversationHistory({ scopeId, replace: true }));
210
- if (surfaceId) {
217
+ if (surfaceId && facetsStatus !== "idle") {
211
218
  void dispatch(fetchSourceFacets({ force: true }));
212
219
  }
213
220
  };
@@ -248,9 +255,10 @@ function useConversationHistoryController(props) {
248
255
  (conv) => getConversationHref ? getConversationHref(conv) : `/chat/${conv.conversationId}`,
249
256
  [getConversationHref]
250
257
  );
258
+ const searching = serverSearch && searchTerm.trim() !== "";
251
259
  useEffect(() => {
252
- if (surfaceId) void dispatch(fetchSourceFacets(void 0));
253
- }, [dispatch, surfaceId]);
260
+ if (searching) void dispatch(fetchSourceFacets(void 0));
261
+ }, [dispatch, searching]);
254
262
  const commitSourceFilter = useCallback(
255
263
  (features, includeEmpty) => {
256
264
  dispatch(
@@ -276,18 +284,6 @@ function useConversationHistoryController(props) {
276
284
  }
277
285
  const featureKey = sourceKey(conv.sourceFeature);
278
286
  const isEmpty = featureKey === EMPTY_SOURCE_KEY;
279
- const selected = new Set(scope.includeSourceFeatures);
280
- if (scope.includeSourceApps.length > 0) {
281
- const apps = new Set(scope.includeSourceApps);
282
- for (const f of facets) {
283
- if (apps.has(sourceKey(f.sourceApp))) {
284
- selected.add(sourceKey(f.sourceFeature));
285
- }
286
- }
287
- }
288
- selected.delete(EMPTY_SOURCE_KEY);
289
- let includeEmpty = scope.includeEmptySource;
290
- const unfiltered = selected.size === 0 && !includeEmpty;
291
287
  return {
292
288
  app: conv.sourceApp,
293
289
  feature: conv.sourceFeature,
@@ -300,27 +296,49 @@ function useConversationHistoryController(props) {
300
296
  toast.success(`Showing only ${featureLabel(featureKey)}`);
301
297
  },
302
298
  onHide: () => {
303
- let next;
304
- if (unfiltered) {
305
- next = /* @__PURE__ */ new Set();
306
- let hasEmpty = false;
307
- for (const f of facets) {
308
- const k = sourceKey(f.sourceFeature);
309
- if (k === EMPTY_SOURCE_KEY) hasEmpty = true;
310
- else next.add(k);
299
+ void (async () => {
300
+ const read = await dispatch(fetchSourceFacets(void 0));
301
+ const known = fetchSourceFacets.fulfilled.match(read) ? read.payload : [];
302
+ if (known.length === 0) {
303
+ toast.error(
304
+ `Couldn't hide ${featureLabel(featureKey)}: your sources didn't load. Try again.`
305
+ );
306
+ return;
311
307
  }
312
- includeEmpty = hasEmpty;
313
- } else {
314
- next = selected;
315
- }
316
- if (isEmpty) includeEmpty = false;
317
- else next.delete(featureKey);
318
- commitSourceFilter(next, includeEmpty);
319
- toast.success(`Hiding ${featureLabel(featureKey)} here`);
308
+ const selected = new Set(scope.includeSourceFeatures);
309
+ if (scope.includeSourceApps.length > 0) {
310
+ const apps = new Set(scope.includeSourceApps);
311
+ for (const f of known) {
312
+ if (apps.has(sourceKey(f.sourceApp))) {
313
+ selected.add(sourceKey(f.sourceFeature));
314
+ }
315
+ }
316
+ }
317
+ selected.delete(EMPTY_SOURCE_KEY);
318
+ let includeEmpty = scope.includeEmptySource;
319
+ const unfiltered = selected.size === 0 && !includeEmpty;
320
+ let next;
321
+ if (unfiltered) {
322
+ next = /* @__PURE__ */ new Set();
323
+ let hasEmpty = false;
324
+ for (const f of known) {
325
+ const k = sourceKey(f.sourceFeature);
326
+ if (k === EMPTY_SOURCE_KEY) hasEmpty = true;
327
+ else next.add(k);
328
+ }
329
+ includeEmpty = hasEmpty;
330
+ } else {
331
+ next = selected;
332
+ }
333
+ if (isEmpty) includeEmpty = false;
334
+ else next.delete(featureKey);
335
+ commitSourceFilter(next, includeEmpty);
336
+ toast.success(`Hiding ${featureLabel(featureKey)} here`);
337
+ })();
320
338
  }
321
339
  };
322
340
  },
323
- [surfaceId, scope, facets, commitSourceFilter]
341
+ [dispatch, surfaceId, scope, commitSourceFilter]
324
342
  );
325
343
  const getSourceMenuCtx = useCallback(
326
344
  (conv) => {
@@ -337,6 +355,7 @@ function useConversationHistoryController(props) {
337
355
  [getSourceMenuBase, removeFromList]
338
356
  );
339
357
  return {
358
+ rootRef,
340
359
  scope,
341
360
  byDate,
342
361
  byAgent,
@@ -414,7 +433,7 @@ const DenseView = ({
414
433
  ) : cachedSearchItems;
415
434
  const empty = !allLanesOff && status !== "loading" && status !== "idle" && !error && count === 0 && !searchTerm.trim() && emptyState;
416
435
  const showControls = showSearch || showGroupingToggle || !!headerActions || !!surfaceId;
417
- return /* @__PURE__ */ jsxs("div", { className: cn("flex h-full min-h-0 flex-col", className), children: [
436
+ return /* @__PURE__ */ jsxs("div", { ref: ctl.rootRef, className: cn("flex h-full min-h-0 flex-col", className), children: [
418
437
  headerSlot,
419
438
  topSlot,
420
439
  showControls && /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-1 border-b border-border px-2 py-1", children: [
@@ -477,7 +496,7 @@ const DenseView = ({
477
496
  ] }),
478
497
  /* @__PURE__ */ jsxs("div", { className: "min-h-0 flex-1 overflow-y-auto", children: [
479
498
  error && /* @__PURE__ */ jsx(ReadFailure, { error, what: "your conversations", onRetry: onRefresh, className: "m-2" }),
480
- status === "loading" && count === 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-3 py-3 text-[11px] text-muted-foreground", children: [
499
+ (status === "loading" || status === "idle") && count === 0 && !allLanesOff && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-3 py-3 text-[11px] text-muted-foreground", children: [
481
500
  /* @__PURE__ */ jsx(Loader2, { size: 12, className: "animate-spin" }),
482
501
  "Loading conversations\u2026"
483
502
  ] }),
@@ -680,6 +699,7 @@ const ConsumerView = ({
680
699
  return /* @__PURE__ */ jsxs(
681
700
  "div",
682
701
  {
702
+ ref: ctl.rootRef,
683
703
  className: cn(
684
704
  "matrx-touch-targets flex h-full min-h-0 flex-col bg-card",
685
705
  className
@@ -736,7 +756,7 @@ const ConsumerView = ({
736
756
  topSlot,
737
757
  /* @__PURE__ */ jsxs("div", { className: "min-h-0 flex-1 overflow-y-auto pt-1", children: [
738
758
  error && !searchActive && /* @__PURE__ */ jsx(ReadFailure, { error, what: "your conversations", onRetry: onRefresh, className: "m-2" }),
739
- status === "loading" && count === 0 && !searchActive && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-3 py-3 text-xs text-muted-foreground", children: [
759
+ (status === "loading" || status === "idle") && count === 0 && !searchActive && !allLanesOff && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-3 py-3 text-xs text-muted-foreground", children: [
740
760
  /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin" }),
741
761
  "Loading conversations\u2026"
742
762
  ] }),
@@ -212,8 +212,8 @@ const ConversationSourceFilterTree = ({
212
212
  const facetsStatus = useAppSelector(selectSourceFacetsStatus);
213
213
  const facetsError = useAppSelector(selectSourceFacetsError);
214
214
  useEffect(() => {
215
- void dispatch(fetchSourceFacets(void 0));
216
- }, [dispatch]);
215
+ if (open) void dispatch(fetchSourceFacets(void 0));
216
+ }, [dispatch, open]);
217
217
  const includeLanes = scope.includeLanes;
218
218
  const visibleFacets = useMemo(
219
219
  () => includeLanes === null ? facets : facets.filter((f) => f.lane !== null && includeLanes.includes(f.lane)),
@@ -459,7 +459,7 @@ const ConversationSourceFilterTree = ({
459
459
  }) })
460
460
  ] }),
461
461
  /* @__PURE__ */ jsxs("div", { className: "max-h-[min(60dvh,420px)] overflow-y-auto py-1", children: [
462
- facetsStatus === "loading" && tree.length === 0 && /* @__PURE__ */ jsx("div", { className: "px-3 py-3 text-[11px] text-muted-foreground", children: "Loading sources\u2026" }),
462
+ (facetsStatus === "loading" || facetsStatus === "idle") && tree.length === 0 && /* @__PURE__ */ jsx("div", { className: "px-3 py-3 text-[11px] text-muted-foreground", children: "Loading sources\u2026" }),
463
463
  facetsStatus === "failed" && tree.length === 0 && /* @__PURE__ */ jsx(
464
464
  ReadFailure,
465
465
  {
@@ -32,7 +32,11 @@ import {
32
32
  surfaceLevelPlace
33
33
  } from "../../../redux/execution-system/context-rules/context-hierarchy.js";
34
34
  import { resolveClientSurface } from "../../../redux/execution-system/utils/build-tool-injection.js";
35
- import { resolveMandateKillSwitch } from "../../../redux/execution-system/context-rules/mandate-kill-switch.js";
35
+ import {
36
+ peekMandateKillSwitch,
37
+ resolveMandateKillSwitch
38
+ } from "../../../redux/execution-system/context-rules/mandate-kill-switch.js";
39
+ import { whenPageIdle } from "@ai-matrx/kit/idle-scheduler";
36
40
  import { useMachineFramesVisible } from "../../shared/transcript-audience.js";
37
41
  import { useChatCanvasTab } from "../../../../host/canvas.js";
38
42
  import { CONVERSATION_CONTEXT_KIND } from "../../../../host/canvas-tabs.js";
@@ -53,10 +57,11 @@ function valueGroupName(surfaceName) {
53
57
  return /^(page\s+)?context(\s+items?)?$/i.test(label) ? "" : label;
54
58
  }
55
59
  function useMandateKillSwitch(mandateKey) {
56
- const [state, setState] = useState({
57
- key: null,
58
- on: false
59
- });
60
+ const known = peekMandateKillSwitch(mandateKey);
61
+ const [state, setState] = useState(() => ({
62
+ key: known === void 0 ? null : mandateKey ?? null,
63
+ on: known === true
64
+ }));
60
65
  useEffect(() => {
61
66
  if (!mandateKey) return void 0;
62
67
  let live = true;
@@ -142,10 +147,16 @@ function ConversationContextChip({
142
147
  const saveMany = useSaveContextRules();
143
148
  const hierarchy = useConversationContextHierarchy(conversationId);
144
149
  useEffect(() => {
145
- void dispatch(ensureSurfaceFeatureLoaded(CONTEXT_RULES_FEATURE));
150
+ const idle = new AbortController();
151
+ void whenPageIdle(idle.signal).then((ready) => {
152
+ if (ready) void dispatch(ensureSurfaceFeatureLoaded(CONTEXT_RULES_FEATURE));
153
+ });
146
154
  const onFocus = () => void dispatch(reloadContextRules());
147
155
  window.addEventListener("focus", onFocus);
148
- return () => window.removeEventListener("focus", onFocus);
156
+ return () => {
157
+ idle.abort();
158
+ window.removeEventListener("focus", onFocus);
159
+ };
149
160
  }, [dispatch]);
150
161
  const mandateKey = useAppSelector(
151
162
  (state) => state.conversations.byConversationId[conversationId]?.mandateKey ?? null
@@ -91,6 +91,7 @@ import { useConversationAbilities } from "../../../redux/execution-system/host-c
91
91
  import { ComposerAttachmentSourceChips } from "../../../../host/composer-extensions.js";
92
92
  import { ConversationCompanionPills } from "./ConversationCompanionPills.js";
93
93
  import { RailPill } from "./rail-pill.js";
94
+ import { selectIsDraftConversation } from "../../../redux/execution-system/conversations/conversations.selectors.js";
94
95
  function entryHasValue(e) {
95
96
  const v = e.value;
96
97
  if (v === void 0 || v === null) return false;
@@ -160,14 +161,15 @@ function ConversationContextRailBody({
160
161
  const hasLists = useAppSelector(selectHasAgentListsContent(conversationId));
161
162
  const taskCounts = useAppSelector(selectAgentTaskCounts(conversationId));
162
163
  const todoCounts = useAppSelector(selectUserTodoCounts(conversationId));
164
+ const isDraft = useAppSelector(selectIsDraftConversation(conversationId));
163
165
  useEffect(() => {
164
- if (!conversationId) return void 0;
166
+ if (!conversationId || isDraft) return void 0;
165
167
  void dispatch(ensureAgentLists(conversationId));
166
168
  dispatch(subscribeAgentLists(conversationId));
167
169
  return () => {
168
170
  dispatch(unsubscribeAgentLists(conversationId));
169
171
  };
170
- }, [conversationId, dispatch]);
172
+ }, [conversationId, isDraft, dispatch]);
171
173
  const agentContextPolicies = useAppSelector(
172
174
  (state) => agentId ? selectAgentContextPolicies(state, agentId) : void 0
173
175
  );
@@ -9,6 +9,7 @@ import {
9
9
  import {
10
10
  computeTargetIconColor,
11
11
  computeTargetKindLabel,
12
+ useComputeBinding,
12
13
  useComputeTargetActions
13
14
  } from "./use-compute-target-actions.js";
14
15
  import { describeBoundTargetState } from "../../../../compute/targets.js";
@@ -22,6 +23,11 @@ function TargetIcon({
22
23
  function DesktopPresenceIndicator({
23
24
  conversationId
24
25
  }) {
26
+ const { boundView, sandboxBlocked } = useComputeBinding(conversationId);
27
+ if (sandboxBlocked || !boundView) return null;
28
+ return /* @__PURE__ */ jsx(BoundTargetButton, { conversationId });
29
+ }
30
+ function BoundTargetButton({ conversationId }) {
25
31
  const {
26
32
  loading,
27
33
  boundView,
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import { useContext, useState } from "react";
3
+ import { useContext, useEffect, useState } from "react";
4
4
  import { AppWindow, ChevronDown, Cpu, Gauge, Layers, Star, Webhook } from "lucide-react";
5
5
  import { Button } from "@ai-matrx/design-system/controls";
6
6
  import { MANDATE_KEYS } from "@ai-matrx/agents/mandates";
@@ -29,7 +29,9 @@ import { ComposerOutputPanel } from "./ComposerOutput.js";
29
29
  import { ComposerEffortRows, useComposerEffort } from "./ComposerEffortPill.js";
30
30
  import { composerShows } from "./composer-mode-visibility.js";
31
31
  import { composerPillClass } from "./composer-chip.js";
32
- import { useComposerAgent, useEffectiveModelId } from "./useComposerAgent.js";
32
+ import { useComposerAgent, useComposerPresets, useEffectiveModelId } from "./useComposerAgent.js";
33
+ import { getModelRecords } from "../../../../identity/model-catalog.js";
34
+ import { chatHostHasDatabase } from "../../../../../host/configure.js";
33
35
  import { useComposerAgentFilter } from "./useComposerAgentFilter.js";
34
36
  import { ComposerAgentPeek } from "./ComposerAgentPeek.js";
35
37
  const AGENT_PICKER_PANEL = "w-[680px] h-[min(528px,80dvh)] p-0";
@@ -37,14 +39,22 @@ const MODEL_PICKER_PANEL = "w-[min(1060px,calc(100vw-24px))] h-[min(640px,80dvh)
37
39
  const CUSTOM_AGENT_LABEL = "Custom Agent";
38
40
  function pillLabel(info, mode) {
39
41
  if (!composerShows(mode, "agent.presets")) return info.agentName ?? "Agent";
40
- const name = info.isCustom ? CUSTOM_AGENT_LABEL : info.preset?.name ?? info.agentName ?? "Agent";
42
+ const name = info.isCustom ? CUSTOM_AGENT_LABEL : info.agentName ?? "Agent";
41
43
  return info.effectiveModelLabel ? `${name} \xB7 ${info.effectiveModelLabel}` : name;
42
44
  }
43
45
  function ComposerAgentPill({ conversationId, mode, size, agentControl, menuSide, folded = false }) {
44
- const [open, setOpen] = useState(false);
46
+ const [open, setOpenState] = useState(false);
47
+ const [engaged, setEngaged] = useState(false);
48
+ const setOpen = (next) => {
49
+ setOpenState(next);
50
+ if (next) setEngaged(true);
51
+ };
52
+ useEffect(() => {
53
+ if (engaged && chatHostHasDatabase()) void getModelRecords().loadOptions();
54
+ }, [engaged]);
45
55
  const isMobile = useIsMobile();
46
56
  const effort = useComposerEffort(conversationId, useEffectiveModelId(conversationId));
47
- const info = useComposerAgent(conversationId);
57
+ const info = useComposerAgent(conversationId, { engaged });
48
58
  const label = pillLabel(info, mode);
49
59
  const onSelectAgent = agentControl?.onSelectAgent;
50
60
  const agentFilter = useComposerAgentFilter(conversationId);
@@ -78,7 +88,10 @@ function ComposerAgentPill({ conversationId, mode, size, agentControl, menuSide,
78
88
  activeAgentId: info.agentId,
79
89
  agentFilter,
80
90
  contentSide: menuSide,
81
- triggerSlot: pill
91
+ triggerSlot: pill,
92
+ onOpenChange: (next) => {
93
+ if (next) setEngaged(true);
94
+ }
82
95
  }
83
96
  );
84
97
  }
@@ -145,6 +158,7 @@ function ChatAgentMenu({
145
158
  const agentFilter = useComposerAgentFilter(conversationId);
146
159
  const onSelectAgent = agentControl?.onSelectAgent;
147
160
  const agentLabel = info.isCustom ? CUSTOM_AGENT_LABEL : info.agentName ?? "Agent";
161
+ const { presets } = useComposerPresets(info.agentId);
148
162
  const chooseCustom = () => {
149
163
  if (!info.customAgentId || !onSelectAgent) return;
150
164
  close();
@@ -183,7 +197,7 @@ function ChatAgentMenu({
183
197
  onSelectAgent ? /* @__PURE__ */ jsxs(Fragment, { children: [
184
198
  inSheet ? null : /* @__PURE__ */ jsx(ComposerMenuDivider, {}),
185
199
  /* @__PURE__ */ jsx(ComposerMenuLabel, { children: "Choose your chat agent" }),
186
- info.presets.map((preset) => /* @__PURE__ */ jsx(
200
+ presets.map((preset) => /* @__PURE__ */ jsx(
187
201
  ComposerMenuRow,
188
202
  {
189
203
  icon: Star,
@@ -8,7 +8,7 @@ import { useOpenRunControlsWindow } from "../../../../../host/window-openers.js"
8
8
  import { useOpenCloudBrowserCanvas } from "../../../../../host/ui-slots.js";
9
9
  import { ChatConnectionsStrip } from "../ChatConnectionsStrip.js";
10
10
  import { ComposerConnectPromo } from "./ComposerConnectPromo.js";
11
- import { useComputeTargetActions } from "../use-compute-target-actions.js";
11
+ import { useComputeBinding } from "../use-compute-target-actions.js";
12
12
  import { ComposerEnvironmentPanel } from "./ComposerPlusMenu.js";
13
13
  import { COMPOSER_ROW_CLASS } from "./composer-chip.js";
14
14
  import { ComposerChip } from "../ComposerChip.js";
@@ -26,10 +26,10 @@ function ComposerChipsRowBody({
26
26
  chipShape = "md"
27
27
  }) {
28
28
  const [envOpen, setEnvOpen] = useState(false);
29
- const compute = useComputeTargetActions(conversationId);
29
+ const compute = useComputeBinding(conversationId);
30
30
  const openRunControlsWindow = useOpenRunControlsWindow();
31
31
  const openCloudBrowser = useOpenCloudBrowserCanvas();
32
- const boundName = compute.boundView?.target?.name ?? null;
32
+ const boundName = compute.boundView?.name ?? null;
33
33
  const EnvIcon = boundName ? Server : Cloud;
34
34
  return /* @__PURE__ */ jsxs(
35
35
  "div",
@@ -32,6 +32,7 @@ import { selectBuilderAdvancedSettings } from "../../../../redux/execution-syste
32
32
  import { setBuilderAdvancedSettings } from "../../../../redux/execution-system/instance-ui-state/instance-ui-state.slice.js";
33
33
  import { DEFAULT_BUILDER_ADVANCED_SETTINGS } from "../../../../types/instance.types.js";
34
34
  import { useSkills } from "../../../../identity/skill-catalog.js";
35
+ import { whenPageIdle } from "@ai-matrx/kit/idle-scheduler";
35
36
  import { useClippedContentGuard } from "../../../../../host/ui-slots.js";
36
37
  import { ComposerMenuDivider, ComposerMenuHelp, ComposerMenuLabel, ComposerMenuRow } from "./ComposerMenu.js";
37
38
  import {
@@ -67,7 +68,7 @@ const TYPE_ICONS = {
67
68
  const kindLabel = (kind) => knownShapeLabel(kind) ?? (humanizeIdentifier(kind) || kind);
68
69
  const LOCKED_REASON = "This agent always answers in its own shape";
69
70
  const pickWarning = (lockedShapes) => lockedPickWarning(lockedShapes, kindLabel);
70
- function useLockState(conversationId) {
71
+ function useLockState(conversationId, engaged) {
71
72
  const agentId = useAppSelector(
72
73
  (state) => state.conversations.byConversationId[conversationId]?.agentId ?? null
73
74
  );
@@ -80,30 +81,36 @@ function useLockState(conversationId) {
80
81
  if (!cancelled) setRead({ agentId, result });
81
82
  };
82
83
  const fail = (error) => settle({ status: "failed", error, retry: () => setAttempt((n) => n + 1) });
83
- fetchAgentOutputSchemas([agentId]).then((byId) => {
84
- const verdict = classifyLockRead(byId, agentId);
85
- if (verdict.status === "failed") {
86
- fail(new Error("The agent's output schema could not be read."));
87
- return;
88
- }
89
- settle(verdict);
90
- }, fail);
84
+ const controller = new AbortController();
85
+ const ready = engaged ? Promise.resolve(true) : whenPageIdle(controller.signal);
86
+ void ready.then((idle) => {
87
+ if (!idle || cancelled) return;
88
+ fetchAgentOutputSchemas([agentId]).then((byId) => {
89
+ const verdict = classifyLockRead(byId, agentId);
90
+ if (verdict.status === "failed") {
91
+ fail(new Error("The agent's output schema could not be read."));
92
+ return;
93
+ }
94
+ settle(verdict);
95
+ }, fail);
96
+ });
91
97
  return () => {
92
98
  cancelled = true;
99
+ controller.abort();
93
100
  };
94
- }, [agentId, attempt]);
101
+ }, [agentId, attempt, engaged]);
95
102
  if (!agentId) return { status: "none" };
96
103
  return read && read.agentId === agentId ? read.result : { status: "reading" };
97
104
  }
98
- function useComposerOutput(conversationId) {
105
+ function useComposerOutput(conversationId, engaged) {
99
106
  const dispatch = useAppDispatch();
100
- const { skills } = useSkills();
101
107
  const settings = useAppSelector(selectBuilderAdvancedSettings(conversationId)) ?? DEFAULT_BUILDER_ADVANCED_SETTINGS;
102
108
  const types = readOutputTypes(settings.outputTypes);
103
109
  const outputKinds = settings.outputKinds ?? [];
104
110
  const addedSkills = settings.addedSkills ?? [];
111
+ const { skills } = useSkills({ skipAutoFetch: !engaged && addedSkills.length === 0 });
105
112
  const kinds = selectedOutputKinds(outputKinds, addedSkills, skills);
106
- const lock = useLockState(conversationId);
113
+ const lock = useLockState(conversationId, engaged);
107
114
  const migrator = useRef(null);
108
115
  useEffect(() => {
109
116
  migrator.current ??= createKindSkillMigrator();
@@ -314,7 +321,7 @@ function ShapePicker({
314
321
  ] });
315
322
  }
316
323
  function ComposerOutputPanel({ conversationId }) {
317
- const output = useComposerOutput(conversationId);
324
+ const output = useComposerOutput(conversationId, true);
318
325
  return /* @__PURE__ */ jsxs("div", { className: "@container flex min-h-0 flex-1 flex-col", children: [
319
326
  /* @__PURE__ */ jsx(ComposerMenuLabel, { children: "Output types" }),
320
327
  /* @__PURE__ */ jsx(TypeGrid, { types: output.types, onToggle: output.toggleType }),
@@ -334,7 +341,7 @@ function ComposerOutputPill({
334
341
  }) {
335
342
  const [open, setOpen] = useState(false);
336
343
  const [roomierSide, setRoomierSide] = useState(null);
337
- const output = useComposerOutput(conversationId);
344
+ const output = useComposerOutput(conversationId, false);
338
345
  return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, modal: false, children: [
339
346
  /* @__PURE__ */ jsxs(SplitButton, { className: "min-w-0", children: [
340
347
  /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(