@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.
- package/CHANGELOG.md +17 -0
- package/dist/agents/components/chat/ChatHistorySidebar.js +3 -0
- package/dist/agents/components/chat/ChatSidebarMenu.js +3 -0
- package/dist/agents/components/chat/PinnedAgentsSection.d.ts +3 -5
- package/dist/agents/components/chat/PinnedAgentsSection.js +7 -4
- package/dist/agents/components/conversation-history/ConversationHistorySidebar.d.ts +14 -3
- package/dist/agents/components/conversation-history/ConversationHistorySidebar.js +57 -37
- package/dist/agents/components/conversation-history/ConversationSourceFilterTree.js +3 -3
- package/dist/agents/components/inputs/smart-input/ConversationContextChip.js +18 -7
- package/dist/agents/components/inputs/smart-input/ConversationContextRail.js +4 -2
- package/dist/agents/components/inputs/smart-input/DesktopPresenceIndicator.js +6 -0
- package/dist/agents/components/inputs/smart-input/composer/ComposerAgentPill.js +21 -7
- package/dist/agents/components/inputs/smart-input/composer/ComposerChipsRow.js +3 -3
- package/dist/agents/components/inputs/smart-input/composer/ComposerOutput.js +22 -15
- package/dist/agents/components/inputs/smart-input/composer/ComposerSplash.js +19 -3
- package/dist/agents/components/inputs/smart-input/composer/useComposerAgent.d.ts +24 -3
- package/dist/agents/components/inputs/smart-input/composer/useComposerAgent.js +32 -30
- package/dist/agents/components/inputs/smart-input/use-compute-target-actions.d.ts +20 -0
- package/dist/agents/components/inputs/smart-input/use-compute-target-actions.js +11 -0
- package/dist/agents/hooks/useConversationMaterialized.js +42 -9
- package/dist/agents/hooks/useCreatorOwnershipSync.js +7 -3
- package/dist/agents/hooks/useWorkingDocument.d.ts +12 -5
- package/dist/agents/hooks/useWorkingDocument.js +18 -6
- package/dist/agents/identity/agent-identity.d.ts +7 -1
- package/dist/agents/identity/agent-identity.js +11 -6
- package/dist/agents/identity/model-catalog.d.ts +7 -0
- package/dist/agents/identity/model-catalog.js +16 -1
- package/dist/agents/redux/conversation-history/slice.d.ts +2 -0
- package/dist/agents/redux/conversation-history/slice.js +2 -0
- package/dist/agents/redux/conversation-history/thunks.d.ts +2 -0
- package/dist/agents/redux/conversation-history/thunks.js +6 -3
- package/dist/agents/redux/conversation-history/types.d.ts +6 -0
- package/dist/agents/redux/conversation-history/types.js +1 -0
- package/dist/agents/redux/conversation-list/conversation-rows.d.ts +7 -0
- package/dist/agents/redux/conversation-list/conversation-rows.js +3 -1
- package/dist/agents/redux/execution-system/client-capabilities/desktop-native.provider.js +4 -1
- package/dist/agents/redux/execution-system/context-rules/mandate-kill-switch.d.ts +4 -0
- package/dist/agents/redux/execution-system/context-rules/mandate-kill-switch.js +26 -2
- package/dist/agents/redux/execution-system/conversations/conversations.selectors.d.ts +10 -0
- package/dist/agents/redux/execution-system/conversations/conversations.selectors.js +3 -0
- package/dist/agents/redux/execution-system/instance-user-input/useComposerDraftRestore.js +6 -1
- package/dist/agents/redux/execution-system/instance-working-document/scratchpad.thunks.js +9 -0
- package/dist/agents/redux/execution-system/thunks/launch-agent-execution.thunk.js +1 -8
- package/dist/agents/ui-first-tools/ui/lists/TaskPanelChip.js +4 -2
- package/dist/mandates/service.js +47 -32
- package/dist/next/navigation.js +5 -1
- package/package.json +6 -6
|
@@ -11,6 +11,9 @@ import { isMandateKey } from "@ai-matrx/agents/mandates";
|
|
|
11
11
|
import { useAppSelector } from "../../../../../store/hooks.js";
|
|
12
12
|
import { selectActiveUserName } from "../../../../../host/identity.js";
|
|
13
13
|
import { useMandateSet } from "../../../../../mandates/useMandateSet.js";
|
|
14
|
+
import { resolveMandate } from "../../../../../mandates/service.js";
|
|
15
|
+
import { toast } from "../../../../../host/notify.js";
|
|
16
|
+
import { useIdleReady } from "@ai-matrx/kit/idle-scheduler";
|
|
14
17
|
import { useSessionKnob } from "../../../../../host/prefs-react.js";
|
|
15
18
|
import { cn } from "@ai-matrx/design-system";
|
|
16
19
|
import { COMPOSER_KNOBS } from "./composer-mode-cookie.js";
|
|
@@ -73,9 +76,23 @@ function ComposerQuickActions({
|
|
|
73
76
|
const organizationId = useAppSelector(selectOrganizationId);
|
|
74
77
|
const actions = parseQuickActions(knob);
|
|
75
78
|
const quickActionKeys = actions.map((a) => a.mandateKey);
|
|
79
|
+
const idle = useIdleReady();
|
|
76
80
|
const mandates = useMandateSet(quickActionKeys, {
|
|
77
|
-
enabled: Boolean(organizationId)
|
|
81
|
+
enabled: Boolean(organizationId) && idle
|
|
78
82
|
});
|
|
83
|
+
const [launching, setLaunching] = useState(null);
|
|
84
|
+
const launch = (action) => {
|
|
85
|
+
const known = mandates[action.mandateKey]?.mandate?.agentId;
|
|
86
|
+
if (known) {
|
|
87
|
+
onLaunchAgent(known);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
setLaunching(action.mandateKey);
|
|
91
|
+
resolveMandate(action.mandateKey).then((mandate) => onLaunchAgent(mandate.agentId)).catch((error) => {
|
|
92
|
+
console.error(`[composer] quick action ${action.mandateKey} could not resolve:`, error);
|
|
93
|
+
toast.error(`"${action.label}" is not available yet`);
|
|
94
|
+
}).finally(() => setLaunching(null));
|
|
95
|
+
};
|
|
79
96
|
const scrollerRef = useRef(null);
|
|
80
97
|
const [canScroll, setCanScroll] = useState(false);
|
|
81
98
|
useEffect(() => {
|
|
@@ -105,9 +122,8 @@ function ComposerQuickActions({
|
|
|
105
122
|
className: "flex min-w-0 flex-nowrap gap-2 overflow-x-auto px-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
|
|
106
123
|
children: actions.map((action) => {
|
|
107
124
|
const state = mandates[action.mandateKey];
|
|
108
|
-
const agentId = state?.mandate?.agentId ?? null;
|
|
109
125
|
const unavailable = Boolean(state && !state.loading && state.error);
|
|
110
|
-
return /* @__PURE__ */ jsx(Button, { variant: "outline", disabled:
|
|
126
|
+
return /* @__PURE__ */ jsx(Button, { variant: "outline", disabled: unavailable || launching === action.mandateKey, title: unavailable ? `"${action.label}" is not available yet \u2014 its agent has not been assigned (${action.mandateKey}).` : void 0, onClick: () => launch(action), className: "shrink-0", children: action.label }, `${action.mandateKey}:${action.label}`);
|
|
111
127
|
})
|
|
112
128
|
}
|
|
113
129
|
),
|
|
@@ -18,9 +18,30 @@ export interface ComposerAgentInfo {
|
|
|
18
18
|
/** The Matrx Chat agent (`chat.cx_default`) — shown as "Custom Agent". */
|
|
19
19
|
customAgentId: string | null;
|
|
20
20
|
isCustom: boolean;
|
|
21
|
-
preset: ComposerPreset | null;
|
|
22
|
-
presets: ComposerPreset[];
|
|
23
21
|
}
|
|
24
22
|
/** The model this conversation will run on: its override, else the agent's own. */
|
|
25
23
|
export declare function useEffectiveModelId(conversationId: string): string | null;
|
|
26
|
-
|
|
24
|
+
/**
|
|
25
|
+
* The pill's CLOSED state (2026-10-09: /chat/new loads nearly instantly) — read
|
|
26
|
+
* from what the run already loaded (the run tier names the agent) plus ONE
|
|
27
|
+
* model's record and classes. Nothing here reads a list: the agent catalogue,
|
|
28
|
+
* the model options, every model's classes and the chat presets load when a
|
|
29
|
+
* person opens the menu (`useComposerPresets`).
|
|
30
|
+
*
|
|
31
|
+
* `engaged` = the menu is (or was) open. The `chat.cx_default` job ("Custom
|
|
32
|
+
* Agent") is asked then, or once the page is idle — whichever comes first — so
|
|
33
|
+
* the closed label can say "Custom Agent" without the ask holding up paint.
|
|
34
|
+
*/
|
|
35
|
+
export declare function useComposerAgent(conversationId: string, options?: {
|
|
36
|
+
engaged?: boolean;
|
|
37
|
+
}): ComposerAgentInfo;
|
|
38
|
+
/**
|
|
39
|
+
* Chat mode's presets (`chat-agent` tagged agents, each with its model) — the
|
|
40
|
+
* agent catalogue and the model options, read when the menu that lists them
|
|
41
|
+
* MOUNTS (it mounts only while open). Both reads are shared and deduped with
|
|
42
|
+
* every other picker on the page.
|
|
43
|
+
*/
|
|
44
|
+
export declare function useComposerPresets(agentId: string | null): {
|
|
45
|
+
preset: ComposerPreset | null;
|
|
46
|
+
presets: ComposerPreset[];
|
|
47
|
+
};
|
|
@@ -2,19 +2,16 @@
|
|
|
2
2
|
import { useEffect } from "react";
|
|
3
3
|
import { MANDATE_KEYS } from "@ai-matrx/agents/mandates";
|
|
4
4
|
import { useAgentCatalogRows } from "@ai-matrx/agents/catalog/react";
|
|
5
|
-
import {
|
|
5
|
+
import { useAppSelector } from "../../../../../store/hooks.js";
|
|
6
6
|
import { useMandate } from "../../../../../mandates/useMandate.js";
|
|
7
7
|
import { selectAgentIdFromInstance } from "../../../../redux/execution-system/conversations/conversations.selectors.js";
|
|
8
8
|
import {
|
|
9
9
|
selectAgentModelId
|
|
10
10
|
} from "../../../../redux/agent-definition/selectors.js";
|
|
11
11
|
import { selectInstanceOverrideState } from "../../../../redux/execution-system/instance-model-overrides/instance-model-overrides.selectors.js";
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
} from "@ai-matrx/agents/models";
|
|
16
|
-
import { useModelOptions } from "../../../../identity/model-catalog.js";
|
|
17
|
-
import { useModelClassLabels, useModelRecords } from "../../../../identity/model-catalog.js";
|
|
12
|
+
import { selectModelLabelById } from "@ai-matrx/agents/models";
|
|
13
|
+
import { useModelLabelForOneModel, useModelOptions, useModelRecords } from "../../../../identity/model-catalog.js";
|
|
14
|
+
import { useIdleReady } from "@ai-matrx/kit/idle-scheduler";
|
|
18
15
|
import { useEffectiveClassPin } from "../../../../redux/execution-system/instance-model-overrides/useEffectiveClassPin.js";
|
|
19
16
|
import { ensureAgentCatalog, useAgentName } from "../../../../identity/agent-identity.js";
|
|
20
17
|
const CHAT_AGENT_TAG = "chat-agent";
|
|
@@ -36,34 +33,23 @@ function useEffectiveModelId(conversationId) {
|
|
|
36
33
|
const overrideModel = overrideState?.overrides?.model ?? null;
|
|
37
34
|
return (typeof overrideModel === "string" ? overrideModel : null) ?? (typeof baseModel === "string" ? baseModel : null) ?? agentModelId ?? null;
|
|
38
35
|
}
|
|
39
|
-
function useComposerAgent(conversationId) {
|
|
40
|
-
const dispatch = useAppDispatch();
|
|
41
|
-
useModelOptions();
|
|
42
|
-
useEffect(() => {
|
|
43
|
-
void ensureAgentCatalog();
|
|
44
|
-
}, [dispatch]);
|
|
36
|
+
function useComposerAgent(conversationId, options) {
|
|
45
37
|
const agentId = useAppSelector(selectAgentIdFromInstance(conversationId)) ?? null;
|
|
46
38
|
const agentName = useAgentName(agentId) ?? null;
|
|
47
39
|
const effectiveModelId = useEffectiveModelId(conversationId);
|
|
48
|
-
useModelClassLabels();
|
|
49
40
|
const classPins = useEffectiveClassPin(conversationId);
|
|
50
41
|
const effectivePin = classPins.effectivePin ?? null;
|
|
51
42
|
const agentOwnPin = classPins.basePin ?? null;
|
|
52
|
-
const effectiveModelLabel =
|
|
43
|
+
const effectiveModelLabel = useModelLabelForOneModel(effectiveModelId, effectivePin) ?? null;
|
|
53
44
|
const agentOwnModelId = useAgentOwnModelId(conversationId);
|
|
54
|
-
const agentModelLabel =
|
|
55
|
-
const
|
|
45
|
+
const agentModelLabel = useModelLabelForOneModel(agentOwnModelId, agentOwnPin) ?? null;
|
|
46
|
+
const conversationMandateKey = useAppSelector(
|
|
47
|
+
(state) => state.conversations.byConversationId[conversationId]?.mandateKey ?? null
|
|
48
|
+
);
|
|
49
|
+
const idle = useIdleReady();
|
|
50
|
+
const askCustom = Boolean(options?.engaged) || idle;
|
|
51
|
+
const { mandate: customMandate } = useMandate(askCustom ? MANDATE_KEYS.chat__cx_default : "", { optional: true });
|
|
56
52
|
const customAgentId = customMandate?.agentId ?? null;
|
|
57
|
-
const rows = useAgentCatalogRows();
|
|
58
|
-
const presetRows = rows.filter(isPresetRow);
|
|
59
|
-
const modelLabels = useModelRecords(
|
|
60
|
-
(state) => presetRows.map((row) => selectModelLabelById(state, row.modelId) ?? null).join("\0")
|
|
61
|
-
).split("\0");
|
|
62
|
-
const presets = presetRows.map((row, index) => ({
|
|
63
|
-
id: row.id,
|
|
64
|
-
name: row.name ?? "Untitled agent",
|
|
65
|
-
modelLabel: modelLabels[index] || null
|
|
66
|
-
}));
|
|
67
53
|
return {
|
|
68
54
|
agentId,
|
|
69
55
|
agentName,
|
|
@@ -74,13 +60,29 @@ function useComposerAgent(conversationId) {
|
|
|
74
60
|
),
|
|
75
61
|
agentModelLabel,
|
|
76
62
|
customAgentId,
|
|
77
|
-
isCustom: Boolean(agentId && customAgentId && agentId === customAgentId)
|
|
78
|
-
preset: presets.find((p) => p.id === agentId) ?? null,
|
|
79
|
-
presets
|
|
63
|
+
isCustom: conversationMandateKey === MANDATE_KEYS.chat__cx_default || Boolean(agentId && customAgentId && agentId === customAgentId)
|
|
80
64
|
};
|
|
81
65
|
}
|
|
66
|
+
function useComposerPresets(agentId) {
|
|
67
|
+
useModelOptions();
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
void ensureAgentCatalog();
|
|
70
|
+
}, []);
|
|
71
|
+
const rows = useAgentCatalogRows();
|
|
72
|
+
const presetRows = rows.filter(isPresetRow);
|
|
73
|
+
const modelLabels = useModelRecords(
|
|
74
|
+
(state) => presetRows.map((row) => selectModelLabelById(state, row.modelId) ?? null).join("\0")
|
|
75
|
+
).split("\0");
|
|
76
|
+
const presets = presetRows.map((row, index) => ({
|
|
77
|
+
id: row.id,
|
|
78
|
+
name: row.name ?? "Untitled agent",
|
|
79
|
+
modelLabel: modelLabels[index] || null
|
|
80
|
+
}));
|
|
81
|
+
return { preset: presets.find((p) => p.id === agentId) ?? null, presets };
|
|
82
|
+
}
|
|
82
83
|
export {
|
|
83
84
|
CHAT_AGENT_TAG,
|
|
84
85
|
useComposerAgent,
|
|
86
|
+
useComposerPresets,
|
|
85
87
|
useEffectiveModelId
|
|
86
88
|
};
|
|
@@ -14,6 +14,26 @@ export declare function pickComputeLensTargets(targets: ComputeTarget[], boundRo
|
|
|
14
14
|
totalCount: number;
|
|
15
15
|
};
|
|
16
16
|
export declare function useSandboxBindingBlocked(conversationId: string): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* The bound box ONLY — named from the conversation's record, with no targets
|
|
19
|
+
* list. For a surface that shows what this chat runs on at rest (the Cloud
|
|
20
|
+
* chip, the bound-box button): the list of every computer and sandbox
|
|
21
|
+
* (`GET /api/compute-targets`) is read when a picker OPENS — the panels that
|
|
22
|
+
* offer a choice call `useComputeTargetActions` themselves, and they mount on
|
|
23
|
+
* open (owner, 2026-10-09: nothing non-essential at startup). The view's
|
|
24
|
+
* `state` stays `"checking"` here: liveness comes with the list.
|
|
25
|
+
*/
|
|
26
|
+
export declare function useComputeBinding(conversationId: string): {
|
|
27
|
+
boundView: import("../../../../compute/targets.js").BoundTargetView | null;
|
|
28
|
+
sandboxBlocked: boolean;
|
|
29
|
+
hasBinding: boolean;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Everything a compute PICKER needs: the bound box, every target this person
|
|
33
|
+
* can use, and the bind. Reads the targets list — call it only from a surface
|
|
34
|
+
* the person opened (a popover / menu panel) or one that shows a bound box's
|
|
35
|
+
* liveness; an at-rest chip uses `useComputeBinding`.
|
|
36
|
+
*/
|
|
17
37
|
export declare function useComputeTargetActions(conversationId: string): {
|
|
18
38
|
loading: boolean;
|
|
19
39
|
sandboxBlocked: boolean;
|
|
@@ -43,6 +43,16 @@ function useSandboxBindingBlocked(conversationId) {
|
|
|
43
43
|
const chatIncognito = useAppSelector(selectChatIncognitoActive);
|
|
44
44
|
return isEphemeral || chatIncognito && sourceFeature === "chat-route";
|
|
45
45
|
}
|
|
46
|
+
function useComputeBinding(conversationId) {
|
|
47
|
+
const binding = useVerifiedSandboxBinding(conversationId);
|
|
48
|
+
const sandboxBlocked = useSandboxBindingBlocked(conversationId);
|
|
49
|
+
const boundView = resolveBoundTargetView({
|
|
50
|
+
ref: binding.ref,
|
|
51
|
+
status: binding.status,
|
|
52
|
+
targets: null
|
|
53
|
+
});
|
|
54
|
+
return { boundView, sandboxBlocked, hasBinding: !!binding.ref };
|
|
55
|
+
}
|
|
46
56
|
function useComputeTargetActions(conversationId) {
|
|
47
57
|
const dispatch = useAppDispatch();
|
|
48
58
|
const { data, loading, refetch } = useComputeTargets();
|
|
@@ -125,6 +135,7 @@ export {
|
|
|
125
135
|
computeTargetIconColor,
|
|
126
136
|
computeTargetKindLabel,
|
|
127
137
|
pickComputeLensTargets,
|
|
138
|
+
useComputeBinding,
|
|
128
139
|
useComputeTargetActions,
|
|
129
140
|
useSandboxBindingBlocked
|
|
130
141
|
};
|
|
@@ -4,6 +4,7 @@ import { useAppSelector } from "../../store/hooks.js";
|
|
|
4
4
|
import { selectPrimaryRequest } from "../redux/execution-system/active-requests/active-requests.selectors.js";
|
|
5
5
|
import { waitForConversationPersisted } from "../redux/execution-system/conversations/conversation-persistence.js";
|
|
6
6
|
import { selectUserId } from "../../host/identity.js";
|
|
7
|
+
import { selectIsDraftConversation } from "../redux/execution-system/conversations/conversations.selectors.js";
|
|
7
8
|
const materializedIds = /* @__PURE__ */ new Set();
|
|
8
9
|
function materializedKey(userId, conversationId) {
|
|
9
10
|
return `${userId}:${conversationId}`;
|
|
@@ -21,12 +22,48 @@ function markMaterialized(userId, conversationId) {
|
|
|
21
22
|
materializedIds.add(key);
|
|
22
23
|
for (const listener of cacheListeners) listener();
|
|
23
24
|
}
|
|
25
|
+
const probesInFlight = /* @__PURE__ */ new Map();
|
|
26
|
+
function joinProbe(conversationId, key, poll) {
|
|
27
|
+
const probeKey = `${key}:${poll ? "poll" : "once"}`;
|
|
28
|
+
let probe = probesInFlight.get(probeKey);
|
|
29
|
+
if (!probe) {
|
|
30
|
+
const controller = new AbortController();
|
|
31
|
+
const promise = waitForConversationPersisted(conversationId, {
|
|
32
|
+
signal: controller.signal,
|
|
33
|
+
// Existing/reloaded conversations need one cheap proof read. A live
|
|
34
|
+
// stream keeps waiting because its first atomic commit is still pending.
|
|
35
|
+
timeoutMs: poll ? void 0 : 0
|
|
36
|
+
}).finally(() => {
|
|
37
|
+
if (probesInFlight.get(probeKey)?.promise === promise) {
|
|
38
|
+
probesInFlight.delete(probeKey);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
probe = { promise, controller, waiters: 0 };
|
|
42
|
+
probesInFlight.set(probeKey, probe);
|
|
43
|
+
}
|
|
44
|
+
const joined = probe;
|
|
45
|
+
joined.waiters += 1;
|
|
46
|
+
let left = false;
|
|
47
|
+
return {
|
|
48
|
+
promise: joined.promise,
|
|
49
|
+
leave: () => {
|
|
50
|
+
if (left) return;
|
|
51
|
+
left = true;
|
|
52
|
+
joined.waiters -= 1;
|
|
53
|
+
if (joined.waiters <= 0) {
|
|
54
|
+
joined.controller.abort();
|
|
55
|
+
if (probesInFlight.get(probeKey) === joined) probesInFlight.delete(probeKey);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
24
60
|
function useConversationMaterialized(conversationId) {
|
|
25
61
|
const userId = useAppSelector(selectUserId);
|
|
26
62
|
const status = useAppSelector(
|
|
27
63
|
(state) => conversationId ? selectPrimaryRequest(conversationId)(state)?.status : void 0
|
|
28
64
|
);
|
|
29
65
|
const shouldPoll = status === "streaming" || status === "awaiting-tools" || status === "complete";
|
|
66
|
+
const isDraft = useAppSelector(selectIsDraftConversation(conversationId));
|
|
30
67
|
const confirmedByCache = useSyncExternalStore(
|
|
31
68
|
subscribeToCache,
|
|
32
69
|
() => userId && conversationId ? materializedIds.has(materializedKey(userId, conversationId)) : false,
|
|
@@ -34,19 +71,15 @@ function useConversationMaterialized(conversationId) {
|
|
|
34
71
|
);
|
|
35
72
|
useEffect(() => {
|
|
36
73
|
if (!userId || !conversationId) return;
|
|
74
|
+
if (isDraft && !shouldPoll) return;
|
|
37
75
|
const key = materializedKey(userId, conversationId);
|
|
38
76
|
if (materializedIds.has(key)) return;
|
|
39
|
-
const
|
|
40
|
-
void
|
|
41
|
-
signal: controller.signal,
|
|
42
|
-
// Existing/reloaded conversations need one cheap proof read. A live
|
|
43
|
-
// stream keeps waiting because its first atomic commit is still pending.
|
|
44
|
-
timeoutMs: shouldPoll ? void 0 : 0
|
|
45
|
-
}).then((persisted) => {
|
|
77
|
+
const probe = joinProbe(conversationId, key, shouldPoll);
|
|
78
|
+
void probe.promise.then((persisted) => {
|
|
46
79
|
if (persisted) markMaterialized(userId, conversationId);
|
|
47
80
|
});
|
|
48
|
-
return
|
|
49
|
-
}, [conversationId, shouldPoll, userId]);
|
|
81
|
+
return probe.leave;
|
|
82
|
+
}, [conversationId, isDraft, shouldPoll, userId]);
|
|
50
83
|
return confirmedByCache;
|
|
51
84
|
}
|
|
52
85
|
export {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useEffect } from "react";
|
|
3
|
+
import { whenPageIdle } from "@ai-matrx/kit/idle-scheduler";
|
|
3
4
|
import { useAppDispatch, useAppSelector } from "../../store/hooks.js";
|
|
4
5
|
import { setIsCreator } from "../../host/prefs.js";
|
|
5
6
|
import { adminSeatOpen } from "../../host/admin-seat.js";
|
|
@@ -11,9 +12,12 @@ function useCreatorOwnershipSync(agentId) {
|
|
|
11
12
|
(state) => agentId ? selectAgentIsOwner(state, agentId) : null
|
|
12
13
|
);
|
|
13
14
|
useEffect(() => {
|
|
14
|
-
if (agentId
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
if (!agentId || isOwnerFlag !== null) return void 0;
|
|
16
|
+
const controller = new AbortController();
|
|
17
|
+
void whenPageIdle(controller.signal).then((idle) => {
|
|
18
|
+
if (idle) void dispatch(fetchAgentAccessLevel(agentId));
|
|
19
|
+
});
|
|
20
|
+
return () => controller.abort();
|
|
17
21
|
}, [dispatch, agentId, isOwnerFlag]);
|
|
18
22
|
const adminSeat = useAppSelector((state) => adminSeatOpen(state));
|
|
19
23
|
const isOwner = !!agentId && (isOwnerFlag === true || adminSeat);
|
|
@@ -18,11 +18,18 @@ export declare function useWorkingDocumentContextSync(conversationId: string, ki
|
|
|
18
18
|
export declare function useScratchpadContextSync(conversationId: string): void;
|
|
19
19
|
/**
|
|
20
20
|
* Always-on per-conversation bridge: restore the conversation's persisted
|
|
21
|
-
* WORKING document + attached scratchpads,
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
21
|
+
* WORKING document + attached scratchpads, then keep the conversation's
|
|
22
|
+
* document context entries current. Mount this once where a conversation is
|
|
23
|
+
* always present (the Smart Input), so the agent receives the current
|
|
24
|
+
* documents regardless of which editor (if any) is open.
|
|
25
|
+
*
|
|
26
|
+
* Nothing is read at mount that cannot change what is sent (owner, 2026-10-09:
|
|
27
|
+
* the page loads near-instantly):
|
|
28
|
+
* • a DRAFT (minted here, not yet sent) has no edges to restore — the
|
|
29
|
+
* restore runs the moment the first send makes the conversation real;
|
|
30
|
+
* • the user's global ACTIVE scratchpad is read only when this conversation
|
|
31
|
+
* sends it (its scratch gate is on) or a scratchpad surface opens (those
|
|
32
|
+
* surfaces hydrate it themselves).
|
|
26
33
|
*/
|
|
27
34
|
export declare function useConversationDocumentsBridge(conversationId: string): void;
|
|
28
35
|
export interface UseWorkingDocumentResult {
|
|
@@ -69,7 +69,10 @@ import {
|
|
|
69
69
|
getCxWorkingDocumentById,
|
|
70
70
|
updateCxWorkingDocumentTitle
|
|
71
71
|
} from "../redux/execution-system/instance-working-document/cx-working-document.service.js";
|
|
72
|
-
import {
|
|
72
|
+
import {
|
|
73
|
+
selectIsCacheOnly,
|
|
74
|
+
selectIsDraftConversation
|
|
75
|
+
} from "../redux/execution-system/conversations/conversations.selectors.js";
|
|
73
76
|
import {
|
|
74
77
|
useOpenScratchpadPanel,
|
|
75
78
|
useOpenWorkingDocumentPanel
|
|
@@ -332,8 +335,9 @@ function useScratchpadContextSync(conversationId) {
|
|
|
332
335
|
function useConversationDocumentsBridge(conversationId) {
|
|
333
336
|
const dispatch = useAppDispatch();
|
|
334
337
|
const userId = useAppSelector(selectUserId);
|
|
338
|
+
const isDraft = useAppSelector(selectIsDraftConversation(conversationId));
|
|
335
339
|
useEffect(() => {
|
|
336
|
-
if (!userId) return;
|
|
340
|
+
if (!userId || isDraft) return;
|
|
337
341
|
const ownsHydration = acquireDocumentBridge(conversationId);
|
|
338
342
|
if (!ownsHydration) {
|
|
339
343
|
return () => releaseDocumentBridge(conversationId);
|
|
@@ -344,14 +348,22 @@ function useConversationDocumentsBridge(conversationId) {
|
|
|
344
348
|
"Couldn't restore this conversation's working document \u2014 the agent won't see it until you reopen the conversation."
|
|
345
349
|
);
|
|
346
350
|
});
|
|
347
|
-
void dispatch(
|
|
348
|
-
() => dispatch(hydrateAttachedScratchpadsThunk({ conversationId })).unwrap()
|
|
349
|
-
).catch((err) => {
|
|
351
|
+
void dispatch(hydrateAttachedScratchpadsThunk({ conversationId })).unwrap().catch((err) => {
|
|
350
352
|
console.error("[scratchpad] bridge hydrate failed", err);
|
|
351
353
|
toast.error("Couldn't restore your scratchpad for this conversation.");
|
|
352
354
|
});
|
|
353
355
|
return () => releaseDocumentBridge(conversationId);
|
|
354
|
-
}, [dispatch, conversationId, userId]);
|
|
356
|
+
}, [dispatch, conversationId, userId, isDraft]);
|
|
357
|
+
const scratchGateOn = useAppSelector(
|
|
358
|
+
selectWorkingDocEnabled(conversationId, "scratch")
|
|
359
|
+
);
|
|
360
|
+
useEffect(() => {
|
|
361
|
+
if (!userId || !scratchGateOn) return;
|
|
362
|
+
void dispatch(hydrateActiveScratchpadThunk()).unwrap().catch((err) => {
|
|
363
|
+
console.error("[scratchpad] bridge hydrate failed", err);
|
|
364
|
+
toast.error("Couldn't load your scratchpad \u2014 the agent won't see it this turn.");
|
|
365
|
+
});
|
|
366
|
+
}, [dispatch, userId, scratchGateOn]);
|
|
355
367
|
useWorkingDocumentContextSync(conversationId, "working");
|
|
356
368
|
useScratchpadContextSync(conversationId);
|
|
357
369
|
}
|
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
* useAgentView(id) — the record, completed from the catalog row (derived, no copy)
|
|
13
13
|
* readAgentName / readAgentDescription / readAgentView(state, id) — thunks, callbacks
|
|
14
14
|
* ensureAgentCatalog() / isAgentCatalogStale() — load the list
|
|
15
|
+
*
|
|
16
|
+
* The name/description hooks read the catalogue only when the record has not read
|
|
17
|
+
* the field and is not reading it (`catalogNeededFor`): the list loads when a
|
|
18
|
+
* picker opens, never to print one name.
|
|
15
19
|
*/
|
|
16
20
|
import type { AgentSummary } from "@ai-matrx/agents/catalog";
|
|
17
21
|
import type { ChatRootState } from "../../store/root-state.js";
|
|
@@ -32,7 +36,9 @@ export interface AgentIdentity {
|
|
|
32
36
|
* One agent's catalog row, loading the catalog (then a server search) when it is missing — chat's
|
|
33
37
|
* own binding of the store (the same contract as `@ai-matrx/agents/catalog/react` `useAgentSummary`).
|
|
34
38
|
*/
|
|
35
|
-
export declare function useAgentSummaryRow(agentId: string | null | undefined
|
|
39
|
+
export declare function useAgentSummaryRow(agentId: string | null | undefined, options?: {
|
|
40
|
+
load?: boolean;
|
|
41
|
+
}): AgentSummary | undefined;
|
|
36
42
|
type MaybeRecord = AgentDefinitionRecord | undefined;
|
|
37
43
|
/**
|
|
38
44
|
* The agent's record completed from its catalog row: every field the record has READ wins; the
|
|
@@ -39,7 +39,8 @@ function isAgentCatalogStale() {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
const noopSubscribe = () => () => void 0;
|
|
42
|
-
function useAgentSummaryRow(agentId) {
|
|
42
|
+
function useAgentSummaryRow(agentId, options) {
|
|
43
|
+
const load = options?.load ?? true;
|
|
43
44
|
const catalog = useCatalog();
|
|
44
45
|
const row = useSyncExternalStore(
|
|
45
46
|
catalog ? catalog.subscribe : noopSubscribe,
|
|
@@ -48,7 +49,7 @@ function useAgentSummaryRow(agentId) {
|
|
|
48
49
|
);
|
|
49
50
|
const present = row !== void 0;
|
|
50
51
|
useEffect(() => {
|
|
51
|
-
if (!catalog || !agentId || present) return;
|
|
52
|
+
if (!catalog || !agentId || present || !load) return;
|
|
52
53
|
let cancelled = false;
|
|
53
54
|
void catalog.ensureLoaded().then(() => {
|
|
54
55
|
if (cancelled || catalog.getState().byId[agentId]) return;
|
|
@@ -57,9 +58,13 @@ function useAgentSummaryRow(agentId) {
|
|
|
57
58
|
return () => {
|
|
58
59
|
cancelled = true;
|
|
59
60
|
};
|
|
60
|
-
}, [catalog, agentId, present]);
|
|
61
|
+
}, [catalog, agentId, present, load]);
|
|
61
62
|
return row;
|
|
62
63
|
}
|
|
64
|
+
function catalogNeededFor(record, fields) {
|
|
65
|
+
if (record?._loading) return false;
|
|
66
|
+
return !fields.every((field) => loaded(record, field));
|
|
67
|
+
}
|
|
63
68
|
function loaded(record, field) {
|
|
64
69
|
return !!record?._loadedFields && hasField(record._loadedFields, field);
|
|
65
70
|
}
|
|
@@ -110,18 +115,18 @@ function readAgentView(state, agentId) {
|
|
|
110
115
|
return agentViewOf(recordOf(state, agentId), catalogRow(agentId));
|
|
111
116
|
}
|
|
112
117
|
function useAgentName(agentId) {
|
|
113
|
-
const row = useAgentSummaryRow(agentId);
|
|
114
118
|
const record = useAppSelector((s) => recordOf(s, agentId));
|
|
119
|
+
const row = useAgentSummaryRow(agentId, { load: catalogNeededFor(record, ["name"]) });
|
|
115
120
|
return nameOf(record, row);
|
|
116
121
|
}
|
|
117
122
|
function useAgentDescription(agentId) {
|
|
118
|
-
const row = useAgentSummaryRow(agentId);
|
|
119
123
|
const record = useAppSelector((s) => recordOf(s, agentId));
|
|
124
|
+
const row = useAgentSummaryRow(agentId, { load: catalogNeededFor(record, ["description"]) });
|
|
120
125
|
return descriptionOf(record, row);
|
|
121
126
|
}
|
|
122
127
|
function useAgentIdentity(agentId) {
|
|
123
|
-
const summary = useAgentSummaryRow(agentId);
|
|
124
128
|
const record = useAppSelector((s) => recordOf(s, agentId));
|
|
129
|
+
const summary = useAgentSummaryRow(agentId, { load: catalogNeededFor(record, ["name", "description"]) });
|
|
125
130
|
return { name: nameOf(record, summary), description: descriptionOf(record, summary), summary };
|
|
126
131
|
}
|
|
127
132
|
function readAgentName(state, agentId) {
|
|
@@ -44,3 +44,10 @@ export declare function useModelOptions(): {
|
|
|
44
44
|
};
|
|
45
45
|
/** One model's FULL record (controls included), or undefined until it has loaded. */
|
|
46
46
|
export declare function useModelFull(modelId: string | null | undefined): ModelRecord | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* The label a CLOSED control shows for the model a conversation runs on —
|
|
49
|
+
* "Qwen3.8 27B · Matrx Lightning" — from ONE model's reads: its record (the same
|
|
50
|
+
* deduped read `useModelFull` makes) and its own classes. Never the options
|
|
51
|
+
* list or every model's classes; those load when a picker opens.
|
|
52
|
+
*/
|
|
53
|
+
export declare function useModelLabelForOneModel(modelId: string | null | undefined, offeringId: string | null | undefined): string | undefined;
|
|
@@ -110,11 +110,25 @@ function useModelFull(modelId) {
|
|
|
110
110
|
const isFull = useModelRecords((s) => selectModelFullyLoaded(s, modelId));
|
|
111
111
|
useEffect(() => {
|
|
112
112
|
if (!chatHostHasDatabase()) return;
|
|
113
|
-
void getModelRecords().loadOptions();
|
|
114
113
|
if (modelId && !isFull) void getModelRecords().loadModel(modelId);
|
|
115
114
|
}, [modelId, isFull]);
|
|
116
115
|
return isFull ? record : void 0;
|
|
117
116
|
}
|
|
117
|
+
function useModelLabelForOneModel(modelId, offeringId) {
|
|
118
|
+
const label = useModelRecords((s) => selectModelLabelWithClass(s, modelId, offeringId));
|
|
119
|
+
const known = useModelRecords((s) => Boolean(selectModelLabelById(s, modelId)));
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (!modelId || !chatHostHasDatabase()) return;
|
|
122
|
+
const records = getModelRecords();
|
|
123
|
+
if (!known) {
|
|
124
|
+
void records.loadModel(modelId).then(() => {
|
|
125
|
+
if (!selectModelLabelById(records.getState(), modelId)) void records.loadIdentity(modelId);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
void records.loadModelClasses(modelId);
|
|
129
|
+
}, [modelId, known]);
|
|
130
|
+
return label;
|
|
131
|
+
}
|
|
118
132
|
export {
|
|
119
133
|
getModelCatalog,
|
|
120
134
|
getModelRecords,
|
|
@@ -123,6 +137,7 @@ export {
|
|
|
123
137
|
useModelClassLabels,
|
|
124
138
|
useModelFull,
|
|
125
139
|
useModelLabel,
|
|
140
|
+
useModelLabelForOneModel,
|
|
126
141
|
useModelLabelWithClass,
|
|
127
142
|
useModelOptions,
|
|
128
143
|
useModelRecord,
|
|
@@ -16,6 +16,8 @@ export declare const configureScope: import("@reduxjs/toolkit").ActionCreatorWit
|
|
|
16
16
|
includeOriginClasses?: string[];
|
|
17
17
|
grouping?: HistoryGrouping;
|
|
18
18
|
pageSize?: number;
|
|
19
|
+
/** Slim row read (`ConversationHistoryScopeState.nameRowsOnly`). */
|
|
20
|
+
nameRowsOnly?: boolean;
|
|
19
21
|
/** When true, drops items/offset so a fresh fetch repopulates. */
|
|
20
22
|
reset?: boolean;
|
|
21
23
|
}, "conversationHistory/configureScope">, setScopeAgentIds: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
|
|
@@ -58,6 +58,7 @@ const slice = createSlice({
|
|
|
58
58
|
includeOriginClasses,
|
|
59
59
|
grouping,
|
|
60
60
|
pageSize,
|
|
61
|
+
nameRowsOnly,
|
|
61
62
|
reset
|
|
62
63
|
} = action.payload;
|
|
63
64
|
const scope = ensureScope(state, scopeId);
|
|
@@ -79,6 +80,7 @@ const slice = createSlice({
|
|
|
79
80
|
}
|
|
80
81
|
if (grouping !== void 0) scope.grouping = grouping;
|
|
81
82
|
if (pageSize !== void 0) scope.pageSize = pageSize;
|
|
83
|
+
if (nameRowsOnly !== void 0) scope.nameRowsOnly = nameRowsOnly;
|
|
82
84
|
if (reset) invalidateScopeWindow(scope);
|
|
83
85
|
},
|
|
84
86
|
/**
|
|
@@ -34,6 +34,8 @@ export interface FetchConversationHistoryArgs {
|
|
|
34
34
|
includeEmptySource?: boolean;
|
|
35
35
|
/** Overrides the scope's stored pageSize for this fetch. */
|
|
36
36
|
pageSize?: number;
|
|
37
|
+
/** Overrides the scope's stored `nameRowsOnly` (slim row read). */
|
|
38
|
+
nameRowsOnly?: boolean;
|
|
37
39
|
/**
|
|
38
40
|
* When true (default), replaces existing items with the freshly-fetched
|
|
39
41
|
* first page. When false, appends a new page using the scope's `offset`.
|
|
@@ -2,6 +2,7 @@ import { createAsyncThunk } from "@reduxjs/toolkit";
|
|
|
2
2
|
import { supabase } from "../../../host/db.js";
|
|
3
3
|
import { applyFavoritesFromUes } from "../conversation-list/conversation-list.thunks.js";
|
|
4
4
|
import {
|
|
5
|
+
CONVERSATION_NAME_ROW_COLUMNS,
|
|
5
6
|
CONVERSATION_ROW_COLUMNS,
|
|
6
7
|
mapConversationDbRow
|
|
7
8
|
} from "../conversation-list/conversation-rows.js";
|
|
@@ -31,7 +32,7 @@ const fetchConversationHistory = createAsyncThunk(
|
|
|
31
32
|
const replace = args.replace ?? true;
|
|
32
33
|
const state = getState();
|
|
33
34
|
const existing = state.conversationHistory.scopes[args.scopeId];
|
|
34
|
-
if (!existing || args.agentIds !== void 0 || args.excludeSourceFeatures !== void 0 || args.includeSourceFeatures !== void 0 || args.includeSourceApps !== void 0 || args.includeEmptySource !== void 0 || args.pageSize !== void 0) {
|
|
35
|
+
if (!existing || args.agentIds !== void 0 || args.excludeSourceFeatures !== void 0 || args.includeSourceFeatures !== void 0 || args.includeSourceApps !== void 0 || args.includeEmptySource !== void 0 || args.pageSize !== void 0 || args.nameRowsOnly !== void 0) {
|
|
35
36
|
dispatch(
|
|
36
37
|
configureScope({
|
|
37
38
|
scopeId: args.scopeId,
|
|
@@ -40,7 +41,8 @@ const fetchConversationHistory = createAsyncThunk(
|
|
|
40
41
|
includeSourceFeatures: args.includeSourceFeatures ?? existing?.includeSourceFeatures ?? [],
|
|
41
42
|
includeSourceApps: args.includeSourceApps ?? existing?.includeSourceApps ?? [],
|
|
42
43
|
includeEmptySource: args.includeEmptySource ?? existing?.includeEmptySource ?? false,
|
|
43
|
-
pageSize: args.pageSize ?? existing?.pageSize ?? defaultScopeState.pageSize
|
|
44
|
+
pageSize: args.pageSize ?? existing?.pageSize ?? defaultScopeState.pageSize,
|
|
45
|
+
nameRowsOnly: args.nameRowsOnly ?? existing?.nameRowsOnly ?? defaultScopeState.nameRowsOnly
|
|
44
46
|
})
|
|
45
47
|
);
|
|
46
48
|
}
|
|
@@ -66,7 +68,8 @@ const fetchConversationHistory = createAsyncThunk(
|
|
|
66
68
|
);
|
|
67
69
|
return { scopeId: args.scopeId, items: [], hasMore: false, nextOffset: 0, replace };
|
|
68
70
|
}
|
|
69
|
-
|
|
71
|
+
const columns = scope.nameRowsOnly ? CONVERSATION_NAME_ROW_COLUMNS : CONVERSATION_ROW_COLUMNS;
|
|
72
|
+
let query = supabase.schema("chat").from("conversation").select(columns).is("deleted_at", null).eq("is_ephemeral", false).order("updated_at", { ascending: false }).range(offset, offset + pageSize - 1);
|
|
70
73
|
if (viewerId && !(agentIds.length > 0 && adminSeatOpen(getState()))) {
|
|
71
74
|
query = query.eq("created_by", viewerId);
|
|
72
75
|
}
|
|
@@ -97,6 +97,12 @@ export interface ConversationHistoryScopeState {
|
|
|
97
97
|
grouping: HistoryGrouping;
|
|
98
98
|
/** Page size for range-based pagination. */
|
|
99
99
|
pageSize: number;
|
|
100
|
+
/**
|
|
101
|
+
* Read only what a history ROW shows (`CONVERSATION_NAME_ROW_COLUMNS`: name,
|
|
102
|
+
* time, provenance for its menu) — the /chat sidebar's slim read. Rows from
|
|
103
|
+
* it carry no description / message count / model.
|
|
104
|
+
*/
|
|
105
|
+
nameRowsOnly: boolean;
|
|
100
106
|
/** How many items we've fetched so far (the next `range` offset). */
|
|
101
107
|
offset: number;
|
|
102
108
|
/** Items in display order (already sorted by `updated_at` desc). */
|