@ai-matrx/chat 0.4.7 → 0.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @ai-matrx/chat
2
2
 
3
+ ## 0.4.9 — the shell chat remembers each page's own chat, and its title menu lists your conversations
4
+
5
+ - `shellChatHome`: outside boards and Education, the conversation is remembered PER PAGE
6
+ (`shellChatPageSurfaceKey(pathname)`), not one conversation following the person site-wide. A page
7
+ never chatted on starts fresh; open / closed stays remembered per page family.
8
+ - `ChatPanelTitleMenu`: a Popover with New chat · Rename · Open in full chat over the person's
9
+ conversations (`ConversationHistorySidebar`, searchable, opened in place). New required prop
10
+ `onOpenConversation(conversationId, agentId)`; the shell dock passes `openExisting`.
11
+
12
+ Consumer action: none (the dock is inside the package).
13
+
14
+ ## 0.4.8
15
+
16
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
17
+ `git diff npm/chat/v0.4.7..npm/chat/v0.4.8 -- apps/shared/chat`).
18
+ No source changes intended and no consumer action required.
19
+
3
20
  ## 0.4.7
4
21
 
5
22
  - The host-source column test gets an explicit 120s budget: the column now renders the real text engine
@@ -5,7 +5,11 @@ import {
5
5
  classifyUsageRefusal
6
6
  } from "../../../../host/usage-gate.js";
7
7
  import { parseNdjsonStream } from "../../../../host/server/stream-parser.js";
8
- import { isStreamTransportLost, StreamTransportError } from "@ai-matrx/agents/matrx";
8
+ import {
9
+ isStreamTransportLost,
10
+ readStreamCursor,
11
+ StreamTransportError
12
+ } from "@ai-matrx/agents/matrx";
9
13
  import { monitorStream } from "@ai-matrx/data/net";
10
14
  import { mintClientTempId } from "@ai-matrx/kit/ids";
11
15
  import { withPerformedScript } from "../../../speech-script/types.js";
@@ -139,15 +143,7 @@ import { getCapabilitiesForConversation } from "../../../runtime/get-model-capab
139
143
  import { toast } from "../../../../host/notify.js";
140
144
  import { isDirectiveApplyEvent } from "@ai-matrx/agents/envelope";
141
145
  import { proposeDirective } from "../../proposed-directives/proposedDirectivesSlice.js";
