@agents24/chat-react 0.5.0 → 0.5.2
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/README.md +90 -21
- package/compatibility.json +10 -10
- package/dist/chunk-SSUO4EPD.js +189 -0
- package/dist/chunk-SSUO4EPD.js.map +1 -0
- package/dist/chunk-ZCNHLFRB.js +108 -0
- package/dist/chunk-ZCNHLFRB.js.map +1 -0
- package/dist/index.cjs +112 -95
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +362 -68
- package/dist/index.js.map +1 -1
- package/dist/mcp-oauth.d.ts +13 -0
- package/dist/runtime/index.cjs +594 -0
- package/dist/runtime/index.cjs.map +1 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +458 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/input-contract.d.ts +20 -0
- package/dist/runtime/use-agent-chat-runtime.d.ts +83 -0
- package/dist/scaffold/app.d.ts +30 -0
- package/dist/scaffold/components/chat/chat-composer.d.ts +21 -0
- package/dist/scaffold/components/chat/chat-message.d.ts +21 -0
- package/dist/scaffold/components/chat/chat-shell.d.ts +8 -0
- package/dist/scaffold/components/chat/collapsible-user-message.d.ts +3 -0
- package/dist/scaffold/components/chat/external-link-dialog.d.ts +2 -0
- package/dist/scaffold/components/chat/identity-control.d.ts +4 -0
- package/dist/scaffold/components/chat/message-parts.d.ts +12 -0
- package/dist/scaffold/components/chat/theme-menu.d.ts +1 -0
- package/dist/scaffold/components/chat/thread-sidebar.d.ts +16 -0
- package/dist/scaffold/components/chat/timeline-loading.d.ts +7 -0
- package/dist/scaffold/components/chat/turn-outline.d.ts +4 -0
- package/dist/scaffold/components/theme-provider.d.ts +20 -0
- package/dist/scaffold/components/ui/attachment.d.ts +24 -0
- package/dist/scaffold/components/ui/bubble.d.ts +17 -0
- package/dist/scaffold/components/ui/button-group.d.ts +11 -0
- package/dist/scaffold/components/ui/button.d.ts +10 -0
- package/dist/scaffold/components/ui/dialog.d.ts +17 -0
- package/dist/scaffold/components/ui/dropdown-menu.d.ts +29 -0
- package/dist/scaffold/components/ui/input.d.ts +3 -0
- package/dist/scaffold/components/ui/marker.d.ts +11 -0
- package/dist/scaffold/components/ui/message-scroller.d.ts +10 -0
- package/dist/scaffold/components/ui/message.d.ts +10 -0
- package/dist/scaffold/components/ui/popover.d.ts +6 -0
- package/dist/scaffold/components/ui/select.d.ts +8 -0
- package/dist/scaffold/components/ui/separator.d.ts +4 -0
- package/dist/scaffold/components/ui/sheet.d.ts +14 -0
- package/dist/scaffold/components/ui/sidebar.d.ts +69 -0
- package/dist/scaffold/components/ui/skeleton.d.ts +2 -0
- package/dist/scaffold/components/ui/textarea.d.ts +3 -0
- package/dist/scaffold/components/ui/tooltip.d.ts +15 -0
- package/dist/scaffold/hooks/use-mobile.d.ts +1 -0
- package/dist/scaffold/identity.d.ts +10 -0
- package/dist/scaffold/index.cjs +2270 -0
- package/dist/scaffold/index.cjs.map +1 -0
- package/dist/scaffold/index.d.ts +14 -0
- package/dist/scaffold/index.js +2244 -0
- package/dist/scaffold/index.js.map +1 -0
- package/dist/scaffold/lib/utils.d.ts +2 -0
- package/dist/styles.css +137 -378
- package/dist/types.d.ts +3 -13
- package/dist/ui/adapters.d.ts +1 -1
- package/dist/ui/agent-chat-actions.d.ts +12 -5
- package/dist/ui/agent-chat-composer.d.ts +2 -1
- package/dist/ui/agent-chat-context-compression-row.d.ts +5 -0
- package/dist/ui/agent-chat-context-status.d.ts +13 -0
- package/dist/ui/agent-chat-message.d.ts +7 -5
- package/dist/ui/agent-response-timeline.d.ts +29 -7
- package/dist/ui/ask-user-interaction-state.d.ts +20 -0
- package/dist/ui/ask-user-interaction.d.ts +8 -0
- package/dist/ui/index.cjs +1391 -271
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.d.ts +5 -0
- package/dist/ui/index.js +2380 -65
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/tool-sessions.d.ts +70 -0
- package/dist/ui/use-audio-recorder.d.ts +8 -0
- package/package.json +27 -12
- package/scaffold/components.json +25 -0
- package/scaffold/manifest.json +28 -0
- package/scaffold/src/app.tsx +71 -0
- package/scaffold/src/chat.css +54 -0
- package/scaffold/src/components/chat/chat-composer.tsx +237 -0
- package/scaffold/src/components/chat/chat-message.tsx +76 -0
- package/scaffold/src/components/chat/chat-shell.tsx +257 -0
- package/scaffold/src/components/chat/collapsible-user-message.tsx +37 -0
- package/scaffold/src/components/chat/external-link-dialog.tsx +48 -0
- package/scaffold/src/components/chat/identity-control.tsx +69 -0
- package/scaffold/src/components/chat/message-parts.tsx +83 -0
- package/scaffold/src/components/chat/theme-menu.tsx +79 -0
- package/scaffold/src/components/chat/thread-sidebar.tsx +114 -0
- package/scaffold/src/components/chat/timeline-loading.ts +17 -0
- package/scaffold/src/components/chat/turn-outline.tsx +73 -0
- package/scaffold/src/components/theme-provider.tsx +91 -0
- package/scaffold/src/components/ui/attachment.tsx +204 -0
- package/scaffold/src/components/ui/bubble.tsx +125 -0
- package/scaffold/src/components/ui/button-group.tsx +83 -0
- package/scaffold/src/components/ui/button.tsx +65 -0
- package/scaffold/src/components/ui/dialog.tsx +171 -0
- package/scaffold/src/components/ui/dropdown-menu.tsx +271 -0
- package/scaffold/src/components/ui/input.tsx +19 -0
- package/scaffold/src/components/ui/marker.tsx +69 -0
- package/scaffold/src/components/ui/message-scroller.tsx +129 -0
- package/scaffold/src/components/ui/message.tsx +92 -0
- package/scaffold/src/components/ui/popover.tsx +40 -0
- package/scaffold/src/components/ui/select.tsx +92 -0
- package/scaffold/src/components/ui/separator.tsx +28 -0
- package/scaffold/src/components/ui/sheet.tsx +150 -0
- package/scaffold/src/components/ui/sidebar.tsx +677 -0
- package/scaffold/src/components/ui/skeleton.tsx +13 -0
- package/scaffold/src/components/ui/textarea.tsx +18 -0
- package/scaffold/src/components/ui/tooltip.tsx +56 -0
- package/scaffold/src/hooks/use-mobile.ts +24 -0
- package/scaffold/src/identity.ts +17 -0
- package/scaffold/src/index.css +222 -0
- package/scaffold/src/index.tsx +39 -0
- package/scaffold/src/lib/utils.ts +6 -0
- package/scaffold/styles.css +3 -0
- package/dist/chunk-CHAU7FNW.js +0 -140
- package/dist/chunk-CHAU7FNW.js.map +0 -1
- package/dist/chunk-LI67M74F.js +0 -445
- package/dist/chunk-LI67M74F.js.map +0 -1
- package/dist/chunk-R7WFL3YR.js +0 -1327
- package/dist/chunk-R7WFL3YR.js.map +0 -1
- package/dist/renderers.d.ts +0 -10
- package/dist/templates/agent-chat-app.d.ts +0 -18
- package/dist/templates/agent-chat-layout.d.ts +0 -77
- package/dist/templates/agent-chat-shell.d.ts +0 -37
- package/dist/templates/appearance-control.d.ts +0 -5
- package/dist/templates/index.cjs +0 -2600
- package/dist/templates/index.cjs.map +0 -1
- package/dist/templates/index.d.ts +0 -4
- package/dist/templates/index.js +0 -846
- package/dist/templates/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,36 +1,105 @@
|
|
|
1
1
|
# `@agents24/chat-react`
|
|
2
2
|
|
|
3
|
-
Last Updated: 2026-
|
|
3
|
+
Last Updated: 2026-08-06
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Reusable React chat UI, runtime orchestration, and the canonical source-owned Agents24 chat scaffold.
|
|
6
6
|
|
|
7
7
|
```tsx
|
|
8
|
-
import {
|
|
9
|
-
import "@agents24/chat-react/styles.css";
|
|
10
|
-
|
|
11
|
-
<
|
|
12
|
-
agent={{ name: "Support Agent" }}
|
|
13
|
-
client={client}
|
|
14
|
-
mcpRedirectUri={window.location.href}
|
|
15
|
-
targetSelector={{ label: "Deployment ID", value: deploymentId, onApply: setDeploymentId }}
|
|
16
|
-
/>;
|
|
8
|
+
import { Agents24ChatScaffold } from "@agents24/chat-react/scaffold";
|
|
9
|
+
import "@agents24/chat-react/scaffold/styles.css";
|
|
10
|
+
|
|
11
|
+
<Agents24ChatScaffold client={client} agent={{ name: "Support Agent" }} />;
|
|
17
12
|
```
|
|
18
13
|
|
|
19
14
|
## Entry points
|
|
20
15
|
|
|
21
|
-
- `@agents24/chat-react`:
|
|
22
|
-
- `@agents24/chat-react/ui`:
|
|
23
|
-
- `@agents24/chat-react/
|
|
24
|
-
- `@agents24/chat-react/
|
|
16
|
+
- `@agents24/chat-react`: secure MCP popup helpers, streaming helpers, and `LatestThreadScroller`.
|
|
17
|
+
- `@agents24/chat-react/ui`: `AgentResponseTimeline`, `AgentChatPart`, tool rows/groups, `AskUserInteraction`, and reusable shadcn-derived message, response, composer, attachment, and HITL primitives.
|
|
18
|
+
- `@agents24/chat-react/runtime`: shared V2 input-contract validation, bootstrap, thread, batch upload, model, HITL, MCP, and error orchestration over `@agents24/react`.
|
|
19
|
+
- `@agents24/chat-react/scaffold`: the complete sidebar/header/transcript/composer application.
|
|
20
|
+
- `@agents24/chat-react/scaffold/styles.css`: scoped shadcn/Tailwind scaffold styles.
|
|
21
|
+
|
|
22
|
+
The scaffold renders only the text/file/image/audio controls authorized by bootstrap `features.inputs`. It enforces required modalities and shared count/byte limits before send, and its microphone recorder negotiates and preserves the browser's actual supported audio MIME.
|
|
23
|
+
- `@agents24/chat-react/styles.css`: generic low-level primitive styles for custom hosts.
|
|
24
|
+
|
|
25
|
+
The scaffold accepts an `AgentClient` and optional controlled `activeThreadId`. Its `onActiveThreadIdChange` callback reports `created`, `opened`, `new`, or `deleted`; the host decides the URL shape and history policy.
|
|
26
|
+
|
|
27
|
+
## Timeline customization
|
|
28
|
+
|
|
29
|
+
`@agents24/react` projects backend response blocks into canonical ordered `message.parts`. The default scaffold renders that sequence through `AgentResponseTimeline`, preserving interleaved text, tools, HITL, and resumed text.
|
|
30
|
+
|
|
31
|
+
To reorder the polished default for display only:
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { AgentResponseTimeline } from "@agents24/chat-react/ui";
|
|
35
|
+
import type { Agents24ChatTimelineSlotProps } from "@agents24/chat-react/scaffold";
|
|
36
|
+
|
|
37
|
+
function Timeline({ parts, ...props }: Agents24ChatTimelineSlotProps) {
|
|
38
|
+
const displayParts = [...parts].sort(
|
|
39
|
+
(left, right) => Number(right.kind === "tool") - Number(left.kind === "tool")
|
|
40
|
+
);
|
|
41
|
+
return <AgentResponseTimeline {...props} parts={displayParts} />;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
<Agents24ChatScaffold client={client} components={{ Timeline }} />;
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
For a fully custom timeline, render `parts` directly and reuse `AgentChatPart` selectively:
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
import { AgentChatPart } from "@agents24/chat-react/ui";
|
|
51
|
+
import type { Agents24ChatTimelineSlotProps } from "@agents24/chat-react/scaffold";
|
|
52
|
+
|
|
53
|
+
function Timeline({ message, parts, isLoading, getHitlActionState, onHitlAction }: Agents24ChatTimelineSlotProps) {
|
|
54
|
+
return (
|
|
55
|
+
<section aria-label="Assistant response">
|
|
56
|
+
{parts.filter((part) => part.kind !== "reasoning").map((part) => (
|
|
57
|
+
<AgentChatPart
|
|
58
|
+
key={part.id}
|
|
59
|
+
message={message}
|
|
60
|
+
part={part}
|
|
61
|
+
isLoading={isLoading}
|
|
62
|
+
getHitlActionState={getHitlActionState}
|
|
63
|
+
onHitlAction={onHitlAction}
|
|
64
|
+
/>
|
|
65
|
+
))}
|
|
66
|
+
</section>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Never mutate `message.parts`; a replacement Timeline owns its display order, accessibility, and HITL visibility.
|
|
72
|
+
|
|
73
|
+
## Streaming presentation
|
|
74
|
+
|
|
75
|
+
Choose the level that fits the host:
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
// 1. Default renderer
|
|
79
|
+
<MessageResponse streaming={isStreaming} streamingId={part.id}>
|
|
80
|
+
{part.text}
|
|
81
|
+
</MessageResponse>
|
|
82
|
+
|
|
83
|
+
// 2. Arbitrary component with headless smoothing
|
|
84
|
+
const { displayedText } = useStreamingText({
|
|
85
|
+
id: part.id,
|
|
86
|
+
isStreaming,
|
|
87
|
+
text: part.text,
|
|
88
|
+
});
|
|
89
|
+
return <MyMarkdownRenderer value={displayedText} />;
|
|
90
|
+
|
|
91
|
+
// 3. Raw canonical state, with no smoother
|
|
92
|
+
return <MyMarkdownRenderer value={part.text} />;
|
|
93
|
+
```
|
|
25
94
|
|
|
26
|
-
`
|
|
95
|
+
`useStreamingText` is presentation-only and has no dependency on `MessageResponse`, package context, or transport.
|
|
27
96
|
|
|
28
|
-
|
|
97
|
+
## Ask-User HITL
|
|
29
98
|
|
|
30
|
-
`
|
|
99
|
+
`user_input` HITL renders one required question at a time (up to three), with 2–4 numbered options and an inline free-text Other row. A single-choice selection advances immediately and submits from the final question; multi-select and existing drafts advance through the compact header chevrons. Free text reveals an inline Next or Submit action only after text is entered. The component is LTR by default and follows an inherited `dir="rtl"` without host-specific navigation logic. Submit `action: "respond"` with ordered answers. Resolved requests collapse to **Asked 1 question** or **Asked N questions**; expanding shows each question with its submitted answer.
|
|
31
100
|
|
|
32
|
-
|
|
101
|
+
The default scaffold uses the Soft shadcn preset `b2D1FHzlY`. Hosted Agent shares can select Soft or the scoped Sharp theme from preset `b4XdOjWBjV`. `create-agents24-app` copies the exact editable source and can apply another preset.
|
|
33
102
|
|
|
34
|
-
|
|
103
|
+
For strict Vite CSP, load the application stylesheet with an HTML `<link>` and import the scaffold stylesheet from CSS. Do not add `unsafe-inline`.
|
|
35
104
|
|
|
36
|
-
React and ReactDOM `>=19 <20` are required.
|
|
105
|
+
The package does not own credentials, routes, Fetch/SSE, token refresh, or product-specific citations, TTS, artifacts, and debug controls. React and ReactDOM `>=19 <20` are required.
|
package/compatibility.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"contractHash": "
|
|
2
|
+
"contractHash": "13f59fd11fc838b1f7a51afe15afcdb6f1429fed94e37a1341c97061882e57d2",
|
|
3
3
|
"contractVersion": "0.2.0",
|
|
4
4
|
"corpus": "conformance/v1/corpus.json",
|
|
5
5
|
"generatedBy": "@agents24/sdk-contract",
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
"reactDom": ">=19 <20"
|
|
16
16
|
},
|
|
17
17
|
"@agents24/client": {
|
|
18
|
-
"contract": "
|
|
19
|
-
"runtime": "run-stream.
|
|
18
|
+
"contract": "13f59fd11fc838b1f7a51afe15afcdb6f1429fed94e37a1341c97061882e57d2",
|
|
19
|
+
"runtime": "run-stream.v3"
|
|
20
20
|
},
|
|
21
21
|
"@agents24/node": {
|
|
22
|
-
"contract": "
|
|
22
|
+
"contract": "13f59fd11fc838b1f7a51afe15afcdb6f1429fed94e37a1341c97061882e57d2",
|
|
23
23
|
"node": "^22.0.0 || ^24.0.0"
|
|
24
24
|
},
|
|
25
25
|
"@agents24/react": {
|
|
@@ -27,18 +27,18 @@
|
|
|
27
27
|
"react": ">=19 <20"
|
|
28
28
|
},
|
|
29
29
|
"@talmudpedia/runtime-sdk": {
|
|
30
|
-
"contract": "
|
|
30
|
+
"contract": "13f59fd11fc838b1f7a51afe15afcdb6f1429fed94e37a1341c97061882e57d2",
|
|
31
31
|
"protocolOwner": "@agents24/client"
|
|
32
32
|
},
|
|
33
33
|
"agents24": {
|
|
34
|
-
"contract": "
|
|
34
|
+
"contract": "13f59fd11fc838b1f7a51afe15afcdb6f1429fed94e37a1341c97061882e57d2",
|
|
35
35
|
"python": ">=3.10"
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"protocols": {
|
|
39
39
|
"runtime": {
|
|
40
40
|
"minimum": "2.0.0",
|
|
41
|
-
"wire": "run-stream.
|
|
41
|
+
"wire": "run-stream.v3"
|
|
42
42
|
},
|
|
43
43
|
"threadSummary": {
|
|
44
44
|
"minimum": "1.0.0",
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
},
|
|
48
48
|
"schemaVersion": "agents24.compatibility.v1",
|
|
49
49
|
"targetOperationCounts": {
|
|
50
|
-
"client":
|
|
51
|
-
"node":
|
|
52
|
-
"python":
|
|
50
|
+
"client": 22,
|
|
51
|
+
"node": 78,
|
|
52
|
+
"python": 69
|
|
53
53
|
}
|
|
54
54
|
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// src/streaming-text.ts
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
var DEFAULT_COMPLETED_TEXT_CACHE_SIZE = 200;
|
|
4
|
+
var STREAM_RUNWAY_MS = 80;
|
|
5
|
+
var STREAM_IDLE_RELEASE_MS = 120;
|
|
6
|
+
var defaultCompletedTextCache = /* @__PURE__ */ new Map();
|
|
7
|
+
var defaultStreamingTextCache = {
|
|
8
|
+
get: (id) => defaultCompletedTextCache.get(id),
|
|
9
|
+
set: (id, text) => {
|
|
10
|
+
defaultCompletedTextCache.set(id, text);
|
|
11
|
+
if (defaultCompletedTextCache.size > DEFAULT_COMPLETED_TEXT_CACHE_SIZE) {
|
|
12
|
+
const firstKey = defaultCompletedTextCache.keys().next().value;
|
|
13
|
+
if (firstKey) defaultCompletedTextCache.delete(firstKey);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var getActiveStreamingTextPartId = (message, streamingMessageId) => {
|
|
18
|
+
if (message.role !== "assistant" || streamingMessageId !== message.id) return null;
|
|
19
|
+
const trailingPart = message.parts[message.parts.length - 1];
|
|
20
|
+
return trailingPart?.kind === "text" ? trailingPart.id : null;
|
|
21
|
+
};
|
|
22
|
+
var isActiveStreamingTextPart = (message, part, streamingMessageId) => part.kind === "text" && part.id === getActiveStreamingTextPartId(message, streamingMessageId);
|
|
23
|
+
var graphemeEndOffsets = (value) => {
|
|
24
|
+
if (!value) return [];
|
|
25
|
+
if (typeof Intl !== "undefined" && "Segmenter" in Intl) {
|
|
26
|
+
const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
27
|
+
return Array.from(segmenter.segment(value), ({ index, segment }) => index + segment.length);
|
|
28
|
+
}
|
|
29
|
+
const characters = Array.from(value);
|
|
30
|
+
const offsets = [];
|
|
31
|
+
let offset = 0;
|
|
32
|
+
let regionalIndicators = 0;
|
|
33
|
+
characters.forEach((character, index) => {
|
|
34
|
+
const previous = characters[index - 1];
|
|
35
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
36
|
+
const isRegionalIndicator = codePoint >= 127462 && codePoint <= 127487;
|
|
37
|
+
const continuesCluster = index > 0 && (/^\p{Mark}$/u.test(character) || codePoint >= 65024 && codePoint <= 65039 || codePoint >= 127995 && codePoint <= 127999 || character === "\u200D" || previous === "\u200D" || isRegionalIndicator && regionalIndicators % 2 === 1);
|
|
38
|
+
if (!continuesCluster) {
|
|
39
|
+
if (offset > 0) offsets.push(offset);
|
|
40
|
+
regionalIndicators = 0;
|
|
41
|
+
}
|
|
42
|
+
offset += character.length;
|
|
43
|
+
regionalIndicators = isRegionalIndicator ? regionalIndicators + 1 : 0;
|
|
44
|
+
});
|
|
45
|
+
offsets.push(offset);
|
|
46
|
+
return offsets;
|
|
47
|
+
};
|
|
48
|
+
function useStreamingText({
|
|
49
|
+
id,
|
|
50
|
+
isStreaming,
|
|
51
|
+
text,
|
|
52
|
+
cache = defaultStreamingTextCache,
|
|
53
|
+
charsPerSecond = 72,
|
|
54
|
+
catchupThreshold = 40,
|
|
55
|
+
maxCatchupChars = 20
|
|
56
|
+
}) {
|
|
57
|
+
const cacheAdapter = cache === false ? null : cache;
|
|
58
|
+
const [displayedText, setDisplayedText] = useState(() => {
|
|
59
|
+
const cachedText = cacheAdapter?.get(id);
|
|
60
|
+
return cachedText && text.startsWith(cachedText) ? cachedText : text;
|
|
61
|
+
});
|
|
62
|
+
const targetRef = useRef(text);
|
|
63
|
+
const displayedRef = useRef(displayedText);
|
|
64
|
+
const rafRef = useRef(null);
|
|
65
|
+
const lastFrameAtRef = useRef(null);
|
|
66
|
+
const targetUpdatedAtRef = useRef(null);
|
|
67
|
+
const progressRemainderRef = useRef(0);
|
|
68
|
+
const idRef = useRef(id);
|
|
69
|
+
const streamingRef = useRef(isStreaming);
|
|
70
|
+
const reduceMotionRef = useRef(false);
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (isStreaming || !text) return;
|
|
73
|
+
cacheAdapter?.set(id, text);
|
|
74
|
+
}, [cacheAdapter, id, isStreaming, text]);
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
targetRef.current = text;
|
|
77
|
+
targetUpdatedAtRef.current = typeof performance === "undefined" ? null : performance.now();
|
|
78
|
+
if (!isStreaming || reduceMotionRef.current || !text.startsWith(displayedRef.current)) {
|
|
79
|
+
displayedRef.current = text;
|
|
80
|
+
setDisplayedText(text);
|
|
81
|
+
progressRemainderRef.current = 0;
|
|
82
|
+
}
|
|
83
|
+
}, [isStreaming, text]);
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (idRef.current === id) return;
|
|
86
|
+
idRef.current = id;
|
|
87
|
+
const cachedText = cacheAdapter?.get(id);
|
|
88
|
+
const initial = cachedText && text.startsWith(cachedText) ? cachedText : text;
|
|
89
|
+
displayedRef.current = initial;
|
|
90
|
+
setDisplayedText(initial);
|
|
91
|
+
lastFrameAtRef.current = null;
|
|
92
|
+
targetUpdatedAtRef.current = typeof performance === "undefined" ? null : performance.now();
|
|
93
|
+
progressRemainderRef.current = 0;
|
|
94
|
+
}, [cacheAdapter, id, isStreaming, text]);
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
streamingRef.current = isStreaming;
|
|
97
|
+
if (typeof window === "undefined") {
|
|
98
|
+
displayedRef.current = text;
|
|
99
|
+
setDisplayedText(text);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const reduceMotion2 = typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
103
|
+
reduceMotionRef.current = reduceMotion2;
|
|
104
|
+
if (reduceMotion2 || !isStreaming) {
|
|
105
|
+
displayedRef.current = text;
|
|
106
|
+
setDisplayedText(text);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const cachedText = cacheAdapter?.get(id);
|
|
110
|
+
if (cachedText === text) {
|
|
111
|
+
displayedRef.current = text;
|
|
112
|
+
setDisplayedText(text);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (!text.startsWith(displayedRef.current)) {
|
|
116
|
+
displayedRef.current = text;
|
|
117
|
+
setDisplayedText(text);
|
|
118
|
+
}
|
|
119
|
+
const tick = (timestamp) => {
|
|
120
|
+
const previousTimestamp = lastFrameAtRef.current ?? timestamp;
|
|
121
|
+
lastFrameAtRef.current = timestamp;
|
|
122
|
+
const target = targetRef.current;
|
|
123
|
+
const current = displayedRef.current;
|
|
124
|
+
if (!streamingRef.current || reduceMotionRef.current) {
|
|
125
|
+
displayedRef.current = target;
|
|
126
|
+
setDisplayedText(target);
|
|
127
|
+
rafRef.current = null;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (!target.startsWith(current)) {
|
|
131
|
+
displayedRef.current = target;
|
|
132
|
+
setDisplayedText(target);
|
|
133
|
+
progressRemainderRef.current = 0;
|
|
134
|
+
rafRef.current = window.requestAnimationFrame(tick);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const pending = target.slice(current.length);
|
|
138
|
+
const pendingOffsets = graphemeEndOffsets(pending);
|
|
139
|
+
if (pendingOffsets.length === 0) {
|
|
140
|
+
rafRef.current = window.requestAnimationFrame(tick);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const elapsedMs = Math.max(0, timestamp - previousTimestamp);
|
|
144
|
+
const targetIdleMs = targetUpdatedAtRef.current === null ? STREAM_IDLE_RELEASE_MS : Math.max(0, timestamp - targetUpdatedAtRef.current);
|
|
145
|
+
const runway = targetIdleMs >= STREAM_IDLE_RELEASE_MS ? 0 : Math.min(
|
|
146
|
+
pendingOffsets.length - 1,
|
|
147
|
+
Math.max(1, Math.ceil(charsPerSecond * STREAM_RUNWAY_MS / 1e3))
|
|
148
|
+
);
|
|
149
|
+
const eligibleGraphemes = pendingOffsets.length - runway;
|
|
150
|
+
progressRemainderRef.current += elapsedMs / 1e3 * charsPerSecond;
|
|
151
|
+
let step = Math.floor(progressRemainderRef.current);
|
|
152
|
+
if (current.length === 0 && eligibleGraphemes > 0) step = Math.max(1, step);
|
|
153
|
+
const catchupStep = pendingOffsets.length > catchupThreshold ? Math.min(maxCatchupChars, Math.ceil(pendingOffsets.length / 10)) : 0;
|
|
154
|
+
step = Math.min(eligibleGraphemes, Math.max(step, catchupStep));
|
|
155
|
+
if (step > 0) {
|
|
156
|
+
progressRemainderRef.current = Math.max(0, progressRemainderRef.current - step);
|
|
157
|
+
const next = current + pending.slice(0, pendingOffsets[step - 1]);
|
|
158
|
+
displayedRef.current = next;
|
|
159
|
+
setDisplayedText(next);
|
|
160
|
+
}
|
|
161
|
+
rafRef.current = window.requestAnimationFrame(tick);
|
|
162
|
+
};
|
|
163
|
+
if (rafRef.current === null) {
|
|
164
|
+
rafRef.current = window.requestAnimationFrame(tick);
|
|
165
|
+
}
|
|
166
|
+
return () => {
|
|
167
|
+
if (rafRef.current !== null) {
|
|
168
|
+
cancelAnimationFrame(rafRef.current);
|
|
169
|
+
rafRef.current = null;
|
|
170
|
+
}
|
|
171
|
+
lastFrameAtRef.current = null;
|
|
172
|
+
progressRemainderRef.current = 0;
|
|
173
|
+
};
|
|
174
|
+
}, [cacheAdapter, catchupThreshold, charsPerSecond, id, isStreaming, maxCatchupChars]);
|
|
175
|
+
const reduceMotion = typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
176
|
+
return {
|
|
177
|
+
displayedText: !isStreaming || reduceMotion ? text : displayedText,
|
|
178
|
+
isAnimating: isStreaming,
|
|
179
|
+
mode: isStreaming ? "streaming" : "static",
|
|
180
|
+
parseIncompleteMarkdown: isStreaming
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export {
|
|
185
|
+
getActiveStreamingTextPartId,
|
|
186
|
+
isActiveStreamingTextPart,
|
|
187
|
+
useStreamingText
|
|
188
|
+
};
|
|
189
|
+
//# sourceMappingURL=chunk-SSUO4EPD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/streaming-text.ts"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\n\nimport type { ChatMessage, ChatPart } from \"./types\";\n\nexport type StreamingTextMode = \"streaming\" | \"static\";\n\nexport type StreamingTextCache = {\n get: (id: string) => string | undefined;\n set: (id: string, text: string) => void;\n};\n\nexport type UseStreamingTextOptions = {\n id: string;\n isStreaming: boolean;\n text: string;\n cache?: StreamingTextCache | false;\n charsPerSecond?: number;\n catchupThreshold?: number;\n maxCatchupChars?: number;\n};\n\nexport type UseStreamingTextResult = {\n displayedText: string;\n isAnimating: boolean;\n mode: StreamingTextMode;\n parseIncompleteMarkdown: boolean;\n};\n\nconst DEFAULT_COMPLETED_TEXT_CACHE_SIZE = 200;\nconst STREAM_RUNWAY_MS = 80;\nconst STREAM_IDLE_RELEASE_MS = 120;\nconst defaultCompletedTextCache = new Map<string, string>();\n\nconst defaultStreamingTextCache: StreamingTextCache = {\n get: (id) => defaultCompletedTextCache.get(id),\n set: (id, text) => {\n defaultCompletedTextCache.set(id, text);\n if (defaultCompletedTextCache.size > DEFAULT_COMPLETED_TEXT_CACHE_SIZE) {\n const firstKey = defaultCompletedTextCache.keys().next().value;\n if (firstKey) defaultCompletedTextCache.delete(firstKey);\n }\n },\n};\n\nexport const getActiveStreamingTextPartId = (\n message: ChatMessage,\n streamingMessageId: string | null\n) => {\n if (message.role !== \"assistant\" || streamingMessageId !== message.id) return null;\n const trailingPart = message.parts[message.parts.length - 1];\n return trailingPart?.kind === \"text\" ? trailingPart.id : null;\n};\n\nexport const isActiveStreamingTextPart = (\n message: ChatMessage,\n part: ChatPart,\n streamingMessageId: string | null\n) => part.kind === \"text\" && part.id === getActiveStreamingTextPartId(message, streamingMessageId);\n\nconst graphemeEndOffsets = (value: string): number[] => {\n if (!value) return [];\n\n if (typeof Intl !== \"undefined\" && \"Segmenter\" in Intl) {\n const segmenter = new Intl.Segmenter(undefined, { granularity: \"grapheme\" });\n return Array.from(segmenter.segment(value), ({ index, segment }) => index + segment.length);\n }\n\n const characters = Array.from(value);\n const offsets: number[] = [];\n let offset = 0;\n let regionalIndicators = 0;\n characters.forEach((character, index) => {\n const previous = characters[index - 1];\n const codePoint = character.codePointAt(0) ?? 0;\n const isRegionalIndicator = codePoint >= 0x1f1e6 && codePoint <= 0x1f1ff;\n const continuesCluster =\n index > 0 &&\n (/^\\p{Mark}$/u.test(character) ||\n (codePoint >= 0xfe00 && codePoint <= 0xfe0f) ||\n (codePoint >= 0x1f3fb && codePoint <= 0x1f3ff) ||\n character === \"\\u200d\" ||\n previous === \"\\u200d\" ||\n (isRegionalIndicator && regionalIndicators % 2 === 1));\n if (!continuesCluster) {\n if (offset > 0) offsets.push(offset);\n regionalIndicators = 0;\n }\n offset += character.length;\n regionalIndicators = isRegionalIndicator ? regionalIndicators + 1 : 0;\n });\n offsets.push(offset);\n return offsets;\n};\n\nexport function useStreamingText({\n id,\n isStreaming,\n text,\n cache = defaultStreamingTextCache,\n charsPerSecond = 72,\n catchupThreshold = 40,\n maxCatchupChars = 20,\n}: UseStreamingTextOptions): UseStreamingTextResult {\n const cacheAdapter = cache === false ? null : cache;\n const [displayedText, setDisplayedText] = useState(() => {\n const cachedText = cacheAdapter?.get(id);\n return cachedText && text.startsWith(cachedText) ? cachedText : text;\n });\n const targetRef = useRef(text);\n const displayedRef = useRef(displayedText);\n const rafRef = useRef<number | null>(null);\n const lastFrameAtRef = useRef<number | null>(null);\n const targetUpdatedAtRef = useRef<number | null>(null);\n const progressRemainderRef = useRef(0);\n const idRef = useRef(id);\n const streamingRef = useRef(isStreaming);\n const reduceMotionRef = useRef(false);\n\n useEffect(() => {\n if (isStreaming || !text) return;\n cacheAdapter?.set(id, text);\n }, [cacheAdapter, id, isStreaming, text]);\n\n useEffect(() => {\n targetRef.current = text;\n targetUpdatedAtRef.current =\n typeof performance === \"undefined\" ? null : performance.now();\n\n if (\n !isStreaming ||\n reduceMotionRef.current ||\n !text.startsWith(displayedRef.current)\n ) {\n displayedRef.current = text;\n setDisplayedText(text);\n progressRemainderRef.current = 0;\n }\n }, [isStreaming, text]);\n\n useEffect(() => {\n if (idRef.current === id) return;\n\n idRef.current = id;\n const cachedText = cacheAdapter?.get(id);\n const initial = cachedText && text.startsWith(cachedText) ? cachedText : text;\n displayedRef.current = initial;\n setDisplayedText(initial);\n lastFrameAtRef.current = null;\n targetUpdatedAtRef.current =\n typeof performance === \"undefined\" ? null : performance.now();\n progressRemainderRef.current = 0;\n }, [cacheAdapter, id, isStreaming, text]);\n\n useEffect(() => {\n streamingRef.current = isStreaming;\n\n if (typeof window === \"undefined\") {\n displayedRef.current = text;\n setDisplayedText(text);\n return;\n }\n\n const reduceMotion =\n typeof window.matchMedia === \"function\" &&\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n reduceMotionRef.current = reduceMotion;\n\n if (reduceMotion || !isStreaming) {\n displayedRef.current = text;\n setDisplayedText(text);\n return;\n }\n\n const cachedText = cacheAdapter?.get(id);\n if (cachedText === text) {\n displayedRef.current = text;\n setDisplayedText(text);\n return;\n }\n\n if (!text.startsWith(displayedRef.current)) {\n displayedRef.current = text;\n setDisplayedText(text);\n }\n\n const tick = (timestamp: number) => {\n const previousTimestamp = lastFrameAtRef.current ?? timestamp;\n lastFrameAtRef.current = timestamp;\n\n const target = targetRef.current;\n const current = displayedRef.current;\n\n if (!streamingRef.current || reduceMotionRef.current) {\n displayedRef.current = target;\n setDisplayedText(target);\n rafRef.current = null;\n return;\n }\n\n if (!target.startsWith(current)) {\n displayedRef.current = target;\n setDisplayedText(target);\n progressRemainderRef.current = 0;\n rafRef.current = window.requestAnimationFrame(tick);\n return;\n }\n\n const pending = target.slice(current.length);\n const pendingOffsets = graphemeEndOffsets(pending);\n if (pendingOffsets.length === 0) {\n rafRef.current = window.requestAnimationFrame(tick);\n return;\n }\n\n const elapsedMs = Math.max(0, timestamp - previousTimestamp);\n const targetIdleMs =\n targetUpdatedAtRef.current === null\n ? STREAM_IDLE_RELEASE_MS\n : Math.max(0, timestamp - targetUpdatedAtRef.current);\n const runway =\n targetIdleMs >= STREAM_IDLE_RELEASE_MS\n ? 0\n : Math.min(\n pendingOffsets.length - 1,\n Math.max(1, Math.ceil((charsPerSecond * STREAM_RUNWAY_MS) / 1000))\n );\n const eligibleGraphemes = pendingOffsets.length - runway;\n\n progressRemainderRef.current += (elapsedMs / 1000) * charsPerSecond;\n let step = Math.floor(progressRemainderRef.current);\n if (current.length === 0 && eligibleGraphemes > 0) step = Math.max(1, step);\n\n const catchupStep =\n pendingOffsets.length > catchupThreshold\n ? Math.min(maxCatchupChars, Math.ceil(pendingOffsets.length / 10))\n : 0;\n step = Math.min(eligibleGraphemes, Math.max(step, catchupStep));\n\n if (step > 0) {\n progressRemainderRef.current = Math.max(0, progressRemainderRef.current - step);\n const next = current + pending.slice(0, pendingOffsets[step - 1]);\n displayedRef.current = next;\n setDisplayedText(next);\n }\n rafRef.current = window.requestAnimationFrame(tick);\n };\n\n if (rafRef.current === null) {\n rafRef.current = window.requestAnimationFrame(tick);\n }\n\n return () => {\n if (rafRef.current !== null) {\n cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n }\n lastFrameAtRef.current = null;\n progressRemainderRef.current = 0;\n };\n }, [cacheAdapter, catchupThreshold, charsPerSecond, id, isStreaming, maxCatchupChars]);\n\n const reduceMotion =\n typeof window !== \"undefined\" &&\n typeof window.matchMedia === \"function\" &&\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\n return {\n displayedText: !isStreaming || reduceMotion ? text : displayedText,\n isAnimating: isStreaming,\n mode: isStreaming ? \"streaming\" : \"static\",\n parseIncompleteMarkdown: isStreaming,\n };\n}\n"],"mappings":"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","names":["reduceMotion"]}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// src/mcp-oauth.ts
|
|
2
|
+
var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
3
|
+
function browserChatCrypto() {
|
|
4
|
+
const value = globalThis.crypto;
|
|
5
|
+
if (!value?.subtle || !value.randomUUID) {
|
|
6
|
+
throw new Error("Secure browser crypto is required for MCP authorization.");
|
|
7
|
+
}
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
function base64Url(bytes) {
|
|
11
|
+
let binary = "";
|
|
12
|
+
bytes.forEach((byte) => {
|
|
13
|
+
binary += String.fromCharCode(byte);
|
|
14
|
+
});
|
|
15
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
16
|
+
}
|
|
17
|
+
async function createMcpPkceProof() {
|
|
18
|
+
const crypto = browserChatCrypto();
|
|
19
|
+
const verifier = base64Url(crypto.getRandomValues(new Uint8Array(32)));
|
|
20
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
|
|
21
|
+
return { challenge: base64Url(new Uint8Array(digest)), verifier };
|
|
22
|
+
}
|
|
23
|
+
function isLoopback(hostname) {
|
|
24
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
25
|
+
}
|
|
26
|
+
function normalizedOrigin(value) {
|
|
27
|
+
try {
|
|
28
|
+
const url = new URL(value);
|
|
29
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopback(url.hostname))) {
|
|
30
|
+
throw new Error("unsafe callback origin");
|
|
31
|
+
}
|
|
32
|
+
if (url.origin === "null") throw new Error("opaque callback origin");
|
|
33
|
+
return url.origin;
|
|
34
|
+
} catch {
|
|
35
|
+
throw new Error("MCP authorization returned an invalid callback origin.");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function validateMcpCallbackResult(result, expectedServerId) {
|
|
39
|
+
const serverId = String(result.server_id || "").trim();
|
|
40
|
+
const connectionId = String(result.connection_id || "").trim();
|
|
41
|
+
if (!serverId || serverId !== expectedServerId) {
|
|
42
|
+
throw new Error("MCP authorization completed for an unexpected server.");
|
|
43
|
+
}
|
|
44
|
+
if (!UUID_PATTERN.test(connectionId)) {
|
|
45
|
+
throw new Error("MCP authorization returned an invalid connection identity.");
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
function waitForMcpOauthComplete({
|
|
50
|
+
popup,
|
|
51
|
+
callbackOrigin,
|
|
52
|
+
popupNonce,
|
|
53
|
+
requireConnectionId = false,
|
|
54
|
+
serverId,
|
|
55
|
+
timeoutMs = 12e4
|
|
56
|
+
}) {
|
|
57
|
+
if (!popup) return Promise.reject(new Error("Could not open the MCP authorization popup."));
|
|
58
|
+
const expectedOrigin = normalizedOrigin(callbackOrigin);
|
|
59
|
+
const expectedServerId = String(serverId || "").trim();
|
|
60
|
+
const expectedPopupNonce = String(popupNonce || "").trim();
|
|
61
|
+
if (!expectedServerId) return Promise.reject(new Error("MCP authorization is missing its server identity."));
|
|
62
|
+
if (!expectedPopupNonce) return Promise.reject(new Error("MCP authorization is missing its popup binding."));
|
|
63
|
+
const returnFocusTo = typeof document !== "undefined" ? document.activeElement : null;
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
let settled = false;
|
|
66
|
+
const finish = (result) => {
|
|
67
|
+
if (settled) return;
|
|
68
|
+
settled = true;
|
|
69
|
+
window.clearTimeout(timeout);
|
|
70
|
+
window.clearInterval(closeTimer);
|
|
71
|
+
window.removeEventListener("message", onMessage);
|
|
72
|
+
returnFocusTo?.focus?.();
|
|
73
|
+
if (result instanceof Error) reject(result);
|
|
74
|
+
else resolve(result);
|
|
75
|
+
};
|
|
76
|
+
const onMessage = (event) => {
|
|
77
|
+
if (event.source !== popup || event.origin !== expectedOrigin) return;
|
|
78
|
+
const data = event.data;
|
|
79
|
+
if (data?.type !== "agents24.mcp.callback" || data.server_id !== expectedServerId || data.popup_nonce !== expectedPopupNonce) return;
|
|
80
|
+
const code = String(data.code || "").trim();
|
|
81
|
+
if (!code || code.length > 2048) return;
|
|
82
|
+
const connectionId = String(data.connection_id || "").trim();
|
|
83
|
+
if ((requireConnectionId || connectionId) && !UUID_PATTERN.test(connectionId)) return;
|
|
84
|
+
finish({
|
|
85
|
+
code,
|
|
86
|
+
...connectionId ? { connectionId } : {},
|
|
87
|
+
popupNonce: expectedPopupNonce,
|
|
88
|
+
serverId: expectedServerId
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
const timeout = window.setTimeout(
|
|
92
|
+
() => finish(new Error("MCP authorization timed out.")),
|
|
93
|
+
Math.max(1, timeoutMs)
|
|
94
|
+
);
|
|
95
|
+
const closeTimer = window.setInterval(() => {
|
|
96
|
+
if (popup.closed) finish(new Error("MCP authorization window was closed before connecting."));
|
|
97
|
+
}, 500);
|
|
98
|
+
window.addEventListener("message", onMessage);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export {
|
|
103
|
+
browserChatCrypto,
|
|
104
|
+
createMcpPkceProof,
|
|
105
|
+
validateMcpCallbackResult,
|
|
106
|
+
waitForMcpOauthComplete
|
|
107
|
+
};
|
|
108
|
+
//# sourceMappingURL=chunk-ZCNHLFRB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/mcp-oauth.ts"],"sourcesContent":["import type { McpCallbackResult } from \"@agents24/client\";\nimport type { ChatMcpOauthCompletion, WaitForMcpOauthCompleteInput } from \"./types\";\n\nconst UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\n\ntype CryptoLike = {\n getRandomValues<T extends Uint8Array>(array: T): T;\n randomUUID(): string;\n subtle: { digest(algorithm: string, data: Uint8Array): Promise<ArrayBuffer> };\n};\n\nexport function browserChatCrypto(): CryptoLike {\n const value = (globalThis as unknown as { crypto?: CryptoLike }).crypto;\n if (!value?.subtle || !value.randomUUID) {\n throw new Error(\"Secure browser crypto is required for MCP authorization.\");\n }\n return value;\n}\n\nfunction base64Url(bytes: Uint8Array): string {\n let binary = \"\";\n bytes.forEach((byte) => { binary += String.fromCharCode(byte); });\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/g, \"\");\n}\n\nexport async function createMcpPkceProof(): Promise<{ challenge: string; verifier: string }> {\n const crypto = browserChatCrypto();\n const verifier = base64Url(crypto.getRandomValues(new Uint8Array(32)));\n const digest = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(verifier));\n return { challenge: base64Url(new Uint8Array(digest)), verifier };\n}\n\nfunction isLoopback(hostname: string): boolean {\n return hostname === \"localhost\" || hostname === \"127.0.0.1\" || hostname === \"[::1]\";\n}\n\nfunction normalizedOrigin(value: string): string {\n try {\n const url = new URL(value);\n if (url.protocol !== \"https:\" && !(url.protocol === \"http:\" && isLoopback(url.hostname))) {\n throw new Error(\"unsafe callback origin\");\n }\n if (url.origin === \"null\") throw new Error(\"opaque callback origin\");\n return url.origin;\n } catch {\n throw new Error(\"MCP authorization returned an invalid callback origin.\");\n }\n}\n\nexport function validateMcpCallbackResult(\n result: McpCallbackResult,\n expectedServerId: string,\n): McpCallbackResult {\n const serverId = String(result.server_id || \"\").trim();\n const connectionId = String(result.connection_id || \"\").trim();\n if (!serverId || serverId !== expectedServerId) {\n throw new Error(\"MCP authorization completed for an unexpected server.\");\n }\n if (!UUID_PATTERN.test(connectionId)) {\n throw new Error(\"MCP authorization returned an invalid connection identity.\");\n }\n return result;\n}\n\nexport function waitForMcpOauthComplete({\n popup,\n callbackOrigin,\n popupNonce,\n requireConnectionId = false,\n serverId,\n timeoutMs = 120_000,\n}: WaitForMcpOauthCompleteInput): Promise<ChatMcpOauthCompletion> {\n if (!popup) return Promise.reject(new Error(\"Could not open the MCP authorization popup.\"));\n const expectedOrigin = normalizedOrigin(callbackOrigin);\n const expectedServerId = String(serverId || \"\").trim();\n const expectedPopupNonce = String(popupNonce || \"\").trim();\n if (!expectedServerId) return Promise.reject(new Error(\"MCP authorization is missing its server identity.\"));\n if (!expectedPopupNonce) return Promise.reject(new Error(\"MCP authorization is missing its popup binding.\"));\n const returnFocusTo = typeof document !== \"undefined\"\n ? document.activeElement as { focus?: () => void } | null\n : null;\n\n return new Promise((resolve, reject) => {\n let settled = false;\n const finish = (result: ChatMcpOauthCompletion | Error) => {\n if (settled) return;\n settled = true;\n window.clearTimeout(timeout);\n window.clearInterval(closeTimer);\n window.removeEventListener(\"message\", onMessage);\n returnFocusTo?.focus?.();\n if (result instanceof Error) reject(result);\n else resolve(result);\n };\n const onMessage = (event: MessageEvent) => {\n if (event.source !== popup || event.origin !== expectedOrigin) return;\n const data = event.data as {\n type?: string;\n code?: string;\n connection_id?: string;\n popup_nonce?: string;\n server_id?: string;\n } | null;\n if (\n data?.type !== \"agents24.mcp.callback\" ||\n data.server_id !== expectedServerId ||\n data.popup_nonce !== expectedPopupNonce\n ) return;\n const code = String(data.code || \"\").trim();\n if (!code || code.length > 2048) return;\n const connectionId = String(data.connection_id || \"\").trim();\n if ((requireConnectionId || connectionId) && !UUID_PATTERN.test(connectionId)) return;\n finish({\n code,\n ...(connectionId ? { connectionId } : {}),\n popupNonce: expectedPopupNonce,\n serverId: expectedServerId,\n });\n };\n const timeout = window.setTimeout(\n () => finish(new Error(\"MCP authorization timed out.\")),\n Math.max(1, timeoutMs),\n );\n const closeTimer = window.setInterval(() => {\n if (popup.closed) finish(new Error(\"MCP authorization window was closed before connecting.\"));\n }, 500);\n window.addEventListener(\"message\", onMessage);\n });\n}\n"],"mappings":"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","names":[]}
|