@parallelworks/ui 0.83.3 → 0.84.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/ai/adapter/openai/createOpenAIStream.js +1 -1
- package/dist/ai/adapter/openai/sse.d.ts +3 -2
- package/dist/ai/adapter/openai/sse.js +4 -2
- package/dist/ai/adapter/types.d.ts +2 -1
- package/dist/ai/agent/index.d.ts +1 -1
- package/dist/ai/agent/toolfmt.d.ts +1 -0
- package/dist/ai/agent/toolfmt.js +6 -2
- package/dist/ai/ai.css +14 -0
- package/dist/ai/components/ChatActivity.js +6 -4
- package/dist/ai/components/ChatMessage.js +42 -30
- package/dist/ai/components/ChatMessageList.js +17 -9
- package/dist/ai/components/agent/AgentMessageParts.d.ts +1 -1
- package/dist/ai/components/agent/AgentMessageParts.js +77 -23
- package/dist/ai/core/ChatProvider.d.ts +3 -1
- package/dist/ai/core/ChatProvider.js +27 -6
- package/dist/ai/core/chatReducer.d.ts +6 -1
- package/dist/ai/core/chatReducer.js +18 -1
- package/dist/ai/core/config.d.ts +2 -1
- package/dist/ai/core/parts.d.ts +7 -4
- package/dist/ai/core/parts.js +23 -2
- package/dist/ai/core/useSSEStream.d.ts +8 -0
- package/dist/ai/core/useSSEStream.js +80 -22
- package/dist/ai/index.js +2 -2
- package/dist/ai/types.d.ts +14 -0
- package/dist/components/Avatar.d.ts +4 -2
- package/dist/components/Avatar.js +2 -1
- package/dist/components/Kbd.d.ts +5 -2
- package/dist/components/Kbd.js +4 -1
- package/dist/components/LetterBadge.d.ts +7 -3
- package/dist/components/LetterBadge.js +2 -3
- package/dist/components/MarkdownImpl.d.ts +3 -0
- package/dist/components/MarkdownImpl.js +16 -2
- package/dist/components/RunStatusIcon.d.ts +5 -2
- package/dist/components/RunStatusIcon.js +2 -1
- package/dist/components/StatusBadge.d.ts +17 -11
- package/dist/components/StatusBadge.js +10 -5
- package/dist/components/Tag.d.ts +5 -3
- package/dist/components/Tag.js +7 -1
- package/dist/components/relativeTime.d.ts +12 -9
- package/dist/components/relativeTime.js +4 -2
- package/dist/index.d.ts +3 -3
- package/dist/styles.css +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { toast } from "../../components/Toast.js";
|
|
2
2
|
import { usePersistedState } from "../../components/Provider.js";
|
|
3
|
-
import { applyPartDelta, finalizeParts, toolTurnMessages } from "./parts.js";
|
|
3
|
+
import { applyPartDelta, finalizeParts, splitAnswer, toolTurnMessages } from "./parts.js";
|
|
4
4
|
import { chatReducer, initialState } from "./chatReducer.js";
|
|
5
5
|
import { ChatConfigProvider, resolveChatConfig } from "./config.js";
|
|
6
6
|
import { providerIssueFor } from "./providerIssues.js";
|
|
@@ -88,7 +88,7 @@ function useChatStream() {
|
|
|
88
88
|
function ChatProvider({ adapter, currentUser, navigation, activeConversationId = null, config, sidebar: sidebarProp, onSidebarChange, drawerOpen: drawerOpenProp, onDrawerOpenChange, children }) {
|
|
89
89
|
const [state, dispatch] = useReducer(chatReducer, initialState);
|
|
90
90
|
const hasLoadedConversationsRef = useRef(false);
|
|
91
|
-
const { onStreamingChunk, startStreaming, stopAnimationLoop, resetAnimationRefs, targetStreamContentRef, streamActiveRef, streamingConversationIdRef, backgroundStreamsRef, abortControllerRef } = useSSEStream(dispatch);
|
|
91
|
+
const { onStreamingChunk, startStreaming, stopAnimationLoop, drainReveal, afterReveal, resetAnimationRefs, targetStreamContentRef, streamActiveRef, streamingConversationIdRef, backgroundStreamsRef, abortControllerRef } = useSSEStream(dispatch);
|
|
92
92
|
const currentConversationIdRef = useRef(null);
|
|
93
93
|
const [storedModel, storeModel] = usePersistedState("aiChat.model", null);
|
|
94
94
|
const storedModelRef = useRef(storedModel);
|
|
@@ -338,9 +338,15 @@ function ChatProvider({ adapter, currentUser, navigation, activeConversationId =
|
|
|
338
338
|
if (thinkingStart === null) thinkingStart = Date.now();
|
|
339
339
|
};
|
|
340
340
|
let liveParts = [];
|
|
341
|
+
let liveText = "";
|
|
341
342
|
const { content: assistantContent, messageId: assistantMessageId, toolCalls, model: resolvedModel, reasoning: assistantReasoning, responsesOutput } = await adapter.streamCompletion(request, {
|
|
342
343
|
onContent: (chunk) => {
|
|
343
344
|
markStreamStart();
|
|
345
|
+
liveText += chunk;
|
|
346
|
+
if (liveParts.length) liveParts = applyPartDelta(liveParts, {
|
|
347
|
+
type: "text",
|
|
348
|
+
text: chunk
|
|
349
|
+
});
|
|
344
350
|
onStreamingChunk(chunk);
|
|
345
351
|
},
|
|
346
352
|
onReasoning: (reasoning) => {
|
|
@@ -354,6 +360,13 @@ function ChatProvider({ adapter, currentUser, navigation, activeConversationId =
|
|
|
354
360
|
const bgStream = backgroundStreamsRef.current.get(targetConversationId);
|
|
355
361
|
if (bgStream) bgStream.reasoning += reasoning;
|
|
356
362
|
},
|
|
363
|
+
onUsage: (usage) => {
|
|
364
|
+
dispatch({
|
|
365
|
+
type: "SET_STREAMING_USAGE",
|
|
366
|
+
usage,
|
|
367
|
+
conversationId: targetConversationId
|
|
368
|
+
});
|
|
369
|
+
},
|
|
357
370
|
onMessageId: (messageId) => {
|
|
358
371
|
markStreamStart();
|
|
359
372
|
const bgStream = backgroundStreamsRef.current.get(targetConversationId);
|
|
@@ -362,16 +375,20 @@ function ChatProvider({ adapter, currentUser, navigation, activeConversationId =
|
|
|
362
375
|
onPart: (delta) => {
|
|
363
376
|
markStreamStart();
|
|
364
377
|
if (routePart?.(delta)) return;
|
|
365
|
-
liveParts = applyPartDelta(liveParts
|
|
366
|
-
|
|
378
|
+
liveParts = applyPartDelta(liveParts.length || !liveText ? liveParts : [{
|
|
379
|
+
kind: "text",
|
|
380
|
+
text: liveText
|
|
381
|
+
}], delta);
|
|
382
|
+
afterReveal(() => dispatch({
|
|
367
383
|
type: "APPLY_STREAMING_PART",
|
|
368
384
|
delta,
|
|
369
385
|
conversationId: targetConversationId
|
|
370
|
-
});
|
|
386
|
+
}));
|
|
371
387
|
const bgStream = backgroundStreamsRef.current.get(targetConversationId);
|
|
372
388
|
if (bgStream) bgStream.parts = liveParts;
|
|
373
389
|
}
|
|
374
390
|
}, controller.signal);
|
|
391
|
+
await drainReveal();
|
|
375
392
|
const thinkingDuration = assistantReasoning && thinkingStart !== null ? (thinkingEnd ?? Date.now()) - thinkingStart : null;
|
|
376
393
|
const assistantMessage = {
|
|
377
394
|
id: assistantMessageId || crypto.randomUUID(),
|
|
@@ -385,7 +402,7 @@ function ChatProvider({ adapter, currentUser, navigation, activeConversationId =
|
|
|
385
402
|
reasoning: assistantReasoning || void 0,
|
|
386
403
|
reasoningDuration: thinkingDuration ?? void 0,
|
|
387
404
|
responsesOutput: responsesOutput.length ? responsesOutput : void 0,
|
|
388
|
-
...liveParts.length ?
|
|
405
|
+
...liveParts.length ? splitAnswer(finalizeParts(liveParts)) : {}
|
|
389
406
|
};
|
|
390
407
|
dispatch({
|
|
391
408
|
type: "ADD_MESSAGE",
|
|
@@ -426,6 +443,8 @@ function ChatProvider({ adapter, currentUser, navigation, activeConversationId =
|
|
|
426
443
|
state.selectedProvider,
|
|
427
444
|
onStreamingChunk,
|
|
428
445
|
stopAnimationLoop,
|
|
446
|
+
drainReveal,
|
|
447
|
+
afterReveal,
|
|
429
448
|
streamingConversationIdRef,
|
|
430
449
|
backgroundStreamsRef,
|
|
431
450
|
abortControllerRef
|
|
@@ -977,12 +996,14 @@ function ChatProvider({ adapter, currentUser, navigation, activeConversationId =
|
|
|
977
996
|
streamingContent: state.streamingContent,
|
|
978
997
|
streamingReasoning: state.streamingReasoning,
|
|
979
998
|
streamingParts: state.streamingParts,
|
|
999
|
+
streamingUsage: state.streamingUsage,
|
|
980
1000
|
thinkingStartTime: state.thinkingStartTime
|
|
981
1001
|
}), [
|
|
982
1002
|
state.isThinking,
|
|
983
1003
|
state.streamingContent,
|
|
984
1004
|
state.streamingReasoning,
|
|
985
1005
|
state.streamingParts,
|
|
1006
|
+
state.streamingUsage,
|
|
986
1007
|
state.thinkingStartTime
|
|
987
1008
|
]);
|
|
988
1009
|
return /* @__PURE__ */ jsx(ChatConfigProvider, {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PartDelta } from '../adapter/types.js';
|
|
2
|
-
import type { ChatMessage, ChatModel, Conversation, ConversationSummary, MessagePart, ProviderInfo, ProviderIssue, UnreachableSession } from '../types.js';
|
|
2
|
+
import type { ChatMessage, ChatModel, Conversation, ConversationSummary, MessagePart, ProviderInfo, ProviderIssue, TokenUsage, UnreachableSession } from '../types.js';
|
|
3
3
|
export interface ChatState {
|
|
4
4
|
conversations: ConversationSummary[];
|
|
5
5
|
currentConversation: Conversation | null;
|
|
@@ -19,6 +19,7 @@ export interface ChatState {
|
|
|
19
19
|
streamingContent: string;
|
|
20
20
|
streamingReasoning: string;
|
|
21
21
|
streamingParts: MessagePart[];
|
|
22
|
+
streamingUsage: TokenUsage | null;
|
|
22
23
|
thinkingStartTime: number | null;
|
|
23
24
|
queuedMessages: ChatMessage[];
|
|
24
25
|
}
|
|
@@ -70,6 +71,10 @@ export type ChatAction = {
|
|
|
70
71
|
} | {
|
|
71
72
|
type: 'CLEAR_STREAMING_CONTENT';
|
|
72
73
|
conversationId?: string;
|
|
74
|
+
} | {
|
|
75
|
+
type: 'SET_STREAMING_USAGE';
|
|
76
|
+
usage: TokenUsage;
|
|
77
|
+
conversationId: string;
|
|
73
78
|
} | {
|
|
74
79
|
type: 'ADD_MESSAGE';
|
|
75
80
|
message: ChatMessage;
|
|
@@ -19,6 +19,7 @@ var initialState = {
|
|
|
19
19
|
streamingContent: "",
|
|
20
20
|
streamingReasoning: "",
|
|
21
21
|
streamingParts: [],
|
|
22
|
+
streamingUsage: null,
|
|
22
23
|
thinkingStartTime: null,
|
|
23
24
|
queuedMessages: []
|
|
24
25
|
};
|
|
@@ -53,6 +54,7 @@ function chatReducer(state, action) {
|
|
|
53
54
|
streamingContent: "",
|
|
54
55
|
streamingReasoning: "",
|
|
55
56
|
streamingParts: [],
|
|
57
|
+
streamingUsage: null,
|
|
56
58
|
isStreaming: false,
|
|
57
59
|
isThinking: false,
|
|
58
60
|
thinkingStartTime: null,
|
|
@@ -67,6 +69,7 @@ function chatReducer(state, action) {
|
|
|
67
69
|
streamingContent: "",
|
|
68
70
|
streamingReasoning: "",
|
|
69
71
|
streamingParts: [],
|
|
72
|
+
streamingUsage: null,
|
|
70
73
|
isStreaming: false,
|
|
71
74
|
isThinking: false,
|
|
72
75
|
thinkingStartTime: null,
|
|
@@ -116,6 +119,10 @@ function chatReducer(state, action) {
|
|
|
116
119
|
return {
|
|
117
120
|
...state,
|
|
118
121
|
streamingContent: state.streamingContent + action.content,
|
|
122
|
+
streamingParts: state.streamingParts.length ? applyPartDelta(state.streamingParts, {
|
|
123
|
+
type: "text",
|
|
124
|
+
text: action.content
|
|
125
|
+
}) : state.streamingParts,
|
|
119
126
|
isThinking: false
|
|
120
127
|
};
|
|
121
128
|
case "APPEND_STREAMING_REASONING":
|
|
@@ -130,7 +137,16 @@ function chatReducer(state, action) {
|
|
|
130
137
|
if (state.currentConversation?.id !== action.conversationId) return state;
|
|
131
138
|
return {
|
|
132
139
|
...state,
|
|
133
|
-
streamingParts: applyPartDelta(state.streamingParts
|
|
140
|
+
streamingParts: applyPartDelta(state.streamingParts.length || !state.streamingContent || action.delta.type === "text" ? state.streamingParts : [{
|
|
141
|
+
kind: "text",
|
|
142
|
+
text: state.streamingContent
|
|
143
|
+
}], action.delta)
|
|
144
|
+
};
|
|
145
|
+
case "SET_STREAMING_USAGE":
|
|
146
|
+
if (state.currentConversation?.id !== action.conversationId) return state;
|
|
147
|
+
return {
|
|
148
|
+
...state,
|
|
149
|
+
streamingUsage: action.usage
|
|
134
150
|
};
|
|
135
151
|
case "CLEAR_STREAMING_CONTENT":
|
|
136
152
|
if (action.conversationId && state.currentConversation?.id !== action.conversationId) return state;
|
|
@@ -139,6 +155,7 @@ function chatReducer(state, action) {
|
|
|
139
155
|
streamingContent: "",
|
|
140
156
|
streamingReasoning: "",
|
|
141
157
|
streamingParts: [],
|
|
158
|
+
streamingUsage: null,
|
|
142
159
|
isThinking: false,
|
|
143
160
|
thinkingStartTime: null
|
|
144
161
|
};
|
package/dist/ai/core/config.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ComponentType, type MouseEventHandler, type ReactNode } from 'react';
|
|
2
2
|
import type { Components } from 'streamdown';
|
|
3
3
|
import { type ChatStrings } from '../strings.js';
|
|
4
|
-
import type { ApprovalAnswerValue, ApprovalPart, ChatLinkTarget, ChatMessage, CspKind, MessagePart, MessagePartKind, ToolCallPart } from '../types.js';
|
|
4
|
+
import type { ApprovalAnswerValue, ApprovalPart, ChatLinkTarget, ChatMessage, CspKind, MessagePart, MessagePartKind, ToolCallPart, ToolPresentation } from '../types.js';
|
|
5
5
|
export interface PartRendererProps {
|
|
6
6
|
part: MessagePart;
|
|
7
7
|
message: ChatMessage;
|
|
@@ -36,6 +36,7 @@ export interface ChatUIConfig {
|
|
|
36
36
|
providerIcon?: (cspKind: CspKind, className?: string) => ReactNode;
|
|
37
37
|
partRenderers?: Partial<Record<MessagePartKind, ComponentType<PartRendererProps>>>;
|
|
38
38
|
toolRenderers?: Record<string, ComponentType<ToolRendererProps>>;
|
|
39
|
+
toolPresentation?: (part: ToolCallPart) => ToolPresentation | undefined;
|
|
39
40
|
};
|
|
40
41
|
strings?: {
|
|
41
42
|
[S in keyof ChatStrings]?: Partial<ChatStrings[S]>;
|
package/dist/ai/core/parts.d.ts
CHANGED
|
@@ -6,12 +6,15 @@ import type { MessagePart } from '../types.js';
|
|
|
6
6
|
* and the provider's local accumulation. Unknown target ids are dropped
|
|
7
7
|
* rather than invented — an end without a start has nothing to render. */
|
|
8
8
|
export declare function applyPartDelta(parts: MessagePart[], delta: PartDelta): MessagePart[];
|
|
9
|
-
/** The wire history for an assistant turn whose tool calls the gateway ran:
|
|
10
|
-
* each round's calls, with the text that came before them, and each result,
|
|
11
|
-
* then the answer. A turn with no finished tool calls comes back as the
|
|
12
|
-
* answer alone. */
|
|
13
9
|
export declare function toolTurnMessages(parts: MessagePart[] | null | undefined, answer: CompletionMessage): CompletionMessage[];
|
|
14
10
|
/** A turn can end (completed or stopped) with tool calls still marked
|
|
15
11
|
* running — the transport died before their tool_end arrived. A stored
|
|
16
12
|
* message must not spin forever, so close them out as interrupted. */
|
|
17
13
|
export declare function finalizeParts(parts: MessagePart[]): MessagePart[];
|
|
14
|
+
/** A finished reply whose text was streamed among its parts: the text after
|
|
15
|
+
* the last part is the answer, shown below them as the message's content,
|
|
16
|
+
* the shape a stored conversation is read back in. */
|
|
17
|
+
export declare function splitAnswer(parts: MessagePart[]): {
|
|
18
|
+
parts: MessagePart[];
|
|
19
|
+
content?: string | null;
|
|
20
|
+
};
|
package/dist/ai/core/parts.js
CHANGED
|
@@ -102,6 +102,13 @@ function isStreamedText(part) {
|
|
|
102
102
|
* each round's calls, with the text that came before them, and each result,
|
|
103
103
|
* then the answer. A turn with no finished tool calls comes back as the
|
|
104
104
|
* answer alone. */
|
|
105
|
+
function answerAfterCalls(parts, trailing, answer) {
|
|
106
|
+
if (parts.at(-1)?.kind === "text") return trailing || null;
|
|
107
|
+
const texts = parts.flatMap((p) => p.kind === "text" ? [p.text] : []);
|
|
108
|
+
const content = typeof answer.content === "string" ? answer.content : null;
|
|
109
|
+
if (!content || content === texts.join("\n\n") || content === texts.join("")) return null;
|
|
110
|
+
return content;
|
|
111
|
+
}
|
|
105
112
|
function toolTurnMessages(parts, answer) {
|
|
106
113
|
const rounds = [];
|
|
107
114
|
let text = "";
|
|
@@ -129,7 +136,7 @@ function toolTurnMessages(parts, answer) {
|
|
|
129
136
|
if (rounds.length === 0) return answer.content || answer.tool_calls?.length ? [answer] : [];
|
|
130
137
|
const final = hasText ? {
|
|
131
138
|
...answer,
|
|
132
|
-
content: text
|
|
139
|
+
content: answerAfterCalls(parts ?? [], text, answer)
|
|
133
140
|
} : answer;
|
|
134
141
|
return [...rounds.flatMap((round) => [{
|
|
135
142
|
role: "assistant",
|
|
@@ -158,5 +165,19 @@ function finalizeParts(parts) {
|
|
|
158
165
|
result: p.result ?? "interrupted"
|
|
159
166
|
} : p);
|
|
160
167
|
}
|
|
168
|
+
/** A finished reply whose text was streamed among its parts: the text after
|
|
169
|
+
* the last part is the answer, shown below them as the message's content,
|
|
170
|
+
* the shape a stored conversation is read back in. */
|
|
171
|
+
function splitAnswer(parts) {
|
|
172
|
+
if (!parts.some((p) => p.kind === "text")) return { parts };
|
|
173
|
+
const last = parts.at(-1);
|
|
174
|
+
return last?.kind === "text" ? {
|
|
175
|
+
parts: parts.slice(0, -1),
|
|
176
|
+
content: last.text
|
|
177
|
+
} : {
|
|
178
|
+
parts,
|
|
179
|
+
content: null
|
|
180
|
+
};
|
|
181
|
+
}
|
|
161
182
|
//#endregion
|
|
162
|
-
export { applyPartDelta, finalizeParts, toolTurnMessages };
|
|
183
|
+
export { applyPartDelta, finalizeParts, splitAnswer, toolTurnMessages };
|
|
@@ -16,6 +16,10 @@ interface UseSSEStreamResult {
|
|
|
16
16
|
stopAnimationLoop: () => {
|
|
17
17
|
unrevealed: string;
|
|
18
18
|
};
|
|
19
|
+
/** Resolves once the buffered text has been revealed, or after maxMs */
|
|
20
|
+
drainReveal: (maxMs?: number) => Promise<void>;
|
|
21
|
+
/** Runs fn once the text received so far has been revealed */
|
|
22
|
+
afterReveal: (fn: () => void) => void;
|
|
19
23
|
/** Reset all animation refs (used when navigating away) */
|
|
20
24
|
resetAnimationRefs: () => void;
|
|
21
25
|
/** Ref to the target content buffer */
|
|
@@ -29,5 +33,9 @@ interface UseSSEStreamResult {
|
|
|
29
33
|
/** AbortController ref for canceling fetch requests */
|
|
30
34
|
abortControllerRef: React.MutableRefObject<AbortController | null>;
|
|
31
35
|
}
|
|
36
|
+
/** Moves a reveal boundary to the end of the word it falls in, when that end is near. */
|
|
37
|
+
export declare function toWordEnd(text: string, end: number): number;
|
|
38
|
+
/** Characters to reveal after dt ms with remaining characters buffered. */
|
|
39
|
+
export declare function revealCount(remaining: number, dt: number): number;
|
|
32
40
|
export declare function useSSEStream(dispatch: React.Dispatch<ChatAction>): UseSSEStreamResult;
|
|
33
41
|
export {};
|
|
@@ -1,40 +1,74 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef } from "react";
|
|
2
2
|
//#region src/ai/core/useSSEStream.ts
|
|
3
|
+
var MIN_CHARS_PER_MS = .12;
|
|
4
|
+
var CATCH_UP_MS = 300;
|
|
5
|
+
var MIN_DISPATCH_MS = 15;
|
|
6
|
+
var WORD_RUN_ON = 16;
|
|
7
|
+
/** Moves a reveal boundary to the end of the word it falls in, when that end is near. */
|
|
8
|
+
function toWordEnd(text, end) {
|
|
9
|
+
if (end >= text.length || /\s/.test(text[end - 1] ?? " ")) return end;
|
|
10
|
+
const limit = Math.min(text.length, end + WORD_RUN_ON);
|
|
11
|
+
for (let i = end; i < limit; i++) if (/\s/.test(text[i] ?? "")) return i;
|
|
12
|
+
return end;
|
|
13
|
+
}
|
|
14
|
+
/** Characters to reveal after dt ms with remaining characters buffered. */
|
|
15
|
+
function revealCount(remaining, dt) {
|
|
16
|
+
if (remaining <= 0) return 0;
|
|
17
|
+
const rate = Math.max(MIN_CHARS_PER_MS, remaining / CATCH_UP_MS);
|
|
18
|
+
return Math.min(remaining, Math.max(1, Math.round(rate * dt)));
|
|
19
|
+
}
|
|
3
20
|
function useSSEStream(dispatch) {
|
|
4
21
|
const targetStreamContentRef = useRef("");
|
|
5
22
|
const revealedLengthRef = useRef(0);
|
|
6
23
|
const streamRafRef = useRef(null);
|
|
7
24
|
const streamActiveRef = useRef(false);
|
|
8
|
-
const
|
|
9
|
-
const
|
|
25
|
+
const lastRevealTimeRef = useRef(0);
|
|
26
|
+
const drainWaitersRef = useRef([]);
|
|
27
|
+
const pendingRef = useRef([]);
|
|
10
28
|
const streamingConversationIdRef = useRef(null);
|
|
11
29
|
const backgroundStreamsRef = useRef(/* @__PURE__ */ new Map());
|
|
12
30
|
const abortControllerRef = useRef(null);
|
|
31
|
+
const runPending = useCallback((revealed) => {
|
|
32
|
+
while (pendingRef.current.length && (pendingRef.current[0]?.at ?? 0) <= revealed) pendingRef.current.shift()?.fn();
|
|
33
|
+
}, []);
|
|
34
|
+
const settleDrain = useCallback(() => {
|
|
35
|
+
const waiters = drainWaitersRef.current;
|
|
36
|
+
drainWaitersRef.current = [];
|
|
37
|
+
for (const resolve of waiters) resolve();
|
|
38
|
+
}, []);
|
|
13
39
|
const animateStreamContent = useCallback(() => {
|
|
14
40
|
const now = performance.now();
|
|
15
41
|
const target = targetStreamContentRef.current;
|
|
16
42
|
const revealed = revealedLengthRef.current;
|
|
17
43
|
const remaining = target.length - revealed;
|
|
18
44
|
const conversationId = streamingConversationIdRef.current;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const dt =
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
45
|
+
if (remaining > 0 && conversationId) {
|
|
46
|
+
if (!lastRevealTimeRef.current) lastRevealTimeRef.current = now - MIN_DISPATCH_MS;
|
|
47
|
+
const dt = now - lastRevealTimeRef.current;
|
|
48
|
+
if (dt >= MIN_DISPATCH_MS) {
|
|
49
|
+
const newRevealed = toWordEnd(target, revealed + revealCount(remaining, dt));
|
|
50
|
+
revealedLengthRef.current = newRevealed;
|
|
51
|
+
dispatch({
|
|
52
|
+
type: "APPEND_STREAMING_CONTENT",
|
|
53
|
+
content: target.slice(revealed, newRevealed),
|
|
54
|
+
conversationId
|
|
55
|
+
});
|
|
56
|
+
lastRevealTimeRef.current = now;
|
|
57
|
+
runPending(newRevealed);
|
|
58
|
+
}
|
|
30
59
|
}
|
|
31
|
-
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
lastFrameTimeRef.current = 0;
|
|
60
|
+
const caughtUp = revealedLengthRef.current >= targetStreamContentRef.current.length;
|
|
61
|
+
if (caughtUp) {
|
|
62
|
+
settleDrain();
|
|
63
|
+
lastRevealTimeRef.current = 0;
|
|
36
64
|
}
|
|
37
|
-
|
|
65
|
+
if (streamActiveRef.current || !caughtUp) streamRafRef.current = requestAnimationFrame(animateStreamContent);
|
|
66
|
+
else streamRafRef.current = null;
|
|
67
|
+
}, [
|
|
68
|
+
dispatch,
|
|
69
|
+
settleDrain,
|
|
70
|
+
runPending
|
|
71
|
+
]);
|
|
38
72
|
const onStreamingChunk = useCallback((chunk) => {
|
|
39
73
|
const convId = streamingConversationIdRef.current;
|
|
40
74
|
if (convId) {
|
|
@@ -55,10 +89,32 @@ function useSSEStream(dispatch) {
|
|
|
55
89
|
cancelAnimationFrame(streamRafRef.current);
|
|
56
90
|
streamRafRef.current = null;
|
|
57
91
|
}
|
|
92
|
+
runPending(Number.POSITIVE_INFINITY);
|
|
58
93
|
targetStreamContentRef.current = "";
|
|
59
94
|
revealedLengthRef.current = 0;
|
|
60
|
-
|
|
61
|
-
|
|
95
|
+
lastRevealTimeRef.current = 0;
|
|
96
|
+
settleDrain();
|
|
97
|
+
}, [settleDrain, runPending]);
|
|
98
|
+
const drainReveal = useCallback((maxMs = 1500) => {
|
|
99
|
+
if (streamRafRef.current === null || revealedLengthRef.current >= targetStreamContentRef.current.length) return Promise.resolve();
|
|
100
|
+
return new Promise((resolve) => {
|
|
101
|
+
const timer = setTimeout(resolve, maxMs);
|
|
102
|
+
drainWaitersRef.current.push(() => {
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
resolve();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}, []);
|
|
108
|
+
const afterReveal = useCallback((fn) => {
|
|
109
|
+
const at = targetStreamContentRef.current.length;
|
|
110
|
+
if (!streamActiveRef.current || revealedLengthRef.current >= at && pendingRef.current.length === 0) {
|
|
111
|
+
fn();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
pendingRef.current.push({
|
|
115
|
+
at,
|
|
116
|
+
fn
|
|
117
|
+
});
|
|
62
118
|
}, []);
|
|
63
119
|
const stopAnimationLoop = useCallback(() => {
|
|
64
120
|
const unrevealed = targetStreamContentRef.current.slice(revealedLengthRef.current);
|
|
@@ -74,6 +130,8 @@ function useSSEStream(dispatch) {
|
|
|
74
130
|
onStreamingChunk,
|
|
75
131
|
startStreaming,
|
|
76
132
|
stopAnimationLoop,
|
|
133
|
+
drainReveal,
|
|
134
|
+
afterReveal,
|
|
77
135
|
resetAnimationRefs,
|
|
78
136
|
targetStreamContentRef,
|
|
79
137
|
streamActiveRef,
|
|
@@ -83,4 +141,4 @@ function useSSEStream(dispatch) {
|
|
|
83
141
|
};
|
|
84
142
|
}
|
|
85
143
|
//#endregion
|
|
86
|
-
export { useSSEStream };
|
|
144
|
+
export { revealCount, toWordEnd, useSSEStream };
|
package/dist/ai/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ChatAdapterError } from "./adapter/types.js";
|
|
2
2
|
import { classifyPseudoUserMessage } from "./agent/classifier.js";
|
|
3
3
|
import { defaultToolStrings } from "./agent/toolStrings.js";
|
|
4
|
-
import { AGENT_COLOR_NAMES, agentColorHex, bashExitCode, clip, fullToolArgs, isGroupableTool, resultFailed, rollupSentence, sanitizeLabel, sanitizePanelText, summarizeToolArgs, toolHeaderFor, toolSummary } from "./agent/toolfmt.js";
|
|
4
|
+
import { AGENT_COLOR_NAMES, agentColorHex, bashExitCode, clip, fullToolArgs, isGroupableTool, resultFailed, rollupClauses, rollupSentence, sanitizeLabel, sanitizePanelText, summarizeToolArgs, toolHeaderFor, toolSummary } from "./agent/toolfmt.js";
|
|
5
5
|
import { MAX_EDIT_DIFF_LINES, MAX_WRITE_DIFF_LINES, computeEditDiff, computeWriteRows, editSummary, isEditDiffTool, lineDelta, parseEditArgs, wordEmphasisRanges } from "./agent/diff.js";
|
|
6
6
|
import { languageForPath, tokenizeDiffLines } from "./agent/diffHighlight.js";
|
|
7
7
|
import { DEFAULT_PERMISSION_MODE, NAMED_PERMISSION_MODES, clampPermissionMode, isNamedPermissionMode, permissionModesUpTo } from "./agent/permissions.js";
|
|
@@ -46,4 +46,4 @@ import ChatMessageList from "./components/ChatMessageList.js";
|
|
|
46
46
|
import SessionView from "./components/SessionView.js";
|
|
47
47
|
import ChatThread from "./components/ChatThread.js";
|
|
48
48
|
import { NoticeBar, NoticeCard, OutputCard } from "./ui/Notice.js";
|
|
49
|
-
export { AGENT_COLOR_NAMES, ActivityLine, AgentMessageParts, AllocationSelector, ApprovalBlock, AttachmentManager, AttachmentUpload, BlockedGroupBanner, BranchNavigator, CHAT_COLUMN_SCOPE, ChatAdapterError, ChatEmptyState, ChatInput, ChatLayout, ChatMessageList, ChatMessage_default as ChatMessageView, ChatProvider, ChatSidebar, ChatStage, ChatThread, ComposerContext, ComposerControls, ComposerSettings, ComposerUsage, ConnectToolsLink, ConversationLayout, ConversationSidebar, DEFAULT_PERMISSION_MODE, DRAWER_BELOW_PX, DragOverlay, FOCUS_SIDEBAR_SEARCH_EVENT, INTERRUPTED_NOTICE, KeyboardShortcutsProvider, MAX_EDIT_DIFF_LINES, MAX_WRITE_DIFF_LINES, ManageProvidersLink, ModelSelector, NAMED_PERMISSION_MODES, NoticeBar, NoticeCard, OutputCard, PermissionPicker, ProviderIssueBanner, RenameDialog, SessionView, ShareButton, ShareDialog, SidebarEmpty, SidebarGroupHeading, SidebarPanel, SidebarRailItem, SidebarRow, SidebarRowBody, SidebarRowTitle, SidebarToggle, StageList, StageListRowContent, agentColorHex, agentTypeLabel, applyPartDelta, askQuestionFromArgs, bashExitCode, chatReducer, clampPermissionMode, classifyPseudoUserMessage, clip, computeEditDiff, computeWriteRows, defaultChatStrings, defaultToolStrings, editSummary, finalizeParts, firstNonEmptyLine, fullToolArgs, getGreeting, getProviderKeyFromModelId, groupConversationsByDate, hiddenTranscriptTool, initialState, interruptionNotice, isEditDiffTool, isGroupableTool, isMac, isNamedPermissionMode, isOrgModel, languageForPath, lineDelta, parseEditArgs, parseSubagentNotice, pasteInlineMaxBytes, permissionModesUpTo, planFromArgs, providerIssueFor, replyText, resolveChatConfig, resultFailed, rollupSentence, sanitizeLabel, sanitizePanelText, sidebarDrawerProps, slashMatches, stageListRowClassName, subagentDeliveryLine, summarizeToolArgs, taskCallFromArgs, thinkingHeading, tokenizeDiffLines, toolHeaderFor, toolSummary, useChat, useChatConfig, useChatStream, useConversationActions, useConversationShortcuts, useDragDrop, useKeyboardShortcuts, useRowDialogs, useSessionMenu, wordEmphasisRanges };
|
|
49
|
+
export { AGENT_COLOR_NAMES, ActivityLine, AgentMessageParts, AllocationSelector, ApprovalBlock, AttachmentManager, AttachmentUpload, BlockedGroupBanner, BranchNavigator, CHAT_COLUMN_SCOPE, ChatAdapterError, ChatEmptyState, ChatInput, ChatLayout, ChatMessageList, ChatMessage_default as ChatMessageView, ChatProvider, ChatSidebar, ChatStage, ChatThread, ComposerContext, ComposerControls, ComposerSettings, ComposerUsage, ConnectToolsLink, ConversationLayout, ConversationSidebar, DEFAULT_PERMISSION_MODE, DRAWER_BELOW_PX, DragOverlay, FOCUS_SIDEBAR_SEARCH_EVENT, INTERRUPTED_NOTICE, KeyboardShortcutsProvider, MAX_EDIT_DIFF_LINES, MAX_WRITE_DIFF_LINES, ManageProvidersLink, ModelSelector, NAMED_PERMISSION_MODES, NoticeBar, NoticeCard, OutputCard, PermissionPicker, ProviderIssueBanner, RenameDialog, SessionView, ShareButton, ShareDialog, SidebarEmpty, SidebarGroupHeading, SidebarPanel, SidebarRailItem, SidebarRow, SidebarRowBody, SidebarRowTitle, SidebarToggle, StageList, StageListRowContent, agentColorHex, agentTypeLabel, applyPartDelta, askQuestionFromArgs, bashExitCode, chatReducer, clampPermissionMode, classifyPseudoUserMessage, clip, computeEditDiff, computeWriteRows, defaultChatStrings, defaultToolStrings, editSummary, finalizeParts, firstNonEmptyLine, fullToolArgs, getGreeting, getProviderKeyFromModelId, groupConversationsByDate, hiddenTranscriptTool, initialState, interruptionNotice, isEditDiffTool, isGroupableTool, isMac, isNamedPermissionMode, isOrgModel, languageForPath, lineDelta, parseEditArgs, parseSubagentNotice, pasteInlineMaxBytes, permissionModesUpTo, planFromArgs, providerIssueFor, replyText, resolveChatConfig, resultFailed, rollupClauses, rollupSentence, sanitizeLabel, sanitizePanelText, sidebarDrawerProps, slashMatches, stageListRowClassName, subagentDeliveryLine, summarizeToolArgs, taskCallFromArgs, thinkingHeading, tokenizeDiffLines, toolHeaderFor, toolSummary, useChat, useChatConfig, useChatStream, useConversationActions, useConversationShortcuts, useDragDrop, useKeyboardShortcuts, useRowDialogs, useSessionMenu, wordEmphasisRanges };
|
package/dist/ai/types.d.ts
CHANGED
|
@@ -70,6 +70,20 @@ export interface ToolDecision {
|
|
|
70
70
|
/** The reader changed the call's arguments before allowing it. */
|
|
71
71
|
edited?: boolean;
|
|
72
72
|
}
|
|
73
|
+
/** How a host names one of its own tool calls in a transcript, in place of
|
|
74
|
+
* the raw call: words a person reads, with the arguments behind the row. */
|
|
75
|
+
export interface ToolPresentation {
|
|
76
|
+
/** The row's label, such as "Checked costs for misc-dev". */
|
|
77
|
+
label: string;
|
|
78
|
+
/** One line under a finished row, such as "3 resources, $67.50". */
|
|
79
|
+
summary?: string;
|
|
80
|
+
/** A run of this tool's calls folded into one row, such as "Checked costs for 12 allocations". */
|
|
81
|
+
group?: (count: number) => string;
|
|
82
|
+
/** This tool's clause in a row that folds different tools, such as "checked costs for 12 allocations". */
|
|
83
|
+
rollup?: (count: number) => string;
|
|
84
|
+
/** Keeps the host's drawing open under the row, as for a card that asks or shows live progress. Otherwise it opens with the row. */
|
|
85
|
+
open?: boolean;
|
|
86
|
+
}
|
|
73
87
|
export interface ToolCallPart {
|
|
74
88
|
kind: 'tool_call';
|
|
75
89
|
id: string;
|
|
@@ -5,7 +5,9 @@ export type AvatarStatus = 'online' | 'offline' | 'busy' | 'away';
|
|
|
5
5
|
* its color, tells it from a person's. */
|
|
6
6
|
export type AvatarKind = 'person' | 'agent';
|
|
7
7
|
export type AvatarShape = 'circle' | 'square';
|
|
8
|
-
|
|
8
|
+
/** Any other span attribute and the ref reach the avatar's outer element, so it can be a Tooltip's
|
|
9
|
+
* or a popover's direct child. */
|
|
10
|
+
export interface AvatarProps extends Omit<ComponentPropsWithRef<'span'>, 'children' | 'className'> {
|
|
9
11
|
/** Image URL. Until it loads, or when it fails, the initials of `name` show. */
|
|
10
12
|
src?: string | null | undefined;
|
|
11
13
|
name?: string | undefined;
|
|
@@ -50,7 +52,7 @@ export declare function avatarInitials(name: string | undefined, { twoLetters }?
|
|
|
50
52
|
* categorical palette (the default), or by kind, where people share `--avatar-person` (with
|
|
51
53
|
* `--avatar-person-text`) and agents `--avatar-agent` (with `--avatar-agent-text`, and the
|
|
52
54
|
* edge `--avatar-agent-ring`). */
|
|
53
|
-
export declare function Avatar({ src, name, kind, shape, colorKey, colorClassName, size: sizeProp, status, decorative, tooltip, className, }: AvatarProps): import("react").JSX.Element;
|
|
55
|
+
export declare function Avatar({ src, name, kind, shape, colorKey, colorClassName, size: sizeProp, status, decorative, tooltip, className, ...rest }: AvatarProps): import("react").JSX.Element;
|
|
54
56
|
/** Any other span attribute and the ref reach the stack's element, so the group can be a
|
|
55
57
|
* Tooltip's or a popover's direct child. */
|
|
56
58
|
export interface AvatarGroupProps extends Omit<ComponentPropsWithRef<'span'>, 'children' | 'className'> {
|
|
@@ -70,7 +70,7 @@ var AvatarGroupContext = createContext(null);
|
|
|
70
70
|
* categorical palette (the default), or by kind, where people share `--avatar-person` (with
|
|
71
71
|
* `--avatar-person-text`) and agents `--avatar-agent` (with `--avatar-agent-text`, and the
|
|
72
72
|
* edge `--avatar-agent-ring`). */
|
|
73
|
-
function Avatar$1({ src, name, kind = "person", shape = kind === "agent" ? "square" : "circle", colorKey, colorClassName, size: sizeProp, status, decorative = false, tooltip = false, className }) {
|
|
73
|
+
function Avatar$1({ src, name, kind = "person", shape = kind === "agent" ? "square" : "circle", colorKey, colorClassName, size: sizeProp, status, decorative = false, tooltip = false, className, ...rest }) {
|
|
74
74
|
const { common } = useStrings();
|
|
75
75
|
const byKind = useAvatarColoring() === "kind";
|
|
76
76
|
const group = use(AvatarGroupContext);
|
|
@@ -82,6 +82,7 @@ function Avatar$1({ src, name, kind = "person", shape = kind === "agent" ? "squa
|
|
|
82
82
|
const radius = shape === "square" ? SQUARE_CLASSES[size] : "rounded-full";
|
|
83
83
|
const fill = colorClassName ?? (byKind ? KIND_CLASSES[kind] : colorBy ? categoryClasses(colorBy) : "bg-gradient-to-br from-[var(--gradient-start,var(--theme-accent))] to-[var(--gradient-mid,var(--theme-link))] text-(--theme-accent-text)");
|
|
84
84
|
const avatar = /* @__PURE__ */ jsxs("span", {
|
|
85
|
+
...rest,
|
|
85
86
|
...decorative ? { "aria-hidden": true } : {
|
|
86
87
|
role: "img",
|
|
87
88
|
"aria-label": named
|
package/dist/components/Kbd.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ComponentPropsWithRef } from 'react';
|
|
1
2
|
import { type UIStrings } from './Provider.js';
|
|
2
3
|
type KeyNames = UIStrings['keys'];
|
|
3
4
|
/** Whether the keyboard is an Apple one, whose Command key does what Control does elsewhere. */
|
|
@@ -21,7 +22,9 @@ export declare function useShortcutFormat(platform?: KeyPlatform): {
|
|
|
21
22
|
};
|
|
22
23
|
export type KbdVariant = 'keycap' | 'plain';
|
|
23
24
|
export type KbdSize = 'xs' | 'sm';
|
|
24
|
-
|
|
25
|
+
/** Any other attribute and the ref reach the outer `<kbd>`, so it can be a Tooltip's or a popover's
|
|
26
|
+
* direct child. */
|
|
27
|
+
export interface KbdProps extends Omit<ComponentPropsWithRef<'kbd'>, 'children' | 'className'> {
|
|
25
28
|
/** A combination joined by `+`, such as `Mod+K` (Command on an Apple keyboard, Control
|
|
26
29
|
* elsewhere) or `Shift+Enter`; or keys side by side, as given: `['↑', '↓']`, `['1–9']`. */
|
|
27
30
|
keys: string | readonly string[];
|
|
@@ -36,5 +39,5 @@ export interface KbdProps {
|
|
|
36
39
|
/** Keys to press, as keycaps: a muted fill, a hairline border and monospace text. Off an
|
|
37
40
|
* Apple keyboard the keys of a combination are joined with a plus, as the platform writes
|
|
38
41
|
* them. Renders `<kbd>`, with a `<kbd>` for each key of a combination. */
|
|
39
|
-
export declare function Kbd({ keys, variant, size, platform, className }: KbdProps): import("react").JSX.Element;
|
|
42
|
+
export declare function Kbd({ keys, variant, size, platform, className, ...rest }: KbdProps): import("react").JSX.Element;
|
|
40
43
|
export {};
|
package/dist/components/Kbd.js
CHANGED
|
@@ -137,22 +137,25 @@ var keycap = {
|
|
|
137
137
|
/** Keys to press, as keycaps: a muted fill, a hairline border and monospace text. Off an
|
|
138
138
|
* Apple keyboard the keys of a combination are joined with a plus, as the platform writes
|
|
139
139
|
* them. Renders `<kbd>`, with a `<kbd>` for each key of a combination. */
|
|
140
|
-
function Kbd({ keys, variant = "keycap", size = "sm", platform, className }) {
|
|
140
|
+
function Kbd({ keys, variant = "keycap", size = "sm", platform, className, ...rest }) {
|
|
141
141
|
const { keys: names } = useStrings();
|
|
142
142
|
const on = platform ?? currentPlatform();
|
|
143
143
|
const parts = formatShortcut(keys, names, on);
|
|
144
144
|
const chord = typeof keys === "string";
|
|
145
145
|
if (variant === "plain") return /* @__PURE__ */ jsx("kbd", {
|
|
146
|
+
...rest,
|
|
146
147
|
className: cx("font-sans text-(--theme-muted-text-color) tabular-nums tracking-wide", size === "xs" ? "text-[10px]" : "text-[11px]", className),
|
|
147
148
|
children: chord ? shortcutText(keys, names, on) : parts.join(" ")
|
|
148
149
|
});
|
|
149
150
|
const cap = cx("inline-flex items-center justify-center rounded border border-(--theme-border) bg-(--theme-muted-panel-bg) font-mono leading-none font-normal text-(--theme-app) not-italic", keycap[size]);
|
|
150
151
|
if (parts.length === 1) return /* @__PURE__ */ jsx("kbd", {
|
|
152
|
+
...rest,
|
|
151
153
|
className: cx(cap, className),
|
|
152
154
|
children: parts[0]
|
|
153
155
|
});
|
|
154
156
|
const plus = chord && on !== "apple";
|
|
155
157
|
return /* @__PURE__ */ jsx("kbd", {
|
|
158
|
+
...rest,
|
|
156
159
|
className: cx("inline-flex flex-wrap items-center gap-0.5 align-middle", className),
|
|
157
160
|
children: parts.map((part, i) => /* @__PURE__ */ jsxs(Fragment, { children: [plus && i > 0 && /* @__PURE__ */ jsx("span", {
|
|
158
161
|
"aria-hidden": "true",
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import type { ComponentPropsWithRef } from 'react';
|
|
1
2
|
/** A compact letter badge ("AI", "PUB", "SSH") used as a type mark in place of
|
|
2
3
|
* an icon. Size the box and text via className (e.g. "h-5 w-5 text-[8px]"). */
|
|
3
|
-
|
|
4
|
+
/** Any other span attribute and the ref reach its element, so it can be a Tooltip's or a popover's
|
|
5
|
+
* direct child. */
|
|
6
|
+
export interface LetterBadgeProps extends Omit<ComponentPropsWithRef<'span'>, 'children' | 'className'> {
|
|
4
7
|
text: string;
|
|
5
|
-
className?: string;
|
|
6
|
-
}
|
|
8
|
+
className?: string | undefined;
|
|
9
|
+
}
|
|
10
|
+
export declare function LetterBadge({ text, className, ...rest }: LetterBadgeProps): import("react").JSX.Element;
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { cx } from "../cx.js";
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
3
|
//#region src/components/LetterBadge.tsx
|
|
4
|
-
|
|
5
|
-
* an icon. Size the box and text via className (e.g. "h-5 w-5 text-[8px]"). */
|
|
6
|
-
function LetterBadge({ text, className }) {
|
|
4
|
+
function LetterBadge({ text, className, ...rest }) {
|
|
7
5
|
return /* @__PURE__ */ jsx("span", {
|
|
6
|
+
...rest,
|
|
8
7
|
className: cx("inline-flex shrink-0 items-center justify-center rounded-[3px] border theme-border font-bold leading-none tracking-tight text-(--theme-app)", className),
|
|
9
8
|
children: text
|
|
10
9
|
});
|
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
import 'katex/dist/katex.min.css';
|
|
2
2
|
import type { MarkdownProps } from './Markdown.js';
|
|
3
|
+
/** While a reply streams, a table's opening rows read as stray pipes until its
|
|
4
|
+
* delimiter row arrives; they are held back until then. */
|
|
5
|
+
export declare function withoutUnfinishedTable(text: string): string;
|
|
3
6
|
export default function MarkdownImpl({ children, isStreaming, controls, components, httpsImagesOnly, compact, }: MarkdownProps): import("react").JSX.Element;
|
|
@@ -112,6 +112,20 @@ function makeCode(controls, diagrams) {
|
|
|
112
112
|
});
|
|
113
113
|
};
|
|
114
114
|
}
|
|
115
|
+
var TABLE_ROW = /^\s*\|/;
|
|
116
|
+
var TABLE_DELIMITER = /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)*\|?\s*$/;
|
|
117
|
+
/** While a reply streams, a table's opening rows read as stray pipes until its
|
|
118
|
+
* delimiter row arrives; they are held back until then. */
|
|
119
|
+
function withoutUnfinishedTable(text) {
|
|
120
|
+
if ((text.match(/^\s*(```|~~~)/gm)?.length ?? 0) % 2 === 1) return text;
|
|
121
|
+
const lines = text.split("\n");
|
|
122
|
+
let start = lines.length;
|
|
123
|
+
while (start > 0 && TABLE_ROW.test(lines[start - 1] ?? "")) start--;
|
|
124
|
+
if (start === lines.length) return text;
|
|
125
|
+
const rows = lines.slice(start);
|
|
126
|
+
if (rows.length >= 2 && TABLE_DELIMITER.test(rows[1] ?? "")) return text;
|
|
127
|
+
return lines.slice(0, start).join("\n");
|
|
128
|
+
}
|
|
115
129
|
function MarkdownImpl({ children, isStreaming, controls, components, httpsImagesOnly, compact }) {
|
|
116
130
|
const needsMermaid = !!children && children.includes("```mermaid");
|
|
117
131
|
const [, setMermaidReady] = useState(() => !!pluginsWithMermaid);
|
|
@@ -158,9 +172,9 @@ function MarkdownImpl({ children, isStreaming, controls, components, httpsImages
|
|
|
158
172
|
}),
|
|
159
173
|
...components
|
|
160
174
|
},
|
|
161
|
-
children: children || ""
|
|
175
|
+
children: (isStreaming ? withoutUnfinishedTable(children || "") : children) || ""
|
|
162
176
|
})
|
|
163
177
|
});
|
|
164
178
|
}
|
|
165
179
|
//#endregion
|
|
166
|
-
export { MarkdownImpl as default };
|
|
180
|
+
export { MarkdownImpl as default, withoutUnfinishedTable };
|