@gram-ai/elements 1.33.2 → 1.35.0
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/dist/compat-shims-CO9JXXV4.cjs.map +1 -1
- package/dist/compat-shims-DxtUrORi.js.map +1 -1
- package/dist/compat-shims.d.ts +9 -8
- package/dist/components/Chat/index.d.ts +2 -1
- package/dist/components/ChatHistory.d.ts +1 -1
- package/dist/components/FrontendTools/index.d.ts +1 -1
- package/dist/components/Replay.d.ts +1 -1
- package/dist/components/Replay.stories.d.ts +2 -2
- package/dist/components/ShadowRoot.d.ts +1 -1
- package/dist/components/ShareButton/index.d.ts +1 -1
- package/dist/components/ui/avatar.d.ts +3 -3
- package/dist/components/ui/button.d.ts +2 -2
- package/dist/components/ui/buttonVariants.d.ts +2 -2
- package/dist/components/ui/calendar.d.ts +2 -1
- package/dist/components/ui/collapsible.d.ts +3 -3
- package/dist/components/ui/dialog.d.ts +10 -10
- package/dist/components/ui/popover.d.ts +4 -4
- package/dist/components/ui/skeleton.d.ts +1 -1
- package/dist/components/ui/time-range-picker.d.ts +18 -1
- package/dist/components/ui/time-range-picker.test.d.ts +1 -0
- package/dist/components/ui/tool-ui.d.ts +7 -7
- package/dist/components/ui/tooltip.d.ts +4 -4
- package/dist/contexts/ConnectionStatusContext.d.ts +1 -1
- package/dist/contexts/ElementsProvider.d.ts +1 -1
- package/dist/contexts/ToolApprovalContext.d.ts +2 -2
- package/dist/contexts/ToolExecutionContext.d.ts +1 -1
- package/dist/contexts/portal-container.d.ts +1 -1
- package/dist/elements.cjs +1 -1
- package/dist/elements.css +1 -1
- package/dist/elements.js +19 -16
- package/dist/hooks/useDensity.d.ts +1 -1
- package/dist/hooks/useElements.d.ts +2 -1
- package/dist/hooks/useGramThreadListAdapter.d.ts +26 -0
- package/dist/hooks/useRadius.d.ts +1 -1
- package/dist/hooks/useThemeProps.d.ts +1 -1
- package/dist/hooks/useToolApproval.d.ts +2 -1
- package/dist/{index-reVrRxP1.js → index-BhIowiZF.js} +9744 -9493
- package/dist/index-BhIowiZF.js.map +1 -0
- package/dist/{index-DAWGW1Nj.cjs → index-D0jIGQr7.cjs} +43 -43
- package/dist/index-D0jIGQr7.cjs.map +1 -0
- package/dist/{index-CGoLfO5p.js → index-Dz13dSDa.js} +108 -51
- package/dist/index-Dz13dSDa.js.map +1 -0
- package/dist/index-PXd3rs95.cjs +194 -0
- package/dist/index-PXd3rs95.cjs.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/lib/errorTracking.d.ts +1 -1
- package/dist/lib/messageConverter.d.ts +58 -8
- package/dist/lib/models.d.ts +1 -1
- package/dist/lib/tools.d.ts +11 -10
- package/dist/lib/utils.d.ts +2 -0
- package/dist/plugins/generative-ui/catalog.d.ts +3 -3
- package/dist/plugins/generative-ui/ui/accordion-wrapper.d.ts +2 -2
- package/dist/plugins/generative-ui/ui/accordion.d.ts +4 -4
- package/dist/plugins/generative-ui/ui/action-button.d.ts +1 -1
- package/dist/plugins/generative-ui/ui/alert-wrapper.d.ts +2 -1
- package/dist/plugins/generative-ui/ui/alert.d.ts +3 -3
- package/dist/plugins/generative-ui/ui/avatar-wrapper.d.ts +2 -1
- package/dist/plugins/generative-ui/ui/avatar.d.ts +6 -6
- package/dist/plugins/generative-ui/ui/badge.d.ts +2 -2
- package/dist/plugins/generative-ui/ui/button-wrapper.d.ts +2 -1
- package/dist/plugins/generative-ui/ui/button.d.ts +3 -3
- package/dist/plugins/generative-ui/ui/card-wrapper.d.ts +1 -1
- package/dist/plugins/generative-ui/ui/card.d.ts +7 -7
- package/dist/plugins/generative-ui/ui/checkbox-wrapper.d.ts +2 -1
- package/dist/plugins/generative-ui/ui/checkbox.d.ts +1 -1
- package/dist/plugins/generative-ui/ui/data-table.d.ts +1 -1
- package/dist/plugins/generative-ui/ui/dialog.d.ts +10 -10
- package/dist/plugins/generative-ui/ui/dropdown-menu.d.ts +15 -15
- package/dist/plugins/generative-ui/ui/grid.d.ts +1 -1
- package/dist/plugins/generative-ui/ui/index.d.ts +57 -40
- package/dist/plugins/generative-ui/ui/input-wrapper.d.ts +2 -1
- package/dist/plugins/generative-ui/ui/input.d.ts +1 -1
- package/dist/plugins/generative-ui/ui/label.d.ts +1 -1
- package/dist/plugins/generative-ui/ui/list.d.ts +2 -1
- package/dist/plugins/generative-ui/ui/metric.d.ts +1 -1
- package/dist/plugins/generative-ui/ui/pagination.d.ts +7 -7
- package/dist/plugins/generative-ui/ui/popover.d.ts +7 -7
- package/dist/plugins/generative-ui/ui/progress.d.ts +1 -1
- package/dist/plugins/generative-ui/ui/radio-group.d.ts +2 -2
- package/dist/plugins/generative-ui/ui/select-wrapper.d.ts +2 -1
- package/dist/plugins/generative-ui/ui/select.d.ts +10 -10
- package/dist/plugins/generative-ui/ui/separator.d.ts +1 -1
- package/dist/plugins/generative-ui/ui/skeleton-wrapper.d.ts +2 -1
- package/dist/plugins/generative-ui/ui/skeleton.d.ts +1 -1
- package/dist/plugins/generative-ui/ui/stack.d.ts +1 -1
- package/dist/plugins/generative-ui/ui/switch.d.ts +1 -1
- package/dist/plugins/generative-ui/ui/table.d.ts +8 -8
- package/dist/plugins/generative-ui/ui/tabs-wrapper.d.ts +2 -2
- package/dist/plugins/generative-ui/ui/tabs.d.ts +4 -4
- package/dist/plugins/generative-ui/ui/text.d.ts +1 -1
- package/dist/plugins/generative-ui/ui/textarea.d.ts +1 -1
- package/dist/plugins/generative-ui/ui/tooltip.d.ts +4 -4
- package/dist/plugins.cjs +1 -1
- package/dist/plugins.js +1 -1
- package/dist/{profiler-noho3NG9.js → profiler-CtGKTWWP.js} +2 -2
- package/dist/{profiler-noho3NG9.js.map → profiler-CtGKTWWP.js.map} +1 -1
- package/dist/{profiler-B3tfiOx4.cjs → profiler-l7_HjTyw.cjs} +2 -2
- package/dist/{profiler-B3tfiOx4.cjs.map → profiler-l7_HjTyw.cjs.map} +1 -1
- package/dist/react-shim.cjs.map +1 -1
- package/dist/react-shim.d.ts +1 -1
- package/dist/react-shim.js +1 -4
- package/dist/react-shim.js.map +1 -1
- package/dist/server/bun.cjs.map +1 -1
- package/dist/server/bun.js.map +1 -1
- package/dist/server/express.cjs.map +1 -1
- package/dist/server/express.js.map +1 -1
- package/dist/server/fastify.cjs.map +1 -1
- package/dist/server/fastify.js.map +1 -1
- package/dist/server/hono.cjs.map +1 -1
- package/dist/server/hono.js.map +1 -1
- package/dist/server/nextjs.cjs.map +1 -1
- package/dist/server/nextjs.js.map +1 -1
- package/dist/server/tanstack-start.cjs.map +1 -1
- package/dist/server/tanstack-start.js.map +1 -1
- package/dist/{startRecording-7Oy6wM18.cjs → startRecording-DEw2Aeq4.cjs} +2 -2
- package/dist/{startRecording-7Oy6wM18.cjs.map → startRecording-DEw2Aeq4.cjs.map} +1 -1
- package/dist/{startRecording-mkmig-2n.js → startRecording-iYEL0-vr.js} +2 -2
- package/dist/{startRecording-mkmig-2n.js.map → startRecording-iYEL0-vr.js.map} +1 -1
- package/dist/types/index.d.ts +93 -4
- package/package.json +8 -9
- package/src/compat-shims.ts +16 -2
- package/src/components/Chat/index.tsx +4 -1
- package/src/components/Chat/stories/FrontendTools.stories.tsx +1 -1
- package/src/components/Chat/stories/ToolApproval.stories.tsx +2 -2
- package/src/components/Chat/stories/Tools.stories.tsx +13 -5
- package/src/components/ChatHistory.tsx +3 -1
- package/src/components/FrontendTools/index.tsx +1 -1
- package/src/components/MessageContent.tsx +1 -0
- package/src/components/Replay.stories.tsx +2 -3
- package/src/components/Replay.tsx +17 -10
- package/src/components/ShadowRoot.tsx +2 -2
- package/src/components/ShareButton/index.tsx +4 -2
- package/src/components/assistant-ui/assistant-modal.tsx +5 -3
- package/src/components/assistant-ui/attachment.tsx +1 -1
- package/src/components/assistant-ui/error-boundary.tsx +1 -1
- package/src/components/assistant-ui/markdown-text.tsx +1 -1
- package/src/components/assistant-ui/thread.tsx +256 -14
- package/src/components/assistant-ui/tool-mention-autocomplete.tsx +1 -1
- package/src/components/ui/avatar.tsx +3 -3
- package/src/components/ui/calendar.tsx +1 -1
- package/src/components/ui/collapsible.tsx +7 -3
- package/src/components/ui/dialog.tsx +18 -10
- package/src/components/ui/generative-ui.tsx +9 -4
- package/src/components/ui/popover.tsx +4 -4
- package/src/components/ui/skeleton.tsx +4 -1
- package/src/components/ui/time-range-picker.stories.tsx +164 -154
- package/src/components/ui/time-range-picker.test.ts +57 -0
- package/src/components/ui/time-range-picker.tsx +40 -9
- package/src/components/ui/tool-ui.tsx +18 -9
- package/src/components/ui/tooltip.tsx +4 -4
- package/src/contexts/ChatIdContext.tsx +1 -1
- package/src/contexts/ConnectionStatusContext.tsx +6 -5
- package/src/contexts/ElementsProvider.tsx +109 -37
- package/src/contexts/ReplayContext.ts +1 -1
- package/src/contexts/ToolApprovalContext.tsx +5 -1
- package/src/contexts/ToolExecutionContext.tsx +1 -1
- package/src/contexts/portal-container.tsx +1 -1
- package/src/hooks/useAuth.ts +2 -1
- package/src/hooks/useDensity.ts +1 -1
- package/src/hooks/useElements.ts +2 -1
- package/src/hooks/useFollowOnSuggestions.ts +3 -6
- package/src/hooks/useGramThreadListAdapter.tsx +118 -9
- package/src/hooks/useMCPTools.ts +2 -2
- package/src/hooks/useModel.ts +1 -3
- package/src/hooks/usePluginComponents.ts +3 -1
- package/src/hooks/useRadius.ts +1 -1
- package/src/hooks/useSession.ts +3 -1
- package/src/hooks/useThemeProps.ts +5 -5
- package/src/hooks/useToolApproval.ts +2 -1
- package/src/index.ts +16 -0
- package/src/lib/cassette.ts +21 -27
- package/src/lib/contextCompaction.test.ts +2 -2
- package/src/lib/contextCompaction.ts +20 -8
- package/src/lib/errorTracking.ts +1 -4
- package/src/lib/messageConverter.test.ts +11 -13
- package/src/lib/messageConverter.ts +105 -58
- package/src/lib/models.ts +19 -7
- package/src/lib/token.ts +2 -5
- package/src/lib/tool-mentions.ts +5 -2
- package/src/lib/tools.byte-cap.test.ts +1 -1
- package/src/lib/tools.test.ts +1 -1
- package/src/lib/tools.ts +15 -5
- package/src/lib/utils.ts +22 -2
- package/src/lib.d.ts +8 -1
- package/src/plugins/chart/chart.test.ts +3 -4
- package/src/plugins/chart/component.tsx +7 -6
- package/src/plugins/chart/ui/area-chart.tsx +1 -1
- package/src/plugins/chart/ui/line-chart.tsx +1 -1
- package/src/plugins/generative-ui/ui/accordion-wrapper.tsx +2 -2
- package/src/plugins/generative-ui/ui/accordion.tsx +4 -4
- package/src/plugins/generative-ui/ui/action-button.tsx +4 -2
- package/src/plugins/generative-ui/ui/alert-wrapper.tsx +1 -1
- package/src/plugins/generative-ui/ui/alert.tsx +7 -3
- package/src/plugins/generative-ui/ui/avatar-wrapper.tsx +5 -1
- package/src/plugins/generative-ui/ui/avatar.tsx +12 -6
- package/src/plugins/generative-ui/ui/badge.tsx +1 -1
- package/src/plugins/generative-ui/ui/button-wrapper.tsx +1 -1
- package/src/plugins/generative-ui/ui/button.tsx +1 -1
- package/src/plugins/generative-ui/ui/card-wrapper.tsx +1 -1
- package/src/plugins/generative-ui/ui/card.tsx +28 -7
- package/src/plugins/generative-ui/ui/checkbox-wrapper.tsx +1 -1
- package/src/plugins/generative-ui/ui/checkbox.tsx +1 -1
- package/src/plugins/generative-ui/ui/data-table.tsx +1 -1
- package/src/plugins/generative-ui/ui/dialog.tsx +15 -10
- package/src/plugins/generative-ui/ui/dropdown-menu.tsx +33 -15
- package/src/plugins/generative-ui/ui/grid.tsx +1 -1
- package/src/plugins/generative-ui/ui/index.ts +154 -40
- package/src/plugins/generative-ui/ui/input-wrapper.tsx +1 -1
- package/src/plugins/generative-ui/ui/input.tsx +5 -1
- package/src/plugins/generative-ui/ui/label.tsx +1 -1
- package/src/plugins/generative-ui/ui/list.tsx +5 -1
- package/src/plugins/generative-ui/ui/metric.tsx +2 -1
- package/src/plugins/generative-ui/ui/pagination.tsx +12 -7
- package/src/plugins/generative-ui/ui/popover.tsx +13 -7
- package/src/plugins/generative-ui/ui/progress.tsx +1 -1
- package/src/plugins/generative-ui/ui/radio-group.tsx +2 -2
- package/src/plugins/generative-ui/ui/select-wrapper.tsx +1 -1
- package/src/plugins/generative-ui/ui/select.tsx +14 -10
- package/src/plugins/generative-ui/ui/separator.tsx +1 -1
- package/src/plugins/generative-ui/ui/skeleton-wrapper.tsx +1 -1
- package/src/plugins/generative-ui/ui/skeleton.tsx +4 -1
- package/src/plugins/generative-ui/ui/stack.tsx +1 -1
- package/src/plugins/generative-ui/ui/switch.tsx +1 -1
- package/src/plugins/generative-ui/ui/table.tsx +29 -8
- package/src/plugins/generative-ui/ui/tabs-wrapper.tsx +5 -2
- package/src/plugins/generative-ui/ui/tabs.tsx +4 -4
- package/src/plugins/generative-ui/ui/text.tsx +1 -1
- package/src/plugins/generative-ui/ui/textarea.tsx +4 -1
- package/src/plugins/generative-ui/ui/tooltip.tsx +4 -4
- package/src/react-shim.ts +9 -4
- package/src/server/bun.ts +1 -1
- package/src/server/express.ts +1 -1
- package/src/server/fastify.ts +1 -1
- package/src/server/hono.ts +1 -1
- package/src/server/nextjs.ts +1 -1
- package/src/server/tanstack-start.ts +1 -1
- package/src/storybook.d.ts +5 -0
- package/src/types/index.ts +112 -4
- package/dist/index-BCV7Zf9E.cjs +0 -194
- package/dist/index-BCV7Zf9E.cjs.map +0 -1
- package/dist/index-CGoLfO5p.js.map +0 -1
- package/dist/index-DAWGW1Nj.cjs.map +0 -1
- package/dist/index-reVrRxP1.js.map +0 -1
|
@@ -36,7 +36,7 @@ interface ConnectionStatusProviderProps {
|
|
|
36
36
|
|
|
37
37
|
export const ConnectionStatusProvider = ({
|
|
38
38
|
children,
|
|
39
|
-
}: ConnectionStatusProviderProps) => {
|
|
39
|
+
}: ConnectionStatusProviderProps): React.JSX.Element => {
|
|
40
40
|
const [state, setState] = useState<ConnectionState>("connected");
|
|
41
41
|
const [retryCount, setRetryCount] = useState(0);
|
|
42
42
|
const [isOnline, setIsOnline] = useState(
|
|
@@ -138,7 +138,7 @@ export const ConnectionStatusProvider = ({
|
|
|
138
138
|
);
|
|
139
139
|
};
|
|
140
140
|
|
|
141
|
-
export const useConnectionStatus = () => {
|
|
141
|
+
export const useConnectionStatus = (): ConnectionStatusContextValue => {
|
|
142
142
|
const context = useContext(ConnectionStatusContext);
|
|
143
143
|
if (!context) {
|
|
144
144
|
throw new Error(
|
|
@@ -153,6 +153,7 @@ export const useConnectionStatus = () => {
|
|
|
153
153
|
* Returns null if not within a ConnectionStatusProvider (for backwards compatibility).
|
|
154
154
|
*/
|
|
155
155
|
|
|
156
|
-
export const useConnectionStatusOptional =
|
|
157
|
-
|
|
158
|
-
|
|
156
|
+
export const useConnectionStatusOptional =
|
|
157
|
+
(): ConnectionStatusContextValue | null => {
|
|
158
|
+
return useContext(ConnectionStatusContext);
|
|
159
|
+
};
|
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
wrapToolsWithApproval,
|
|
18
18
|
wrapToolsWithByteCap,
|
|
19
19
|
type ApprovalHelpers,
|
|
20
|
-
type FrontendTool,
|
|
21
20
|
} from "@/lib/tools";
|
|
22
21
|
import { compactForModel } from "@/lib/contextCompaction";
|
|
23
22
|
import { describeStreamError } from "@/lib/streamErrorMessage";
|
|
@@ -49,6 +48,8 @@ import {
|
|
|
49
48
|
type ChatTransport,
|
|
50
49
|
type UIMessage,
|
|
51
50
|
} from "ai";
|
|
51
|
+
|
|
52
|
+
type UIMessagePart = UIMessage["parts"][number];
|
|
52
53
|
import {
|
|
53
54
|
ReactNode,
|
|
54
55
|
useCallback,
|
|
@@ -67,30 +68,47 @@ import { ElementsContext } from "./contexts";
|
|
|
67
68
|
import { ToolApprovalProvider } from "./ToolApprovalContext";
|
|
68
69
|
import { ToolExecutionProvider } from "./ToolExecutionContext";
|
|
69
70
|
|
|
71
|
+
// Reads the active local thread id from the runtime's threads store. Goes
|
|
72
|
+
// through assistant-ui's public ThreadListRuntime.getState() API.
|
|
73
|
+
function getActiveLocalThreadId(
|
|
74
|
+
runtimeRef: React.RefObject<ReturnType<typeof useChatRuntime> | null>,
|
|
75
|
+
): string | undefined {
|
|
76
|
+
const threadsState = runtimeRef.current?.threads.getState();
|
|
77
|
+
if (!threadsState) return undefined;
|
|
78
|
+
// `mainThreadId` is always populated by the SDK; the secondary read is a
|
|
79
|
+
// defensive fallback in case the SDK ever returns a state shape with an
|
|
80
|
+
// older `threadIds` field instead. The cast widens to an indexable shape
|
|
81
|
+
// because `ThreadListState` doesn't declare that historical field.
|
|
82
|
+
const legacy = (threadsState as { threadIds?: readonly string[] }).threadIds;
|
|
83
|
+
return threadsState.mainThreadId ?? legacy?.[0];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
type ExecutableTool = {
|
|
87
|
+
execute?: (args: unknown, options?: unknown) => Promise<unknown>;
|
|
88
|
+
};
|
|
89
|
+
|
|
70
90
|
/**
|
|
71
91
|
* Extracts executable tools from frontend tool definitions.
|
|
72
92
|
* Frontend tools created via defineFrontendTool have an unstable_tool property
|
|
73
93
|
* that contains the tool definition with execute function.
|
|
94
|
+
*
|
|
95
|
+
* The AI SDK's `ToolExecuteFunction<INPUT, OUTPUT>` signature is too strict on
|
|
96
|
+
* its second parameter (a typed `ToolCallOptions`) and too broad on its return
|
|
97
|
+
* (`AsyncIterable | PromiseLike | OUTPUT`) to match `ExecutableTool.execute`
|
|
98
|
+
* directly. The reference is copied as-is — no runtime wrapping — and only the
|
|
99
|
+
* type surface is widened.
|
|
74
100
|
*/
|
|
75
101
|
function extractExecutableTools(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
): Record<
|
|
79
|
-
string,
|
|
80
|
-
{ execute?: (args: unknown, options?: unknown) => Promise<unknown> }
|
|
81
|
-
> {
|
|
102
|
+
frontendTools: Record<string, AssistantTool> | undefined,
|
|
103
|
+
): Record<string, ExecutableTool> {
|
|
82
104
|
if (!frontendTools) return {};
|
|
83
105
|
|
|
84
106
|
return Object.fromEntries(
|
|
85
107
|
Object.entries(frontendTools).map(([name, tool]) => {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
{
|
|
91
|
-
execute: toolDef?.execute,
|
|
92
|
-
},
|
|
93
|
-
];
|
|
108
|
+
const toolDef = tool.unstable_tool as {
|
|
109
|
+
execute?: ExecutableTool["execute"];
|
|
110
|
+
};
|
|
111
|
+
return [name, { execute: toolDef.execute }];
|
|
94
112
|
}),
|
|
95
113
|
);
|
|
96
114
|
}
|
|
@@ -141,13 +159,14 @@ function cleanMessagesForModel(messages: UIMessage[]): UIMessage[] {
|
|
|
141
159
|
return message;
|
|
142
160
|
}
|
|
143
161
|
|
|
144
|
-
// Process each part: strip providerOptions/providerMetadata and filter reasoning
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
162
|
+
// Process each part: strip providerOptions/providerMetadata and filter reasoning.
|
|
163
|
+
// `callProviderMetadata` is not declared on `UIMessagePart`, so we widen the
|
|
164
|
+
// part to an indexable record just for the destructure.
|
|
165
|
+
const cleanedParts = partsArray.map((part) => {
|
|
166
|
+
const { callProviderMetadata: _omit, ...cleanPart } =
|
|
167
|
+
part as UIMessagePart & { callProviderMetadata?: unknown };
|
|
168
|
+
void _omit;
|
|
169
|
+
return cleanPart as UIMessagePart;
|
|
151
170
|
});
|
|
152
171
|
|
|
153
172
|
return {
|
|
@@ -205,6 +224,7 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
|
|
|
205
224
|
projectSlug: config.projectSlug,
|
|
206
225
|
variant: config.variant,
|
|
207
226
|
});
|
|
227
|
+
// oxlint-disable-next-line react-hooks/exhaustive-deps -- one-time init at mount; later config changes are intentionally ignored
|
|
208
228
|
}, []);
|
|
209
229
|
|
|
210
230
|
// Generate a stable chat ID for server-side persistence (when history is disabled)
|
|
@@ -214,7 +234,11 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
|
|
|
214
234
|
// State to expose the current chat ID via context
|
|
215
235
|
const [currentChatId, setCurrentChatId] = useState<string | null>(null);
|
|
216
236
|
|
|
217
|
-
const {
|
|
237
|
+
const {
|
|
238
|
+
data: mcpTools,
|
|
239
|
+
mcpHeaders,
|
|
240
|
+
isLoading: mcpQueryLoading,
|
|
241
|
+
} = useMCPTools({
|
|
218
242
|
auth,
|
|
219
243
|
mcp: config.mcp,
|
|
220
244
|
mcps: config.mcps,
|
|
@@ -222,6 +246,11 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
|
|
|
222
246
|
toolsToInclude: config.tools?.toolsToInclude,
|
|
223
247
|
gramEnvironment: config.gramEnvironment,
|
|
224
248
|
});
|
|
249
|
+
// Treat auth-loading as "tools not yet resolved" too — the MCP query is
|
|
250
|
+
// disabled (and so not "loading") until auth settles, so without this a
|
|
251
|
+
// tool-list consumer would briefly see an empty, settled state before tools
|
|
252
|
+
// arrive.
|
|
253
|
+
const mcpToolsLoading = auth.isLoading || mcpQueryLoading;
|
|
225
254
|
|
|
226
255
|
// Store approval helpers in ref so they can be used in async contexts
|
|
227
256
|
const approvalHelpersRef = useRef<ApprovalHelpers>({
|
|
@@ -276,8 +305,10 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
|
|
|
276
305
|
// in a way that's accessible from the transport's sendMessages function.
|
|
277
306
|
const currentRemoteIdRef = useRef<string | null>(null);
|
|
278
307
|
|
|
279
|
-
// Create chat transport configuration
|
|
280
|
-
|
|
308
|
+
// Create chat transport configuration. This is the built-in client-side
|
|
309
|
+
// streaming transport; a consumer can override it via config.transport (see
|
|
310
|
+
// below) to route the conversation through a server-side assistant instead.
|
|
311
|
+
const defaultTransport = useMemo<ChatTransport<UIMessage>>(
|
|
281
312
|
() => ({
|
|
282
313
|
sendMessages: async ({ messages, abortSignal }) => {
|
|
283
314
|
const usingCustomModel = !!config.languageModel;
|
|
@@ -298,12 +329,7 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
|
|
|
298
329
|
// chatId is already set correctly from the synced ref
|
|
299
330
|
} else if (isLocalThreadId(chatId) || !chatId) {
|
|
300
331
|
// For local thread IDs or no ID, check/generate UUID mapping
|
|
301
|
-
|
|
302
|
-
const runtimeAny = runtimeRef.current as any;
|
|
303
|
-
const threadsState = runtimeAny?.threads?.getState?.();
|
|
304
|
-
const localThreadId = (threadsState?.mainThreadId ??
|
|
305
|
-
threadsState?.threadIds?.[0]) as string | undefined;
|
|
306
|
-
|
|
332
|
+
const localThreadId = getActiveLocalThreadId(runtimeRef);
|
|
307
333
|
const lookupKey = chatId ?? localThreadId;
|
|
308
334
|
if (lookupKey) {
|
|
309
335
|
const existingUuid = localIdToUuidMapRef.current.get(lookupKey);
|
|
@@ -512,8 +538,11 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
|
|
|
512
538
|
config.contextCompaction?.maxTokens,
|
|
513
539
|
config.contextCompaction?.compactAtFraction,
|
|
514
540
|
config.contextCompaction?.keepRecent,
|
|
541
|
+
config.gramEnvironment,
|
|
542
|
+
config.api?.headers,
|
|
515
543
|
model,
|
|
516
544
|
mcpTools,
|
|
545
|
+
mcpHeaders,
|
|
517
546
|
getApprovalHelpers,
|
|
518
547
|
apiUrl,
|
|
519
548
|
auth.isLoading,
|
|
@@ -521,6 +550,44 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
|
|
|
521
550
|
],
|
|
522
551
|
);
|
|
523
552
|
|
|
553
|
+
// A consumer-supplied transport (e.g. a server-side assistant transport) takes
|
|
554
|
+
// precedence over the built-in client-side one. It may be a ChatTransport or a
|
|
555
|
+
// factory: a factory is invoked here, inside the provider, with a getChatId()
|
|
556
|
+
// sourced from the synced thread state, so the transport can read the active
|
|
557
|
+
// chat id at send time without reaching into Elements internals. Local
|
|
558
|
+
// (unpersisted) thread ids read as null so the transport can treat them as a
|
|
559
|
+
// brand-new conversation.
|
|
560
|
+
const getChatId = useCallback(() => {
|
|
561
|
+
const id = currentRemoteIdRef.current;
|
|
562
|
+
return id && !isLocalThreadId(id) ? id : null;
|
|
563
|
+
}, []);
|
|
564
|
+
// Capture the active local thread identity now and return a bind function
|
|
565
|
+
// closing over it. Consumer transports call this at the start of
|
|
566
|
+
// `sendMessages`; once a server-minted chat id is known, invoking the
|
|
567
|
+
// returned function reconciles the captured thread to it — the same
|
|
568
|
+
// reconciliation the built-in transport does inline when it generates an id.
|
|
569
|
+
// Closing over the captured id (instead of re-reading active state at bind
|
|
570
|
+
// time) is what makes a thread switch or a parallel send on another thread
|
|
571
|
+
// during the round-trip safe.
|
|
572
|
+
const adoptChatId = useCallback(() => {
|
|
573
|
+
const capturedLocalThreadId = getActiveLocalThreadId(runtimeRef);
|
|
574
|
+
return (chatId: string) => {
|
|
575
|
+
if (capturedLocalThreadId) {
|
|
576
|
+
localIdToUuidMapRef.current.set(capturedLocalThreadId, chatId);
|
|
577
|
+
}
|
|
578
|
+
currentRemoteIdRef.current = chatId;
|
|
579
|
+
mcpHeaders["Gram-Chat-ID"] = chatId;
|
|
580
|
+
setCurrentChatId(chatId);
|
|
581
|
+
};
|
|
582
|
+
}, [mcpHeaders, setCurrentChatId]);
|
|
583
|
+
const configTransport = config.transport;
|
|
584
|
+
const transport = useMemo<ChatTransport<UIMessage>>(() => {
|
|
585
|
+
if (typeof configTransport === "function") {
|
|
586
|
+
return configTransport({ getChatId, adoptChatId });
|
|
587
|
+
}
|
|
588
|
+
return configTransport ?? defaultTransport;
|
|
589
|
+
}, [configTransport, defaultTransport, getChatId, adoptChatId]);
|
|
590
|
+
|
|
524
591
|
const historyEnabled = config.history?.enabled ?? false;
|
|
525
592
|
|
|
526
593
|
// Shared context value for ElementsContext
|
|
@@ -535,8 +602,9 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
|
|
|
535
602
|
setIsOpen,
|
|
536
603
|
plugins,
|
|
537
604
|
mcpTools,
|
|
605
|
+
mcpToolsLoading,
|
|
538
606
|
}),
|
|
539
|
-
[config, model, isExpanded, isOpen, plugins, mcpTools],
|
|
607
|
+
[config, model, isExpanded, isOpen, plugins, mcpTools, mcpToolsLoading],
|
|
540
608
|
);
|
|
541
609
|
|
|
542
610
|
const frontendTools = config.tools?.frontendTools ?? {};
|
|
@@ -610,8 +678,7 @@ interface ElementsProviderWithHistoryProps {
|
|
|
610
678
|
headers: Record<string, string>;
|
|
611
679
|
contextValue: React.ContextType<typeof ElementsContext>;
|
|
612
680
|
runtimeRef: React.RefObject<ReturnType<typeof useChatRuntime> | null>;
|
|
613
|
-
|
|
614
|
-
frontendTools: Record<string, AssistantTool | FrontendTool<any, any>>;
|
|
681
|
+
frontendTools: Record<string, AssistantTool>;
|
|
615
682
|
localIdToUuidMap: Map<string, string>;
|
|
616
683
|
currentRemoteIdRef: React.RefObject<string | null>;
|
|
617
684
|
executableTools: ExecutableToolSet;
|
|
@@ -658,6 +725,9 @@ const ElementsProviderWithHistory = ({
|
|
|
658
725
|
apiUrl,
|
|
659
726
|
headers,
|
|
660
727
|
localIdToUuidMap,
|
|
728
|
+
threadListFilters: contextValue?.config.history?.threadListFilters,
|
|
729
|
+
deferThreadIdMinting: contextValue?.config.history?.deferThreadIdMinting,
|
|
730
|
+
transformChatMessage: contextValue?.config.history?.transformChatMessage,
|
|
661
731
|
});
|
|
662
732
|
const initialThreadId = contextValue?.config.history?.initialThreadId;
|
|
663
733
|
|
|
@@ -665,6 +735,7 @@ const ElementsProviderWithHistory = ({
|
|
|
665
735
|
// half-finished: the tool-result is patched in but the agent never resumes,
|
|
666
736
|
// so the next user message lands on top of an unresolved tool-call sequence.
|
|
667
737
|
const useChatRuntimeHook = useCallback(() => {
|
|
738
|
+
// oxlint-disable-next-line react-hooks/rules-of-hooks -- intentional: useChatRuntime is invoked by useRemoteThreadListRuntime as a hook for each thread
|
|
668
739
|
return useChatRuntime({
|
|
669
740
|
transport,
|
|
670
741
|
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
|
|
@@ -736,8 +807,7 @@ interface ElementsProviderWithoutHistoryProps {
|
|
|
736
807
|
transport: ChatTransport<UIMessage>;
|
|
737
808
|
contextValue: React.ContextType<typeof ElementsContext>;
|
|
738
809
|
runtimeRef: React.RefObject<ReturnType<typeof useChatRuntime> | null>;
|
|
739
|
-
|
|
740
|
-
frontendTools: Record<string, AssistantTool | FrontendTool<any, any>>;
|
|
810
|
+
frontendTools: Record<string, AssistantTool>;
|
|
741
811
|
executableTools: ExecutableToolSet;
|
|
742
812
|
currentChatId: string | null;
|
|
743
813
|
}
|
|
@@ -786,7 +856,9 @@ const ElementsProviderWithoutHistory = ({
|
|
|
786
856
|
|
|
787
857
|
const queryClient = new QueryClient();
|
|
788
858
|
|
|
789
|
-
export const ElementsProvider = (
|
|
859
|
+
export const ElementsProvider = (
|
|
860
|
+
props: ElementsProviderProps,
|
|
861
|
+
): React.JSX.Element => {
|
|
790
862
|
return (
|
|
791
863
|
<QueryClientProvider client={queryClient}>
|
|
792
864
|
<ConnectionStatusProvider>
|
|
@@ -2,6 +2,6 @@ import { createContext, useContext } from "react";
|
|
|
2
2
|
|
|
3
3
|
export const ReplayContext = createContext<{ isReplay: boolean } | null>(null);
|
|
4
4
|
|
|
5
|
-
export function useReplayContext() {
|
|
5
|
+
export function useReplayContext(): { isReplay: boolean } | null {
|
|
6
6
|
return useContext(ReplayContext);
|
|
7
7
|
}
|
|
@@ -26,7 +26,11 @@ interface ToolApprovalContextType {
|
|
|
26
26
|
getPendingApproval: (toolCallId: string) => PendingApproval | undefined;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
export function ToolApprovalProvider({
|
|
29
|
+
export function ToolApprovalProvider({
|
|
30
|
+
children,
|
|
31
|
+
}: {
|
|
32
|
+
children: ReactNode;
|
|
33
|
+
}): React.JSX.Element {
|
|
30
34
|
const [pendingApprovals, setPendingApprovals] = useState<
|
|
31
35
|
Map<string, PendingApproval>
|
|
32
36
|
>(new Map());
|
|
@@ -36,7 +36,7 @@ interface ToolExecutionProviderProps {
|
|
|
36
36
|
export function ToolExecutionProvider({
|
|
37
37
|
children,
|
|
38
38
|
tools,
|
|
39
|
-
}: ToolExecutionProviderProps) {
|
|
39
|
+
}: ToolExecutionProviderProps): React.JSX.Element {
|
|
40
40
|
const executeTool = useCallback(
|
|
41
41
|
async (
|
|
42
42
|
toolName: string,
|
package/src/hooks/useAuth.ts
CHANGED
|
@@ -131,7 +131,8 @@ export const useAuth = ({
|
|
|
131
131
|
// useMCPTools). Today the dashboard uses this to forward
|
|
132
132
|
// `Authorization: Bearer <user-session JWT>` so the runtime gateway can
|
|
133
133
|
// resolve the user's upstream credentials for issuer-gated toolsets.
|
|
134
|
-
const
|
|
134
|
+
const authHeaders = auth?.headers;
|
|
135
|
+
const extraHeaders = useMemo(() => authHeaders ?? {}, [authHeaders]);
|
|
135
136
|
|
|
136
137
|
const ensureValidHeaders = useCallback(async (): Promise<
|
|
137
138
|
Record<string, string>
|
package/src/hooks/useDensity.ts
CHANGED
|
@@ -102,7 +102,7 @@ type DensityToken = keyof (typeof densityClasses)["normal"];
|
|
|
102
102
|
* Hook to get density classes based on theme config
|
|
103
103
|
* Use: const d = useDensity(); then d('p-md') returns the appropriate padding class
|
|
104
104
|
*/
|
|
105
|
-
export const useDensity = () => {
|
|
105
|
+
export const useDensity = (): ((token: DensityToken) => string) => {
|
|
106
106
|
const { config } = useElements();
|
|
107
107
|
const density = config.theme?.density ?? "normal";
|
|
108
108
|
|
package/src/hooks/useElements.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { useContext } from "react";
|
|
2
2
|
import { ElementsContext } from "@/contexts/contexts";
|
|
3
|
+
import type { ElementsContextType } from "@/types";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* @private Internal hook to access the ElementsContext
|
|
6
7
|
*
|
|
7
8
|
*/
|
|
8
|
-
export const useElements = () => {
|
|
9
|
+
export const useElements = (): ElementsContextType => {
|
|
9
10
|
const context = useContext(ElementsContext);
|
|
10
11
|
if (!context) {
|
|
11
12
|
throw new Error("useElements must be used within a ElementsProvider");
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { useReplayContext } from "@/contexts/ReplayContext";
|
|
2
|
-
import { getApiUrl } from "@/lib/api";
|
|
3
2
|
import { useAssistantState } from "@assistant-ui/react";
|
|
4
3
|
import { generateObject } from "ai";
|
|
5
4
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
@@ -64,8 +63,6 @@ export function useFollowOnSuggestions(): {
|
|
|
64
63
|
const isRunning = useAssistantState(({ thread }) => thread.isRunning);
|
|
65
64
|
const messages = useAssistantState(({ thread }) => thread.messages);
|
|
66
65
|
|
|
67
|
-
const apiUrl = getApiUrl(config);
|
|
68
|
-
|
|
69
66
|
const fetchSuggestions = useCallback(async () => {
|
|
70
67
|
if (!isEnabled || auth.isLoading || !auth.headers) return;
|
|
71
68
|
|
|
@@ -89,7 +86,7 @@ export function useFollowOnSuggestions(): {
|
|
|
89
86
|
let lastAssistantMessage = "";
|
|
90
87
|
for (let i = recentMessages.length - 1; i >= 0; i--) {
|
|
91
88
|
const msg = recentMessages[i];
|
|
92
|
-
if (msg.role === "assistant") {
|
|
89
|
+
if (msg && msg.role === "assistant") {
|
|
93
90
|
lastAssistantMessage = msg.content;
|
|
94
91
|
break;
|
|
95
92
|
}
|
|
@@ -191,7 +188,7 @@ ${conversation}`,
|
|
|
191
188
|
abortControllerRef.current = null;
|
|
192
189
|
}
|
|
193
190
|
}
|
|
194
|
-
}, [isEnabled,
|
|
191
|
+
}, [isEnabled, auth.headers, auth.isLoading, messages, model]);
|
|
195
192
|
|
|
196
193
|
// Fetch suggestions when:
|
|
197
194
|
// 1. The thread stops running (assistant finished responding)
|
|
@@ -224,7 +221,7 @@ ${conversation}`,
|
|
|
224
221
|
if (lastProcessedMessageIdRef.current === lastMessage.id) return;
|
|
225
222
|
|
|
226
223
|
lastProcessedMessageIdRef.current = lastMessage.id;
|
|
227
|
-
fetchSuggestions();
|
|
224
|
+
void fetchSuggestions();
|
|
228
225
|
}, [isRunning, messages, fetchSuggestions, auth.isLoading, auth.headers]);
|
|
229
226
|
|
|
230
227
|
// Cleanup on unmount
|
|
@@ -10,9 +10,11 @@ import { createAssistantStream, type AssistantStream } from "assistant-stream";
|
|
|
10
10
|
import {
|
|
11
11
|
GramChatOverview,
|
|
12
12
|
GramChat,
|
|
13
|
+
GramChatMessage,
|
|
13
14
|
convertGramMessagesToExported,
|
|
14
15
|
convertGramMessagesToUIMessages,
|
|
15
16
|
} from "@/lib/messageConverter";
|
|
17
|
+
import { sleep } from "@/lib/utils";
|
|
16
18
|
import {
|
|
17
19
|
useCallback,
|
|
18
20
|
useEffect,
|
|
@@ -37,11 +39,61 @@ export function isLocalThreadId(threadId: string | null | undefined): boolean {
|
|
|
37
39
|
return !!threadId?.startsWith(LOCAL_THREAD_ID_PREFIX);
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Polls the shared local→remote id map until the transport assigns an id for
|
|
44
|
+
* `threadId`, or a deadline passes. Used in deferred-minting mode so a new
|
|
45
|
+
* thread adopts the backend-minted chat id instead of a client-generated one.
|
|
46
|
+
* The timeout is generous to tolerate cold serverless boots on the first send.
|
|
47
|
+
*/
|
|
48
|
+
async function waitForMappedId(
|
|
49
|
+
map: Map<string, string> | undefined,
|
|
50
|
+
threadId: string,
|
|
51
|
+
timeoutMs = 30_000,
|
|
52
|
+
intervalMs = 50,
|
|
53
|
+
): Promise<string | undefined> {
|
|
54
|
+
if (!map) return undefined;
|
|
55
|
+
const deadline = Date.now() + timeoutMs;
|
|
56
|
+
for (;;) {
|
|
57
|
+
const id = map.get(threadId);
|
|
58
|
+
if (id) return id;
|
|
59
|
+
if (Date.now() >= deadline) return undefined;
|
|
60
|
+
await sleep(intervalMs);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Transforms or drops a persisted chat message before it is rendered from
|
|
66
|
+
* history. Return the (possibly rewritten) message, or `null` to omit it.
|
|
67
|
+
*/
|
|
68
|
+
export type ChatMessageTransform = (
|
|
69
|
+
message: GramChatMessage,
|
|
70
|
+
) => GramChatMessage | null;
|
|
71
|
+
|
|
40
72
|
export interface ThreadListAdapterOptions {
|
|
41
73
|
apiUrl: string;
|
|
42
74
|
headers: Record<string, string>;
|
|
43
75
|
/** Map to translate local thread IDs to UUIDs (shared with transport) */
|
|
44
76
|
localIdToUuidMap?: Map<string, string>;
|
|
77
|
+
/**
|
|
78
|
+
* Extra query parameters forwarded to `chat.list` to filter which threads are
|
|
79
|
+
* listed. Opaque to the adapter — the consumer chooses the keys.
|
|
80
|
+
*/
|
|
81
|
+
threadListFilters?: Record<string, string>;
|
|
82
|
+
/**
|
|
83
|
+
* Don't client-mint a chat id for a brand-new thread. When true, `initialize`
|
|
84
|
+
* waits for the transport to assign the id (via the shared `localIdToUuidMap`,
|
|
85
|
+
* e.g. a server-minted id reported through the transport context's
|
|
86
|
+
* `adoptChatId` bind closure) instead of generating one with
|
|
87
|
+
* `crypto.randomUUID()`. Use this when the backend owns chat-id creation.
|
|
88
|
+
*/
|
|
89
|
+
deferThreadIdMinting?: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Optional hook to transform or drop each persisted message before it is
|
|
92
|
+
* converted for rendering. Return a message to render it (possibly rewritten),
|
|
93
|
+
* or `null` to omit it. Keeps product-specific transcript conventions out of
|
|
94
|
+
* the library — see {@link HistoryConfig.transformChatMessage}.
|
|
95
|
+
*/
|
|
96
|
+
transformChatMessage?: ChatMessageTransform;
|
|
45
97
|
}
|
|
46
98
|
|
|
47
99
|
interface ListChatsResponse {
|
|
@@ -57,15 +109,41 @@ class GramThreadHistoryAdapter {
|
|
|
57
109
|
private apiUrl: string;
|
|
58
110
|
private headers: Record<string, string>;
|
|
59
111
|
private store: AssistantApi;
|
|
112
|
+
// Read lazily rather than captured: the adapter is constructed once, but the
|
|
113
|
+
// consumer may swap `transformChatMessage` across renders, so resolve it from
|
|
114
|
+
// the live options on every load instead of snapshotting it here.
|
|
115
|
+
private getTransformChatMessage?: () => ChatMessageTransform | undefined;
|
|
60
116
|
|
|
61
117
|
constructor(
|
|
62
118
|
apiUrl: string,
|
|
63
119
|
headers: Record<string, string>,
|
|
64
120
|
store: AssistantApi,
|
|
121
|
+
getTransformChatMessage?: () => ChatMessageTransform | undefined,
|
|
65
122
|
) {
|
|
66
123
|
this.apiUrl = apiUrl;
|
|
67
124
|
this.headers = headers;
|
|
68
125
|
this.store = store;
|
|
126
|
+
this.getTransformChatMessage = getTransformChatMessage;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Applies the consumer-supplied `transformChatMessage` hook to a loaded
|
|
131
|
+
* transcript: rewrites each message and drops any the hook returns `null` for.
|
|
132
|
+
* Without a hook configured the messages pass through untouched.
|
|
133
|
+
*/
|
|
134
|
+
private applyTransform(messages: GramChatMessage[]): GramChatMessage[] {
|
|
135
|
+
const transform = this.getTransformChatMessage?.();
|
|
136
|
+
if (!transform) {
|
|
137
|
+
return messages;
|
|
138
|
+
}
|
|
139
|
+
const result: GramChatMessage[] = [];
|
|
140
|
+
for (const message of messages) {
|
|
141
|
+
const transformed = transform(message);
|
|
142
|
+
if (transformed) {
|
|
143
|
+
result.push(transformed);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
69
147
|
}
|
|
70
148
|
|
|
71
149
|
async load() {
|
|
@@ -86,7 +164,7 @@ class GramThreadHistoryAdapter {
|
|
|
86
164
|
}
|
|
87
165
|
|
|
88
166
|
const chat = (await response.json()) as GramChat;
|
|
89
|
-
return convertGramMessagesToExported(chat.messages);
|
|
167
|
+
return convertGramMessagesToExported(this.applyTransform(chat.messages));
|
|
90
168
|
} catch (error) {
|
|
91
169
|
console.error("Error loading chat:", error);
|
|
92
170
|
return { messages: [], headId: null };
|
|
@@ -120,7 +198,9 @@ class GramThreadHistoryAdapter {
|
|
|
120
198
|
}
|
|
121
199
|
|
|
122
200
|
const chat = (await response.json()) as GramChat;
|
|
123
|
-
return convertGramMessagesToUIMessages(
|
|
201
|
+
return convertGramMessagesToUIMessages(
|
|
202
|
+
this.applyTransform(chat.messages),
|
|
203
|
+
);
|
|
124
204
|
|
|
125
205
|
// // Filter out system messages (assistant-ui doesn't support them in the import path)
|
|
126
206
|
// const filteredMessages = chat.messages.filter(
|
|
@@ -175,6 +255,7 @@ function useGramThreadHistoryAdapter(
|
|
|
175
255
|
optionsRef.current.apiUrl,
|
|
176
256
|
optionsRef.current.headers,
|
|
177
257
|
store,
|
|
258
|
+
() => optionsRef.current.transformChatMessage,
|
|
178
259
|
),
|
|
179
260
|
);
|
|
180
261
|
// Cast to ThreadHistoryAdapter - the withFormat generic doesn't match but works at runtime
|
|
@@ -213,12 +294,14 @@ export function useGramThreadListAdapter(
|
|
|
213
294
|
|
|
214
295
|
async list() {
|
|
215
296
|
try {
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
297
|
+
const { apiUrl, headers, threadListFilters } = optionsRef.current;
|
|
298
|
+
const qs = threadListFilters
|
|
299
|
+
? new URLSearchParams(threadListFilters).toString()
|
|
300
|
+
: "";
|
|
301
|
+
const listUrl = qs
|
|
302
|
+
? `${apiUrl}/rpc/chat.list?${qs}`
|
|
303
|
+
: `${apiUrl}/rpc/chat.list`;
|
|
304
|
+
const response = await fetch(listUrl, { headers });
|
|
222
305
|
|
|
223
306
|
if (!response.ok) {
|
|
224
307
|
console.error("Failed to list chats:", response.status);
|
|
@@ -252,6 +335,30 @@ export function useGramThreadListAdapter(
|
|
|
252
335
|
externalId: existingUuid,
|
|
253
336
|
};
|
|
254
337
|
}
|
|
338
|
+
// When the backend owns chat-id creation, don't client-mint: wait for
|
|
339
|
+
// the transport to report the server-minted id (it assigns it during
|
|
340
|
+
// the first send via the adoptChatId bind closure, populating the
|
|
341
|
+
// shared map — the same path the built-in transport uses).
|
|
342
|
+
if (optionsRef.current.deferThreadIdMinting) {
|
|
343
|
+
const mappedUuid = await waitForMappedId(
|
|
344
|
+
optionsRef.current.localIdToUuidMap,
|
|
345
|
+
threadId,
|
|
346
|
+
);
|
|
347
|
+
if (mappedUuid) {
|
|
348
|
+
return {
|
|
349
|
+
remoteId: mappedUuid,
|
|
350
|
+
externalId: mappedUuid,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
// Falling through to crypto.randomUUID() here would defeat deferred
|
|
354
|
+
// minting: the client id would race the server-minted one reported
|
|
355
|
+
// later via the adoptChatId bind closure, leaving runtime state
|
|
356
|
+
// and the local→remote map disagreeing. Surface the failure to
|
|
357
|
+
// the user instead.
|
|
358
|
+
throw new Error(
|
|
359
|
+
"Backend did not mint a chat id in time — first send may have failed or is still in flight. Retry the send.",
|
|
360
|
+
);
|
|
361
|
+
}
|
|
255
362
|
// Otherwise generate a new one and store it
|
|
256
363
|
const uuid = crypto.randomUUID();
|
|
257
364
|
optionsRef.current.localIdToUuidMap?.set(threadId, uuid);
|
|
@@ -298,7 +405,9 @@ export function useGramThreadListAdapter(
|
|
|
298
405
|
// Title generation happens async server-side via Temporal after first completion.
|
|
299
406
|
// This delay allows the OpenRouter LLM call to complete before we fetch the title.
|
|
300
407
|
const TITLE_GENERATION_DELAY_MS = 2000;
|
|
301
|
-
await new Promise((r) =>
|
|
408
|
+
await new Promise((r) => {
|
|
409
|
+
setTimeout(r, TITLE_GENERATION_DELAY_MS);
|
|
410
|
+
});
|
|
302
411
|
|
|
303
412
|
try {
|
|
304
413
|
// TODO: rename generateTitle endpoint to getTitle
|
package/src/hooks/useMCPTools.ts
CHANGED
|
@@ -42,10 +42,10 @@ export function useMCPTools({
|
|
|
42
42
|
);
|
|
43
43
|
|
|
44
44
|
const envQueryKey = Object.entries(environment ?? {}).map(
|
|
45
|
-
([k, v]) => `${k}:${v}`,
|
|
45
|
+
([k, v]) => `${k}:${String(v)}`,
|
|
46
46
|
);
|
|
47
47
|
const authQueryKey = Object.entries(auth.headers ?? {}).map(
|
|
48
|
-
([k, v]) => `${k}:${v}`,
|
|
48
|
+
([k, v]) => `${k}:${String(v)}`,
|
|
49
49
|
);
|
|
50
50
|
const serversQueryKey = servers.map(
|
|
51
51
|
(s) => `${s.url}|${s.name ?? ""}|${s.environment ?? ""}`,
|
package/src/hooks/useModel.ts
CHANGED
|
@@ -5,9 +5,7 @@ import { useAuth } from "./useAuth";
|
|
|
5
5
|
import { useElements } from "./useElements";
|
|
6
6
|
|
|
7
7
|
// Creates an OpenRouter client to be used for "internal Gram" usage, such as follow-on suggestions
|
|
8
|
-
export const useModel = (
|
|
9
|
-
model: string = "openai/gpt-5.4-mini",
|
|
10
|
-
): LanguageModel => {
|
|
8
|
+
export const useModel = (model = "openai/gpt-5.4-mini"): LanguageModel => {
|
|
11
9
|
const { config } = useElements();
|
|
12
10
|
|
|
13
11
|
const auth = useAuth({
|
|
@@ -15,7 +15,9 @@ type ComponentsByLanguage =
|
|
|
15
15
|
>
|
|
16
16
|
| undefined;
|
|
17
17
|
|
|
18
|
-
export function useComponentsByLanguage(
|
|
18
|
+
export function useComponentsByLanguage(
|
|
19
|
+
plugins: Plugin[],
|
|
20
|
+
): ComponentsByLanguage {
|
|
19
21
|
return useMemo(() => {
|
|
20
22
|
return plugins.reduce((acc, plugin) => {
|
|
21
23
|
if (acc?.[plugin.language] && !plugin.overrideExisting) {
|