@ai-matrx/chat 0.7.4 → 0.7.6

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,24 @@
1
1
  # @ai-matrx/chat
2
2
 
3
+ ## 0.7.6
4
+ ### Unreleased
5
+
6
+ - The signed-out page assistant pill (`ScrollAssistantLauncher` → `GuestAmbientAssistant`) now sits on a
7
+ card surface reading "Ask AI Matrx about this page"; it was a bare quiet button that read as loose text
8
+ floating over the page beside an X (live /applets audit, 2026-10-09).
9
+ - Configured tool rows: the `Badge` and `Table` field components now render through
10
+ `@ai-matrx/design-system`'s `Badge` (neutral) and `Table` primitives instead of local copies.
11
+ - The default file chip's size line uses `@ai-matrx/kit/format`'s `formatFileSize` (gigabytes read
12
+ as GB, not thousands of MB); the identity refs use `@ai-matrx/kit/uuid`'s `isUuidShape`.
13
+
14
+ **Consumer action:** none.
15
+
16
+ ## 0.7.5
17
+
18
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
19
+ `git diff npm/chat/v0.7.4..npm/chat/v0.7.5 -- apps/shared/chat`).
20
+ No source changes intended and no consumer action required.
21
+
3
22
  ## 0.7.4
4
23
  ### Unreleased
5
24
 
@@ -36,13 +36,20 @@ function GuestAmbientAssistant({
36
36
  });
