@townco/ui 0.1.58 → 0.1.65
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/core/hooks/use-chat-messages.js +71 -43
- package/dist/gui/components/ChatLayout.d.ts +0 -2
- package/dist/gui/components/ChatLayout.js +102 -157
- package/dist/gui/components/Response.d.ts +2 -2
- package/dist/gui/components/Response.js +5 -88
- package/dist/sdk/schemas/message.d.ts +99 -2
- package/dist/sdk/schemas/message.js +29 -2
- package/dist/sdk/transports/http.js +50 -7
- package/dist/sdk/transports/stdio.js +33 -3
- package/package.json +5 -4
- package/src/styles/global.css +24 -0
|
@@ -15,6 +15,10 @@ export function useChatMessages(client, startSession) {
|
|
|
15
15
|
const updateMessage = useChatStore((state) => state.updateMessage);
|
|
16
16
|
const setError = useChatStore((state) => state.setError);
|
|
17
17
|
const setLatestContextSize = useChatStore((state) => state.setLatestContextSize);
|
|
18
|
+
const addToolCall = useChatStore((state) => state.addToolCall);
|
|
19
|
+
const updateToolCall = useChatStore((state) => state.updateToolCall);
|
|
20
|
+
const addToolCallToCurrentMessage = useChatStore((state) => state.addToolCallToCurrentMessage);
|
|
21
|
+
const updateToolCallInCurrentMessage = useChatStore((state) => state.updateToolCallInCurrentMessage);
|
|
18
22
|
/**
|
|
19
23
|
* Send a message to the agent
|
|
20
24
|
*/
|
|
@@ -94,54 +98,74 @@ export function useChatMessages(client, startSession) {
|
|
|
94
98
|
let accumulatedContent = "";
|
|
95
99
|
let streamCompleted = false;
|
|
96
100
|
for await (const chunk of messageStream) {
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
streamingStartTime: undefined, // Clear streaming start time
|
|
127
|
-
...(chunk.tokenUsage ? { tokenUsage: chunk.tokenUsage } : {}),
|
|
128
|
-
});
|
|
129
|
-
setIsStreaming(false);
|
|
130
|
-
setStreamingStartTime(null); // Clear global streaming start time
|
|
131
|
-
streamCompleted = true;
|
|
132
|
-
break;
|
|
133
|
-
}
|
|
134
|
-
else {
|
|
135
|
-
// Update streaming content
|
|
136
|
-
if (chunk.contentDelta.type === "text") {
|
|
137
|
-
accumulatedContent += chunk.contentDelta.text;
|
|
101
|
+
// Handle different chunk types using discriminated union
|
|
102
|
+
if (chunk.type === "content") {
|
|
103
|
+
// Content chunk - text streaming
|
|
104
|
+
// Update context size if provided (check both _meta.context_size and direct context_size)
|
|
105
|
+
const chunkMeta = chunk._meta;
|
|
106
|
+
const contextSizeData = chunkMeta?.context_size || chunk.context_size;
|
|
107
|
+
if (contextSizeData != null) {
|
|
108
|
+
const contextSize = contextSizeData;
|
|
109
|
+
logger.info("✅ Received context_size from backend", {
|
|
110
|
+
context_size: contextSize,
|
|
111
|
+
totalEstimated: contextSize.totalEstimated,
|
|
112
|
+
source: chunkMeta?.context_size ? "_meta" : "direct",
|
|
113
|
+
});
|
|
114
|
+
setLatestContextSize(contextSize);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
logger.debug("Chunk does not have context_size", {
|
|
118
|
+
hasContextSize: "context_size" in chunk,
|
|
119
|
+
hasMeta: "_meta" in chunk,
|
|
120
|
+
metaKeys: chunkMeta ? Object.keys(chunkMeta) : null,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
if (chunk.tokenUsage) {
|
|
124
|
+
logger.debug("Received tokenUsage from backend", {
|
|
125
|
+
tokenUsage: chunk.tokenUsage,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
if (chunk.isComplete) {
|
|
129
|
+
// Update final message
|
|
138
130
|
updateMessage(assistantMessageId, {
|
|
139
131
|
content: accumulatedContent,
|
|
132
|
+
isStreaming: false,
|
|
133
|
+
streamingStartTime: undefined, // Clear streaming start time
|
|
140
134
|
...(chunk.tokenUsage ? { tokenUsage: chunk.tokenUsage } : {}),
|
|
141
135
|
});
|
|
142
|
-
|
|
143
|
-
|
|
136
|
+
setIsStreaming(false);
|
|
137
|
+
setStreamingStartTime(null); // Clear global streaming start time
|
|
138
|
+
streamCompleted = true;
|
|
139
|
+
break;
|
|
144
140
|
}
|
|
141
|
+
else {
|
|
142
|
+
// Update streaming content
|
|
143
|
+
if (chunk.contentDelta.type === "text") {
|
|
144
|
+
accumulatedContent += chunk.contentDelta.text;
|
|
145
|
+
updateMessage(assistantMessageId, {
|
|
146
|
+
content: accumulatedContent,
|
|
147
|
+
...(chunk.tokenUsage ? { tokenUsage: chunk.tokenUsage } : {}),
|
|
148
|
+
});
|
|
149
|
+
// Small delay to allow Ink to render between chunks (~60fps)
|
|
150
|
+
await new Promise((resolve) => setTimeout(resolve, 16));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else if (chunk.type === "tool_call") {
|
|
155
|
+
// Tool call chunk - tool invocation
|
|
156
|
+
logger.debug("Received tool_call chunk", { chunk });
|
|
157
|
+
// Add to session-level tool calls (for sidebar)
|
|
158
|
+
addToolCall(activeSessionId, chunk.toolCall);
|
|
159
|
+
// Also add to current assistant message (for inline display)
|
|
160
|
+
addToolCallToCurrentMessage(chunk.toolCall);
|
|
161
|
+
}
|
|
162
|
+
else if (chunk.type === "tool_call_update") {
|
|
163
|
+
// Tool call update chunk - tool results
|
|
164
|
+
logger.debug("Received tool_call_update chunk", { chunk });
|
|
165
|
+
// Update session-level tool calls (for sidebar)
|
|
166
|
+
updateToolCall(activeSessionId, chunk.toolCallUpdate);
|
|
167
|
+
// Also update in current assistant message (for inline display)
|
|
168
|
+
updateToolCallInCurrentMessage(chunk.toolCallUpdate);
|
|
145
169
|
}
|
|
146
170
|
}
|
|
147
171
|
// Ensure streaming state is cleared even if no explicit isComplete was received
|
|
@@ -176,6 +200,10 @@ export function useChatMessages(client, startSession) {
|
|
|
176
200
|
setStreamingStartTime,
|
|
177
201
|
setError,
|
|
178
202
|
setLatestContextSize,
|
|
203
|
+
addToolCall,
|
|
204
|
+
updateToolCall,
|
|
205
|
+
addToolCallToCurrentMessage,
|
|
206
|
+
updateToolCallInCurrentMessage,
|
|
179
207
|
]);
|
|
180
208
|
return {
|
|
181
209
|
messages,
|
|
@@ -36,8 +36,6 @@ export interface ChatLayoutMessagesProps extends React.HTMLAttributes<HTMLDivEle
|
|
|
36
36
|
onScrollChange?: (isAtBottom: boolean) => void;
|
|
37
37
|
/** Whether to show scroll to bottom button */
|
|
38
38
|
showScrollToBottom?: boolean;
|
|
39
|
-
/** Whether to scroll to bottom on initial mount (default: true) */
|
|
40
|
-
initialScrollToBottom?: boolean;
|
|
41
39
|
}
|
|
42
40
|
declare const ChatLayoutMessages: React.ForwardRefExoticComponent<ChatLayoutMessagesProps & React.RefAttributes<HTMLDivElement>>;
|
|
43
41
|
export interface ChatLayoutFooterProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
@@ -38,190 +38,135 @@ const ChatLayoutBody = React.forwardRef(({ showToaster = true, className, childr
|
|
|
38
38
|
return (_jsxs("div", { ref: ref, className: cn("relative flex flex-1 flex-col overflow-hidden", className), ...props, children: [children, showToaster && _jsx(Toaster, {})] }));
|
|
39
39
|
});
|
|
40
40
|
ChatLayoutBody.displayName = "ChatLayout.Body";
|
|
41
|
-
const ChatLayoutMessages = React.forwardRef(({ className, children, onScrollChange, showScrollToBottom = true,
|
|
42
|
-
const [showScrollButton, setShowScrollButton] = React.useState(false);
|
|
41
|
+
const ChatLayoutMessages = React.forwardRef(({ className, children, onScrollChange, showScrollToBottom = true, ...props }, ref) => {
|
|
43
42
|
const scrollContainerRef = React.useRef(null);
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const
|
|
43
|
+
const [isAtBottom, setIsAtBottom] = React.useState(true);
|
|
44
|
+
const isAtBottomRef = React.useRef(true);
|
|
45
|
+
const isUserScrollingRef = React.useRef(false);
|
|
46
|
+
const lastScrollHeightRef = React.useRef(0);
|
|
47
|
+
const lastMutationTimeRef = React.useRef(0);
|
|
48
|
+
const isSmoothScrollingRef = React.useRef(false); // Protect smooth scrolls from interruption
|
|
49
|
+
// Keep ref in sync with state
|
|
50
|
+
React.useEffect(() => {
|
|
51
|
+
isAtBottomRef.current = isAtBottom;
|
|
52
|
+
}, [isAtBottom]);
|
|
47
53
|
// Merge refs
|
|
48
54
|
React.useImperativeHandle(ref, () => scrollContainerRef.current);
|
|
49
|
-
// Check if
|
|
50
|
-
const
|
|
55
|
+
// Check if at bottom
|
|
56
|
+
const checkIfAtBottom = React.useCallback(() => {
|
|
51
57
|
const container = scrollContainerRef.current;
|
|
52
58
|
if (!container)
|
|
53
|
-
return
|
|
59
|
+
return true;
|
|
54
60
|
const { scrollTop, scrollHeight, clientHeight } = container;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return isAtBottom;
|
|
60
|
-
}, [onScrollChange, showScrollToBottom]);
|
|
61
|
-
// Handle scroll events
|
|
62
|
-
const handleScroll = React.useCallback(() => {
|
|
63
|
-
// If this is a programmatic scroll, don't update wasAtBottomRef
|
|
64
|
-
if (isAutoScrollingRef.current) {
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
// This is a user-initiated scroll, update the position
|
|
68
|
-
const isAtBottom = checkScrollPosition();
|
|
69
|
-
wasAtBottomRef.current = isAtBottom;
|
|
70
|
-
}, [checkScrollPosition]);
|
|
71
|
-
// Scroll to bottom function
|
|
72
|
-
const scrollToBottom = React.useCallback((smooth = true) => {
|
|
61
|
+
return scrollTop + clientHeight >= scrollHeight - 100;
|
|
62
|
+
}, []);
|
|
63
|
+
// Scroll to bottom with protection for smooth scrolls
|
|
64
|
+
const scrollToBottom = React.useCallback((behavior = "smooth") => {
|
|
73
65
|
const container = scrollContainerRef.current;
|
|
74
66
|
if (!container)
|
|
75
67
|
return;
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
68
|
+
// If we're doing a smooth scroll, don't interrupt with instant
|
|
69
|
+
if (isSmoothScrollingRef.current && behavior === "instant") {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (behavior === "smooth") {
|
|
73
|
+
isSmoothScrollingRef.current = true;
|
|
74
|
+
// Clear the smooth scroll protection after animation completes
|
|
75
|
+
setTimeout(() => {
|
|
76
|
+
isSmoothScrollingRef.current = false;
|
|
77
|
+
}, 500);
|
|
78
|
+
}
|
|
79
79
|
container.scrollTo({
|
|
80
80
|
top: container.scrollHeight,
|
|
81
|
-
behavior
|
|
81
|
+
behavior,
|
|
82
82
|
});
|
|
83
|
-
// Clear the flag after scroll completes
|
|
84
|
-
// For instant scrolling, clear immediately; for smooth, wait
|
|
85
|
-
setTimeout(() => {
|
|
86
|
-
isAutoScrollingRef.current = false;
|
|
87
|
-
}, smooth ? 300 : 0);
|
|
88
83
|
}, []);
|
|
89
|
-
//
|
|
90
|
-
React.useEffect(() => {
|
|
91
|
-
const container = scrollContainerRef.current;
|
|
92
|
-
if (!container)
|
|
93
|
-
return;
|
|
94
|
-
// If user was at the bottom, scroll to new content
|
|
95
|
-
if (wasAtBottomRef.current && !isAutoScrollingRef.current) {
|
|
96
|
-
// Use requestAnimationFrame to ensure DOM has updated
|
|
97
|
-
requestAnimationFrame(() => {
|
|
98
|
-
scrollToBottom(false); // Use instant scroll for streaming to avoid jarring smooth animations
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
// Update scroll position state (but don't change wasAtBottomRef if we're auto-scrolling)
|
|
102
|
-
if (!isAutoScrollingRef.current) {
|
|
103
|
-
checkScrollPosition();
|
|
104
|
-
}
|
|
105
|
-
}, [children, scrollToBottom, checkScrollPosition]);
|
|
106
|
-
// Track last scroll height to detect when content stops loading
|
|
107
|
-
const lastScrollHeightRef = React.useRef(0);
|
|
108
|
-
const scrollStableCountRef = React.useRef(0);
|
|
109
|
-
// Scroll to bottom on initial mount and during session loading
|
|
110
|
-
// Keep scrolling until content stabilizes (no more changes)
|
|
84
|
+
// Handle user scroll events
|
|
111
85
|
React.useEffect(() => {
|
|
112
|
-
if (!initialScrollToBottom)
|
|
113
|
-
return;
|
|
114
86
|
const container = scrollContainerRef.current;
|
|
115
87
|
if (!container)
|
|
116
88
|
return;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
89
|
+
let scrollTimeout;
|
|
90
|
+
const handleScroll = () => {
|
|
91
|
+
// Mark as user scrolling
|
|
92
|
+
isUserScrollingRef.current = true;
|
|
93
|
+
clearTimeout(scrollTimeout);
|
|
94
|
+
// Update isAtBottom state
|
|
95
|
+
const atBottom = checkIfAtBottom();
|
|
96
|
+
setIsAtBottom(atBottom);
|
|
97
|
+
isAtBottomRef.current = atBottom;
|
|
98
|
+
onScrollChange?.(atBottom);
|
|
99
|
+
// Reset user scrolling flag after scroll ends
|
|
100
|
+
scrollTimeout = setTimeout(() => {
|
|
101
|
+
isUserScrollingRef.current = false;
|
|
102
|
+
}, 150);
|
|
120
103
|
};
|
|
121
|
-
|
|
122
|
-
const currentHeight = container.scrollHeight;
|
|
123
|
-
if (currentHeight === lastScrollHeightRef.current) {
|
|
124
|
-
scrollStableCountRef.current++;
|
|
125
|
-
}
|
|
126
|
-
else {
|
|
127
|
-
scrollStableCountRef.current = 0;
|
|
128
|
-
lastScrollHeightRef.current = currentHeight;
|
|
129
|
-
}
|
|
130
|
-
// If content is still loading (height changing) or we haven't scrolled yet,
|
|
131
|
-
// keep auto-scrolling. Stop after content is stable for a few renders.
|
|
132
|
-
if (scrollStableCountRef.current < 3) {
|
|
133
|
-
isAutoScrollingRef.current = true;
|
|
134
|
-
scrollToBottomInstant();
|
|
135
|
-
hasInitialScrolledRef.current = true;
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
// Content is stable, stop auto-scrolling
|
|
139
|
-
isAutoScrollingRef.current = false;
|
|
140
|
-
}
|
|
141
|
-
}, [initialScrollToBottom, children]);
|
|
142
|
-
// Also use a timer-based approach as backup for session replay
|
|
143
|
-
// which may not trigger children changes
|
|
144
|
-
React.useEffect(() => {
|
|
145
|
-
if (!initialScrollToBottom)
|
|
146
|
-
return;
|
|
147
|
-
const container = scrollContainerRef.current;
|
|
148
|
-
if (!container)
|
|
149
|
-
return;
|
|
150
|
-
// Keep scrolling to bottom for the first 2 seconds of session load
|
|
151
|
-
// to catch async message replay
|
|
152
|
-
let cancelled = false;
|
|
153
|
-
const scrollInterval = setInterval(() => {
|
|
154
|
-
if (cancelled)
|
|
155
|
-
return;
|
|
156
|
-
if (container.scrollHeight > container.clientHeight) {
|
|
157
|
-
isAutoScrollingRef.current = true;
|
|
158
|
-
container.scrollTop = container.scrollHeight;
|
|
159
|
-
wasAtBottomRef.current = true;
|
|
160
|
-
hasInitialScrolledRef.current = true;
|
|
161
|
-
}
|
|
162
|
-
}, 100);
|
|
163
|
-
// Stop after 2 seconds
|
|
164
|
-
const timeout = setTimeout(() => {
|
|
165
|
-
clearInterval(scrollInterval);
|
|
166
|
-
isAutoScrollingRef.current = false;
|
|
167
|
-
}, 2000);
|
|
104
|
+
container.addEventListener("scroll", handleScroll, { passive: true });
|
|
168
105
|
return () => {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
clearTimeout(timeout);
|
|
106
|
+
container.removeEventListener("scroll", handleScroll);
|
|
107
|
+
clearTimeout(scrollTimeout);
|
|
172
108
|
};
|
|
173
|
-
}, [
|
|
174
|
-
//
|
|
109
|
+
}, [checkIfAtBottom, onScrollChange]);
|
|
110
|
+
// Auto-scroll when content changes using MutationObserver + ResizeObserver
|
|
175
111
|
React.useEffect(() => {
|
|
176
|
-
if (!isAutoScrollingRef.current) {
|
|
177
|
-
const isAtBottom = checkScrollPosition();
|
|
178
|
-
wasAtBottomRef.current = isAtBottom;
|
|
179
|
-
}
|
|
180
|
-
}, [checkScrollPosition]);
|
|
181
|
-
// Detect user interaction with scroll area (wheel, touch) - IMMEDIATELY break auto-scroll
|
|
182
|
-
const handleUserInteraction = React.useCallback(() => {
|
|
183
|
-
// Immediately mark that user is interacting
|
|
184
|
-
isAutoScrollingRef.current = false;
|
|
185
|
-
// For wheel/touch events, temporarily break auto-scroll
|
|
186
|
-
// The actual scroll event will update wasAtBottomRef properly
|
|
187
|
-
// This prevents the race condition where content updates before scroll completes
|
|
188
112
|
const container = scrollContainerRef.current;
|
|
189
113
|
if (!container)
|
|
190
114
|
return;
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
"ArrowDown",
|
|
206
|
-
"PageUp",
|
|
207
|
-
"PageDown",
|
|
208
|
-
"Home",
|
|
209
|
-
"End",
|
|
210
|
-
];
|
|
211
|
-
if (scrollKeys.includes(e.key)) {
|
|
212
|
-
isAutoScrollingRef.current = false;
|
|
213
|
-
// Check position on next frame after the scroll happens
|
|
115
|
+
const scrollIfNeeded = () => {
|
|
116
|
+
// Only auto-scroll if user was at bottom and isn't actively scrolling
|
|
117
|
+
if (!isAtBottomRef.current || isUserScrollingRef.current) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const now = Date.now();
|
|
121
|
+
const timeSinceLastMutation = now - lastMutationTimeRef.current;
|
|
122
|
+
const currentScrollHeight = container.scrollHeight;
|
|
123
|
+
const heightDelta = currentScrollHeight - lastScrollHeightRef.current;
|
|
124
|
+
lastMutationTimeRef.current = now;
|
|
125
|
+
lastScrollHeightRef.current = currentScrollHeight;
|
|
126
|
+
// Detect if this is likely a new message (large height increase + time gap)
|
|
127
|
+
// vs streaming (small incremental changes)
|
|
128
|
+
const isLikelyNewMessage = heightDelta > 80 && timeSinceLastMutation > 150;
|
|
214
129
|
requestAnimationFrame(() => {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
130
|
+
if (isLikelyNewMessage) {
|
|
131
|
+
scrollToBottom("smooth");
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
scrollToBottom("instant");
|
|
135
|
+
}
|
|
136
|
+
setIsAtBottom(true);
|
|
137
|
+
isAtBottomRef.current = true;
|
|
221
138
|
});
|
|
139
|
+
};
|
|
140
|
+
// Watch for DOM changes (new messages, content updates)
|
|
141
|
+
const mutationObserver = new MutationObserver(scrollIfNeeded);
|
|
142
|
+
mutationObserver.observe(container, {
|
|
143
|
+
childList: true,
|
|
144
|
+
subtree: true,
|
|
145
|
+
characterData: true,
|
|
146
|
+
});
|
|
147
|
+
// Watch for size changes (images loading, content expanding)
|
|
148
|
+
const resizeObserver = new ResizeObserver(scrollIfNeeded);
|
|
149
|
+
resizeObserver.observe(container);
|
|
150
|
+
// Also observe direct children for size changes
|
|
151
|
+
for (const child of Array.from(container.children)) {
|
|
152
|
+
resizeObserver.observe(child);
|
|
222
153
|
}
|
|
223
|
-
|
|
224
|
-
|
|
154
|
+
// Initialize scroll height
|
|
155
|
+
lastScrollHeightRef.current = container.scrollHeight;
|
|
156
|
+
return () => {
|
|
157
|
+
mutationObserver.disconnect();
|
|
158
|
+
resizeObserver.disconnect();
|
|
159
|
+
};
|
|
160
|
+
}, [scrollToBottom]);
|
|
161
|
+
// Button click handler - smooth scroll and re-engage auto-scroll
|
|
162
|
+
const handleScrollToBottomClick = React.useCallback(() => {
|
|
163
|
+
setIsAtBottom(true);
|
|
164
|
+
isAtBottomRef.current = true;
|
|
165
|
+
scrollToBottom("smooth");
|
|
166
|
+
}, [scrollToBottom]);
|
|
167
|
+
return (_jsxs("div", { className: "relative flex-1 overflow-hidden", children: [_jsx("div", { ref: scrollContainerRef, className: cn("h-full overflow-y-auto flex flex-col", className), tabIndex: 0, ...props, children: _jsx("div", { className: "mx-auto max-w-chat flex-1 w-full flex flex-col", children: children }) }), showScrollToBottom && (_jsx("button", { type: "button", onClick: handleScrollToBottomClick, className: cn("absolute bottom-4 left-1/2 -translate-x-1/2 z-10", "flex items-center justify-center p-2 rounded-full", "bg-card border border-border shadow-lg", "text-foreground", "hover:bg-accent hover:text-accent-foreground", "transition-all duration-200 ease-in-out", isAtBottom
|
|
168
|
+
? "pointer-events-none scale-0 opacity-0"
|
|
169
|
+
: "pointer-events-auto scale-100 opacity-100"), "aria-label": "Scroll to bottom", children: _jsx(ArrowDown, { className: "size-4" }) }))] }));
|
|
225
170
|
});
|
|
226
171
|
ChatLayoutMessages.displayName = "ChatLayout.Messages";
|
|
227
172
|
const ChatLayoutFooter = React.forwardRef(({ className, children, ...props }, ref) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
/**
|
|
3
3
|
* Response component inspired by shadcn.io/ai
|
|
4
|
-
* Streaming-optimized markdown renderer for
|
|
4
|
+
* Streaming-optimized markdown renderer using streamdown for incremental parsing
|
|
5
5
|
*/
|
|
6
6
|
export interface ResponseProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
7
7
|
/** The markdown content to render */
|
|
@@ -13,4 +13,4 @@ export interface ResponseProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
13
13
|
/** Custom empty state message */
|
|
14
14
|
emptyMessage?: string;
|
|
15
15
|
}
|
|
16
|
-
export declare const Response: React.
|
|
16
|
+
export declare const Response: React.NamedExoticComponent<ResponseProps & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
3
|
import * as React from "react";
|
|
3
|
-
import
|
|
4
|
-
import remarkGfm from "remark-gfm";
|
|
4
|
+
import { Streamdown } from "streamdown";
|
|
5
5
|
import { cn } from "../lib/utils.js";
|
|
6
|
-
export const Response = React.forwardRef(({ content, isStreaming = false, showEmpty = true, emptyMessage = "", className, ...props }, ref) => {
|
|
6
|
+
export const Response = React.memo(React.forwardRef(({ content, isStreaming = false, showEmpty = true, emptyMessage = "", className, ...props }, ref) => {
|
|
7
7
|
// Show empty state during streaming if no content yet
|
|
8
8
|
if (!content && isStreaming && showEmpty) {
|
|
9
9
|
return (_jsx("div", { ref: ref, className: cn("opacity-70 italic text-paragraph-sm", className), ...props, children: emptyMessage }));
|
|
@@ -11,89 +11,6 @@ export const Response = React.forwardRef(({ content, isStreaming = false, showEm
|
|
|
11
11
|
if (!content) {
|
|
12
12
|
return null;
|
|
13
13
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
table: ({ node, ...props }) => (_jsx("div", { className: "overflow-x-auto my-4", children: _jsx("table", { className: "min-w-full border-collapse border border-border rounded-md", ...props }) })),
|
|
17
|
-
thead: ({ node, ...props }) => (_jsx("thead", { className: "bg-card border-b border-border", ...props })),
|
|
18
|
-
tbody: ({ node, ...props }) => _jsx("tbody", { ...props }),
|
|
19
|
-
tr: ({ node, ...props }) => (_jsx("tr", { className: "border-b border-border hover:bg-card transition-colors", ...props })),
|
|
20
|
-
th: ({ node, ...props }) => (_jsx("th", { className: "px-4 py-2 text-left font-semibold text-foreground border-r border-border last:border-r-0", ...props })),
|
|
21
|
-
td: ({ node, ...props }) => (_jsx("td", { className: "px-4 py-2 text-foreground border-r border-border last:border-r-0", ...props })),
|
|
22
|
-
// Task list styling
|
|
23
|
-
input: ({ node, checked, ...props }) => {
|
|
24
|
-
if (props.type === "checkbox") {
|
|
25
|
-
return (_jsx("input", { type: "checkbox", checked: checked || false, disabled: true, readOnly: true, className: "mr-2 w-4 h-4 accent-[primary] cursor-not-allowed", ...props }));
|
|
26
|
-
}
|
|
27
|
-
return _jsx("input", { ...props });
|
|
28
|
-
},
|
|
29
|
-
// Code block styling with enhanced shadows
|
|
30
|
-
code: ({ node, ...props }) => {
|
|
31
|
-
const inline = !props.className?.includes("language-");
|
|
32
|
-
if (inline) {
|
|
33
|
-
return (_jsx("code", { className: "px-1.5 py-0.5 bg-card border border-border rounded text-code text-foreground", ...props }));
|
|
34
|
-
}
|
|
35
|
-
return (_jsx("code", { className: "block p-4 bg-card border border-border rounded-md overflow-x-auto text-code text-foreground shadow-sm", ...props }));
|
|
36
|
-
},
|
|
37
|
-
pre: ({ node, ...props }) => (_jsx("pre", { className: "my-4 rounded-lg", ...props })),
|
|
38
|
-
// Heading styling with improved hierarchy
|
|
39
|
-
h1: ({ node, ...props }) => (_jsx("h1", { className: "text-heading-3 mt-6 mb-4 text-foreground border-b border-border pb-2", ...props })),
|
|
40
|
-
h2: ({ node, ...props }) => (_jsx("h2", { className: "text-subheading mt-5 mb-3 text-foreground border-b border-border/50 pb-1.5", ...props })),
|
|
41
|
-
h3: ({ node, ...props }) => (_jsx("h3", { className: "text-subheading mt-4 mb-2 text-foreground", ...props })),
|
|
42
|
-
h4: ({ node, ...props }) => (_jsx("h4", { className: "text-paragraph-sm font-semibold mt-3 mb-2 text-foreground", ...props })),
|
|
43
|
-
// List styling
|
|
44
|
-
ul: ({ node, ...props }) => {
|
|
45
|
-
// Check if this is a task list by looking for checkbox inputs in children
|
|
46
|
-
const isTaskList = node?.children?.some((child) => typeof child === "object" &&
|
|
47
|
-
child !== null &&
|
|
48
|
-
"type" in child &&
|
|
49
|
-
child.type === "element" &&
|
|
50
|
-
"tagName" in child &&
|
|
51
|
-
child.tagName === "li" &&
|
|
52
|
-
"children" in child &&
|
|
53
|
-
Array.isArray(child.children) &&
|
|
54
|
-
child.children.some((grandChild) => typeof grandChild === "object" &&
|
|
55
|
-
grandChild !== null &&
|
|
56
|
-
"type" in grandChild &&
|
|
57
|
-
grandChild.type === "element" &&
|
|
58
|
-
"tagName" in grandChild &&
|
|
59
|
-
grandChild.tagName === "input" &&
|
|
60
|
-
"properties" in grandChild &&
|
|
61
|
-
typeof grandChild.properties === "object" &&
|
|
62
|
-
grandChild.properties !== null &&
|
|
63
|
-
"type" in grandChild.properties &&
|
|
64
|
-
grandChild.properties.type === "checkbox"));
|
|
65
|
-
return (_jsx("ul", { className: cn("my-2 space-y-1 text-foreground", isTaskList
|
|
66
|
-
? "list-none space-y-2"
|
|
67
|
-
: "list-disc list-outside pl-4"), ...props }));
|
|
68
|
-
},
|
|
69
|
-
ol: ({ node, ...props }) => (_jsx("ol", { className: "list-decimal list-outside pl-4 my-2 space-y-1 text-foreground", ...props })),
|
|
70
|
-
// List item styling
|
|
71
|
-
li: ({ node, ...props }) => {
|
|
72
|
-
// Check if this li contains a checkbox (task list item)
|
|
73
|
-
const isTaskListItem = node?.children?.some((child) => typeof child === "object" &&
|
|
74
|
-
child !== null &&
|
|
75
|
-
"type" in child &&
|
|
76
|
-
child.type === "element" &&
|
|
77
|
-
"tagName" in child &&
|
|
78
|
-
child.tagName === "input" &&
|
|
79
|
-
"properties" in child &&
|
|
80
|
-
typeof child.properties === "object" &&
|
|
81
|
-
child.properties !== null &&
|
|
82
|
-
"type" in child.properties &&
|
|
83
|
-
child.properties.type === "checkbox");
|
|
84
|
-
return (_jsx("li", { className: cn(isTaskListItem ? "flex items-start gap-2" : ""), ...props }));
|
|
85
|
-
},
|
|
86
|
-
// Link styling with hover effect
|
|
87
|
-
a: ({ node, ...props }) => (_jsx("a", { className: "text-primary hover:underline decoration-2 underline-offset-2 transition-all", target: "_blank", rel: "noopener noreferrer", ...props })),
|
|
88
|
-
// Paragraph styling
|
|
89
|
-
p: ({ node, ...props }) => (_jsx("p", { className: "mb-2 text-foreground leading-relaxed", ...props })),
|
|
90
|
-
// Blockquote styling with enhanced visual
|
|
91
|
-
blockquote: ({ node, ...props }) => (_jsx("blockquote", { className: "border-l-4 border-[primary] pl-4 italic my-4 text-foreground bg-card py-2 rounded-r-md shadow-sm", ...props })),
|
|
92
|
-
// Horizontal rule
|
|
93
|
-
hr: ({ node, ...props }) => (_jsx("hr", { className: "my-6 border-t border-border opacity-50", ...props })),
|
|
94
|
-
// Image styling
|
|
95
|
-
img: ({ node, ...props }) => (_jsx("img", { className: "max-w-full h-auto rounded-md border border-border my-4", loading: "lazy", ...props })),
|
|
96
|
-
};
|
|
97
|
-
return (_jsx("div", { ref: ref, className: cn("markdown-content prose prose-sm max-w-none dark:prose-invert", className), ...props, children: _jsx(ReactMarkdown, { remarkPlugins: [remarkGfm], components: components, children: content }) }));
|
|
98
|
-
});
|
|
14
|
+
return (_jsx("div", { ref: ref, ...props, children: _jsx(Streamdown, { className: cn("size-full", "[&>*:first-child]:mt-0 [&>*:last-child]:mb-0", "[&_code]:whitespace-pre-wrap [&_code]:break-words", "[&_pre]:max-w-full [&_pre]:overflow-x-auto", className), children: content }) }));
|
|
15
|
+
}), (prevProps, nextProps) => prevProps.content === nextProps.content);
|
|
99
16
|
Response.displayName = "Response";
|
|
@@ -178,9 +178,10 @@ export declare const Message: z.ZodObject<{
|
|
|
178
178
|
}, z.core.$strip>;
|
|
179
179
|
export type Message = z.infer<typeof Message>;
|
|
180
180
|
/**
|
|
181
|
-
* Streaming message chunk
|
|
181
|
+
* Streaming message chunk - content delta
|
|
182
182
|
*/
|
|
183
|
-
export declare const
|
|
183
|
+
export declare const ContentChunk: z.ZodObject<{
|
|
184
|
+
type: z.ZodLiteral<"content">;
|
|
184
185
|
id: z.ZodString;
|
|
185
186
|
role: z.ZodEnum<{
|
|
186
187
|
user: "user";
|
|
@@ -241,4 +242,100 @@ export declare const MessageChunk: z.ZodObject<{
|
|
|
241
242
|
}, z.core.$strip>>;
|
|
242
243
|
}, z.core.$strip>>;
|
|
243
244
|
}, z.core.$strip>;
|
|
245
|
+
export type ContentChunk = z.infer<typeof ContentChunk>;
|
|
246
|
+
/**
|
|
247
|
+
* Tool call event chunk
|
|
248
|
+
*/
|
|
249
|
+
export declare const ToolCallChunk: z.ZodObject<{
|
|
250
|
+
type: z.ZodLiteral<"tool_call">;
|
|
251
|
+
id: z.ZodString;
|
|
252
|
+
toolCall: z.ZodAny;
|
|
253
|
+
messageId: z.ZodOptional<z.ZodString>;
|
|
254
|
+
}, z.core.$strip>;
|
|
255
|
+
export type ToolCallChunk = z.infer<typeof ToolCallChunk>;
|
|
256
|
+
/**
|
|
257
|
+
* Tool call update chunk
|
|
258
|
+
*/
|
|
259
|
+
export declare const ToolCallUpdateChunk: z.ZodObject<{
|
|
260
|
+
type: z.ZodLiteral<"tool_call_update">;
|
|
261
|
+
id: z.ZodString;
|
|
262
|
+
toolCallUpdate: z.ZodAny;
|
|
263
|
+
messageId: z.ZodOptional<z.ZodString>;
|
|
264
|
+
}, z.core.$strip>;
|
|
265
|
+
export type ToolCallUpdateChunk = z.infer<typeof ToolCallUpdateChunk>;
|
|
266
|
+
/**
|
|
267
|
+
* Message chunk - discriminated union of all chunk types
|
|
268
|
+
*/
|
|
269
|
+
export declare const MessageChunk: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
270
|
+
type: z.ZodLiteral<"content">;
|
|
271
|
+
id: z.ZodString;
|
|
272
|
+
role: z.ZodEnum<{
|
|
273
|
+
user: "user";
|
|
274
|
+
assistant: "assistant";
|
|
275
|
+
system: "system";
|
|
276
|
+
tool: "tool";
|
|
277
|
+
}>;
|
|
278
|
+
contentDelta: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
279
|
+
type: z.ZodLiteral<"text">;
|
|
280
|
+
text: z.ZodString;
|
|
281
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
282
|
+
type: z.ZodLiteral<"image">;
|
|
283
|
+
url: z.ZodOptional<z.ZodString>;
|
|
284
|
+
source: z.ZodOptional<z.ZodObject<{
|
|
285
|
+
type: z.ZodLiteral<"base64">;
|
|
286
|
+
media_type: z.ZodEnum<{
|
|
287
|
+
"image/jpeg": "image/jpeg";
|
|
288
|
+
"image/png": "image/png";
|
|
289
|
+
"image/gif": "image/gif";
|
|
290
|
+
"image/webp": "image/webp";
|
|
291
|
+
}>;
|
|
292
|
+
data: z.ZodString;
|
|
293
|
+
}, z.core.$strip>>;
|
|
294
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
295
|
+
type: z.ZodLiteral<"file">;
|
|
296
|
+
name: z.ZodString;
|
|
297
|
+
path: z.ZodOptional<z.ZodString>;
|
|
298
|
+
url: z.ZodOptional<z.ZodString>;
|
|
299
|
+
mimeType: z.ZodString;
|
|
300
|
+
size: z.ZodOptional<z.ZodNumber>;
|
|
301
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
302
|
+
type: z.ZodLiteral<"tool_call">;
|
|
303
|
+
id: z.ZodString;
|
|
304
|
+
name: z.ZodString;
|
|
305
|
+
arguments: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
306
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
307
|
+
type: z.ZodLiteral<"tool_result">;
|
|
308
|
+
callId: z.ZodString;
|
|
309
|
+
result: z.ZodUnknown;
|
|
310
|
+
error: z.ZodOptional<z.ZodString>;
|
|
311
|
+
}, z.core.$strip>], "type">;
|
|
312
|
+
isComplete: z.ZodBoolean;
|
|
313
|
+
tokenUsage: z.ZodOptional<z.ZodObject<{
|
|
314
|
+
inputTokens: z.ZodOptional<z.ZodNumber>;
|
|
315
|
+
outputTokens: z.ZodOptional<z.ZodNumber>;
|
|
316
|
+
totalTokens: z.ZodOptional<z.ZodNumber>;
|
|
317
|
+
}, z.core.$strip>>;
|
|
318
|
+
contextInputTokens: z.ZodOptional<z.ZodNumber>;
|
|
319
|
+
_meta: z.ZodOptional<z.ZodObject<{
|
|
320
|
+
context_size: z.ZodOptional<z.ZodObject<{
|
|
321
|
+
systemPromptTokens: z.ZodNumber;
|
|
322
|
+
userMessagesTokens: z.ZodNumber;
|
|
323
|
+
assistantMessagesTokens: z.ZodNumber;
|
|
324
|
+
toolInputTokens: z.ZodNumber;
|
|
325
|
+
toolResultsTokens: z.ZodNumber;
|
|
326
|
+
totalEstimated: z.ZodNumber;
|
|
327
|
+
llmReportedInputTokens: z.ZodOptional<z.ZodNumber>;
|
|
328
|
+
}, z.core.$strip>>;
|
|
329
|
+
}, z.core.$strip>>;
|
|
330
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
331
|
+
type: z.ZodLiteral<"tool_call">;
|
|
332
|
+
id: z.ZodString;
|
|
333
|
+
toolCall: z.ZodAny;
|
|
334
|
+
messageId: z.ZodOptional<z.ZodString>;
|
|
335
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
336
|
+
type: z.ZodLiteral<"tool_call_update">;
|
|
337
|
+
id: z.ZodString;
|
|
338
|
+
toolCallUpdate: z.ZodAny;
|
|
339
|
+
messageId: z.ZodOptional<z.ZodString>;
|
|
340
|
+
}, z.core.$strip>], "type">;
|
|
244
341
|
export type MessageChunk = z.infer<typeof MessageChunk>;
|
|
@@ -97,9 +97,10 @@ export const Message = z.object({
|
|
|
97
97
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
98
98
|
});
|
|
99
99
|
/**
|
|
100
|
-
* Streaming message chunk
|
|
100
|
+
* Streaming message chunk - content delta
|
|
101
101
|
*/
|
|
102
|
-
export const
|
|
102
|
+
export const ContentChunk = z.object({
|
|
103
|
+
type: z.literal("content"),
|
|
103
104
|
id: z.string(),
|
|
104
105
|
role: MessageRole,
|
|
105
106
|
contentDelta: Content,
|
|
@@ -128,3 +129,29 @@ export const MessageChunk = z.object({
|
|
|
128
129
|
})
|
|
129
130
|
.optional(),
|
|
130
131
|
});
|
|
132
|
+
/**
|
|
133
|
+
* Tool call event chunk
|
|
134
|
+
*/
|
|
135
|
+
export const ToolCallChunk = z.object({
|
|
136
|
+
type: z.literal("tool_call"),
|
|
137
|
+
id: z.string(),
|
|
138
|
+
toolCall: z.any(), // Will be typed properly by importing ToolCall
|
|
139
|
+
messageId: z.string().optional(),
|
|
140
|
+
});
|
|
141
|
+
/**
|
|
142
|
+
* Tool call update chunk
|
|
143
|
+
*/
|
|
144
|
+
export const ToolCallUpdateChunk = z.object({
|
|
145
|
+
type: z.literal("tool_call_update"),
|
|
146
|
+
id: z.string(),
|
|
147
|
+
toolCallUpdate: z.any(), // Will be typed properly by importing ToolCallUpdate
|
|
148
|
+
messageId: z.string().optional(),
|
|
149
|
+
});
|
|
150
|
+
/**
|
|
151
|
+
* Message chunk - discriminated union of all chunk types
|
|
152
|
+
*/
|
|
153
|
+
export const MessageChunk = z.discriminatedUnion("type", [
|
|
154
|
+
ContentChunk,
|
|
155
|
+
ToolCallChunk,
|
|
156
|
+
ToolCallUpdateChunk,
|
|
157
|
+
]);
|
|
@@ -346,6 +346,7 @@ export class HttpTransport {
|
|
|
346
346
|
const resolver = this.chunkResolvers.shift();
|
|
347
347
|
if (resolver) {
|
|
348
348
|
resolver({
|
|
349
|
+
type: "content",
|
|
349
350
|
id: this.currentSessionId || "unknown",
|
|
350
351
|
role: "assistant",
|
|
351
352
|
contentDelta: { type: "text", text: "" },
|
|
@@ -354,6 +355,7 @@ export class HttpTransport {
|
|
|
354
355
|
}
|
|
355
356
|
else {
|
|
356
357
|
this.messageQueue.push({
|
|
358
|
+
type: "content",
|
|
357
359
|
id: this.currentSessionId || "unknown",
|
|
358
360
|
role: "assistant",
|
|
359
361
|
contentDelta: { type: "text", text: "" },
|
|
@@ -379,7 +381,7 @@ export class HttpTransport {
|
|
|
379
381
|
const chunk = this.messageQueue.shift();
|
|
380
382
|
if (chunk) {
|
|
381
383
|
yield chunk;
|
|
382
|
-
if (chunk.isComplete) {
|
|
384
|
+
if (chunk.type === "content" && chunk.isComplete) {
|
|
383
385
|
return;
|
|
384
386
|
}
|
|
385
387
|
}
|
|
@@ -389,7 +391,7 @@ export class HttpTransport {
|
|
|
389
391
|
const chunk = await new Promise((resolve) => {
|
|
390
392
|
this.chunkResolvers.push(resolve);
|
|
391
393
|
});
|
|
392
|
-
if (chunk.isComplete) {
|
|
394
|
+
if (chunk.type === "content" && chunk.isComplete) {
|
|
393
395
|
yield chunk;
|
|
394
396
|
return;
|
|
395
397
|
}
|
|
@@ -407,6 +409,7 @@ export class HttpTransport {
|
|
|
407
409
|
}
|
|
408
410
|
// Mark the stream as complete
|
|
409
411
|
yield {
|
|
412
|
+
type: "content",
|
|
410
413
|
id: this.currentSessionId || "unknown",
|
|
411
414
|
role: "assistant",
|
|
412
415
|
contentDelta: { type: "text", text: "" },
|
|
@@ -760,7 +763,20 @@ export class HttpTransport {
|
|
|
760
763
|
toolCall: toolCall,
|
|
761
764
|
messageId,
|
|
762
765
|
};
|
|
763
|
-
|
|
766
|
+
// Queue tool call as a chunk for ordered processing
|
|
767
|
+
const toolCallChunk = {
|
|
768
|
+
type: "tool_call",
|
|
769
|
+
id: sessionId,
|
|
770
|
+
toolCall: toolCall,
|
|
771
|
+
messageId,
|
|
772
|
+
};
|
|
773
|
+
const resolver = this.chunkResolvers.shift();
|
|
774
|
+
if (resolver) {
|
|
775
|
+
resolver(toolCallChunk);
|
|
776
|
+
}
|
|
777
|
+
else {
|
|
778
|
+
this.messageQueue.push(toolCallChunk);
|
|
779
|
+
}
|
|
764
780
|
}
|
|
765
781
|
else if (update?.sessionUpdate === "tool_call_update") {
|
|
766
782
|
// Extract messageId and metadata from _meta
|
|
@@ -882,10 +898,23 @@ export class HttpTransport {
|
|
|
882
898
|
toolCallUpdate: toolCallUpdate,
|
|
883
899
|
messageId,
|
|
884
900
|
};
|
|
885
|
-
|
|
901
|
+
// Queue tool call update as a chunk for ordered processing
|
|
902
|
+
const toolCallUpdateChunk = {
|
|
903
|
+
type: "tool_call_update",
|
|
904
|
+
id: sessionId,
|
|
905
|
+
toolCallUpdate: toolCallUpdate,
|
|
906
|
+
messageId,
|
|
907
|
+
};
|
|
908
|
+
const resolver = this.chunkResolvers.shift();
|
|
909
|
+
if (resolver) {
|
|
910
|
+
resolver(toolCallUpdateChunk);
|
|
911
|
+
}
|
|
912
|
+
else {
|
|
913
|
+
this.messageQueue.push(toolCallUpdateChunk);
|
|
914
|
+
}
|
|
915
|
+
logger.debug("Queued tool_call_update chunk", {
|
|
886
916
|
sessionUpdate,
|
|
887
917
|
});
|
|
888
|
-
this.notifySessionUpdate(sessionUpdate);
|
|
889
918
|
}
|
|
890
919
|
else if (update &&
|
|
891
920
|
"sessionUpdate" in update &&
|
|
@@ -966,10 +995,23 @@ export class HttpTransport {
|
|
|
966
995
|
toolCallUpdate: toolOutput,
|
|
967
996
|
messageId,
|
|
968
997
|
};
|
|
969
|
-
|
|
998
|
+
// Queue tool output as a chunk for ordered processing
|
|
999
|
+
const toolCallUpdateChunk = {
|
|
1000
|
+
type: "tool_call_update",
|
|
1001
|
+
id: sessionId,
|
|
1002
|
+
toolCallUpdate: toolOutput,
|
|
1003
|
+
messageId,
|
|
1004
|
+
};
|
|
1005
|
+
const resolver = this.chunkResolvers.shift();
|
|
1006
|
+
if (resolver) {
|
|
1007
|
+
resolver(toolCallUpdateChunk);
|
|
1008
|
+
}
|
|
1009
|
+
else {
|
|
1010
|
+
this.messageQueue.push(toolCallUpdateChunk);
|
|
1011
|
+
}
|
|
1012
|
+
logger.debug("Queued tool_output as tool_call_update chunk", {
|
|
970
1013
|
sessionUpdate,
|
|
971
1014
|
});
|
|
972
|
-
this.notifySessionUpdate(sessionUpdate);
|
|
973
1015
|
}
|
|
974
1016
|
else if (update?.sessionUpdate === "agent_message_chunk") {
|
|
975
1017
|
// Check if this is a replay (not live streaming)
|
|
@@ -1015,6 +1057,7 @@ export class HttpTransport {
|
|
|
1015
1057
|
let chunk = null;
|
|
1016
1058
|
if (contentObj.type === "text" && typeof contentObj.text === "string") {
|
|
1017
1059
|
chunk = {
|
|
1060
|
+
type: "content",
|
|
1018
1061
|
id: params.sessionId,
|
|
1019
1062
|
role: "assistant",
|
|
1020
1063
|
contentDelta: { type: "text", text: contentObj.text },
|
|
@@ -186,7 +186,19 @@ export class StdioTransport {
|
|
|
186
186
|
status: "active",
|
|
187
187
|
toolCall: toolCall,
|
|
188
188
|
};
|
|
189
|
-
|
|
189
|
+
// Queue tool call as a chunk for ordered processing
|
|
190
|
+
const toolCallChunk = {
|
|
191
|
+
type: "tool_call",
|
|
192
|
+
id: sessionId,
|
|
193
|
+
toolCall: toolCall,
|
|
194
|
+
};
|
|
195
|
+
const resolver = self.chunkResolvers.shift();
|
|
196
|
+
if (resolver) {
|
|
197
|
+
resolver(toolCallChunk);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
self.messageQueue.push(toolCallChunk);
|
|
201
|
+
}
|
|
190
202
|
}
|
|
191
203
|
else if (update?.sessionUpdate === "tool_call_update") {
|
|
192
204
|
// Tool call update notification
|
|
@@ -259,7 +271,19 @@ export class StdioTransport {
|
|
|
259
271
|
status: "active",
|
|
260
272
|
toolCallUpdate: toolCallUpdate,
|
|
261
273
|
};
|
|
262
|
-
|
|
274
|
+
// Queue tool call update as a chunk for ordered processing
|
|
275
|
+
const toolCallUpdateChunk = {
|
|
276
|
+
type: "tool_call_update",
|
|
277
|
+
id: sessionId,
|
|
278
|
+
toolCallUpdate: toolCallUpdate,
|
|
279
|
+
};
|
|
280
|
+
const resolver = self.chunkResolvers.shift();
|
|
281
|
+
if (resolver) {
|
|
282
|
+
resolver(toolCallUpdateChunk);
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
self.messageQueue.push(toolCallUpdateChunk);
|
|
286
|
+
}
|
|
263
287
|
}
|
|
264
288
|
else if (update?.sessionUpdate === "agent_message_chunk") {
|
|
265
289
|
// Handle agent message chunks
|
|
@@ -291,6 +315,7 @@ export class StdioTransport {
|
|
|
291
315
|
if (contentObj.type === "text" &&
|
|
292
316
|
typeof contentObj.text === "string") {
|
|
293
317
|
chunk = {
|
|
318
|
+
type: "content",
|
|
294
319
|
id: params.sessionId,
|
|
295
320
|
role: "assistant",
|
|
296
321
|
contentDelta: { type: "text", text: contentObj.text },
|
|
@@ -485,6 +510,7 @@ export class StdioTransport {
|
|
|
485
510
|
const resolver = this.chunkResolvers.shift();
|
|
486
511
|
if (resolver) {
|
|
487
512
|
resolver({
|
|
513
|
+
type: "content",
|
|
488
514
|
id: this.currentSessionId || "unknown",
|
|
489
515
|
role: "assistant",
|
|
490
516
|
contentDelta: { type: "text", text: "" },
|
|
@@ -507,6 +533,9 @@ export class StdioTransport {
|
|
|
507
533
|
const chunk = this.messageQueue.shift();
|
|
508
534
|
if (chunk) {
|
|
509
535
|
yield chunk;
|
|
536
|
+
if (chunk.type === "content" && chunk.isComplete) {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
510
539
|
}
|
|
511
540
|
}
|
|
512
541
|
else {
|
|
@@ -514,7 +543,7 @@ export class StdioTransport {
|
|
|
514
543
|
const chunk = await new Promise((resolve) => {
|
|
515
544
|
this.chunkResolvers.push(resolve);
|
|
516
545
|
});
|
|
517
|
-
if (chunk.isComplete) {
|
|
546
|
+
if (chunk.type === "content" && chunk.isComplete) {
|
|
518
547
|
yield chunk;
|
|
519
548
|
return;
|
|
520
549
|
}
|
|
@@ -532,6 +561,7 @@ export class StdioTransport {
|
|
|
532
561
|
}
|
|
533
562
|
// Mark the stream as complete
|
|
534
563
|
yield {
|
|
564
|
+
type: "content",
|
|
535
565
|
id: this.currentSessionId || "unknown",
|
|
536
566
|
role: "assistant",
|
|
537
567
|
contentDelta: { type: "text", text: "" },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@townco/ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.65",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -42,13 +42,14 @@
|
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@agentclientprotocol/sdk": "^0.5.1",
|
|
45
|
-
"@townco/core": "0.0.36",
|
|
46
45
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
47
46
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
48
47
|
"@radix-ui/react-label": "^2.1.8",
|
|
49
48
|
"@radix-ui/react-select": "^2.2.6",
|
|
50
49
|
"@radix-ui/react-slot": "^1.2.4",
|
|
51
50
|
"@radix-ui/react-tabs": "^1.1.13",
|
|
51
|
+
"@radix-ui/react-tooltip": "^1.2.8",
|
|
52
|
+
"@townco/core": "0.0.43",
|
|
52
53
|
"@uiw/react-json-view": "^2.0.0-alpha.39",
|
|
53
54
|
"bun": "^1.3.1",
|
|
54
55
|
"class-variance-authority": "^0.7.1",
|
|
@@ -56,16 +57,16 @@
|
|
|
56
57
|
"lucide-react": "^0.552.0",
|
|
57
58
|
"react-markdown": "^10.1.0",
|
|
58
59
|
"react-resizable-panels": "^3.0.6",
|
|
59
|
-
"@radix-ui/react-tooltip": "^1.2.8",
|
|
60
60
|
"remark-gfm": "^4.0.1",
|
|
61
61
|
"sonner": "^2.0.7",
|
|
62
|
+
"streamdown": "^1.6.9",
|
|
62
63
|
"tailwind-merge": "^3.3.1",
|
|
63
64
|
"zod": "^4.1.12",
|
|
64
65
|
"zustand": "^5.0.8"
|
|
65
66
|
},
|
|
66
67
|
"devDependencies": {
|
|
67
68
|
"@tailwindcss/postcss": "^4.1.17",
|
|
68
|
-
"@townco/tsconfig": "0.1.
|
|
69
|
+
"@townco/tsconfig": "0.1.62",
|
|
69
70
|
"@types/node": "^24.10.0",
|
|
70
71
|
"@types/react": "^19.2.2",
|
|
71
72
|
"ink": "^6.4.0",
|
package/src/styles/global.css
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
@source "../**/*.{ts,tsx}";
|
|
4
4
|
|
|
5
|
+
/* Include utility classes used by streamdown for markdown rendering */
|
|
6
|
+
@source '../node_modules/streamdown/dist/index.js';
|
|
7
|
+
|
|
5
8
|
@theme {
|
|
6
9
|
/* Semantic Color Tokens */
|
|
7
10
|
--color-background: var(--background);
|
|
@@ -206,6 +209,27 @@
|
|
|
206
209
|
}
|
|
207
210
|
}
|
|
208
211
|
|
|
212
|
+
/* Streamdown code block dark mode styling */
|
|
213
|
+
/* Ensure Shiki dark theme variables are applied in dark mode */
|
|
214
|
+
.dark [data-streamdown="code-block"] {
|
|
215
|
+
background-color: var(--shiki-dark-bg, #24292e);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.dark [data-streamdown="code-block-body"] {
|
|
219
|
+
background-color: var(--shiki-dark-bg, #24292e) !important;
|
|
220
|
+
color: var(--shiki-dark, #e1e4e8) !important;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.dark [data-streamdown="code-block-header"] {
|
|
224
|
+
background-color: color-mix(in srgb, var(--shiki-dark-bg, #24292e) 80%, transparent);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/* Ensure inline code also has proper dark mode styling */
|
|
228
|
+
.dark [data-streamdown="inline-code"] {
|
|
229
|
+
background-color: var(--muted);
|
|
230
|
+
color: var(--foreground);
|
|
231
|
+
}
|
|
232
|
+
|
|
209
233
|
@layer utilities {
|
|
210
234
|
/* Typography utilities following design system */
|
|
211
235
|
.text-heading-1 {
|