142
- function readTransportCursor(event) {
143
- if (typeof event !== "object" || event === null || !("stream_seq" in event) || typeof event.stream_seq !== "number" || !Number.isSafeInteger(event.stream_seq) || event.stream_seq <= 0) {
144
- return null;
145
- }
146
- return {
147
- streamId: "stream_id" in event && typeof event.stream_id === "string" && event.stream_id.length > 0 ? event.stream_id : null,
148
- streamSeq: event.stream_seq
149
- };
150
- }
146
+ const readTransportCursor = readStreamCursor;
151
147
  function renderBlockToContentType(type) {
152
148
  if (type === "image" || type === "image_output") return "image";
153
149
  if (type === "audio_output") return "audio";
@@ -349,6 +345,7 @@ async function processStream({
349
345
  let pendingTransportCursor = null;
350
346
  const batchedDispatch = (action) => dispatch(autoBatched(action));
351
347
  let reasoningLeakReported = false;
348
+ let serverStructuredData = null;
352
349
  const runJsonExtraction = (isFinal) => {
353
350
  if (!jsonTracker) return;
354
351
  const req = getState().activeRequests.byRequestId[requestId];
@@ -367,10 +364,22 @@ async function processStream({
367
364
  const jsonState = isFinal ? jsonTracker.finalize() : jsonTracker.getState();
368
365
  if (isFinal || jsonState.revision !== lastJsonRevision) {
369
366
  lastJsonRevision = jsonState.revision;
367
+ const clientResults = jsonState.results.map(toSnapshot);
368
+ const results = serverStructuredData ? [
369
+ toSnapshot({
370
+ value: serverStructuredData,
371
+ type: Array.isArray(serverStructuredData) ? "array" : "object",
372
+ source: "whole-string",
373
+ isComplete: true,
374
+ repairApplied: false,
375
+ warnings: ["Server structured_output payload"]
376
+ }),
377
+ ...clientResults
378
+ ] : clientResults;
370
379
  batchedDispatch(
371
380
  updateExtractedJson({
372
381
  requestId,
373
- results: jsonState.results.map(toSnapshot),
382
+ results,
374
383
  revision: jsonState.revision,
375
384
  isComplete: isFinal
376
385
  })
@@ -1920,6 +1929,10 @@ async function processStream({
1920
1929
  );
1921
1930
  } else if (isStructuredOutputEvent(event)) {
1922
1931
  otherEvents++;
1932
+ if (event.data.success && event.data.data != null && typeof event.data.data === "object") {
1933
+ serverStructuredData = event.data.data;
1934
+ runJsonExtraction(false);
1935
+ }
1923
1936
  dispatch(
1924
1937
  appendTimeline({
1925
1938
  requestId,
@@ -276,6 +276,8 @@ export interface HeadlessAgentJsonResult {
276
276
  * result passes through untouched.
277
277
  */
278
278
  export declare function failWarnedOutputMissingKeys(getState: () => ChatRootState, opts: Pick<HeadlessAgentJsonOptions, "mandateKey" | "surfaceKey" | "expect">, result: HeadlessAgentJsonResult): Promise<HeadlessAgentJsonResult>;
279
+ /** The first JSON object in `text` that holds EVERY required key, or null. */
280
+ export declare function recoverByRequiredKeys(text: string | undefined, required: readonly string[]): Record<string, unknown> | null;
279
281
  /**
280
282
  * The plain sentence of a warned-holder failure (`failWarnedOutputMissingKeys`),
281
283
  * or `null` for any other result. A lane that swallows failures to `null`
@@ -1,4 +1,4 @@
1
- import { extractFirstJson } from "@ai-matrx/kit/json-extract";
1
+ import { extractAllJson, extractFirstJson } from "@ai-matrx/kit/json-extract";
2
2
  import { extractErrorMessage } from "@ai-matrx/data/net";
3
3
  import { destroyInstanceIfAllowed } from "../conversations/conversations.thunks.js";
4
4
  import { captureError } from "../../../../host/diagnostics.js";
@@ -129,13 +129,18 @@ async function failWarnedOutputMissingKeys(getState, opts, result) {
129
129
  if (required.length === 0) return result;
130
130
  let missing;
131
131
  if (result.success && isRecord) {
132
- const record = data;
133
- const answers = typeof record.answers === "object" && record.answers !== null && !Array.isArray(record.answers) ? record.answers : null;
134
- missing = required.filter((key) => !(key in record) && !(answers && key in answers));
132
+ missing = missingRequiredKeys(data, required);
135
133
  } else {
136
134
  missing = [...required];
137
135
  }
138
136
  if (missing.length === 0) return result;
137
+ const recovered = recoverByRequiredKeys(result.fullResponse, required);
138
+ if (recovered) {
139
+ console.warn(
140
+ `[runHeadlessAgentJson] ${opts.surfaceKey}: the first JSON read lacked ${missing.join(", ")}; a candidate in the answer holding every required key was used instead.`
141
+ );
142
+ return { ...result, success: true, data: recovered, noResult: false, error: void 0, errorDetail: void 0 };
143
+ }
139
144
  const warning = resolved.outputWarnings[0];
140
145
  const holderId = warning.holderId ?? resolved.agentId;
141
146
  let holderName;
@@ -174,6 +179,21 @@ async function failWarnedOutputMissingKeys(getState, opts, result) {
174
179
  errorDetail: `mandate_output_unusable: missing ${keys}`
175
180
  };
176
181
  }
182
+ function missingRequiredKeys(record, required) {
183
+ const answers = typeof record.answers === "object" && record.answers !== null && !Array.isArray(record.answers) ? record.answers : null;
184
+ return required.filter((key) => !(key in record) && !(answers && key in answers));
185
+ }
186
+ function recoverByRequiredKeys(text, required) {
187
+ if (!text || required.length === 0) return null;
188
+ for (const candidate of extractAllJson(text, { allowFuzzy: true })) {
189
+ const value = candidate.value;
190
+ if (typeof value !== "object" || value === null || Array.isArray(value)) continue;
191
+ if (missingRequiredKeys(value, required).length === 0) {
192
+ return value;
193
+ }
194
+ }
195
+ return null;
196
+ }
177
197
  function mandateOutputUnusableSentence(result) {
178
198
  if (result.success) return null;
179
199
  if (!result.errorDetail?.startsWith("mandate_output_unusable")) return null;
@@ -565,5 +585,6 @@ export {
565
585
  failWarnedOutputMissingKeys,
566
586
  livePosture,
567
587
  mandateOutputUnusableSentence,
588
+ recoverByRequiredKeys,
568
589
  runHeadlessAgentJson
569
590
  };
@@ -1,7 +1,11 @@
1
1
  import { retryReconnect } from "./retry-reconnect.js";
2
2
  import { discardRetainedTransportConsumer } from "../redux/execution-system/thunks/process-stream.js";
3
3
  import { createAsyncThunk } from "@reduxjs/toolkit";
4
- import { runtimeOperationRejoinPath, settleRunPickup } from "@ai-matrx/agents/matrx";
4
+ import {
5
+ pickServerJobOperation,
6
+ runtimeOperationRejoinPath,
7
+ settleRunPickup
8
+ } from "@ai-matrx/agents/matrx";
5
9
  import { toast } from "../../host/notify.js";
6
10
  import { captureError } from "../../host/diagnostics.js";
7
11
  import { setRequestStatus } from "../redux/execution-system/active-requests/active-requests.slice.js";
@@ -77,8 +81,9 @@ const reconnectServerOperation = createAsyncThunk(
77
81
  clearFollower();
78
82
  return noFollow;
79
83
  }
80
- op = byLink?.operations.find((o) => !o.is_terminal) ?? null;
81
- settledWhileDisconnected = !op && Boolean(byLink?.operations.length);
84
+ const picked = pickServerJobOperation(byLink?.operations);
85
+ op = picked && !picked.is_terminal ? picked : null;
86
+ settledWhileDisconnected = Boolean(picked?.is_terminal);
82
87
  } catch (err) {
83
88
  const superseded = ctrl.signal.aborted || !stillOwner();
84
89
  clearFollower();
@@ -263,9 +268,8 @@ const reconnectServerOperation = createAsyncThunk(
263
268
  clearFollower();
264
269
  return noFollow;
265
270
  }
266
- const stillLive = refreshed?.operations.find(
267
- (operation) => !operation.is_terminal
268
- );
271
+ const latest = pickServerJobOperation(refreshed?.operations);
272
+ const stillLive = latest && !latest.is_terminal ? latest : null;
269
273
  if (stillLive) {
270
274
  if (followers.get(conversationId) === ctrl) {
271
275
  followers.delete(conversationId);
@@ -1,6 +1,8 @@
1
1
  /** The panel's chat name: its real title, else "New chat". */
2
2
  export declare function useChatPanelTitle(conversationId: string | null): string;
3
- export declare function ChatPanelTitleMenu({ conversationId, onNewChat, }: {
3
+ export declare function ChatPanelTitleMenu({ conversationId, onNewChat, onOpenConversation, }: {
4
4
  conversationId: string | null;
5
5
  onNewChat: () => void;
6
+ /** Open a listed conversation IN this panel. */
7
+ onOpenConversation: (conversationId: string, agentId?: string | null) => void;
6
8
  }): import("react").JSX.Element;
@@ -1,20 +1,18 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useState } from "react";
3
4
  import { ChevronDown, ExternalLink, PencilLine, Plus } from "lucide-react";
4
5
  import { AppLink } from "../../host/ui-slots.js";
5
- import {
6
- DropdownMenu,
7
- DropdownMenuContent,
8
- DropdownMenuItem,
9
- DropdownMenuSeparator,
10
- DropdownMenuTrigger
11
- } from "@ai-matrx/design-system";
6
+ import { Popover, PopoverContent, PopoverTrigger } from "@ai-matrx/design-system";
12
7
  import { useAppSelector } from "../../store/hooks.js";
13
8
  import { selectConversationTitle } from "../../agents/redux/execution-system/conversations/conversations.selectors.js";
14
9
  import { selectConversationListItemById } from "../../agents/redux/conversation-list/conversation-list.selectors.js";
15
10
  import { conversationRenameOpener } from "../../agents/components/conversation-actions/rename/conversationRenameOpener.js";
16
11
  import { conversationTitleText } from "../../utils/content-ir/surfaces/kind-text-label.js";
17
- import { Button } from "@ai-matrx/design-system/controls";
12
+ import { Button, Tile } from "@ai-matrx/design-system/controls";
13
+ import { ConversationHistorySidebar } from "../../agents/components/conversation-history/ConversationHistorySidebar.js";
14
+ const PANEL_HISTORY_SCOPE = "chat-panel-menu";
15
+ const ALL_AGENTS = [];
18
16
  function useChatRealTitle(conversationId) {
19
17
  const conversationTitle = useAppSelector(
20
18
  (state) => conversationId ? selectConversationTitle(conversationId)(state) : null
@@ -29,35 +27,69 @@ function useChatPanelTitle(conversationId) {
29
27
  }
30
28
  function ChatPanelTitleMenu({
31
29
  conversationId,
32
- onNewChat
30
+ onNewChat,
31
+ onOpenConversation
33
32
  }) {
33
+ const [open, setOpen] = useState(false);
34
34
  const realTitle = useChatRealTitle(conversationId);
35
35
  const title = realTitle ?? "New chat";
36
- return /* @__PURE__ */ jsxs(DropdownMenu, { children: [
37
- /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "quiet", iconEnd: /* @__PURE__ */ jsx(ChevronDown, {}), className: "min-w-0", title, children: title }) }),
38
- /* @__PURE__ */ jsxs(DropdownMenuContent, { align: "start", className: "w-56", children: [
39
- /* @__PURE__ */ jsxs(DropdownMenuItem, { onSelect: onNewChat, children: [
40
- /* @__PURE__ */ jsx(Plus, { className: "mr-2 h-4 w-4" }),
41
- "New chat"
42
- ] }),
43
- conversationId ? /* @__PURE__ */ jsxs(Fragment, { children: [
44
- /* @__PURE__ */ jsxs(
45
- DropdownMenuItem,
46
- {
47
- onSelect: () => void conversationRenameOpener.open({ conversationId, title: realTitle }),
48
- children: [
49
- /* @__PURE__ */ jsx(PencilLine, { className: "mr-2 h-4 w-4" }),
50
- "Rename"
51
- ]
52
- }
53
- ),
54
- /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
55
- /* @__PURE__ */ jsx(DropdownMenuItem, { asChild: true, children: /* @__PURE__ */ jsxs(AppLink, { href: `/chat/${conversationId}`, children: [
56
- /* @__PURE__ */ jsx(ExternalLink, { className: "mr-2 h-4 w-4" }),
57
- "Open in full chat"
58
- ] }) })
59
- ] }) : null
60
- ] })
36
+ const close = () => setOpen(false);
37
+ return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, children: [
38
+ /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "quiet", iconEnd: /* @__PURE__ */ jsx(ChevronDown, {}), className: "min-w-0", title, children: title }) }),
39
+ /* @__PURE__ */ jsxs(
40
+ PopoverContent,
41
+ {
42
+ align: "start",
43
+ sideOffset: 6,
44
+ className: "flex h-[min(70dvh,560px)] w-80 flex-col overflow-hidden p-1",
45
+ children: [
46
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 flex-col", children: [
47
+ /* @__PURE__ */ jsx(
48
+ Tile,
49
+ {
50
+ variant: "quiet",
51
+ icon: /* @__PURE__ */ jsx(Plus, {}),
52
+ title: "New chat",
53
+ onClick: () => {
54
+ close();
55
+ onNewChat();
56
+ }
57
+ }
58
+ ),
59
+ conversationId ? /* @__PURE__ */ jsxs(Fragment, { children: [
60
+ /* @__PURE__ */ jsx(
61
+ Tile,
62
+ {
63
+ variant: "quiet",
64
+ icon: /* @__PURE__ */ jsx(PencilLine, {}),
65
+ title: "Rename",
66
+ onClick: () => {
67
+ close();
68
+ void conversationRenameOpener.open({ conversationId, title: realTitle });
69
+ }
70
+ }
71
+ ),
72
+ /* @__PURE__ */ jsx(Tile, { variant: "quiet", icon: /* @__PURE__ */ jsx(ExternalLink, {}), title: "Open in full chat", asChild: true, children: /* @__PURE__ */ jsx(AppLink, { href: `/chat/${conversationId}`, onClick: close }) })
73
+ ] }) : null
74
+ ] }),
75
+ /* @__PURE__ */ jsx("div", { className: "mx-1 my-1 h-px shrink-0 bg-border", role: "separator" }),
76
+ /* @__PURE__ */ jsx("div", { className: "flex min-h-0 flex-1 flex-col", children: /* @__PURE__ */ jsx(
77
+ ConversationHistorySidebar,
78
+ {
79
+ variant: "dense",
80
+ scopeId: PANEL_HISTORY_SCOPE,
81
+ agentIds: ALL_AGENTS,
82
+ activeConversationId: conversationId,
83
+ openInPlace: true,
84
+ onOpenConversation: (conv) => {
85
+ close();
86
+ onOpenConversation(conv.conversationId, conv.agentId ?? null);
87
+ }
88
+ }
89
+ ) })
90
+ ]
91
+ }
92
+ )
61
93
  ] });
62
94
  }
63
95
  export {
@@ -260,7 +260,7 @@ function ShellChatDock({ initialChat, initialWidth, initialMode = null, signedIn
260
260
  className: "bg-card",
261
261
  children: [
262
262
  /* @__PURE__ */ jsxs("div", { className: "shell-chat-dock-header flex h-11 shrink-0 items-center gap-1 pr-2", children: [
263
- /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx(ChatPanelTitleMenu, { conversationId, onNewChat: chat.startNew }) }),
263
+ /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx(ChatPanelTitleMenu, { conversationId, onNewChat: chat.startNew, onOpenConversation: chat.openExisting }) }),
264
264
  /* @__PURE__ */ jsx("div", { className: "shrink-0", children: /* @__PURE__ */ jsx(ComposerModeSwitch, { size: "panel", initialMode }) }),
265
265
  /* @__PURE__ */ jsx(
266
266
  Button,
@@ -33,8 +33,14 @@ export declare function shellChatWorkspaceId(family: string): string;
33
33
  * chat, on every page).
34
34
  */
35
35
  export declare function shellChatHostedElsewhere(pathname: string): boolean;
36
- /** The shell chat's conversation everywhere no page has its own. */
36
+ /**
37
+ * The shell chat's conversation is remembered PER PAGE (Arman, 2026-10-07: one
38
+ * conversation following the person across the whole site "doesn't make sense
39
+ * … it's confusing"). Each page — one agent's builder, one note — comes back to
40
+ * ITS last chat; a page the person never chatted on starts fresh.
41
+ */
37
42
  export declare const SHELL_CHAT_SURFACE_KEY = "canvas-workspace:shell";
43
+ export declare function shellChatPageSurfaceKey(pathname: string): string;
38
44
  /**
39
45
  * Where the shell's chat lives on a page:
40
46
  * - `layoutId` — its remembered open / closed + docked / floating
@@ -61,8 +67,8 @@ export declare const SHELL_CHAT_CLOSED_BY_DEFAULT_PAGES: readonly RegExp[];
61
67
  * has its conversation, signed-in Education has one. The ids, params and
62
68
  * defaults are exactly the ones those pages used when they drew their own chat
63
69
  * column (ChatCanvasWorkspace, until 2026-10-05), so a person's remembered
64
- * layout and conversation carry over untouched. Every other page shares one
65
- * conversation that follows the person, remembered open / closed per family.
70
+ * layout and conversation carry over untouched. Every other page remembers its
71
+ * OWN conversation (shellChatPageSurfaceKey); open / closed is remembered per family.
66
72
  */
67
73
  export declare function shellChatHome(pathname: string, signedIn: boolean): ShellChatHome;
68
74
  /**
@@ -20,6 +20,10 @@ function shellChatHostedElsewhere(pathname) {
20
20
  return OWN_CHAT_PATHS.some((p) => p.test(pathname));
21
21
  }
22
22
  const SHELL_CHAT_SURFACE_KEY = "canvas-workspace:shell";
23
+ function shellChatPageSurfaceKey(pathname) {
24
+ const page = pathname.split("?")[0].replace(/\/+$/, "") || "/";
25
+ return `${SHELL_CHAT_SURFACE_KEY}:${page}`;
26
+ }
23
27
  const BOARD_PAGE = /^\/board\/(?!all(?:\/|$))([^/]+)\/?$/;
24
28
  const EDUCATION_PAGES = /^\/education(?:\/|$)/;
25
29
  const SHELL_CHAT_CLOSED_BY_DEFAULT_PAGES = [/^\/spaces(?:\/|$)/];
@@ -34,7 +38,7 @@ function shellChatHome(pathname, signedIn) {
34
38
  }
35
39
  return {
36
40
  layoutId: shellChatWorkspaceId(shellChatFamily(pathname)),
37
- surfaceKey: SHELL_CHAT_SURFACE_KEY,
41
+ surfaceKey: shellChatPageSurfaceKey(pathname),
38
42
  // `pageChat`, not `chat`: /code keeps its own `?chat=`.
39
43
  addressParam: "pageChat",
40
44
  defaultOpen: SHELL_CHAT_CLOSED_BY_DEFAULT_PAGES.some((p) => p.test(pathname)) ? false : null
@@ -52,5 +56,6 @@ export {
52
56
  shellChatFamily,
53
57
  shellChatHome,
54
58
  shellChatHostedElsewhere,
59
+ shellChatPageSurfaceKey,
55
60
  shellChatWorkspaceId
56
61
  };
@@ -23,14 +23,22 @@ export declare const PREFERENCE_FIELDS: Readonly<Record<string, keyof ChatPrefer
23
23
  export declare function createUnhostedKnobs(): ChatKnobsPort;
24
24
  export declare const CHAT_PREFS_PREFIX = "ai-matrx-chat:";
25
25
  export declare function createWebPrefs(): ChatPrefsPort;
26
+ /** A host-kept prefs port, plus the late arrival of the host's saved values. */
27
+ export interface HostKeptPrefs extends ChatPrefsPort {
28
+ /**
29
+ * The host's saved values arrived after the port was made (read over a socket or IPC). A key
30
+ * already written this session keeps the newer value; every other key is set and announced.
31
+ */
32
+ load(values: Readonly<Record<string, string>>): void;
33
+ }
26
34
  /**
27
35
  * A prefs port the HOST keeps (HOST-OWNED-SOURCE-PLAN §2.2 "Drafts"): a page whose origin
28
36
  * changes between launches (a local host on a new port, a desktop window) cannot keep drafts in
29
- * web storage. The host reads its saved values once (`initial`) and receives every change
30
- * (`persist`, `null` = removed) to write where it keeps them — a file, an IPC store. Reads are
31
- * synchronous from memory, so the first render already sees the saved draft.
37
+ * web storage. The host hands its saved values in (`initial`, or `load` when they arrive later)
38
+ * and receives every change (`persist`, `null` = removed) to write where it keeps them — a file,
39
+ * an IPC store. Reads are synchronous from memory.
32
40
  */
33
41
  export declare function createHostKeptPrefs(options: {
34
- initial: Readonly<Record<string, string>>;
42
+ initial?: Readonly<Record<string, string>>;
35
43
  persist: (key: string, value: string | null) => void;
36
- }): ChatPrefsPort;
44
+ }): HostKeptPrefs;
@@ -169,12 +169,14 @@ function createWebPrefs() {
169
169
  return port;
170
170
  }
171
171
  function createHostKeptPrefs(options) {
172
- const memory = new Map(Object.entries(options.initial));
172
+ const memory = new Map(Object.entries(options.initial ?? {}));
173
+ const written = /* @__PURE__ */ new Set();
173
174
  const listeners = /* @__PURE__ */ new Set();
174
175
  const emit = (key) => {
175
176
  for (const listener of listeners) listener(key);
176
177
  };
177
178
  const write = (key, value) => {
179
+ written.add(key);
178
180
  try {
179
181
  options.persist(key, value);
180
182
  } catch (error) {
@@ -206,7 +208,14 @@ function createHostKeptPrefs(options) {
206
208
  },
207
209
  knob: (key, fallback) => parseKnob(memory.get(key) ?? null, fallback),
208
210
  knobs: createUnhostedKnobs(),
209
- snapshot: () => Object.fromEntries(memory)
211
+ snapshot: () => Object.fromEntries(memory),
212
+ load(values) {
213
+ for (const [key, value] of Object.entries(values)) {
214
+ if (written.has(key) || memory.get(key) === value) continue;
215
+ memory.set(key, value);
216
+ emit(key);
217
+ }
218
+ }
210
219
  };
211
220
  return port;
212
221
  }
@@ -10,7 +10,7 @@ export { createDbIdentity, SIGNED_OUT_IDENTITY } from "./defaults/identity.js";
10
10
  export { createNoPickerOrg, createStoredOrg, CHAT_ACTIVE_ORG_STORAGE_KEY } from "./defaults/org.js";
11
11
  export { createDefaultServer, DEFAULT_CHAT_SERVER_URL, } from "./defaults/server.js";
12
12
  export { createDomNotifier } from "./defaults/notify.js";
13
- export { createWebPrefs, createHostKeptPrefs, CHAT_PREFS_PREFIX } from "./defaults/prefs.js";
13
+ export { createWebPrefs, createHostKeptPrefs, CHAT_PREFS_PREFIX, type HostKeptPrefs } from "./defaults/prefs.js";
14
14
  export { createWindowNavigation } from "./defaults/navigation.js";
15
15
  export { createUnhostedWindows } from "./defaults/windows.js";
16
16
  export { CHAT_WINDOWS, CHAT_WINDOW_IDS, DEFAULT_WINDOW_INSTANCE_ID, isChatWindowId, type ChatWindowId, } from "./windows.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-matrx/chat",
3
- "version": "0.4.7",
3
+ "version": "0.4.9",
4
4
  "description": "THE one AI Matrx person-to-agent chat system: the full chat (running agents, conversations, tool calls, streaming, context, surfaces, voice) as per-module subpaths @ai-matrx/chat/<domain>/<module> over a host contract (/host, /host/react) whose only required value is a db; plus the kernel compatibility entries (., /external-store, /matrx, /react, /tools, styles.css, tokens.css) until P28.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -226,26 +226,26 @@
226
226
  "server-only": "^0.0.1",
227
227
  "uuid": "^14.0.2",
228
228
  "zod": "^4.6.5",
229
- "@ai-matrx/alchemy": "latest",
229
+ "@ai-matrx/agents": "latest",
230
230
  "@ai-matrx/associations": "latest",
231
231
  "@ai-matrx/browser-audio": "latest",
232
+ "@ai-matrx/alchemy": "latest",
232
233
  "@ai-matrx/canvas": "latest",
233
- "@ai-matrx/agents": "latest",
234
234
  "@ai-matrx/content-ir": "latest",
235
+ "@ai-matrx/diff": "latest",
235
236
  "@ai-matrx/content-ir-react": "latest",
236
- "@ai-matrx/data": "latest",
237
237
  "@ai-matrx/design-system": "latest",
238
- "@ai-matrx/diff": "latest",
238
+ "@ai-matrx/data": "latest",
239
239
  "@ai-matrx/icons": "latest",
240
- "@ai-matrx/kit": "latest",
241
240
  "@ai-matrx/media": "latest",
242
241
  "@ai-matrx/print": "latest",
242
+ "@ai-matrx/kit": "latest",
243
243
  "@ai-matrx/realtime": "latest",
244
244
  "@ai-matrx/records": "latest",
245
- "@ai-matrx/records-ui": "latest",
246
245
  "@ai-matrx/rich-content": "latest",
247
246
  "@ai-matrx/rich-editor": "latest",
248
- "@ai-matrx/tap-target": "latest"
247
+ "@ai-matrx/tap-target": "latest",
248
+ "@ai-matrx/records-ui": "latest"
249
249
  },
250
250
  "devDependencies": {
251
251
  "@arethetypeswrong/cli": "^0.18.5",