37
37
  return /* @__PURE__ */ jsxs("div", { className: "ambient-assistant-dock fixed left-1/2 z-[35] w-[min(380px,calc(100vw-2rem))] -translate-x-1/2 animate-in fade-in slide-in-from-bottom-2 duration-200", children: [
38
38
  /* @__PURE__ */ jsx(
39
- Button,
39
+ "div",
40
40
  {
41
- type: "button",
42
- variant: "quiet",
43
- className: "pointer-events-auto w-full justify-start",
44
- onClick: requestSignIn,
45
- children: "Ask AI Matrx"
41
+ "data-testid": "ambient-assistant-guest",
42
+ className: "pointer-events-auto rounded-xl border border-border bg-card shadow-sm",
43
+ children: /* @__PURE__ */ jsx(
44
+ Button,
45
+ {
46
+ type: "button",
47
+ variant: "quiet",
48
+ className: "w-full justify-start",
49
+ onClick: requestSignIn,
50
+ children: "Ask AI Matrx about this page"
51
+ }
52
+ )
46
53
  }
47
54
  ),
48
55
  /* @__PURE__ */ jsx(
@@ -3,14 +3,14 @@ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useEffect } from "react";
4
4
  import { withModelClass, selectModelById, selectModelClassName, selectModelIdentityById, selectModelIdentityLookupStatus } from "@ai-matrx/agents/models";
5
5
  import { humanizeIdentifier } from "@ai-matrx/kit/text-case";
6
+ import { isUuidShape } from "@ai-matrx/kit/uuid";
6
7
  import { getModelRecords, useModelRecords } from "../../identity/model-catalog.js";
7
8
  import { loadToolById, selectToolById, selectToolLookupStatus, useToolCatalog } from "../../identity/tool-catalog.js";
8
9
  import { EntityRef } from "../../../host/ui-slots.js";
9
10
  import { selectIsSuperAdmin } from "../../../host/identity.js";
10
11
  import { useAppSelector } from "../../../store/hooks.js";
11
12
  const cn = (...parts) => parts.filter(Boolean).join(" ");
12
- const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
13
- const isUuidValue = (value) => typeof value === "string" && UUID.test(value.trim());
13
+ const isUuidValue = (value) => typeof value === "string" && isUuidShape(value.trim());
14
14
  function aiModelHref(modelId) {
15
15
  return `/administration/ai/ai-models?model=${encodeURIComponent(modelId)}`;
16
16
  }
@@ -7,7 +7,11 @@
7
7
  * 1. Snapshot the current value from the canonical entity store.
8
8
  * 2. Patch every slice that mirrors the row (list, history scopes, the
9
9
  * active `conversations`/`messages` instance if it's loaded).
10
- * 3. Hit Supabase directly — RLS scopes the update to the owner.
10
+ * 3. Hit Supabase directly — RLS scopes the update to the owner. On THE ADMIN
11
+ * SEAT (an /administration page, host grants admin) the write goes to the
12
+ * server door instead (`PATCH /cx/conversations/{id}`), which admits the
13
+ * verified admin through the admin lane: `chat.conversation` has only
14
+ * `platform_admin_read`, by design, so a direct write there is refused.
11
15
  * 4. On failure, revert to the snapshot in the same slices.
12
16
  *
13
17
  * No new reducers are needed: the existing generic `patchConversation` and
@@ -20,6 +20,17 @@ import {
20
20
  } from "../execution-system/conversations/conversations.slice.js";
21
21
  import { selectConversationIsEphemeral } from "../execution-system/conversations/conversations.selectors.js";
22
22
  import { forkConversationServer } from "../execution-system/message-crud/server/fork-conversation-server.thunk.js";
23
+ import { adminSeatOpen } from "../../../host/admin-seat.js";
24
+ import {
25
+ callConversationSandboxBind,
26
+ callConversationSandboxUnbind,
27
+ callConversationUpdate
28
+ } from "../../../host/server/call-api.js";
29
+ async function writeSettingsThroughServer(dispatch, conversationId, body, action) {
30
+ const result = await dispatch(callConversationUpdate({ conversationId, body }));
31
+ if (result.error) return refusalWords(result.error, action);
32
+ return null;
33
+ }
23
34
  function refusalWords(err, action) {
24
35
  const w = describeWriteFailure(err, { action, remedy: "Try again." });
25
36
  return `${w.title} ${w.description}`;
@@ -61,10 +72,12 @@ const renameConversation = createAsyncThunk(
61
72
  title: nextTitle
62
73
  })
63
74
  );
64
- const { error } = await tryWriteOne(
75
+ const error = adminSeatOpen(getState()) ? await writeSettingsThroughServer(dispatch, conversationId, { title: nextTitle }, "rename this conversation").then(
76
+ (message) => message ? { message } : null
77
+ ) : (await tryWriteOne(
65
78
  supabase.schema("chat").from("conversation").update({ title: nextTitle, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", conversationId).select("id"),
66
79
  { action: "rename", noun: "conversation" }
67
- );
80
+ )).error;
68
81
  if (error) {
69
82
  dispatch(revertRename({ conversationId, previousTitle }));
70
83
  dispatch(
@@ -110,9 +123,14 @@ const setConversationArchived = createAsyncThunk(
110
123
  const before = findConversationRow(getState(), conversationId);
111
124
  const nextStatus = archived ? "archived" : "active";
112
125
  const action = archived ? "archive this conversation" : "restore this conversation";
113
- const { data, error } = await supabase.schema("chat").from("conversation").update({ status: nextStatus, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", conversationId).select("id");
114
- const refusal = error ?? landedOrRefusal(data, `chat.conversation status=${nextStatus} ${conversationId}`);
115
- if (refusal) return rejectWithValue({ message: refusalWords(refusal, action) });
126
+ if (adminSeatOpen(getState())) {
127
+ const refused = await writeSettingsThroughServer(dispatch, conversationId, { status: nextStatus }, action);
128
+ if (refused) return rejectWithValue({ message: refused });
129
+ } else {
130
+ const { data, error } = await supabase.schema("chat").from("conversation").update({ status: nextStatus, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", conversationId).select("id");
131
+ const refusal = error ?? landedOrRefusal(data, `chat.conversation status=${nextStatus} ${conversationId}`);
132
+ if (refusal) return rejectWithValue({ message: refusalWords(refusal, action) });
133
+ }
116
134
  dispatch(
117
135
  patchConversation({ conversationId, patch: { status: nextStatus } })
118
136
  );
@@ -135,19 +153,27 @@ const setConversationArchived = createAsyncThunk(
135
153
  );
136
154
  const setConversationExcludeFromKg = createAsyncThunk(
137
155
  "conversationRow/setExcludeFromKg",
138
- async ({ conversationId, excludeFromKg }, { dispatch, rejectWithValue }) => {
156
+ async ({ conversationId, excludeFromKg }, { dispatch, getState, rejectWithValue }) => {
157
+ const kgAction = excludeFromKg ? "exclude this conversation from the knowledge graph" : "include this conversation in the knowledge graph";
158
+ if (adminSeatOpen(getState())) {
159
+ const refused = await writeSettingsThroughServer(
160
+ dispatch,
161
+ conversationId,
162
+ { exclude_from_kg: excludeFromKg },
163
+ kgAction
164
+ );
165
+ if (refused) return rejectWithValue({ message: refused });
166
+ dispatch(patchConversation({ conversationId, patch: { excludeFromKg } }));
167
+ dispatch(patchConversationInScopes({ conversationId, patch: { excludeFromKg } }));
168
+ return { conversationId, excludeFromKg };
169
+ }
139
170
  const { data, error } = await supabase.schema("chat").from("conversation").update({
140
171
  exclude_from_kg: excludeFromKg,
141
172
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
142
173
  }).eq("id", conversationId).select("id");
143
174
  const refusal = error ?? landedOrRefusal(data, `chat.conversation exclude_from_kg=${excludeFromKg} ${conversationId}`);
144
175
  if (refusal) {
145
- return rejectWithValue({
146
- message: refusalWords(
147
- refusal,
148
- excludeFromKg ? "exclude this conversation from the knowledge graph" : "include this conversation in the knowledge graph"
149
- )
150
- });
176
+ return rejectWithValue({ message: refusalWords(refusal, kgAction) });
151
177
  }
152
178
  dispatch(patchConversation({ conversationId, patch: { excludeFromKg } }));
153
179
  dispatch(
@@ -199,6 +225,32 @@ const setConversationSandbox = createAsyncThunk(
199
225
  if (isEphemeral || (record?.cacheOnly ?? true)) {
200
226
  return { conversationId, persisted: false };
201
227
  }
228
+ if (adminSeatOpen(state)) {
229
+ const revert = () => dispatch(
230
+ patchInstanceConversation({
231
+ conversationId,
232
+ sandboxBinding: previous,
233
+ sandboxBindingPersisted: previousPersisted
234
+ })
235
+ );
236
+ if (ref?.kind === "local-pc") {
237
+ revert();
238
+ return rejectWithValue({
239
+ message: "A local computer can't be bound from the admin section. Open the conversation as its owner to bind it."
240
+ });
241
+ }
242
+ const result = ref ? await dispatch(
243
+ callConversationSandboxBind({ conversationId, body: { sandbox_instance_id: ref.rowId } })
244
+ ) : await dispatch(callConversationSandboxUnbind({ conversationId }));
245
+ if (result.error) {
246
+ revert();
247
+ return rejectWithValue({
248
+ message: refusalWords(result.error, ref ? "bind this sandbox" : "unbind this sandbox")
249
+ });
250
+ }
251
+ dispatch(patchInstanceConversation({ conversationId, sandboxBindingPersisted: true }));
252
+ return { conversationId, persisted: true };
253
+ }
202
254
  const { data: row, error: readError } = await supabase.schema("chat").from("conversation").select("metadata").eq("id", conversationId).single();
203
255
  if (readError) {
204
256
  dispatch(
@@ -12,23 +12,33 @@ import { removeConversationFromScopes } from "../../conversation-history/slice.j
12
12
  import { findConversationRow } from "../../conversation-list/conversation-list.selectors.js";
13
13
  import { clearCacheBypass } from "./cache-bypass.slice.js";
14
14
  import { invalidateConversationCache } from "./invalidate-conversation-cache.thunk.js";
15
+ import { adminSeatOpen } from "../../../../host/admin-seat.js";
16
+ import { callConversationDelete } from "../../../../host/server/call-api.js";
15
17
  const softDeleteConversation = createAsyncThunk(
16
18
  "conversations/softDelete",
17
19
  async ({ conversationId }, { dispatch, getState, rejectWithValue }) => {
18
20
  const snapshot = findConversationRow(getState(), conversationId);
19
- const { data, error } = await supabase.rpc("cx_soft_delete_conversation", {
20
- p_conversation_id: conversationId
21
- });
22
- if (error) {
23
- return rejectWithValue({
24
- message: operationFailed("delete this conversation", error).message
25
- });
26
- }
27
- const deleted = data === true;
28
- if (!deleted) {
29
- return rejectWithValue({
30
- message: operationFailed("delete this conversation").message
21
+ if (adminSeatOpen(getState())) {
22
+ const result = await dispatch(callConversationDelete({ conversationId }));
23
+ if (result.error || result.data?.deleted !== true) {
24
+ return rejectWithValue({
25
+ message: operationFailed("delete this conversation", result.error ?? void 0).message
26
+ });
27
+ }
28
+ } else {
29
+ const { data, error } = await supabase.rpc("cx_soft_delete_conversation", {
30
+ p_conversation_id: conversationId
31
31
  });
32
+ if (error) {
33
+ return rejectWithValue({
34
+ message: operationFailed("delete this conversation", error).message
35
+ });
36
+ }
37
+ if (data !== true) {
38
+ return rejectWithValue({
39
+ message: operationFailed("delete this conversation").message
40
+ });
41
+ }
32
42
  }
33
43
  void dispatch(invalidateConversationCache({ conversationId }));
34
44
  dispatch(clearCacheBypass(conversationId));