@townco/ui 0.1.82 → 0.1.93
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-input.js +13 -6
- package/dist/core/hooks/use-chat-messages.d.ts +17 -0
- package/dist/core/hooks/use-chat-messages.js +294 -10
- package/dist/core/schemas/chat.d.ts +20 -0
- package/dist/core/schemas/chat.js +4 -0
- package/dist/core/schemas/index.d.ts +1 -0
- package/dist/core/schemas/index.js +1 -0
- package/dist/core/schemas/source.d.ts +22 -0
- package/dist/core/schemas/source.js +45 -0
- package/dist/core/store/chat-store.d.ts +4 -0
- package/dist/core/store/chat-store.js +54 -0
- package/dist/gui/components/Actions.d.ts +15 -0
- package/dist/gui/components/Actions.js +22 -0
- package/dist/gui/components/ChatInput.d.ts +9 -1
- package/dist/gui/components/ChatInput.js +24 -6
- package/dist/gui/components/ChatInputCommandMenu.d.ts +1 -0
- package/dist/gui/components/ChatInputCommandMenu.js +22 -5
- package/dist/gui/components/ChatInputParameters.d.ts +13 -0
- package/dist/gui/components/ChatInputParameters.js +67 -0
- package/dist/gui/components/ChatLayout.d.ts +2 -0
- package/dist/gui/components/ChatLayout.js +183 -61
- package/dist/gui/components/ChatPanelTabContent.d.ts +7 -0
- package/dist/gui/components/ChatPanelTabContent.js +17 -7
- package/dist/gui/components/ChatView.js +105 -15
- package/dist/gui/components/CitationChip.d.ts +15 -0
- package/dist/gui/components/CitationChip.js +72 -0
- package/dist/gui/components/EditableUserMessage.d.ts +18 -0
- package/dist/gui/components/EditableUserMessage.js +109 -0
- package/dist/gui/components/MessageActions.d.ts +16 -0
- package/dist/gui/components/MessageActions.js +97 -0
- package/dist/gui/components/MessageContent.js +22 -7
- package/dist/gui/components/Response.d.ts +3 -0
- package/dist/gui/components/Response.js +30 -3
- package/dist/gui/components/Sidebar.js +1 -1
- package/dist/gui/components/TodoSubline.js +1 -1
- package/dist/gui/components/WorkProgress.js +7 -0
- package/dist/gui/components/index.d.ts +6 -1
- package/dist/gui/components/index.js +6 -1
- package/dist/gui/hooks/index.d.ts +1 -0
- package/dist/gui/hooks/index.js +1 -0
- package/dist/gui/hooks/use-favicon.d.ts +6 -0
- package/dist/gui/hooks/use-favicon.js +47 -0
- package/dist/gui/hooks/use-scroll-to-bottom.d.ts +14 -0
- package/dist/gui/hooks/use-scroll-to-bottom.js +317 -1
- package/dist/gui/index.d.ts +1 -1
- package/dist/gui/index.js +1 -1
- package/dist/gui/lib/motion.js +6 -6
- package/dist/gui/lib/remark-citations.d.ts +28 -0
- package/dist/gui/lib/remark-citations.js +70 -0
- package/dist/sdk/client/acp-client.d.ts +38 -1
- package/dist/sdk/client/acp-client.js +67 -3
- package/dist/sdk/schemas/message.d.ts +40 -0
- package/dist/sdk/schemas/message.js +20 -0
- package/dist/sdk/transports/http.d.ts +24 -1
- package/dist/sdk/transports/http.js +189 -1
- package/dist/sdk/transports/stdio.d.ts +1 -0
- package/dist/sdk/transports/stdio.js +39 -0
- package/dist/sdk/transports/types.d.ts +46 -1
- package/dist/sdk/transports/websocket.d.ts +1 -0
- package/dist/sdk/transports/websocket.js +4 -0
- package/dist/tui/components/ChatView.js +3 -4
- package/package.json +5 -3
- package/src/styles/global.css +71 -0
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { createLogger } from "@townco/core";
|
|
3
|
-
import { ArrowUp, Bug, PanelRight, Settings, X } from "lucide-react";
|
|
3
|
+
import { ArrowUp, Bug, Moon, PanelRight, Settings, Sun, X } from "lucide-react";
|
|
4
|
+
import * as React from "react";
|
|
4
5
|
import { useEffect, useState } from "react";
|
|
5
6
|
import { useChatMessages, useChatSession, useToolCalls, } from "../../core/hooks/index.js";
|
|
6
7
|
import { selectTodosForCurrentSession, useChatStore, } from "../../core/store/chat-store.js";
|
|
8
|
+
import { useDocumentTitle } from "../hooks/use-favicon.js";
|
|
7
9
|
import { cn } from "../lib/utils.js";
|
|
8
|
-
import { AppSidebar, ChatEmptyState, ChatHeader, ChatInputActions, ChatInputAttachment, ChatInputCommandMenu, ChatInputField, ChatInputRoot, ChatInputSubmit, ChatInputToolbar, ChatInputVoiceInput, ChatLayout, ContextUsageButton, FilesTabContent, IconButton, Message, MessageContent, PanelTabsHeader, SettingsTabContent, SidebarInset, SidebarProvider, SidebarToggle, SourcesTabContent, Tabs, TabsContent, TodoTabContent, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "./index.js";
|
|
10
|
+
import { AppSidebar, ChatEmptyState, ChatHeader, ChatInputActions, ChatInputAttachment, ChatInputCommandMenu, ChatInputField, ChatInputParameters, ChatInputRoot, ChatInputStop, ChatInputSubmit, ChatInputToolbar, ChatInputVoiceInput, ChatLayout, ContextUsageButton, EditableUserMessage, FilesTabContent, IconButton, Message, MessageActions, MessageContent, PanelTabsHeader, SettingsTabContent, SidebarInset, SidebarProvider, SidebarToggle, SourcesTabContent, Tabs, TabsContent, TodoTabContent, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, useTheme, } from "./index.js";
|
|
9
11
|
const logger = createLogger("gui");
|
|
10
12
|
// Helper component to provide openFiles callback
|
|
11
13
|
function OpenFilesButton({ children, }) {
|
|
@@ -33,7 +35,7 @@ function OpenFilesButton({ children, }) {
|
|
|
33
35
|
// Note: Keyboard shortcut (Cmd+B / Ctrl+B) for toggling the right panel
|
|
34
36
|
// is now handled internally by ChatLayout.Root
|
|
35
37
|
// Chat input with attachment handling
|
|
36
|
-
function ChatInputWithAttachments({ client, startSession, placeholder, commandMenuItems, }) {
|
|
38
|
+
function ChatInputWithAttachments({ client, startSession, placeholder, commandMenuItems, onCancel, promptParameters, }) {
|
|
37
39
|
const attachedFiles = useChatStore((state) => state.input.attachedFiles);
|
|
38
40
|
const addFileAttachment = useChatStore((state) => state.addFileAttachment);
|
|
39
41
|
const removeFileAttachment = useChatStore((state) => state.removeFileAttachment);
|
|
@@ -42,23 +44,22 @@ function ChatInputWithAttachments({ client, startSession, placeholder, commandMe
|
|
|
42
44
|
addFileAttachment(file);
|
|
43
45
|
}
|
|
44
46
|
};
|
|
45
|
-
return (_jsxs(ChatInputRoot, { client: client, startSession: startSession, children: [
|
|
47
|
+
return (_jsxs(ChatInputRoot, { client: client, startSession: startSession, onCancel: onCancel, children: [
|
|
46
48
|
_jsx(ChatInputCommandMenu, { commands: commandMenuItems }), attachedFiles.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2 p-3 border-b border-border", children: attachedFiles.map((file, index) => (_jsxs("div", { className: "relative group rounded-md overflow-hidden border border-border", children: [
|
|
47
49
|
_jsx("img", { src: `data:${file.mimeType};base64,${file.data}`, alt: file.name, className: "h-20 w-20 object-cover" }), _jsx("button", { type: "button", onClick: () => removeFileAttachment(index), className: "absolute top-1 right-1 p-1 rounded-full bg-background/80 hover:bg-background opacity-0 group-hover:opacity-100 transition-opacity", children: _jsx(X, { className: "size-3" }) })
|
|
48
50
|
] }, `attached-${file.name}-${file.data.slice(0, 20)}`))) })), _jsx(ChatInputField, { placeholder: placeholder, autoFocus: true, onFilesDropped: handleFilesSelected }), _jsxs(ChatInputToolbar, { children: [
|
|
49
|
-
_jsxs("div", { className: "flex items-
|
|
50
|
-
_jsx(ChatInputActions, {}), _jsx(ChatInputAttachment, { onFilesSelected: handleFilesSelected })
|
|
51
|
-
|
|
52
|
-
_jsx(ChatInputVoiceInput, {}), _jsx(ChatInputSubmit, { children: _jsx(ArrowUp, { className: "size-4" }) })
|
|
51
|
+
_jsxs("div", { className: "flex items-center gap-1", children: [
|
|
52
|
+
_jsx(ChatInputActions, {}), _jsx(ChatInputAttachment, { onFilesSelected: handleFilesSelected }), promptParameters && promptParameters.length > 0 && (_jsx(ChatInputParameters, { parameters: promptParameters }))] }), _jsxs("div", { className: "flex items-center gap-1", children: [
|
|
53
|
+
_jsx(ChatInputVoiceInput, {}), _jsx(ChatInputStop, {}), _jsx(ChatInputSubmit, { children: _jsx(ArrowUp, { className: "size-4" }) })
|
|
53
54
|
] })
|
|
54
55
|
] })
|
|
55
56
|
] }));
|
|
56
57
|
}
|
|
57
58
|
// Controlled Tabs component for the aside panel
|
|
58
|
-
function AsideTabs({ todos, tools, mcps, subagents, }) {
|
|
59
|
+
function AsideTabs({ todos, sources, tools, mcps, subagents, }) {
|
|
59
60
|
const { activeTab, setActiveTab } = ChatLayout.useChatLayoutContext();
|
|
60
61
|
return (_jsxs(Tabs, { value: activeTab, onValueChange: (value) => setActiveTab(value), className: "flex flex-col h-full", children: [
|
|
61
|
-
_jsx("div", { className: cn("border-b border-border bg-card", "px-4 py-2 h-16", "flex items-center"), children: _jsx(PanelTabsHeader, { showIcons: true, visibleTabs: ["todo", "files", "sources", "settings"], variant: "compact" }) }), _jsx(TabsContent, { value: "todo", className: "flex-1 p-4 mt-0", children: _jsx(TodoTabContent, { todos: todos }) }), _jsx(TabsContent, { value: "files", className: "flex-1 p-4 mt-0", children: _jsx(FilesTabContent, {}) }), _jsx(TabsContent, { value: "sources", className: "flex-1 p-4 mt-0", children: _jsx(SourcesTabContent, {}) }), _jsx(TabsContent, { value: "settings", className: "flex-1 p-4 mt-0 overflow-y-auto", children: _jsx(SettingsTabContent, { tools: tools ?? [], mcps: mcps ?? [], subagents: subagents ?? [] }) })
|
|
62
|
+
_jsx("div", { className: cn("border-b border-border bg-card", "px-4 py-2 h-16", "flex items-center", "sticky top-0 z-10 shrink-0"), children: _jsx(PanelTabsHeader, { showIcons: true, visibleTabs: ["todo", "files", "sources", "settings"], variant: "compact" }) }), _jsx(TabsContent, { value: "todo", className: "flex-1 p-4 mt-0 overflow-y-auto", children: _jsx(TodoTabContent, { todos: todos }) }), _jsx(TabsContent, { value: "files", className: "flex-1 p-4 mt-0 overflow-y-auto", children: _jsx(FilesTabContent, {}) }), _jsx(TabsContent, { value: "sources", className: "flex-1 p-4 mt-0 overflow-y-auto", children: _jsx(SourcesTabContent, { sources: sources }) }), _jsx(TabsContent, { value: "settings", className: "flex-1 p-4 mt-0 overflow-y-auto", children: _jsx(SettingsTabContent, { tools: tools ?? [], mcps: mcps ?? [], subagents: subagents ?? [] }) })
|
|
62
63
|
] }));
|
|
63
64
|
}
|
|
64
65
|
// Header component that uses ChatLayout context (must be inside ChatLayout.Root)
|
|
@@ -81,7 +82,7 @@ function AppChatHeader({ agentName, showHeader, sessionId, debuggerUrl, }) {
|
|
|
81
82
|
export function ChatView({ client, initialSessionId, error: initError, debuggerUrl, }) {
|
|
82
83
|
// Use shared hooks from @townco/ui/core - MUST be called before any early returns
|
|
83
84
|
const { connectionStatus, connect, sessionId, startSession } = useChatSession(client, initialSessionId);
|
|
84
|
-
const { messages, sendMessage } = useChatMessages(client, startSession);
|
|
85
|
+
const { messages, sendMessage, editAndResend, cancel } = useChatMessages(client, startSession);
|
|
85
86
|
useToolCalls(client); // Still need to subscribe to tool call events
|
|
86
87
|
const error = useChatStore((state) => state.error);
|
|
87
88
|
const [agentName, setAgentName] = useState(undefined);
|
|
@@ -90,10 +91,33 @@ export function ChatView({ client, initialSessionId, error: initError, debuggerU
|
|
|
90
91
|
const [agentTools, setAgentTools] = useState([]);
|
|
91
92
|
const [agentMcps, setAgentMcps] = useState([]);
|
|
92
93
|
const [agentSubagents, setAgentSubagents] = useState([]);
|
|
94
|
+
const [agentPromptParameters, setAgentPromptParameters] = useState([]);
|
|
93
95
|
const [placeholder, setPlaceholder] = useState("Type a message or / for commands...");
|
|
94
96
|
const [hideTopBar, setHideTopBar] = useState(false);
|
|
95
97
|
const todos = useChatStore(selectTodosForCurrentSession);
|
|
96
98
|
const _latestContextSize = useChatStore((state) => state.latestContextSize);
|
|
99
|
+
const { resolvedTheme, setTheme } = useTheme();
|
|
100
|
+
// Collect all sources from all messages for the sources panel
|
|
101
|
+
const allSources = React.useMemo(() => {
|
|
102
|
+
const sources = [];
|
|
103
|
+
for (const message of messages) {
|
|
104
|
+
if (message.sources) {
|
|
105
|
+
for (const source of message.sources) {
|
|
106
|
+
sources.push({
|
|
107
|
+
id: source.id,
|
|
108
|
+
title: source.title,
|
|
109
|
+
url: source.url,
|
|
110
|
+
snippet: source.snippet || "",
|
|
111
|
+
sourceName: source.sourceName || "",
|
|
112
|
+
favicon: source.favicon || "",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return sources;
|
|
118
|
+
}, [messages]);
|
|
119
|
+
// Update document title with agent name and animated dots when streaming
|
|
120
|
+
useDocumentTitle(agentName);
|
|
97
121
|
// Log connection status changes
|
|
98
122
|
useEffect(() => {
|
|
99
123
|
logger.debug("Connection status changed", { status: connectionStatus });
|
|
@@ -145,6 +169,12 @@ export function ChatView({ client, initialSessionId, error: initError, debuggerU
|
|
|
145
169
|
subagents: agentInfo.subagents,
|
|
146
170
|
});
|
|
147
171
|
}
|
|
172
|
+
if (agentInfo.promptParameters && agentInfo.promptParameters.length > 0) {
|
|
173
|
+
setAgentPromptParameters(agentInfo.promptParameters);
|
|
174
|
+
logger.info("Agent prompt parameters loaded", {
|
|
175
|
+
promptParameters: agentInfo.promptParameters,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
148
178
|
if (agentInfo.uiConfig?.hideTopBar) {
|
|
149
179
|
setHideTopBar(true);
|
|
150
180
|
}
|
|
@@ -201,6 +231,19 @@ export function ChatView({ client, initialSessionId, error: initError, debuggerU
|
|
|
201
231
|
];
|
|
202
232
|
// Command menu items for chat input
|
|
203
233
|
const commandMenuItems = [
|
|
234
|
+
{
|
|
235
|
+
id: "dark-mode",
|
|
236
|
+
label: resolvedTheme === "dark" ? "Light Mode" : "Dark Mode",
|
|
237
|
+
description: `Switch to ${resolvedTheme === "dark" ? "light" : "dark"} theme`,
|
|
238
|
+
icon: resolvedTheme === "dark" ? (_jsx(Sun, { className: "h-4 w-4" })) : (_jsx(Moon, { className: "h-4 w-4" })),
|
|
239
|
+
category: "action",
|
|
240
|
+
onSelect: () => {
|
|
241
|
+
setTheme(resolvedTheme === "dark" ? "light" : "dark");
|
|
242
|
+
logger.info("User toggled theme", {
|
|
243
|
+
newTheme: resolvedTheme === "dark" ? "light" : "dark",
|
|
244
|
+
});
|
|
245
|
+
},
|
|
246
|
+
},
|
|
204
247
|
{
|
|
205
248
|
id: "settings",
|
|
206
249
|
label: "Open Settings",
|
|
@@ -222,7 +265,13 @@ export function ChatView({ client, initialSessionId, error: initError, debuggerU
|
|
|
222
265
|
_jsx(ChatLayout.Messages, { children: messages.length === 0 ? (
|
|
223
266
|
// Only render empty state once agent info is loaded
|
|
224
267
|
agentName !== undefined ? (_jsx(OpenFilesButton, { children: ({ openFiles, openSettings }) => (_jsx("div", { className: "flex flex-1 items-center px-4", children: _jsx(ChatEmptyState, { title: agentName, description: agentDescription ?? "", suggestedPrompts: suggestedPrompts ?? [], onPromptClick: (prompt) => {
|
|
225
|
-
|
|
268
|
+
// Get current prompt parameters from store
|
|
269
|
+
const currentParams = useChatStore.getState().input
|
|
270
|
+
.selectedPromptParameters;
|
|
271
|
+
sendMessage(prompt, undefined, currentParams &&
|
|
272
|
+
Object.keys(currentParams).length > 0
|
|
273
|
+
? currentParams
|
|
274
|
+
: undefined);
|
|
226
275
|
setPlaceholder("Type a message or / for commands...");
|
|
227
276
|
logger.info("Prompt clicked", { prompt });
|
|
228
277
|
}, onPromptHover: handlePromptHover, onPromptLeave: handlePromptLeave, onOpenFiles: openFiles, onOpenSettings: openSettings, toolsAndMcpsCount: agentTools.length +
|
|
@@ -251,10 +300,51 @@ export function ChatView({ client, initialSessionId, error: initError, debuggerU
|
|
|
251
300
|
? "mt-2"
|
|
252
301
|
: "mt-6";
|
|
253
302
|
}
|
|
254
|
-
|
|
255
|
-
|
|
303
|
+
// Check if any message is streaming
|
|
304
|
+
const anyMessageStreaming = messages.some((m) => m.isStreaming);
|
|
305
|
+
// Calculate which user message number this is (1-indexed for display, 0-indexed for API)
|
|
306
|
+
// Count user messages up to and including this index
|
|
307
|
+
const userMessageIndex = message.role === "user"
|
|
308
|
+
? messages
|
|
309
|
+
.slice(0, index + 1)
|
|
310
|
+
.filter((m) => m.role === "user").length - 1
|
|
311
|
+
: -1;
|
|
312
|
+
return (_jsx(Message, { message: message, className: cn(spacingClass, "group"), isLastMessage: index === messages.length - 1, children: _jsx("div", { className: "flex flex-col w-full", children: message.role === "user" ? (_jsx(EditableUserMessage, { message: message, messageIndex: userMessageIndex, isStreaming: anyMessageStreaming, onEditAndResend: editAndResend, sticky: true })) : (_jsxs(_Fragment, { children: [
|
|
313
|
+
_jsx(MessageContent, { message: message, thinkingDisplayStyle: "collapsible" }), _jsx(MessageActions, { message: message, isStreaming: message.isStreaming, onSendMessage: sendMessage, isLastAssistantMessage: index ===
|
|
314
|
+
messages.findLastIndex((m) => m.role === "assistant"), onRedo: () => {
|
|
315
|
+
// Find the user message that preceded this assistant message
|
|
316
|
+
let precedingUserMessage = null;
|
|
317
|
+
let precedingUserMessageIndex = -1;
|
|
318
|
+
const _userCount = 0;
|
|
319
|
+
for (let i = index - 1; i >= 0; i--) {
|
|
320
|
+
if (messages[i]?.role === "user") {
|
|
321
|
+
precedingUserMessage = messages[i];
|
|
322
|
+
// Count how many user messages came before this one
|
|
323
|
+
precedingUserMessageIndex =
|
|
324
|
+
messages
|
|
325
|
+
.slice(0, i + 1)
|
|
326
|
+
.filter((m) => m.role === "user")
|
|
327
|
+
.length - 1;
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (precedingUserMessage?.content &&
|
|
332
|
+
precedingUserMessageIndex >= 0) {
|
|
333
|
+
// Convert images back to attachments format
|
|
334
|
+
const attachments = precedingUserMessage.images?.map((img, idx) => ({
|
|
335
|
+
name: `image-${idx}`,
|
|
336
|
+
path: "",
|
|
337
|
+
size: 0,
|
|
338
|
+
mimeType: img.mimeType,
|
|
339
|
+
data: img.data,
|
|
340
|
+
}));
|
|
341
|
+
editAndResend(precedingUserMessageIndex, precedingUserMessage.content, attachments);
|
|
342
|
+
}
|
|
343
|
+
} })
|
|
344
|
+
] })) }) }, message.id));
|
|
345
|
+
}) })) }), _jsx(ChatLayout.Footer, { children: _jsx(ChatInputWithAttachments, { client: client, startSession: startSession, placeholder: placeholder, commandMenuItems: commandMenuItems, onCancel: cancel, promptParameters: agentPromptParameters }) })
|
|
256
346
|
] })
|
|
257
|
-
] }), _jsx(ChatLayout.Aside, { breakpoint: "md", children: _jsx(AsideTabs, { todos: todos, tools: agentTools, mcps: agentMcps, subagents: agentSubagents }) })
|
|
347
|
+
] }), _jsx(ChatLayout.Aside, { breakpoint: "md", children: _jsx(AsideTabs, { todos: todos, sources: allSources, tools: agentTools, mcps: agentMcps, subagents: agentSubagents }) })
|
|
258
348
|
] }) })
|
|
259
349
|
] }));
|
|
260
350
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { Source } from "../../core/schemas/source.js";
|
|
3
|
+
export interface CitationChipProps {
|
|
4
|
+
/** The source ID (e.g., "1", "2") */
|
|
5
|
+
sourceId: string;
|
|
6
|
+
/** Available sources to look up the citation */
|
|
7
|
+
sources: Source[];
|
|
8
|
+
/** Optional className */
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* CitationChip displays an inline citation pill that shows source details on hover
|
|
13
|
+
* and scrolls to the Sources panel on click.
|
|
14
|
+
*/
|
|
15
|
+
export declare const CitationChip: React.ForwardRefExoticComponent<CitationChipProps & React.RefAttributes<HTMLSpanElement>>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { ExternalLink } from "lucide-react";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { cn } from "../lib/utils.js";
|
|
6
|
+
import * as ChatLayout from "./ChatLayout.js";
|
|
7
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "./Tooltip.js";
|
|
8
|
+
/**
|
|
9
|
+
* CitationChip displays an inline citation pill that shows source details on hover
|
|
10
|
+
* and scrolls to the Sources panel on click.
|
|
11
|
+
*/
|
|
12
|
+
export const CitationChip = React.forwardRef(({ sourceId, sources, className }, ref) => {
|
|
13
|
+
const source = sources.find((s) => s.id === sourceId);
|
|
14
|
+
// Try to get ChatLayout context for panel control
|
|
15
|
+
let chatLayoutContext = null;
|
|
16
|
+
try {
|
|
17
|
+
// biome-ignore lint/correctness/useHookAtTopLevel: intentionally catching context absence
|
|
18
|
+
chatLayoutContext = ChatLayout.useChatLayoutContext();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Not inside ChatLayout.Root - that's okay, we just won't have panel control
|
|
22
|
+
}
|
|
23
|
+
// Handle click - open Sources panel and scroll to this source
|
|
24
|
+
const handleClick = React.useCallback((e) => {
|
|
25
|
+
e.preventDefault();
|
|
26
|
+
e.stopPropagation();
|
|
27
|
+
if (chatLayoutContext) {
|
|
28
|
+
// Open the Sources panel
|
|
29
|
+
chatLayoutContext.setPanelSize("small");
|
|
30
|
+
chatLayoutContext.setActiveTab("sources");
|
|
31
|
+
// TODO: Add scroll-to-source functionality once we wire up highlighting
|
|
32
|
+
}
|
|
33
|
+
// Also open the source URL in a new tab if available
|
|
34
|
+
if (source?.url) {
|
|
35
|
+
window.open(source.url, "_blank", "noopener,noreferrer");
|
|
36
|
+
}
|
|
37
|
+
}, [chatLayoutContext, source?.url]);
|
|
38
|
+
// If source not found, just show the number
|
|
39
|
+
if (!source) {
|
|
40
|
+
return (_jsxs("span", { ref: ref, className: cn("inline-flex items-center px-1.5 py-0.5 text-xs font-medium", "bg-muted text-muted-foreground rounded-md", "cursor-default", className), children: ["[", sourceId, "]"] }));
|
|
41
|
+
}
|
|
42
|
+
const domain = source.sourceName || getDomain(source.url);
|
|
43
|
+
return (_jsx(TooltipProvider, { delayDuration: 200, children: _jsxs(Tooltip, { children: [
|
|
44
|
+
_jsx(TooltipTrigger, { asChild: true, children: _jsxs("span", { ref: ref, role: "button", tabIndex: 0, onClick: handleClick, onKeyDown: (e) => {
|
|
45
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
46
|
+
handleClick(e);
|
|
47
|
+
}
|
|
48
|
+
}, className: cn("inline-flex items-center gap-1 px-1.5 py-0.5", "text-xs font-medium rounded-md", "bg-accent/50 text-accent-foreground", "hover:bg-accent cursor-pointer transition-colors", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", className), children: [source.favicon && (_jsx("img", { src: source.favicon, alt: "", className: "size-3 rounded-sm", onError: (e) => {
|
|
49
|
+
// Hide broken favicon images
|
|
50
|
+
e.target.style.display = "none";
|
|
51
|
+
} })), _jsx("span", { children: domain })
|
|
52
|
+
] }) }), _jsx(TooltipContent, { side: "top", align: "start", className: "max-w-xs p-0 overflow-hidden", children: _jsxs("div", { className: "p-3", children: [
|
|
53
|
+
_jsxs("div", { className: "flex items-center gap-2 mb-2", children: [source.favicon && (_jsx("img", { src: source.favicon, alt: "", className: "size-4 rounded-sm" })), _jsx("span", { className: "font-medium text-sm", children: domain })
|
|
54
|
+
] }), _jsx("h4", { className: "font-medium text-sm text-foreground line-clamp-2 mb-1", children: source.title }), source.snippet && (_jsx("p", { className: "text-xs text-muted-foreground line-clamp-3 mb-2", children: source.snippet })), _jsxs("div", { className: "flex items-center gap-1 text-xs text-muted-foreground", children: [
|
|
55
|
+
_jsx(ExternalLink, { className: "size-3" }), _jsx("span", { children: "Click to open" })
|
|
56
|
+
] })
|
|
57
|
+
] }) })
|
|
58
|
+
] }) }));
|
|
59
|
+
});
|
|
60
|
+
CitationChip.displayName = "CitationChip";
|
|
61
|
+
/**
|
|
62
|
+
* Helper to extract domain from URL
|
|
63
|
+
*/
|
|
64
|
+
function getDomain(url) {
|
|
65
|
+
try {
|
|
66
|
+
const hostname = new URL(url).hostname;
|
|
67
|
+
return hostname.replace(/^www\./, "");
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return url;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { DisplayMessage } from "./MessageList.js";
|
|
2
|
+
export interface EditableUserMessageProps {
|
|
3
|
+
message: DisplayMessage;
|
|
4
|
+
messageIndex: number;
|
|
5
|
+
isStreaming: boolean;
|
|
6
|
+
onEditAndResend: (messageIndex: number, newContent: string, attachments?: Array<{
|
|
7
|
+
name: string;
|
|
8
|
+
path: string;
|
|
9
|
+
size: number;
|
|
10
|
+
mimeType: string;
|
|
11
|
+
data: string;
|
|
12
|
+
}>) => void;
|
|
13
|
+
/** Whether to make the message content sticky */
|
|
14
|
+
sticky?: boolean;
|
|
15
|
+
}
|
|
16
|
+
declare function PureEditableUserMessage({ message, messageIndex, isStreaming, onEditAndResend, sticky }: EditableUserMessageProps): import("react/jsx-runtime").JSX.Element;
|
|
17
|
+
export declare const EditableUserMessage: import("react").MemoExoticComponent<typeof PureEditableUserMessage>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Check, Copy, Pencil } from "lucide-react";
|
|
4
|
+
import { memo, useCallback, useRef, useState } from "react";
|
|
5
|
+
import { toast } from "sonner";
|
|
6
|
+
import { Action, Actions } from "./Actions.js";
|
|
7
|
+
import { Button } from "./Button.js";
|
|
8
|
+
import { MessageContent } from "./MessageContent.js";
|
|
9
|
+
import { Textarea } from "./Textarea.js";
|
|
10
|
+
function PureEditableUserMessage({ message, messageIndex, isStreaming, onEditAndResend, sticky = false, }) {
|
|
11
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
12
|
+
const [editedContent, setEditedContent] = useState(message.content || "");
|
|
13
|
+
const [isCopied, setIsCopied] = useState(false);
|
|
14
|
+
const containerRef = useRef(null);
|
|
15
|
+
// Handle clicking the sticky message to scroll back to its position
|
|
16
|
+
const handleStickyClick = useCallback(() => {
|
|
17
|
+
if (!sticky || !containerRef.current)
|
|
18
|
+
return;
|
|
19
|
+
// Find the scroll container (parent with overflow-y-auto)
|
|
20
|
+
let scrollContainer = containerRef.current.parentElement;
|
|
21
|
+
while (scrollContainer &&
|
|
22
|
+
!scrollContainer.classList.contains("overflow-y-auto")) {
|
|
23
|
+
scrollContainer = scrollContainer.parentElement;
|
|
24
|
+
}
|
|
25
|
+
if (scrollContainer) {
|
|
26
|
+
// Get the element's position relative to the scroll container
|
|
27
|
+
const _elementRect = containerRef.current.getBoundingClientRect();
|
|
28
|
+
const _containerRect = scrollContainer.getBoundingClientRect();
|
|
29
|
+
const elementTop = containerRef.current.offsetTop;
|
|
30
|
+
// Scroll so the element is at the top of the container
|
|
31
|
+
scrollContainer.scrollTo({
|
|
32
|
+
top: elementTop - 16, // Small offset for padding
|
|
33
|
+
behavior: "smooth",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}, [sticky]);
|
|
37
|
+
const handleCopy = useCallback(async () => {
|
|
38
|
+
if (!message.content) {
|
|
39
|
+
toast.error("There's no text to copy!");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
await navigator.clipboard.writeText(message.content);
|
|
44
|
+
setIsCopied(true);
|
|
45
|
+
toast.success("Copied to clipboard!");
|
|
46
|
+
setTimeout(() => {
|
|
47
|
+
setIsCopied(false);
|
|
48
|
+
}, 2000);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
toast.error("Failed to copy to clipboard");
|
|
52
|
+
}
|
|
53
|
+
}, [message.content]);
|
|
54
|
+
const handleStartEdit = useCallback(() => {
|
|
55
|
+
setEditedContent(message.content || "");
|
|
56
|
+
setIsEditing(true);
|
|
57
|
+
}, [message.content]);
|
|
58
|
+
const handleCancelEdit = useCallback(() => {
|
|
59
|
+
setIsEditing(false);
|
|
60
|
+
setEditedContent(message.content || "");
|
|
61
|
+
}, [message.content]);
|
|
62
|
+
const handleSaveAndResend = useCallback(() => {
|
|
63
|
+
if (!editedContent.trim())
|
|
64
|
+
return;
|
|
65
|
+
// Convert images back to attachments format if the message had images
|
|
66
|
+
const attachments = message.images?.map((img, idx) => ({
|
|
67
|
+
name: `image-${idx}`,
|
|
68
|
+
path: "",
|
|
69
|
+
size: 0,
|
|
70
|
+
mimeType: img.mimeType,
|
|
71
|
+
data: img.data,
|
|
72
|
+
}));
|
|
73
|
+
onEditAndResend(messageIndex, editedContent.trim(), attachments);
|
|
74
|
+
setIsEditing(false);
|
|
75
|
+
}, [messageIndex, editedContent, message.images, onEditAndResend]);
|
|
76
|
+
const handleKeyDown = useCallback((e) => {
|
|
77
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
handleSaveAndResend();
|
|
80
|
+
}
|
|
81
|
+
if (e.key === "Escape") {
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
handleCancelEdit();
|
|
84
|
+
}
|
|
85
|
+
}, [handleSaveAndResend, handleCancelEdit]);
|
|
86
|
+
if (isEditing) {
|
|
87
|
+
return (_jsxs("div", { className: "w-full bg-secondary rounded-2xl px-4 py-4", children: [message.images && message.images.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2 mb-2", children: message.images.map((image, imageIndex) => (_jsx("img", { src: `data:${image.mimeType};base64,${image.data}`, alt: `Attachment ${imageIndex + 1}`, className: "max-w-[200px] max-h-[200px] rounded-lg object-cover opacity-50" }, `image-${image.mimeType}-${image.data.slice(0, 20)}`))) })), _jsx(Textarea, { value: editedContent, onChange: (e) => setEditedContent(e.target.value), onKeyDown: handleKeyDown, className: "min-h-[60px] w-full resize-none bg-transparent border-none shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 p-0 text-base leading-relaxed text-foreground", autoFocus: true, placeholder: "Edit your message..." }), _jsxs("div", { className: "flex items-center justify-end gap-2 mt-2", children: [
|
|
88
|
+
_jsx(Button, { variant: "ghost", size: "sm", onClick: handleCancelEdit, className: "h-7 px-2 text-xs", children: "Cancel" }), _jsx(Button, { size: "sm", onClick: handleSaveAndResend, disabled: !editedContent.trim(), className: "h-7 px-3 text-xs", children: "Send" })
|
|
89
|
+
] })
|
|
90
|
+
] }));
|
|
91
|
+
}
|
|
92
|
+
return (_jsxs("div", { ref: containerRef, className: "w-full group/user-message", children: [
|
|
93
|
+
_jsx("div", { className: sticky ? "sticky top-0 z-10 bg-background cursor-pointer" : "", onClick: sticky ? handleStickyClick : undefined, onKeyDown: sticky
|
|
94
|
+
? (e) => {
|
|
95
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
96
|
+
handleStickyClick();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
: undefined, role: sticky ? "button" : undefined, tabIndex: sticky ? 0 : undefined, children: _jsx(MessageContent, { message: message }) }), !isStreaming && message.content && (_jsxs(Actions, { className: "mt-2 opacity-0 group-hover/user-message:opacity-100 transition-opacity", children: [
|
|
100
|
+
_jsx(Action, { onClick: handleCopy, tooltip: isCopied ? "Copied!" : "Copy", children: isCopied ? (_jsx(Check, { className: "size-4" })) : (_jsx(Copy, { className: "size-4" })) }), _jsx(Action, { onClick: handleStartEdit, tooltip: "Edit", children: _jsx(Pencil, { className: "size-4" }) })
|
|
101
|
+
] }))] }));
|
|
102
|
+
}
|
|
103
|
+
export const EditableUserMessage = memo(PureEditableUserMessage, (prevProps, nextProps) => {
|
|
104
|
+
return (prevProps.isStreaming === nextProps.isStreaming &&
|
|
105
|
+
prevProps.message.id === nextProps.message.id &&
|
|
106
|
+
prevProps.message.content === nextProps.message.content &&
|
|
107
|
+
prevProps.messageIndex === nextProps.messageIndex &&
|
|
108
|
+
prevProps.sticky === nextProps.sticky);
|
|
109
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { DisplayMessage } from "./MessageList.js";
|
|
2
|
+
export interface MessageActionsProps {
|
|
3
|
+
/** The message to show actions for */
|
|
4
|
+
message: DisplayMessage;
|
|
5
|
+
/** Whether the message is currently streaming */
|
|
6
|
+
isStreaming?: boolean;
|
|
7
|
+
/** Callback to regenerate/redo the response */
|
|
8
|
+
onRedo?: () => void;
|
|
9
|
+
/** Callback to send a message to the agent */
|
|
10
|
+
onSendMessage?: (message: string) => void;
|
|
11
|
+
/** Whether this is the last assistant message (shows actions by default) */
|
|
12
|
+
isLastAssistantMessage?: boolean;
|
|
13
|
+
}
|
|
14
|
+
declare function PureMessageActions({ message, isStreaming, onRedo, onSendMessage, isLastAssistantMessage }: MessageActionsProps): import("react/jsx-runtime").JSX.Element | null;
|
|
15
|
+
export declare const MessageActions: import("react").MemoExoticComponent<typeof PureMessageActions>;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Check, Copy, Download, FileSpreadsheet, FileText, FileType, RotateCcw, } from "lucide-react";
|
|
4
|
+
import { memo, useState } from "react";
|
|
5
|
+
import { toast } from "sonner";
|
|
6
|
+
import { cn } from "../lib/utils.js";
|
|
7
|
+
import { Action, Actions } from "./Actions.js";
|
|
8
|
+
import { Button } from "./Button.js";
|
|
9
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "./DropdownMenu.js";
|
|
10
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "./Tooltip.js";
|
|
11
|
+
const EXPORT_FORMATS = [
|
|
12
|
+
{ id: "pdf", label: "PDF", icon: FileText },
|
|
13
|
+
{ id: "excel", label: "Excel", icon: FileSpreadsheet },
|
|
14
|
+
{ id: "csv", label: "CSV", icon: FileSpreadsheet },
|
|
15
|
+
{ id: "text", label: "Text", icon: FileType },
|
|
16
|
+
{ id: "markdown", label: "Markdown", icon: FileText },
|
|
17
|
+
];
|
|
18
|
+
function PureMessageActions({ message, isStreaming, onRedo, onSendMessage, isLastAssistantMessage = false, }) {
|
|
19
|
+
const [isCopied, setIsCopied] = useState(false);
|
|
20
|
+
// Don't show actions while streaming
|
|
21
|
+
if (isStreaming) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
// Only show actions for assistant messages with content
|
|
25
|
+
if (message.role !== "assistant" || !message.content) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
// For non-last messages, show on hover only
|
|
29
|
+
const visibilityClass = isLastAssistantMessage
|
|
30
|
+
? ""
|
|
31
|
+
: "opacity-0 group-hover:opacity-100 transition-opacity";
|
|
32
|
+
const handleCopy = async () => {
|
|
33
|
+
if (!message.content) {
|
|
34
|
+
toast.error("There's no text to copy!");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
await navigator.clipboard.writeText(message.content);
|
|
39
|
+
setIsCopied(true);
|
|
40
|
+
toast.success("Copied to clipboard!");
|
|
41
|
+
// Reset the icon after 2 seconds
|
|
42
|
+
setTimeout(() => {
|
|
43
|
+
setIsCopied(false);
|
|
44
|
+
}, 2000);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
toast.error("Failed to copy to clipboard");
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const handleRedo = () => {
|
|
51
|
+
if (onRedo) {
|
|
52
|
+
onRedo();
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
toast.info("Regenerate not available");
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const handleExport = (format) => {
|
|
59
|
+
if (onSendMessage) {
|
|
60
|
+
onSendMessage(`produce an artifact as ${format} for this session`);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
toast.info("Export not available");
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
return (_jsxs(Actions, { className: cn("mt-2 mb-10", visibilityClass), children: [
|
|
67
|
+
_jsx(Action, { onClick: handleCopy, tooltip: isCopied ? "Copied!" : "Copy", children: isCopied ? _jsx(Check, { className: "size-4" }) : _jsx(Copy, { className: "size-4" }) }), _jsx(Action, { onClick: handleRedo, tooltip: "Redo", children: _jsx(RotateCcw, { className: "size-4" }) }), _jsxs(DropdownMenu, { children: [
|
|
68
|
+
_jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [
|
|
69
|
+
_jsx(TooltipTrigger, { asChild: true, children: _jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { className: cn("relative size-8 p-1.5 text-muted-foreground hover:text-foreground"), size: "sm", type: "button", variant: "ghost", children: [
|
|
70
|
+
_jsx(Download, { className: "size-4" }), _jsx("span", { className: "sr-only", children: "Export" })
|
|
71
|
+
] }) }) }), _jsx(TooltipContent, { children: _jsx("p", { children: "Export" }) })
|
|
72
|
+
] }) }), _jsx(DropdownMenuContent, { align: "start", children: EXPORT_FORMATS.map((format) => (_jsxs(DropdownMenuItem, { onClick: () => handleExport(format.label), children: [
|
|
73
|
+
_jsx(format.icon, { className: "size-4 mr-2" }), format.label] }, format.id))) })
|
|
74
|
+
] })
|
|
75
|
+
] }));
|
|
76
|
+
}
|
|
77
|
+
export const MessageActions = memo(PureMessageActions, (prevProps, nextProps) => {
|
|
78
|
+
if (prevProps.isStreaming !== nextProps.isStreaming) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
if (prevProps.message.id !== nextProps.message.id) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
if (prevProps.message.content !== nextProps.message.content) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
if (prevProps.onRedo !== nextProps.onRedo) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
if (prevProps.onSendMessage !== nextProps.onSendMessage) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
if (prevProps.isLastAssistantMessage !== nextProps.isLastAssistantMessage) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
return true;
|
|
97
|
+
});
|
|
@@ -49,7 +49,7 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
|
|
|
49
49
|
const hasThinking = !!thinking;
|
|
50
50
|
// Check if waiting (streaming but no content yet)
|
|
51
51
|
const isWaiting = message.isStreaming && !message.content && message.role === "assistant";
|
|
52
|
-
content = (_jsxs(_Fragment, { children: [message.role === "assistant" && hasThinking && (_jsx("div", { children: _jsx(Reasoning, { content: thinking, isStreaming: message.isStreaming, mode: thinkingDisplayStyle, autoCollapse: true }) })), isWaiting && streamingStartTime && (_jsx("div", { className: "flex flex-col my-4 rounded-md px-1 -mx-1 w-fit", children: _jsx("div", { className: "flex items-center gap-1.5", children: _jsx("span", { className: "text-paragraph-sm text-text-secondary/70", children: "
|
|
52
|
+
content = (_jsxs(_Fragment, { children: [message.role === "assistant" && hasThinking && (_jsx("div", { children: _jsx(Reasoning, { content: thinking, isStreaming: message.isStreaming, mode: thinkingDisplayStyle, autoCollapse: true }) })), isWaiting && streamingStartTime && (_jsx("div", { className: "flex flex-col my-4 rounded-md px-1 -mx-1 w-fit", children: _jsx("div", { className: "flex items-center gap-1.5", children: _jsx("span", { className: "text-paragraph-sm text-text-secondary/70", children: "Working..." }) }) })), message.role === "assistant" ? ((() => {
|
|
53
53
|
// Sort tool calls by content position
|
|
54
54
|
const sortedToolCalls = (message.toolCalls || [])
|
|
55
55
|
.slice()
|
|
@@ -91,6 +91,14 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
|
|
|
91
91
|
currentConsecutiveTitle = null;
|
|
92
92
|
};
|
|
93
93
|
for (const tc of toolCalls) {
|
|
94
|
+
// todo_write should never be grouped with other tool calls
|
|
95
|
+
const isTodoWrite = tc.title === "todo_write";
|
|
96
|
+
if (isTodoWrite) {
|
|
97
|
+
flushSelectingGroup();
|
|
98
|
+
flushConsecutiveGroup();
|
|
99
|
+
result.push(tc);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
94
102
|
// Handle batch groups (explicit batchId)
|
|
95
103
|
if (tc.batchId) {
|
|
96
104
|
flushSelectingGroup();
|
|
@@ -168,7 +176,7 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
|
|
|
168
176
|
const hasHookPositions = hookNotifications.some((n) => n.contentPosition !== undefined);
|
|
169
177
|
if (!hasHookPositions) {
|
|
170
178
|
// No positions - render hooks at top, then tool calls, then content
|
|
171
|
-
return (_jsxs(_Fragment, { children: [hookNotifications.length > 0 && (_jsx("div", { className: "flex flex-col gap-2 mb-1", children: hookNotifications.map((notification) => (_jsx(HookNotification, { notification: notification }, notification.id))) })), groupedToolCalls.length > 0 && (_jsx("div", { className: "flex flex-col gap-2 mb-1", children: groupedToolCalls.map((item, index) => renderToolCallOrGroup(item, index)) })), _jsx("div", { children: _jsx(Response, { content: message.content, isStreaming: message.isStreaming, showEmpty: false }) })
|
|
179
|
+
return (_jsxs(_Fragment, { children: [hookNotifications.length > 0 && (_jsx("div", { className: "flex flex-col gap-2 mb-1", children: hookNotifications.map((notification) => (_jsx(HookNotification, { notification: notification }, notification.id))) })), groupedToolCalls.length > 0 && (_jsx("div", { className: "flex flex-col gap-2 mb-1", children: groupedToolCalls.map((item, index) => renderToolCallOrGroup(item, index)) })), _jsx("div", { children: _jsx(Response, { content: message.content, isStreaming: message.isStreaming, showEmpty: false, sources: message.sources ?? [] }) })
|
|
172
180
|
] }));
|
|
173
181
|
}
|
|
174
182
|
// Hooks have positions - render them inline with content
|
|
@@ -180,7 +188,7 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
|
|
|
180
188
|
if (position > currentPosition) {
|
|
181
189
|
const textChunk = message.content.slice(currentPosition, position);
|
|
182
190
|
if (textChunk) {
|
|
183
|
-
elements.push(_jsx("div", { children: _jsx(Response, { content: textChunk, isStreaming: false, showEmpty: false }) }, `text-before-hook-${notification.id}`));
|
|
191
|
+
elements.push(_jsx("div", { children: _jsx(Response, { content: textChunk, isStreaming: false, showEmpty: false, sources: message.sources ?? [] }) }, `text-before-hook-${notification.id}`));
|
|
184
192
|
}
|
|
185
193
|
}
|
|
186
194
|
// Add hook notification
|
|
@@ -191,7 +199,7 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
|
|
|
191
199
|
if (currentPosition < message.content.length) {
|
|
192
200
|
const remainingText = message.content.slice(currentPosition);
|
|
193
201
|
if (remainingText) {
|
|
194
|
-
elements.push(_jsx("div", { children: _jsx(Response, { content: remainingText, isStreaming: message.isStreaming, showEmpty: false }) }, "text-end-hooks"));
|
|
202
|
+
elements.push(_jsx("div", { children: _jsx(Response, { content: remainingText, isStreaming: message.isStreaming, showEmpty: false, sources: message.sources ?? [] }) }, "text-end-hooks"));
|
|
195
203
|
}
|
|
196
204
|
}
|
|
197
205
|
// Add tool calls at the end
|
|
@@ -258,7 +266,7 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
|
|
|
258
266
|
const itemId = positionedItem.type === "toolCall"
|
|
259
267
|
? positionedItem.item.id
|
|
260
268
|
: positionedItem.item.id;
|
|
261
|
-
elements.push(_jsx("div", { children: _jsx(Response, { content: textChunk, isStreaming: false, showEmpty: false }) }, `text-before-${itemId}`));
|
|
269
|
+
elements.push(_jsx("div", { children: _jsx(Response, { content: textChunk, isStreaming: false, showEmpty: false, sources: message.sources ?? [] }) }, `text-before-${itemId}`));
|
|
262
270
|
}
|
|
263
271
|
}
|
|
264
272
|
if (positionedItem.type === "hookNotification") {
|
|
@@ -270,7 +278,14 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
|
|
|
270
278
|
else {
|
|
271
279
|
// Tool call - check if it should be batched
|
|
272
280
|
const toolCall = positionedItem.item;
|
|
273
|
-
|
|
281
|
+
// todo_write should never be grouped with other tool calls
|
|
282
|
+
const isTodoWrite = toolCall.title === "todo_write";
|
|
283
|
+
if (isTodoWrite) {
|
|
284
|
+
flushBatch();
|
|
285
|
+
currentBatch = [toolCall];
|
|
286
|
+
flushBatch();
|
|
287
|
+
}
|
|
288
|
+
else if (toolCall.batchId) {
|
|
274
289
|
if (currentBatchId === toolCall.batchId) {
|
|
275
290
|
// Same batch, add to current batch
|
|
276
291
|
currentBatch.push(toolCall);
|
|
@@ -305,7 +320,7 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
|
|
|
305
320
|
if (currentPosition < message.content.length) {
|
|
306
321
|
const remainingText = message.content.slice(currentPosition);
|
|
307
322
|
if (remainingText) {
|
|
308
|
-
elements.push(_jsx("div", { children: _jsx(Response, { content: remainingText, isStreaming: message.isStreaming, showEmpty: false }) }, "text-end"));
|
|
323
|
+
elements.push(_jsx("div", { children: _jsx(Response, { content: remainingText, isStreaming: message.isStreaming, showEmpty: false, sources: message.sources ?? [] }) }, "text-end"));
|
|
309
324
|
}
|
|
310
325
|
}
|
|
311
326
|
// Render preliminary (selecting) tool calls at the end, grouped
|